mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 23:14:32 +00:00
fix(security): enforce explicit ingress owner context
This commit is contained in:
@@ -15,7 +15,7 @@ import { emitAgentEvent, onAgentEvent } from "../infra/agent-events.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { createOutboundTestPlugin, createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
import { agentCommand } from "./agent.js";
|
||||
import { agentCommand, agentCommandFromIngress } from "./agent.js";
|
||||
import * as agentDeliveryModule from "./agent/delivery.js";
|
||||
|
||||
vi.mock("../agents/auth-profiles.js", async (importOriginal) => {
|
||||
@@ -316,6 +316,27 @@ describe("agentCommand", () => {
|
||||
expect(callArgs?.senderIsOwner).toBe(expected);
|
||||
});
|
||||
|
||||
it("requires explicit senderIsOwner 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" } as never, runtime),
|
||||
).rejects.toThrow("senderIsOwner 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);
|
||||
const ingressCall = vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0];
|
||||
expect(ingressCall?.senderIsOwner).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("resumes when session-id is provided", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const store = path.join(home, "sessions.json");
|
||||
|
||||
@@ -77,7 +77,7 @@ import { deliverAgentCommandResult } from "./agent/delivery.js";
|
||||
import { resolveAgentRunContext } from "./agent/run-context.js";
|
||||
import { updateSessionStoreAfterAgentRun } from "./agent/session-store.js";
|
||||
import { resolveSession } from "./agent/session.js";
|
||||
import type { AgentCommandOpts } from "./agent/types.js";
|
||||
import type { AgentCommandIngressOpts, AgentCommandOpts } from "./agent/types.js";
|
||||
|
||||
type PersistSessionEntryParams = {
|
||||
sessionStore: Record<string, SessionEntry>;
|
||||
@@ -160,7 +160,7 @@ function runAgentAttempt(params: {
|
||||
resolvedThinkLevel: ThinkLevel;
|
||||
timeoutMs: number;
|
||||
runId: string;
|
||||
opts: AgentCommandOpts;
|
||||
opts: AgentCommandOpts & { senderIsOwner: boolean };
|
||||
runContext: ReturnType<typeof resolveAgentRunContext>;
|
||||
spawnedBy: string | undefined;
|
||||
messageChannel: ReturnType<typeof resolveMessageChannel>;
|
||||
@@ -172,7 +172,6 @@ function runAgentAttempt(params: {
|
||||
sessionStore?: Record<string, SessionEntry>;
|
||||
storePath?: string;
|
||||
}) {
|
||||
const senderIsOwner = params.opts.senderIsOwner ?? true;
|
||||
const effectivePrompt = resolveFallbackRetryPrompt({
|
||||
body: params.body,
|
||||
isFallbackRetry: params.isFallbackRetry,
|
||||
@@ -292,7 +291,7 @@ function runAgentAttempt(params: {
|
||||
currentThreadTs: params.runContext.currentThreadTs,
|
||||
replyToMode: params.runContext.replyToMode,
|
||||
hasRepliedRef: params.runContext.hasRepliedRef,
|
||||
senderIsOwner,
|
||||
senderIsOwner: params.opts.senderIsOwner,
|
||||
sessionFile: params.sessionFile,
|
||||
workspaceDir: params.workspaceDir,
|
||||
config: params.cfg,
|
||||
@@ -318,8 +317,8 @@ function runAgentAttempt(params: {
|
||||
});
|
||||
}
|
||||
|
||||
export async function agentCommand(
|
||||
opts: AgentCommandOpts,
|
||||
async function agentCommandInternal(
|
||||
opts: AgentCommandOpts & { senderIsOwner: boolean },
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
deps: CliDeps = createDefaultDeps(),
|
||||
) {
|
||||
@@ -922,3 +921,36 @@ export async function agentCommand(
|
||||
clearAgentRunContext(runId);
|
||||
}
|
||||
}
|
||||
|
||||
export async function agentCommand(
|
||||
opts: AgentCommandOpts,
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
deps: CliDeps = createDefaultDeps(),
|
||||
) {
|
||||
return await agentCommandInternal(
|
||||
{
|
||||
...opts,
|
||||
senderIsOwner: opts.senderIsOwner ?? true,
|
||||
},
|
||||
runtime,
|
||||
deps,
|
||||
);
|
||||
}
|
||||
|
||||
export async function agentCommandFromIngress(
|
||||
opts: AgentCommandIngressOpts,
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
deps: CliDeps = createDefaultDeps(),
|
||||
) {
|
||||
if (typeof opts.senderIsOwner !== "boolean") {
|
||||
throw new Error("senderIsOwner must be explicitly set for ingress agent runs.");
|
||||
}
|
||||
return await agentCommandInternal(
|
||||
{
|
||||
...opts,
|
||||
senderIsOwner: opts.senderIsOwner,
|
||||
},
|
||||
runtime,
|
||||
deps,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -81,3 +81,8 @@ export type AgentCommandOpts = {
|
||||
/** Per-call stream param overrides (best-effort). */
|
||||
streamParams?: AgentStreamParams;
|
||||
};
|
||||
|
||||
export type AgentCommandIngressOpts = Omit<AgentCommandOpts, "senderIsOwner"> & {
|
||||
/** Ingress callsites must always pass explicit owner authorization state. */
|
||||
senderIsOwner: boolean;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user