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>
This commit is contained in:
Jacob Tomlinson 2026-03-30 09:05:29 -07:00 committed by GitHub
parent 1a75906a6f
commit 17d0be02f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 60 additions and 2 deletions

View File

@ -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();

View File

@ -424,6 +424,7 @@ async function runResponsesAgentCommand(params: {
sessionKey: string;
runId: string;
messageChannel: string;
senderIsOwner: boolean;
deps: ReturnType<typeof createDefaultDeps>;
}) {
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,
});