fix(memory): keep cutover reads brokered

This commit is contained in:
Galin Iliev
2026-08-11 01:13:48 -07:00
parent 4449aee2d2
commit 16fa3eb4c7
4 changed files with 18 additions and 13 deletions
+5 -3
View File
@@ -119,6 +119,7 @@ import type { CronToolOptions } from "./tools/cron-tool.types.js";
import { wrapToolWithGatewayCallerIdentity } from "./tools/gateway-caller-context.js";
const MEMORY_FLUSH_ALLOWED_TOOL_NAMES = new Set(["read", "write"]);
const MEMORY_ISOLATION_READ_TOOL_NAMES = new Set(["memory_search", "memory_get"]);
function applyModelProviderToolPolicy(
toolsInput: AnyAgentTool[],
@@ -951,10 +952,11 @@ function createOpenClawCodingToolsInternal(options?: OpenClawCodingToolsOptions)
!options?.swarmCollector ||
(tool.name !== "ask_user" && tool.name !== "sessions_send" && tool.name !== "sessions_yield"),
);
// P1C has no authorized mutation or execution path. Apply this after every contributor and
// policy layer so a future core, plugin, or ring-zero tool cannot reopen a durable-write bypass.
// P1C admits only selected-plugin reads. Generic filesystem reads would let a model bypass the
// broker's subject and receipt checks, while every other contributor could reopen an egress or
// mutation path if this final surface gate moved earlier.
const surfaceTools = memoryIsolationCutover
? authorizedTools.filter((tool) => tool.name === "read")
? authorizedTools.filter((tool) => MEMORY_ISOLATION_READ_TOOL_NAMES.has(tool.name))
: authorizedTools;
if (
swarmStructuredOutputTool &&
@@ -29,6 +29,7 @@ import {
} from "./agent-tools.read.js";
import { createApplyPatchTool } from "./apply-patch.js";
import { createMemoryFileMutationGuard } from "./memory-file-mutation-guard.js";
import { createOpenClawTools } from "./openclaw-tools.js";
import { SANDBOX_AGENT_WORKSPACE_MOUNT } from "./sandbox/constants.js";
import { resolveReadOnlyWorkspaceSkillMounts } from "./sandbox/workspace-mounts.js";
import {
@@ -89,7 +90,7 @@ async function expectExecCwdResolvesTo(
}
describe("workspace path resolution", () => {
it("exposes only read for an enforced read-only memory agent", async () => {
it("exposes only selected authorized memory tools for an enforced read-only memory agent", async () => {
await withTempDir("openclaw-memory-cutover-state-", async (stateDir) => {
const originalStateDir = process.env.OPENCLAW_STATE_DIR;
process.env.OPENCLAW_STATE_DIR = stateDir;
@@ -106,8 +107,12 @@ describe("workspace path resolution", () => {
.run();
resetMemoryIsolationCutoverForTest();
vi.mocked(createOpenClawTools).mockImplementationOnce(() =>
["read", "memory_search", "memory_get", "exec"].map((name) => ({ name }) as never),
);
expect(createOpenClawCodingTools({ agentId: "main" }).map((tool) => tool.name)).toEqual([
"read",
"memory_search",
"memory_get",
]);
} finally {
closeOpenClawAgentDatabasesForTest();
+4 -4
View File
@@ -157,10 +157,10 @@ export async function runWorkerEmbeddedTurn(params: RunWorkerEmbeddedTurnParams)
const omittedToolNames = permissionToolPolicy?.readOnly
? new Set<WorkerToolName>(["write", "edit", "apply_patch"])
: undefined;
// P1C's selected-memory pilot exposes no mutation or execution path. The worker builds core
// tools directly, so it must apply the primary agent's final read-only surface itself.
// Workers do not host selected-memory plugin tools. Exposing their generic read tool would
// bypass the broker, so the P1C cutover gives them no filesystem or process capability.
const availableToolNames = params.memoryIsolationCutover
? (["read"] as const)
? ([] as const)
: WORKER_LOCAL_TOOL_NAMES;
const activeToolNames = availableToolNames.filter(
(name) => allowedToolNameSet.has(name) && !omittedToolNames?.has(name),
@@ -254,7 +254,7 @@ export async function runWorkerEmbeddedTurn(params: RunWorkerEmbeddedTurnParams)
);
const discoveredToolNames = new Set(localTools.map((tool) => tool.name));
const requiredToolNames = [
...(params.memoryIsolationCutover ? ["read"] : WORKER_REQUIRED_LOCAL_TOOL_NAMES),
...(params.memoryIsolationCutover ? [] : WORKER_REQUIRED_LOCAL_TOOL_NAMES),
...(browserRuntime ? ["browser"] : []),
];
for (const toolName of requiredToolNames) {
+2 -4
View File
@@ -969,7 +969,7 @@ describe("worker runtime", () => {
]);
});
it("keeps an enforced agent's worker tool surface read-only after state isolation", async () => {
it("keeps an enforced agent's worker tool surface free of raw filesystem tools after state isolation", async () => {
const memoryStateDir = await mkdtemp(path.join(tmpdir(), "openclaw-worker-memory-state-"));
const previousStateDir = process.env.OPENCLAW_STATE_DIR;
process.env.OPENCLAW_STATE_DIR = memoryStateDir;
@@ -992,9 +992,7 @@ describe("worker runtime", () => {
await expect(runWorkerDescriptor(launch)).resolves.toMatchObject({ status: "completed" });
expect(browserRuntimeMocks.createWorkerBrowserToolRuntime).not.toHaveBeenCalled();
expect(gateway.inferenceRequests[0]?.context.tools?.map((tool) => tool.name)).toEqual([
"read",
]);
expect(gateway.inferenceRequests[0]?.context.tools?.map((tool) => tool.name)).toEqual([]);
} finally {
resetMemoryIsolationCutoverForTest();
closeOpenClawAgentDatabasesForTest();