mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 20:35:39 -06:00
fix(memory): keep cutover reads brokered
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user