Gateway: require caller scope for subagent session deletion (#55281)

This commit is contained in:
Jacob Tomlinson
2026-03-26 10:34:09 -07:00
committed by GitHub
parent ec2dbcff9a
commit b5d785f1a5
2 changed files with 51 additions and 14 deletions

View File

@@ -470,16 +470,59 @@ describe("loadGatewayPlugins", () => {
expect(getLastDispatchedClientScopes()).not.toContain("operator.admin");
});
test("keeps admin scope for fallback session deletion", async () => {
test("rejects fallback session deletion without minting admin scope", async () => {
const serverPlugins = serverPluginsModule;
const runtime = await createSubagentRuntime(serverPlugins);
serverPlugins.setFallbackGatewayContext(createTestContext("synthetic-delete-session"));
await runtime.deleteSession({
sessionKey: "s-delete",
deleteTranscript: true,
handleGatewayRequest.mockImplementationOnce(async (opts: HandleGatewayRequestOptions) => {
// Re-run the gateway scope check here so the test proves fallback dispatch
// does not smuggle admin into the request client.
const scopes = Array.isArray(opts.client?.connect?.scopes) ? opts.client.connect.scopes : [];
const auth = methodScopesModule.authorizeOperatorScopesForMethod("sessions.delete", scopes);
if (!auth.allowed) {
opts.respond(false, undefined, {
code: "INVALID_REQUEST",
message: `missing scope: ${auth.missingScope}`,
});
return;
}
opts.respond(true, {});
});
await expect(
runtime.deleteSession({
sessionKey: "s-delete",
deleteTranscript: true,
}),
).rejects.toThrow("missing scope: operator.admin");
expect(getLastDispatchedClientScopes()).toEqual(["operator.write"]);
expect(getLastDispatchedClientScopes()).not.toContain("operator.admin");
});
test("allows session deletion when the request scope already has admin", async () => {
const serverPlugins = serverPluginsModule;
const runtime = await createSubagentRuntime(serverPlugins);
const scope = {
context: createTestContext("request-scope-delete-session"),
client: {
connect: {
scopes: ["operator.admin"],
},
} as GatewayRequestOptions["client"],
isWebchatConnect: () => false,
} satisfies PluginRuntimeGatewayRequestScope;
await expect(
gatewayRequestScopeModule.withPluginRuntimeGatewayRequestScope(scope, () =>
runtime.deleteSession({
sessionKey: "s-delete-admin",
deleteTranscript: true,
}),
),
).resolves.toBeUndefined();
expect(getLastDispatchedClientScopes()).toEqual(["operator.admin"]);
});

View File

@@ -367,16 +367,10 @@ export function createGatewaySubagentRuntime(): PluginRuntime["subagent"] {
return getSessionMessages(params);
},
async deleteSession(params) {
await dispatchGatewayMethod(
"sessions.delete",
{
key: params.sessionKey,
deleteTranscript: params.deleteTranscript ?? true,
},
{
syntheticScopes: [ADMIN_SCOPE],
},
);
await dispatchGatewayMethod("sessions.delete", {
key: params.sessionKey,
deleteTranscript: params.deleteTranscript ?? true,
});
},
};
}