diff --git a/src/gateway/embeddings-http.test.ts b/src/gateway/embeddings-http.test.ts index 8eec15c92fc..aa8b7832497 100644 --- a/src/gateway/embeddings-http.test.ts +++ b/src/gateway/embeddings-http.test.ts @@ -5,6 +5,8 @@ import { getFreePort, installGatewayTestHooks } from "./test-helpers.js"; installGatewayTestHooks({ scope: "suite" }); +const WRITE_SCOPE_HEADER = { "x-openclaw-scopes": "operator.write" }; + let startGatewayServer: typeof import("./server.js").startGatewayServer; let createEmbeddingProviderMock: ReturnType< typeof vi.fn< @@ -81,6 +83,7 @@ async function postEmbeddings(body: unknown, headers?: Record) { headers: { authorization: "Bearer secret", "content-type": "application/json", + ...WRITE_SCOPE_HEADER, ...headers, }, body: JSON.stringify(body), @@ -164,6 +167,64 @@ describe("OpenAI-compatible embeddings HTTP API (e2e)", () => { expect(json.error?.type).toBe("invalid_request_error"); }); + it("rejects operator scopes that lack write access", async () => { + const res = await postEmbeddings( + { + model: "openclaw/default", + input: "hello", + }, + { "x-openclaw-scopes": "operator.read" }, + ); + expect(res.status).toBe(403); + await expect(res.json()).resolves.toMatchObject({ + ok: false, + error: { + type: "forbidden", + message: "missing scope: operator.write", + }, + }); + }); + + it("rejects requests with no declared operator scopes", async () => { + const res = await postEmbeddings( + { + model: "openclaw/default", + input: "hello", + }, + { "x-openclaw-scopes": "" }, + ); + expect(res.status).toBe(403); + await expect(res.json()).resolves.toMatchObject({ + ok: false, + error: { + type: "forbidden", + message: "missing scope: operator.write", + }, + }); + }); + + it("rejects requests when the operator scopes header is missing", async () => { + const res = await fetch(`http://127.0.0.1:${enabledPort}/v1/embeddings`, { + method: "POST", + headers: { + authorization: "Bearer secret", + "content-type": "application/json", + }, + body: JSON.stringify({ + model: "openclaw/default", + input: "hello", + }), + }); + expect(res.status).toBe(403); + await expect(res.json()).resolves.toMatchObject({ + ok: false, + error: { + type: "forbidden", + message: "missing scope: operator.write", + }, + }); + }); + it("rejects invalid agent targets", async () => { const res = await postEmbeddings({ model: "ollama/nomic-embed-text", diff --git a/src/gateway/embeddings-http.ts b/src/gateway/embeddings-http.ts index a67501b87cf..c82821d3d8e 100644 --- a/src/gateway/embeddings-http.ts +++ b/src/gateway/embeddings-http.ts @@ -209,6 +209,7 @@ export async function handleOpenAiEmbeddingsHttpRequest( ): Promise { const handled = await handleGatewayPostJsonEndpoint(req, res, { pathname: "/v1/embeddings", + requiredOperatorMethod: "chat.send", auth: opts.auth, trustedProxies: opts.trustedProxies, allowRealIpFallback: opts.allowRealIpFallback,