@@ -3685,3 +3685,116 @@ func TestMCPServerToolInvocation(t *testing.T) {
36853685 require .True (t , foundToolMessage ,
36863686 "MCP tool result should be persisted as a tool message in the database" )
36873687}
3688+
3689+ func TestChatTemplateAllowlistEnforcement (t * testing.T ) {
3690+ t .Parallel ()
3691+
3692+ ctx := testutil .Context (t , testutil .WaitLong )
3693+ db , ps := dbtestutil .NewDB (t )
3694+
3695+ // Set up a mock OpenAI server. The first streaming call triggers
3696+ // list_templates; subsequent calls respond with text.
3697+ var callCount atomic.Int32
3698+ openAIURL := chattest .NewOpenAI (t , func (req * chattest.OpenAIRequest ) chattest.OpenAIResponse {
3699+ if ! req .Stream {
3700+ return chattest .OpenAINonStreamingResponse ("title" )
3701+ }
3702+ if callCount .Add (1 ) == 1 {
3703+ return chattest .OpenAIStreamingResponse (
3704+ chattest .OpenAIToolCallChunk ("list_templates" , `{}` ),
3705+ )
3706+ }
3707+ return chattest .OpenAIStreamingResponse (
3708+ chattest .OpenAITextChunks ("Here are the templates." )... ,
3709+ )
3710+ })
3711+
3712+ user , model := seedChatDependenciesWithProvider (ctx , t , db , "openai-compat" , openAIURL )
3713+
3714+ // Create two templates the user can see.
3715+ org := dbgen .Organization (t , db , database.Organization {})
3716+ _ = dbgen .OrganizationMember (t , db , database.OrganizationMember {
3717+ UserID : user .ID ,
3718+ OrganizationID : org .ID ,
3719+ })
3720+ tplAllowed := dbgen .Template (t , db , database.Template {
3721+ OrganizationID : org .ID ,
3722+ CreatedBy : user .ID ,
3723+ Name : "allowed-template" ,
3724+ })
3725+ tplBlocked := dbgen .Template (t , db , database.Template {
3726+ OrganizationID : org .ID ,
3727+ CreatedBy : user .ID ,
3728+ Name : "blocked-template" ,
3729+ })
3730+
3731+ // Set the allowlist to only tplAllowed.
3732+ allowlistJSON , err := json .Marshal ([]string {tplAllowed .ID .String ()})
3733+ require .NoError (t , err )
3734+ err = db .UpsertChatTemplateAllowlist (dbauthz .AsSystemRestricted (ctx ), string (allowlistJSON ))
3735+ require .NoError (t , err )
3736+
3737+ server := newActiveTestServer (t , db , ps )
3738+
3739+ chat , err := server .CreateChat (ctx , chatd.CreateOptions {
3740+ OwnerID : user .ID ,
3741+ Title : "allowlist-test" ,
3742+ ModelConfigID : model .ID ,
3743+ InitialUserContent : []codersdk.ChatMessagePart {
3744+ codersdk .ChatMessageText ("List templates" ),
3745+ },
3746+ })
3747+ require .NoError (t , err )
3748+
3749+ // Wait for the chat to finish processing.
3750+ var chatResult database.Chat
3751+ require .Eventually (t , func () bool {
3752+ got , getErr := db .GetChatByID (ctx , chat .ID )
3753+ if getErr != nil {
3754+ return false
3755+ }
3756+ chatResult = got
3757+ return got .Status == database .ChatStatusWaiting || got .Status == database .ChatStatusError
3758+ }, testutil .WaitLong , testutil .IntervalFast )
3759+
3760+ if chatResult .Status == database .ChatStatusError {
3761+ require .FailNowf (t , "chat run failed" , "last_error=%q" , chatResult .LastError .String )
3762+ }
3763+
3764+ // Find the list_templates tool result in the persisted messages.
3765+ var toolResult string
3766+ testutil .Eventually (ctx , t , func (ctx context.Context ) bool {
3767+ messages , dbErr := db .GetChatMessagesByChatID (ctx , database.GetChatMessagesByChatIDParams {
3768+ ChatID : chat .ID ,
3769+ AfterID : 0 ,
3770+ })
3771+ if dbErr != nil {
3772+ return false
3773+ }
3774+ for _ , msg := range messages {
3775+ if msg .Role != database .ChatMessageRoleTool {
3776+ continue
3777+ }
3778+ parts , parseErr := chatprompt .ParseContent (msg )
3779+ if parseErr != nil {
3780+ continue
3781+ }
3782+ for _ , part := range parts {
3783+ if part .Type == codersdk .ChatMessagePartTypeToolResult &&
3784+ part .ToolName == "list_templates" {
3785+ toolResult = string (part .Result )
3786+ return true
3787+ }
3788+ }
3789+ }
3790+ return false
3791+ }, testutil .IntervalFast )
3792+
3793+ require .NotEmpty (t , toolResult , "list_templates tool result should be persisted" )
3794+
3795+ // The result should contain only the allowed template.
3796+ require .Contains (t , toolResult , tplAllowed .ID .String (),
3797+ "allowed template should appear in list_templates result" )
3798+ require .NotContains (t , toolResult , tplBlocked .ID .String (),
3799+ "blocked template should NOT appear in list_templates result" )
3800+ }
0 commit comments