diff --git a/workspaces/intelligent-assistant/.changeset/bright-crabs-hide.md b/workspaces/intelligent-assistant/.changeset/bright-crabs-hide.md new file mode 100644 index 00000000000..34cd9c3e84d --- /dev/null +++ b/workspaces/intelligent-assistant/.changeset/bright-crabs-hide.md @@ -0,0 +1,7 @@ +--- +'@red-hat-developer-hub/backstage-plugin-intelligent-assistant-backend': minor +'@red-hat-developer-hub/backstage-plugin-intelligent-assistant-common': minor +'@red-hat-developer-hub/backstage-plugin-intelligent-assistant': minor +--- + +add saved prompts endpoint diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/README.md b/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/README.md index cc5a5ee60e4..29b51c7c074 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/README.md +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/README.md @@ -344,6 +344,9 @@ p, role:default/team_a, intelligent-assistant.notebooks.use, update, allow p, role:default/team_a, intelligent-assistant.mcp.read, read, allow p, role:default/team_a, intelligent-assistant.mcp.manage, update, allow +# Required for saved prompts +p, role:default/team_a, intelligent-assistant.saved-prompts.manage, update, allow + g, user:default/, role:default/team_a ``` diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/__fixtures__/lcsHandlers.ts b/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/__fixtures__/lcsHandlers.ts index 0ba3cdc5ba8..8caa0e9a670 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/__fixtures__/lcsHandlers.ts +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/__fixtures__/lcsHandlers.ts @@ -287,6 +287,51 @@ export const lcsHandlers: HttpHandler[] = [ }); }), + http.get(`${LOCAL_LCS_ADDR}/v1/saved-prompts/config`, () => + HttpResponse.json({ + max_prompts_per_user: 50, + max_display_name_length: 255, + max_content_length: 10000, + }), + ), + + http.get(`${LOCAL_LCS_ADDR}/v1/saved-prompts`, () => + HttpResponse.json({ + prompts: [ + { + id: 'sp-1', + name: 'Explain error', + content: 'Explain this stack trace', + created_at: '2026-07-22T16:00:00+00:00', + updated_at: '2026-07-22T16:00:00+00:00', + }, + ], + }), + ), + + http.post(`${LOCAL_LCS_ADDR}/v1/saved-prompts`, async ({ request }) => { + const body = (await request.json()) as { name: string; content: string }; + return HttpResponse.json( + { + id: 'sp-new', + name: body.name, + content: body.content, + created_at: '2026-07-22T16:00:00+00:00', + updated_at: '2026-07-22T16:00:00+00:00', + }, + { status: 201 }, + ); + }), + + // Conversations/MCP-style delete: HTTP 200 + JSON (not 204) + http.delete(`${LOCAL_LCS_ADDR}/v1/saved-prompts/:prompt_id`, ({ params }) => + HttpResponse.json({ + prompt_id: params.prompt_id, + deleted: true, + response: 'Saved prompt deleted successfully', + }), + ), + // Catch-all handler for unknown paths http.all(`${LOCAL_LCS_ADDR}/*`, ({ request }) => { console.log(`Caught request to unknown path: ${request.url}`); diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/constant.ts b/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/constant.ts index c6f591c4662..3ddd25f868e 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/constant.ts +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/constant.ts @@ -210,7 +210,11 @@ export const HTML_IGNORED_TAGS = new Set(['script', 'style']); export const POLL_INTERVAL_MS = 1000; // 1 second -export const SKIP_USER_ID_ENDPOINTS = new Set(['/v1/models', '/v1/shields']); +export const SKIP_USER_ID_ENDPOINTS = new Set([ + '/v1/models', + '/v1/shields', + '/v1/saved-prompts/config', +]); // default number of message history being loaded export const DEFAULT_HISTORY_LENGTH = 10; diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/router.test.ts b/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/router.test.ts index feef321b117..fae65cda147 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/router.test.ts +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/router.test.ts @@ -558,6 +558,355 @@ describe('intelligent-assistant router tests', () => { }); }); + describe('saved-prompts routes', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('GET /v1/saved-prompts/config', () => { + it('returns config limits without injecting user_id', async () => { + const upstreamUrls: URL[] = []; + server.use( + http.get( + `${LOCAL_LCS_ADDR}/v1/saved-prompts/config`, + ({ request: req }) => { + upstreamUrls.push(new URL(req.url)); + return HttpResponse.json({ + max_prompts_per_user: 50, + max_display_name_length: 255, + max_content_length: 10000, + }); + }, + ), + ); + + const backendServer = await startBackendServer(); + const response = await request(backendServer).get( + '/api/intelligent-assistant/v1/saved-prompts/config', + ); + + expect(response.statusCode).toEqual(200); + expect(response.body).toEqual({ + max_prompts_per_user: 50, + max_display_name_length: 255, + max_content_length: 10000, + }); + expect(upstreamUrls).toHaveLength(1); + expect(upstreamUrls[0].searchParams.get('user_id')).toBeNull(); + }); + + it('returns 403 when permission is denied', async () => { + const backendServer = await startBackendServer( + {}, + AuthorizeResult.DENY, + ); + const response = await request(backendServer).get( + '/api/intelligent-assistant/v1/saved-prompts/config', + ); + expect(response.statusCode).toEqual(403); + }); + }); + + describe('GET /v1/saved-prompts', () => { + it('lists saved prompts and injects user_id', async () => { + const upstreamUrls: URL[] = []; + server.use( + http.get(`${LOCAL_LCS_ADDR}/v1/saved-prompts`, ({ request: req }) => { + upstreamUrls.push(new URL(req.url)); + return HttpResponse.json({ + prompts: [ + { + id: 'sp-1', + name: 'Explain error', + content: 'Explain this stack trace', + created_at: '2026-07-22T16:00:00+00:00', + updated_at: '2026-07-22T16:00:00+00:00', + }, + ], + }); + }), + ); + + const backendServer = await startBackendServer(); + const response = await request(backendServer).get( + '/api/intelligent-assistant/v1/saved-prompts', + ); + + expect(response.statusCode).toEqual(200); + expect(response.body.prompts).toHaveLength(1); + expect(response.body.prompts[0].id).toEqual('sp-1'); + expect(upstreamUrls).toHaveLength(1); + expect(upstreamUrls[0].searchParams.get('user_id')).toEqual(mockUserId); + }); + + it('returns 403 when permission is denied', async () => { + const backendServer = await startBackendServer( + {}, + AuthorizeResult.DENY, + ); + const response = await request(backendServer).get( + '/api/intelligent-assistant/v1/saved-prompts', + ); + expect(response.statusCode).toEqual(403); + }); + }); + + describe('POST /v1/saved-prompts', () => { + it('creates a saved prompt and injects user_id', async () => { + const upstreamUrls: URL[] = []; + server.use( + http.post( + `${LOCAL_LCS_ADDR}/v1/saved-prompts`, + async ({ request: req }) => { + upstreamUrls.push(new URL(req.url)); + const body = (await req.json()) as { + name: string; + content: string; + }; + return HttpResponse.json( + { + id: 'sp-new', + name: body.name, + content: body.content, + created_at: '2026-07-22T16:00:00+00:00', + updated_at: '2026-07-22T16:00:00+00:00', + }, + { status: 201 }, + ); + }, + ), + ); + + const backendServer = await startBackendServer(); + const response = await request(backendServer) + .post('/api/intelligent-assistant/v1/saved-prompts') + .send({ name: 'Deploy', content: 'Help me deploy' }); + + expect(response.statusCode).toEqual(201); + expect(response.body).toEqual({ + id: 'sp-new', + name: 'Deploy', + content: 'Help me deploy', + created_at: '2026-07-22T16:00:00+00:00', + updated_at: '2026-07-22T16:00:00+00:00', + }); + expect(upstreamUrls).toHaveLength(1); + expect(upstreamUrls[0].searchParams.get('user_id')).toEqual(mockUserId); + }); + + it('returns 403 when permission is denied', async () => { + const backendServer = await startBackendServer( + {}, + AuthorizeResult.DENY, + ); + const response = await request(backendServer) + .post('/api/intelligent-assistant/v1/saved-prompts') + .send({ name: 'Deploy', content: 'Help me deploy' }); + expect(response.statusCode).toEqual(403); + }); + + it('relays Core 422 with sanitized error', async () => { + server.use( + http.post(`${LOCAL_LCS_ADDR}/v1/saved-prompts`, () => { + return new HttpResponse( + JSON.stringify({ + detail: { + response: 'Invalid attribute value', + cause: 'name exceeds max_display_name_length', + }, + }), + { + status: 422, + headers: { 'Content-Type': 'application/json' }, + }, + ); + }), + ); + + const backendServer = await startBackendServer(); + const response = await request(backendServer) + .post('/api/intelligent-assistant/v1/saved-prompts') + .send({ name: 'x'.repeat(300), content: 'Help me deploy' }); + + expect(response.statusCode).toEqual(422); + expect(response.body.error).toContain( + 'Error from lightspeed-core server', + ); + expect(response.body.error).not.toContain('max_display_name_length'); + }); + + it('relays Core 409 with sanitized error', async () => { + server.use( + http.post(`${LOCAL_LCS_ADDR}/v1/saved-prompts`, () => { + return new HttpResponse( + JSON.stringify({ + detail: { + response: 'Saved prompt already exists', + cause: 'duplicate name Deploy', + }, + }), + { + status: 409, + headers: { 'Content-Type': 'application/json' }, + }, + ); + }), + ); + + const backendServer = await startBackendServer(); + const response = await request(backendServer) + .post('/api/intelligent-assistant/v1/saved-prompts') + .send({ name: 'Deploy', content: 'Help me deploy' }); + + expect(response.statusCode).toEqual(409); + expect(response.body.error).toContain( + 'Error from lightspeed-core server', + ); + expect(response.body.error).not.toContain('duplicate name'); + }); + + it('returns 500 when upstream fetch throws', async () => { + server.use( + http.post(`${LOCAL_LCS_ADDR}/v1/saved-prompts`, () => { + return HttpResponse.error(); + }), + ); + + const backendServer = await startBackendServer(); + const response = await request(backendServer) + .post('/api/intelligent-assistant/v1/saved-prompts') + .send({ name: 'Deploy', content: 'Help me deploy' }); + + expect(response.statusCode).toEqual(500); + expect(response.body.error).toContain( + 'Error while creating saved prompt', + ); + }); + }); + + describe('DELETE /v1/saved-prompts/:prompt_id', () => { + it('deletes a saved prompt with 200 JSON body and injects user_id', async () => { + const upstreamUrls: URL[] = []; + server.use( + http.delete( + `${LOCAL_LCS_ADDR}/v1/saved-prompts/:prompt_id`, + ({ request: req, params }) => { + upstreamUrls.push(new URL(req.url)); + return HttpResponse.json({ + prompt_id: params.prompt_id, + deleted: true, + response: 'Saved prompt deleted successfully', + }); + }, + ), + ); + + const backendServer = await startBackendServer(); + const response = await request(backendServer).delete( + '/api/intelligent-assistant/v1/saved-prompts/sp-1', + ); + + expect(response.statusCode).toEqual(200); + expect(response.body).toEqual({ + prompt_id: 'sp-1', + deleted: true, + response: 'Saved prompt deleted successfully', + }); + expect(upstreamUrls).toHaveLength(1); + expect(upstreamUrls[0].searchParams.get('user_id')).toEqual(mockUserId); + }); + + it('returns 403 when permission is denied', async () => { + const backendServer = await startBackendServer( + {}, + AuthorizeResult.DENY, + ); + const response = await request(backendServer).delete( + '/api/intelligent-assistant/v1/saved-prompts/sp-1', + ); + expect(response.statusCode).toEqual(403); + }); + + it('relays Core 403 for non-owned prompt', async () => { + server.use( + http.delete(`${LOCAL_LCS_ADDR}/v1/saved-prompts/:prompt_id`, () => { + return new HttpResponse( + JSON.stringify({ + detail: { + response: 'Forbidden', + cause: 'saved prompt owned by another user', + }, + }), + { + status: 403, + headers: { 'Content-Type': 'application/json' }, + }, + ); + }), + ); + + const backendServer = await startBackendServer(); + const response = await request(backendServer).delete( + '/api/intelligent-assistant/v1/saved-prompts/sp-other', + ); + + expect(response.statusCode).toEqual(403); + }); + + it('relays Core missing prompt as 200 with deleted false', async () => { + server.use( + http.delete( + `${LOCAL_LCS_ADDR}/v1/saved-prompts/:prompt_id`, + ({ params }) => + HttpResponse.json({ + prompt_id: params.prompt_id, + deleted: false, + response: 'Saved prompt not found', + }), + ), + ); + + const backendServer = await startBackendServer(); + const response = await request(backendServer).delete( + '/api/intelligent-assistant/v1/saved-prompts/sp-missing', + ); + + expect(response.statusCode).toEqual(200); + expect(response.body).toEqual({ + prompt_id: 'sp-missing', + deleted: false, + response: 'Saved prompt not found', + }); + }); + + it('relays Core 400 for invalid prompt id', async () => { + server.use( + http.delete(`${LOCAL_LCS_ADDR}/v1/saved-prompts/:prompt_id`, () => { + return new HttpResponse( + JSON.stringify({ + detail: { + response: 'Bad request', + cause: 'invalid saved prompt id', + }, + }), + { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }, + ); + }), + ); + + const backendServer = await startBackendServer(); + const response = await request(backendServer).delete( + '/api/intelligent-assistant/v1/saved-prompts/not-a-valid-id', + ); + + expect(response.statusCode).toEqual(400); + }); + }); + }); + describe('POST /v1/query', () => { afterEach(() => { jest.clearAllMocks(); diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/router.ts b/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/router.ts index 2a296f29a83..49fe5ff63a7 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/router.ts +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/router.ts @@ -35,6 +35,7 @@ import { lightspeedMcpManagePermission, lightspeedMcpReadPermission, lightspeedPermissions, + lightspeedSavedPromptsManagePermission, } from '@red-hat-developer-hub/backstage-plugin-intelligent-assistant-common'; import { Readable } from 'node:stream'; @@ -635,6 +636,25 @@ export async function createRouter( apiProxy, ); + router.get( + '/v1/saved-prompts/config', + generalRateLimiter, + requirePermission(lightspeedSavedPromptsManagePermission), + apiProxy, // SKIP_USER_ID_ENDPOINTS prevents user_id injection for this endpoint + ); + router.get( + '/v1/saved-prompts', + generalRateLimiter, + requirePermission(lightspeedSavedPromptsManagePermission), + apiProxy, + ); + router.delete( + '/v1/saved-prompts/:prompt_id', + generalRateLimiter, + requirePermission(lightspeedSavedPromptsManagePermission), + apiProxy, + ); + router.post( '/v1/feedback', generalRateLimiter, @@ -678,6 +698,51 @@ export async function createRouter( }, ); + router.post( + '/v1/saved-prompts', + generalRateLimiter, + requirePermission(lightspeedSavedPromptsManagePermission), + async (request, response) => { + try { + const { userEntityRef } = getIdentity(request); + + logger.info( + `/v1/saved-prompts receives call from user: ${userEntityRef}`, + ); + + const userQueryParam = `user_id=${encodeURIComponent(userEntityRef)}`; + const requestBody = JSON.stringify(request.body); + const fetchResponse = await fetch( + `${lcsBaseUrl}/v1/saved-prompts?${userQueryParam}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: requestBody, + }, + ); + + if (!fetchResponse.ok) { + await handleLCSFetchError( + fetchResponse, + logger, + 'creating saved prompt', + response, + ); + return; + } + + const data = await fetchResponse.json(); + response.status(fetchResponse.status).json(data); + } catch (error) { + const errormsg = `Error while creating saved prompt: ${error}`; + logger.error(errormsg); + response.status(500).json({ error: errormsg }); + } + }, + ); + router.post( '/v1/query/interrupt', generalRateLimiter, diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/utils.test.ts b/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/utils.test.ts index 58c85b74055..8489a8bb9f2 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/utils.test.ts +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/utils.test.ts @@ -175,13 +175,21 @@ describe('handleLCSFetchError', () => { const userRef = 'user:default/test-user'; describe('rewriteProxyPath', () => { - it.each(['/v1/models', '/v1/shields'])( + it.each(['/v1/models', '/v1/shields', '/v1/saved-prompts/config'])( 'returns path unchanged for skip endpoint %s', path => { expect(rewriteProxyPath(path, userRef)).toBe(path); }, ); + it.each(['/v1/saved-prompts', '/v1/saved-prompts/abc'])( + 'appends user_id for saved-prompts path %s', + path => { + const result = rewriteProxyPath(path, userRef); + expect(result).toBe(`${path}?user_id=${encodeURIComponent(userRef)}`); + }, + ); + it('appends user_id to a path without query params', () => { const result = rewriteProxyPath('/v2/conversations', userRef); expect(result).toBe( diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant-common/report.api.md b/workspaces/intelligent-assistant/plugins/intelligent-assistant-common/report.api.md index e1ff9cbd3c7..4de493cd665 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant-common/report.api.md +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant-common/report.api.md @@ -28,4 +28,41 @@ export const lightspeedNotebooksUsePermission: BasicPermission; // @public export const lightspeedPermissions: BasicPermission[]; + +// @public +export const lightspeedSavedPromptsManagePermission: BasicPermission; + +// @public +export interface SavedPrompt { + content: string; + created_at: string; + id: string; + name: string; + updated_at: string; +} + +// @public +export interface SavedPromptCreateRequest { + content: string; + name: string; +} + +// @public +export interface SavedPromptDeleteResponse { + deleted: boolean; + prompt_id: string; + response: string; +} + +// @public +export interface SavedPromptsConfig { + max_content_length: number; + max_display_name_length: number; + max_prompts_per_user: number; +} + +// @public +export interface SavedPromptsListResponse { + prompts: SavedPrompt[]; +} ``` diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant-common/src/index.ts b/workspaces/intelligent-assistant/plugins/intelligent-assistant-common/src/index.ts index 54e5c3e8756..7ad0200acfc 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant-common/src/index.ts +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant-common/src/index.ts @@ -21,3 +21,4 @@ */ export * from './permissions'; +export * from './savedPrompts'; diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant-common/src/permissions.ts b/workspaces/intelligent-assistant/plugins/intelligent-assistant-common/src/permissions.ts index 6bbf3dc053d..0d202156efe 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant-common/src/permissions.ts +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant-common/src/permissions.ts @@ -86,6 +86,16 @@ export const lightspeedNotebooksUsePermission = createPermission({ }, }); +/** This permission is used to list, create, and delete saved prompts and read saved-prompts config + * @public + */ +export const lightspeedSavedPromptsManagePermission = createPermission({ + name: 'intelligent-assistant.saved-prompts.manage', + attributes: { + action: 'update', + }, +}); + /** * List of all permissions on permission polices. * @@ -99,4 +109,5 @@ export const lightspeedPermissions = [ lightspeedMcpReadPermission, lightspeedMcpManagePermission, lightspeedNotebooksUsePermission, + lightspeedSavedPromptsManagePermission, ]; diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant-common/src/savedPrompts.ts b/workspaces/intelligent-assistant/plugins/intelligent-assistant-common/src/savedPrompts.ts new file mode 100644 index 00000000000..d3fefb44674 --- /dev/null +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant-common/src/savedPrompts.ts @@ -0,0 +1,83 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Saved prompts configuration limits from LCORE GET /v1/saved-prompts/config. + * + * @public + */ +export interface SavedPromptsConfig { + /** Maximum number of saved prompts allowed per user */ + max_prompts_per_user: number; + /** Maximum character length for prompt display name */ + max_display_name_length: number; + /** Maximum character length for prompt content body */ + max_content_length: number; +} + +/** + * A single saved prompt returned by LCORE. + * + * @public + */ +export interface SavedPrompt { + /** Unique identifier of the saved prompt */ + id: string; + /** Display name of the saved prompt */ + name: string; + /** Prompt body text */ + content: string; + /** Creation timestamp as an ISO datetime string from LCORE */ + created_at: string; + /** Last-update timestamp as an ISO datetime string from LCORE */ + updated_at: string; +} + +/** + * Response body for GET /v1/saved-prompts. + * + * @public + */ +export interface SavedPromptsListResponse { + /** Saved prompts for the authenticated user, newest first */ + prompts: SavedPrompt[]; +} + +/** + * Request body for POST /v1/saved-prompts. + * + * @public + */ +export interface SavedPromptCreateRequest { + /** Display name of the saved prompt */ + name: string; + /** Prompt body text */ + content: string; +} + +/** + * Response body for DELETE /v1/saved-prompts/:prompt_id (HTTP 200). + * + * @public + */ +export interface SavedPromptDeleteResponse { + /** Saved prompt identifier that was passed to delete */ + prompt_id: string; + /** Whether the prompt was deleted successfully */ + deleted: boolean; + /** Human-readable outcome of the delete operation */ + response: string; +} diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant/README.md b/workspaces/intelligent-assistant/plugins/intelligent-assistant/README.md index 9dd506a9e3e..660b9aa2355 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant/README.md +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant/README.md @@ -49,6 +49,9 @@ p, role:default/team_a, intelligent-assistant.notebooks.use, update, allow p, role:default/team_a, intelligent-assistant.mcp.read, read, allow p, role:default/team_a, intelligent-assistant.mcp.manage, update, allow +# Required for saved prompts +p, role:default/team_a, intelligent-assistant.saved-prompts.manage, update, allow + g, user:default/, role:default/team_a ```