mirror of
https://github.com/coder/coder.git
synced 2026-10-29 17:44:33 +08:00
feat: add deployment-wide template allowlist for chats (#23262)
- Stores a deployment-wide agents template allowlist in `site_configs` (`agents_template_allowlist`) - Adds `GET/PUT /api/experimental/chats/config/template-allowlist` endpoints - Filters `list_templates`, `read_template`, and `create_workspace` chat tools by allowlist, if defined (empty=all allowed) - Add "Templates" admin settings tab in Agents UI ([what it looks like](https://624de63c6aacee003aa84340-sitjilsyrr.chromatic.com/?path=/story/pages-agentspage-agentsettingspageview--template-allowlist)) > 🤖 This PR was created with the help of Coder Agents, and has been reviewed by my human. 🧑💻
This commit is contained in:
+50
-4
@@ -3371,6 +3371,45 @@ func (p *Server) runChat(
|
||||
GetWorkspaceConn: workspaceCtx.getWorkspaceConn,
|
||||
}),
|
||||
}
|
||||
// getAllowedTemplateIDs returns the current deployment-wide
|
||||
// template allowlist, re-reading from the database on each call
|
||||
// so that admin changes take effect without restarting the chat.
|
||||
// Returns nil (= all allowed) on errors to fail open.
|
||||
getAllowedTemplateIDs := func() map[uuid.UUID]bool {
|
||||
raw, err := p.db.GetChatTemplateAllowlist(ctx)
|
||||
if err != nil {
|
||||
p.logger.Error(ctx, "failed to load template allowlist, all templates will be allowed", slog.Error(err))
|
||||
return nil
|
||||
}
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
var ids []string
|
||||
if jsonErr := json.Unmarshal([]byte(raw), &ids); jsonErr != nil {
|
||||
// Note: the API endpoint (GET /template-allowlist) returns
|
||||
// HTTP 500 for corrupt JSON, giving admins visibility into
|
||||
// the problem. The runtime path here deliberately fails open
|
||||
// so that a corrupt allowlist doesn't block all chats.
|
||||
p.logger.Error(ctx, "failed to parse template allowlist JSON, all templates will be allowed",
|
||||
slog.F("raw", raw), slog.Error(jsonErr))
|
||||
return nil
|
||||
}
|
||||
allowlist := make(map[uuid.UUID]bool, len(ids))
|
||||
for _, s := range ids {
|
||||
if id, parseErr := uuid.Parse(s); parseErr == nil {
|
||||
allowlist[id] = true
|
||||
} else {
|
||||
p.logger.Warn(ctx, "ignoring invalid UUID in template allowlist",
|
||||
slog.F("value", s), slog.Error(parseErr))
|
||||
}
|
||||
}
|
||||
if len(ids) > 0 && len(allowlist) == 0 {
|
||||
p.logger.Error(ctx, "all UUIDs in template allowlist were invalid, all templates will be allowed",
|
||||
slog.F("count", len(ids)))
|
||||
return nil
|
||||
}
|
||||
return allowlist
|
||||
}
|
||||
// Only root chats (not delegated subagents) get workspace
|
||||
// provisioning and subagent tools. Child agents must not
|
||||
// create workspaces or spawn further subagents — they should
|
||||
@@ -3379,12 +3418,14 @@ func (p *Server) runChat(
|
||||
// Workspace provisioning tools.
|
||||
tools = append(tools,
|
||||
chattool.ListTemplates(chattool.ListTemplatesOptions{
|
||||
DB: p.db,
|
||||
OwnerID: chat.OwnerID,
|
||||
DB: p.db,
|
||||
OwnerID: chat.OwnerID,
|
||||
AllowedTemplateIDs: getAllowedTemplateIDs,
|
||||
}),
|
||||
chattool.ReadTemplate(chattool.ReadTemplateOptions{
|
||||
DB: p.db,
|
||||
OwnerID: chat.OwnerID,
|
||||
DB: p.db,
|
||||
OwnerID: chat.OwnerID,
|
||||
AllowedTemplateIDs: getAllowedTemplateIDs,
|
||||
}),
|
||||
chattool.CreateWorkspace(chattool.CreateWorkspaceOptions{
|
||||
DB: p.db,
|
||||
@@ -3395,7 +3436,12 @@ func (p *Server) runChat(
|
||||
AgentInactiveDisconnectTimeout: p.agentInactiveDisconnectTimeout,
|
||||
WorkspaceMu: &workspaceMu,
|
||||
Logger: p.logger,
|
||||
AllowedTemplateIDs: getAllowedTemplateIDs,
|
||||
}),
|
||||
// StartWorkspace intentionally does not enforce the
|
||||
// template allowlist. The allowlist restricts creation
|
||||
// of new workspaces only — existing workspaces can
|
||||
// be restarted regardless of allowlist changes.
|
||||
chattool.StartWorkspace(chattool.StartWorkspaceOptions{
|
||||
DB: p.db,
|
||||
OwnerID: chat.OwnerID,
|
||||
|
||||
Reference in New Issue
Block a user