mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
Relates to CODAGT-713 Depends on #27284 This makes the per-template `agents_allowed` field authoritative in the API and chatd. It adds optional create and metadata update fields with the intended default and omission semantics, supports `agents-allowed:` template search, includes the value in telemetry, and makes `list_templates`, `read_template`, and `create_workspace` read the template row directly. Existing-workspace retries remain idempotent, and blocked same-organisation templates return an actionable message. The experimental `/template-allowlist` routes remain temporarily because the shipped AI Settings page still calls them, but they no longer control chatd enforcement. #27514 moves that page to per-template metadata, #27515 removes the legacy storage, routes, SDK types, and utility, #27517 adds the CLI flags, and #27518 updates the platform controls documentation for the per-template model, directly addressing CRF-5 and CRF-6. The stack is intended to merge as a unit.
170 lines
4.6 KiB
Go
170 lines
4.6 KiB
Go
package chattool
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"unicode/utf8"
|
|
|
|
"charm.land/fantasy"
|
|
"github.com/google/uuid"
|
|
"golang.org/x/xerrors"
|
|
|
|
"cdr.dev/slog/v3"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
)
|
|
|
|
const templateNotAvailableMessage = "template not available for chat workspaces; use list_templates to find allowed templates"
|
|
|
|
func marshalToolResponse(result any) fantasy.ToolResponse {
|
|
data, err := json.Marshal(result)
|
|
if err != nil {
|
|
return fantasy.NewTextResponse("{}")
|
|
}
|
|
return fantasy.NewTextResponse(string(data))
|
|
}
|
|
|
|
// toolResponse builds a fantasy.ToolResponse from a JSON-serializable
|
|
// result map. The map constraint ensures all tool results serialize
|
|
// to JSON objects so the frontend can safely parse them.
|
|
func toolResponse(result map[string]any) fantasy.ToolResponse {
|
|
return marshalToolResponse(result)
|
|
}
|
|
|
|
// buildToolResponse marshals a buildErrorResult into a tool response.
|
|
// Separate from toolResponse to keep the map[string]any constraint
|
|
// on the general helper while allowing typed error structs.
|
|
func buildToolResponse(r buildErrorResult) fantasy.ToolResponse {
|
|
return marshalToolResponse(r)
|
|
}
|
|
|
|
// responseErrorResult converts a codersdk.Response into a structured
|
|
// tool result. We return these via toolResponse rather than
|
|
// NewTextErrorResponse because the fantasy/chatprompt pipeline flattens
|
|
// IsError content into a single string and drops validation details.
|
|
func responseErrorResult(resp codersdk.Response) map[string]any {
|
|
message := resp.Message
|
|
if message == "" {
|
|
message = "request failed"
|
|
}
|
|
|
|
result := map[string]any{
|
|
"error": message,
|
|
}
|
|
if resp.Detail != "" {
|
|
result["detail"] = resp.Detail
|
|
}
|
|
if len(resp.Validations) > 0 {
|
|
result["validations"] = resp.Validations
|
|
}
|
|
return result
|
|
}
|
|
|
|
func latestWorkspaceBuildAndJob(
|
|
ctx context.Context,
|
|
db database.Store,
|
|
workspaceID uuid.UUID,
|
|
) (database.WorkspaceBuild, database.ProvisionerJob, error) {
|
|
build, err := db.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspaceID)
|
|
if err != nil {
|
|
return database.WorkspaceBuild{}, database.ProvisionerJob{}, xerrors.Errorf("get latest build: %w", err)
|
|
}
|
|
|
|
job, err := db.GetProvisionerJobByID(ctx, build.JobID)
|
|
if err != nil {
|
|
return database.WorkspaceBuild{}, database.ProvisionerJob{}, xerrors.Errorf("get provisioner job: %w", err)
|
|
}
|
|
return build, job, nil
|
|
}
|
|
|
|
func publishBuildBinding(
|
|
ctx context.Context,
|
|
db database.Store,
|
|
logger slog.Logger,
|
|
chatID uuid.UUID,
|
|
workspaceID uuid.UUID,
|
|
buildID uuid.UUID,
|
|
onChatUpdated func(database.Chat),
|
|
) {
|
|
updatedChat, bindErr := db.UpdateChatWorkspaceBinding(ctx, database.UpdateChatWorkspaceBindingParams{
|
|
ID: chatID,
|
|
WorkspaceID: uuid.NullUUID{UUID: workspaceID, Valid: true},
|
|
BuildID: uuid.NullUUID{
|
|
UUID: buildID,
|
|
Valid: buildID != uuid.Nil,
|
|
},
|
|
AgentID: uuid.NullUUID{},
|
|
})
|
|
if bindErr != nil {
|
|
logger.Error(ctx, "failed to persist build ID on chat binding",
|
|
slog.F("chat_id", chatID),
|
|
slog.F("build_id", buildID),
|
|
slog.Error(bindErr),
|
|
)
|
|
return
|
|
}
|
|
if onChatUpdated != nil {
|
|
onChatUpdated(updatedChat)
|
|
}
|
|
}
|
|
|
|
func provisionerJobTerminal(status database.ProvisionerJobStatus) bool {
|
|
switch status {
|
|
case database.ProvisionerJobStatusSucceeded,
|
|
database.ProvisionerJobStatusFailed,
|
|
database.ProvisionerJobStatusCanceled:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func truncateRunes(value string, maxLen int) string {
|
|
if maxLen <= 0 || value == "" {
|
|
return ""
|
|
}
|
|
if utf8.RuneCountInString(value) <= maxLen {
|
|
return value
|
|
}
|
|
|
|
runes := []rune(value)
|
|
if maxLen > len(runes) {
|
|
maxLen = len(runes)
|
|
}
|
|
return string(runes[:maxLen])
|
|
}
|
|
|
|
// buildErrorResult is a structured error response that preserves
|
|
// the build ID alongside the error message. This lets the frontend
|
|
// keep showing build logs when a build fails instead of losing
|
|
// them on the error transition.
|
|
type buildErrorResult struct {
|
|
Error string `json:"error"`
|
|
BuildID string `json:"build_id,omitempty"`
|
|
}
|
|
|
|
func newBuildError(msg string, buildID uuid.UUID) buildErrorResult {
|
|
r := buildErrorResult{Error: msg}
|
|
if buildID != uuid.Nil {
|
|
r.BuildID = buildID.String()
|
|
}
|
|
return r
|
|
}
|
|
|
|
// setBuildID adds the build_id field to a tool response map when
|
|
// the build ID is known (non-zero).
|
|
func setBuildID(result map[string]any, buildID uuid.UUID) {
|
|
if buildID != uuid.Nil {
|
|
result["build_id"] = buildID.String()
|
|
}
|
|
}
|
|
|
|
// setNoBuild marks the response with no_build: true when no build
|
|
// was triggered. The frontend uses this flag to suppress the
|
|
// build-log section for already-running workspaces.
|
|
func setNoBuild(result map[string]any, buildID uuid.UUID) {
|
|
if buildID == uuid.Nil {
|
|
result["no_build"] = true
|
|
}
|
|
}
|