mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-22 07:20:59 +00:00
Security: validate and sanitize agent overrides
This commit is contained in:
@@ -12,7 +12,7 @@ import { runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
||||
import * as commandSecretGatewayModule from "../cli/command-secret-gateway.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import * as configModule from "../config/config.js";
|
||||
import * as sessionsModule from "../config/sessions.js";
|
||||
import * as sessionPathsModule from "../config/sessions/paths.js";
|
||||
import { emitAgentEvent, onAgentEvent } from "../infra/agent-events.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
@@ -20,6 +20,24 @@ import { createOutboundTestPlugin, createTestRegistry } from "../test-utils/chan
|
||||
import { agentCommand, agentCommandFromIngress } from "./agent.js";
|
||||
import * as agentDeliveryModule from "./agent/delivery.js";
|
||||
|
||||
vi.mock("../logging/subsystem.js", () => {
|
||||
const createMockLogger = () => ({
|
||||
subsystem: "test",
|
||||
isEnabled: vi.fn(() => true),
|
||||
trace: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
fatal: vi.fn(),
|
||||
raw: vi.fn(),
|
||||
child: vi.fn(() => createMockLogger()),
|
||||
});
|
||||
return {
|
||||
createSubsystemLogger: vi.fn(() => createMockLogger()),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../agents/auth-profiles.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../agents/auth-profiles.js")>();
|
||||
return {
|
||||
@@ -28,10 +46,13 @@ vi.mock("../agents/auth-profiles.js", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../agents/workspace.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../agents/workspace.js")>();
|
||||
vi.mock("../agents/workspace.js", () => {
|
||||
const resolveDefaultAgentWorkspaceDir = () => "/tmp/openclaw-workspace";
|
||||
return {
|
||||
...actual,
|
||||
DEFAULT_AGENT_WORKSPACE_DIR: "/tmp/openclaw-workspace",
|
||||
DEFAULT_AGENTS_FILENAME: "AGENTS.md",
|
||||
DEFAULT_IDENTITY_FILENAME: "IDENTITY.md",
|
||||
resolveDefaultAgentWorkspaceDir,
|
||||
ensureAgentWorkspace: vi.fn(async ({ dir }: { dir: string }) => ({ dir })),
|
||||
};
|
||||
});
|
||||
@@ -406,13 +427,35 @@ describe("agentCommand", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("requires explicit allowModelOverride for ingress runs", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const store = path.join(home, "sessions.json");
|
||||
mockConfig(home, store);
|
||||
await expect(
|
||||
// Runtime guard for non-TS callers; TS callsites are statically typed.
|
||||
agentCommandFromIngress(
|
||||
{
|
||||
message: "hi",
|
||||
to: "+1555",
|
||||
senderIsOwner: false,
|
||||
} as never,
|
||||
runtime,
|
||||
),
|
||||
).rejects.toThrow("allowModelOverride must be explicitly set for ingress agent runs.");
|
||||
});
|
||||
});
|
||||
|
||||
it("honors explicit senderIsOwner for ingress runs", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const store = path.join(home, "sessions.json");
|
||||
mockConfig(home, store);
|
||||
await agentCommandFromIngress({ message: "hi", to: "+1555", senderIsOwner: false }, runtime);
|
||||
await agentCommandFromIngress(
|
||||
{ message: "hi", to: "+1555", senderIsOwner: false, allowModelOverride: false },
|
||||
runtime,
|
||||
);
|
||||
const ingressCall = vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0];
|
||||
expect(ingressCall?.senderIsOwner).toBe(false);
|
||||
expect(ingressCall).not.toHaveProperty("allowModelOverride");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -463,7 +506,7 @@ describe("agentCommand", () => {
|
||||
const store = path.join(customStoreDir, "sessions.json");
|
||||
writeSessionStoreSeed(store, {});
|
||||
mockConfig(home, store);
|
||||
const resolveSessionFilePathSpy = vi.spyOn(sessionsModule, "resolveSessionFilePath");
|
||||
const resolveSessionFilePathSpy = vi.spyOn(sessionPathsModule, "resolveSessionFilePath");
|
||||
|
||||
await agentCommand({ message: "resume me", sessionId: "session-custom-123" }, runtime);
|
||||
|
||||
@@ -718,6 +761,63 @@ describe("agentCommand", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects explicit override values that contain control characters", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const store = path.join(home, "sessions.json");
|
||||
mockConfig(home, store, {
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
agentCommand(
|
||||
{
|
||||
message: "use an invalid override",
|
||||
sessionKey: "agent:main:subagent:invalid-override",
|
||||
provider: "openai\u001b[31m",
|
||||
model: "gpt-4.1-mini",
|
||||
},
|
||||
runtime,
|
||||
),
|
||||
).rejects.toThrow("Provider override contains invalid control characters.");
|
||||
});
|
||||
});
|
||||
|
||||
it("sanitizes provider/model text in model-allowlist errors", async () => {
|
||||
const parseModelRefSpy = vi.spyOn(modelSelectionModule, "parseModelRef");
|
||||
parseModelRefSpy.mockImplementationOnce(() => ({
|
||||
provider: "anthropic\u001b[31m",
|
||||
model: "claude-haiku-4-6\u001b[32m",
|
||||
}));
|
||||
try {
|
||||
await withTempHome(async (home) => {
|
||||
const store = path.join(home, "sessions.json");
|
||||
mockConfig(home, store, {
|
||||
models: {
|
||||
"openai/gpt-4.1-mini": {},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
agentCommand(
|
||||
{
|
||||
message: "use disallowed override",
|
||||
sessionKey: "agent:main:subagent:sanitized-override-error",
|
||||
model: "claude-haiku-4-6",
|
||||
},
|
||||
runtime,
|
||||
),
|
||||
).rejects.toThrow(
|
||||
'Model override "anthropic/claude-haiku-4-6" is not allowed for agent "main".',
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
parseModelRefSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps stored auth profile overrides during one-off cross-provider runs", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const store = path.join(home, "sessions.json");
|
||||
|
||||
Reference in New Issue
Block a user