mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-28 01:21:36 +00:00
Security: gate gateway model overrides separately
This commit is contained in:
@@ -117,6 +117,7 @@ function buildAgentCommandInput(params: {
|
||||
bestEffortDeliver: false as const,
|
||||
// HTTP API callers are authenticated operator clients for this gateway context.
|
||||
senderIsOwner: true as const,
|
||||
allowModelOverride: true as const,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -256,6 +256,7 @@ async function runResponsesAgentCommand(params: {
|
||||
bestEffortDeliver: false,
|
||||
// HTTP API callers are authenticated operator clients for this gateway context.
|
||||
senderIsOwner: true,
|
||||
allowModelOverride: true,
|
||||
},
|
||||
defaultRuntime,
|
||||
params.deps,
|
||||
|
||||
@@ -334,8 +334,10 @@ describe("gateway agent handler", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not forward provider and model overrides for write-scoped callers", async () => {
|
||||
it("rejects provider and model overrides for write-scoped callers", async () => {
|
||||
primeMainAgentRun();
|
||||
mocks.agentCommand.mockClear();
|
||||
const respond = vi.fn();
|
||||
|
||||
await invokeAgent(
|
||||
{
|
||||
@@ -353,14 +355,51 @@ describe("gateway agent handler", () => {
|
||||
scopes: ["operator.write"],
|
||||
},
|
||||
} as AgentHandlerArgs["client"],
|
||||
respond,
|
||||
},
|
||||
);
|
||||
|
||||
expect(mocks.agentCommand).not.toHaveBeenCalled();
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
false,
|
||||
undefined,
|
||||
expect.objectContaining({
|
||||
message: "provider/model overrides are not authorized for this caller.",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards provider and model overrides when internal override authorization is set", async () => {
|
||||
primeMainAgentRun();
|
||||
|
||||
await invokeAgent(
|
||||
{
|
||||
message: "test override",
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main:main",
|
||||
provider: "anthropic",
|
||||
model: "claude-haiku-4-6",
|
||||
idempotencyKey: "test-idem-model-override-internal",
|
||||
},
|
||||
{
|
||||
reqId: "test-idem-model-override-internal",
|
||||
client: {
|
||||
connect: {
|
||||
scopes: ["operator.write"],
|
||||
},
|
||||
internal: {
|
||||
allowModelOverride: true,
|
||||
},
|
||||
} as AgentHandlerArgs["client"],
|
||||
},
|
||||
);
|
||||
|
||||
const lastCall = mocks.agentCommand.mock.calls.at(-1);
|
||||
expect(lastCall?.[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
provider: undefined,
|
||||
model: undefined,
|
||||
provider: "anthropic",
|
||||
model: "claude-haiku-4-6",
|
||||
senderIsOwner: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -71,6 +71,12 @@ function resolveSenderIsOwnerFromClient(client: GatewayRequestHandlerOptions["cl
|
||||
return scopes.includes(ADMIN_SCOPE);
|
||||
}
|
||||
|
||||
function resolveAllowModelOverrideFromClient(
|
||||
client: GatewayRequestHandlerOptions["client"],
|
||||
): boolean {
|
||||
return resolveSenderIsOwnerFromClient(client) || client?.internal?.allowModelOverride === true;
|
||||
}
|
||||
|
||||
async function runSessionResetFromAgent(params: {
|
||||
key: string;
|
||||
reason: "new" | "reset";
|
||||
@@ -194,8 +200,21 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
inputProvenance?: InputProvenance;
|
||||
};
|
||||
const senderIsOwner = resolveSenderIsOwnerFromClient(client);
|
||||
const providerOverride = senderIsOwner ? request.provider : undefined;
|
||||
const modelOverride = senderIsOwner ? request.model : undefined;
|
||||
const allowModelOverride = resolveAllowModelOverrideFromClient(client);
|
||||
const requestedModelOverride = Boolean(request.provider || request.model);
|
||||
if (requestedModelOverride && !allowModelOverride) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
"provider/model overrides are not authorized for this caller.",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const providerOverride = allowModelOverride ? request.provider : undefined;
|
||||
const modelOverride = allowModelOverride ? request.model : undefined;
|
||||
const cfg = loadConfig();
|
||||
const idem = request.idempotencyKey;
|
||||
const normalizedSpawned = normalizeSpawnedRunMetadata({
|
||||
@@ -625,6 +644,7 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
workspaceDir: sessionEntry?.spawnedWorkspaceDir,
|
||||
}),
|
||||
senderIsOwner,
|
||||
allowModelOverride,
|
||||
},
|
||||
runId,
|
||||
idempotencyKey: idem,
|
||||
|
||||
@@ -21,6 +21,10 @@ export type GatewayClient = {
|
||||
canvasHostUrl?: string;
|
||||
canvasCapability?: string;
|
||||
canvasCapabilityExpiresAtMs?: number;
|
||||
/** Internal-only auth context that cannot be supplied through gateway RPC payloads. */
|
||||
internal?: {
|
||||
allowModelOverride?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type RespondFn = (
|
||||
|
||||
@@ -310,6 +310,7 @@ export const handleNodeEvent = async (ctx: NodeEventContext, nodeId: string, evt
|
||||
sourceTool: "gateway.voice.transcript",
|
||||
},
|
||||
senderIsOwner: false,
|
||||
allowModelOverride: false,
|
||||
},
|
||||
defaultRuntime,
|
||||
ctx.deps,
|
||||
@@ -441,6 +442,7 @@ export const handleNodeEvent = async (ctx: NodeEventContext, nodeId: string, evt
|
||||
typeof link?.timeoutSeconds === "number" ? link.timeoutSeconds.toString() : undefined,
|
||||
messageChannel: "node",
|
||||
senderIsOwner: false,
|
||||
allowModelOverride: false,
|
||||
},
|
||||
defaultRuntime,
|
||||
ctx.deps,
|
||||
|
||||
Reference in New Issue
Block a user