From 17d0be02f2800a2bc4524c7d5b587d7fd9f6f28c Mon Sep 17 00:00:00 2001 From: Jacob Tomlinson Date: Mon, 30 Mar 2026 09:05:29 -0700 Subject: [PATCH] fix(gateway): bind OpenResponses HTTP ingress as non-owner (#57778) * fix(gateway): bind OpenResponses HTTP ingress as non-owner Co-authored-by: bmendonca3 <208517100+bmendonca3@users.noreply.github.com> * test(gateway): cover streaming OpenResponses non-owner ingress --------- Co-authored-by: bmendonca3 <208517100+bmendonca3@users.noreply.github.com> --- src/gateway/openresponses-http.test.ts | 56 ++++++++++++++++++++++++++ src/gateway/openresponses-http.ts | 6 ++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/gateway/openresponses-http.test.ts b/src/gateway/openresponses-http.test.ts index 5da8321fe24..ef1275d49fd 100644 --- a/src/gateway/openresponses-http.test.ts +++ b/src/gateway/openresponses-http.test.ts @@ -700,6 +700,62 @@ describe("OpenResponses HTTP API (e2e)", () => { } }); + it("treats HTTP callers as non-owner regardless of requested scopes", async () => { + const port = enabledPort; + + agentCommand.mockClear(); + agentCommand.mockResolvedValueOnce({ payloads: [{ text: "hello" }] } as never); + + const writeScopeResponse = await postResponses(port, { + model: "openclaw", + input: "hi", + }); + expect(writeScopeResponse.status).toBe(200); + const writeScopeOpts = (agentCommand.mock.calls[0] as unknown[] | undefined)?.[0] as + | { senderIsOwner?: boolean } + | undefined; + expect(writeScopeOpts?.senderIsOwner).toBe(false); + await ensureResponseConsumed(writeScopeResponse); + + agentCommand.mockClear(); + agentCommand.mockResolvedValueOnce({ payloads: [{ text: "hello" }] } as never); + + const adminScopeResponse = await postResponses( + port, + { model: "openclaw", input: "hi" }, + { "x-openclaw-scopes": "operator.admin, operator.write" }, + ); + expect(adminScopeResponse.status).toBe(200); + const adminScopeOpts = (agentCommand.mock.calls[0] as unknown[] | undefined)?.[0] as + | { senderIsOwner?: boolean } + | undefined; + // Requested HTTP scopes do not prove owner identity for owner-only tools. + expect(adminScopeOpts?.senderIsOwner).toBe(false); + await ensureResponseConsumed(adminScopeResponse); + + agentCommand.mockClear(); + agentCommand.mockImplementationOnce((async (opts: unknown) => + buildAssistantDeltaResult({ + opts, + emit: emitAgentEvent, + deltas: ["he", "llo"], + text: "hello", + })) as never); + + const streamingResponse = await postResponses( + port, + { stream: true, model: "openclaw", input: "hi" }, + { "x-openclaw-scopes": "operator.admin, operator.write" }, + ); + expect(streamingResponse.status).toBe(200); + const streamingOpts = (agentCommand.mock.calls[0] as unknown[] | undefined)?.[0] as + | { senderIsOwner?: boolean } + | undefined; + expect(streamingOpts?.senderIsOwner).toBe(false); + const streamingEvents = parseSseEvents(await streamingResponse.text()); + expect(streamingEvents.some((event) => event.event === "response.completed")).toBe(true); + }); + it("preserves assistant text alongside non-stream function_call output", async () => { const port = enabledPort; agentCommand.mockClear(); diff --git a/src/gateway/openresponses-http.ts b/src/gateway/openresponses-http.ts index b11043adfe9..3cf09117e1e 100644 --- a/src/gateway/openresponses-http.ts +++ b/src/gateway/openresponses-http.ts @@ -424,6 +424,7 @@ async function runResponsesAgentCommand(params: { sessionKey: string; runId: string; messageChannel: string; + senderIsOwner: boolean; deps: ReturnType; }) { return agentCommandFromIngress( @@ -439,8 +440,7 @@ async function runResponsesAgentCommand(params: { deliver: false, messageChannel: params.messageChannel, bestEffortDeliver: false, - // HTTP API callers are authenticated operator clients for this gateway context. - senderIsOwner: true, + senderIsOwner: params.senderIsOwner, allowModelOverride: true, }, defaultRuntime, @@ -704,6 +704,7 @@ export async function handleOpenResponsesHttpRequest( sessionKey, runId: responseId, messageChannel, + senderIsOwner: false, deps, }); @@ -956,6 +957,7 @@ export async function handleOpenResponsesHttpRequest( sessionKey, runId: responseId, messageChannel, + senderIsOwner: false, deps, });