From 688d2ee3eba510650f69f7164ff9af32bbd03145 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 25 Jun 2025 15:03:32 +0200 Subject: [PATCH 01/10] chore: remove chats experiment (#18535) --- cli/server.go | 18 - cli/testdata/server-config.yaml.golden | 3 - coderd/ai/ai.go | 167 --- coderd/apidoc/docs.go | 592 +------- coderd/apidoc/swagger.json | 552 +------- coderd/chat.go | 366 ----- coderd/chat_test.go | 125 -- coderd/coderd.go | 18 - coderd/database/db2sdk/db2sdk.go | 16 +- coderd/database/dbauthz/dbauthz.go | 42 - coderd/database/dbauthz/dbauthz_test.go | 74 - coderd/database/dbgen/dbgen.go | 24 - coderd/database/dbmem/dbmem.go | 137 -- coderd/database/dbmetrics/querymetrics.go | 49 - coderd/database/dbmock/dbmock.go | 103 -- coderd/database/dump.sql | 40 - coderd/database/foreign_key_constraint.go | 2 - .../migrations/000343_delete_chats.down.sql | 1 + .../migrations/000343_delete_chats.up.sql | 2 + coderd/database/modelmethods.go | 5 - coderd/database/models.go | 17 - coderd/database/querier.go | 7 - coderd/database/queries.sql.go | 201 --- coderd/database/queries/chat.sql | 36 - coderd/database/unique_constraint.go | 2 - coderd/deployment.go | 25 - coderd/httpmw/chat.go | 59 - coderd/httpmw/chat_test.go | 150 -- coderd/rbac/object_gen.go | 11 - coderd/rbac/policy/policy.go | 8 - coderd/rbac/roles.go | 2 - coderd/rbac/roles_test.go | 31 - codersdk/chat.go | 153 --- codersdk/deployment.go | 38 - codersdk/rbacresources_gen.go | 2 - docs/reference/api/chat.md | 372 ----- docs/reference/api/general.md | 50 - docs/reference/api/members.md | 5 - docs/reference/api/schemas.md | 583 +------- go.mod | 6 +- site/package.json | 3 - site/pnpm-lock.yaml | 216 --- site/src/api/api.ts | 24 - site/src/api/queries/chats.ts | 25 - site/src/api/queries/deployment.ts | 7 - site/src/api/rbacresourcesGenerated.ts | 6 - site/src/api/typesGenerated.ts | 47 - site/src/contexts/useAgenticChat.ts | 16 - .../modules/dashboard/Navbar/NavbarView.tsx | 12 - site/src/pages/ChatPage/ChatLanding.tsx | 164 --- site/src/pages/ChatPage/ChatLayout.tsx | 242 ---- site/src/pages/ChatPage/ChatMessages.tsx | 491 ------- .../ChatPage/ChatToolInvocation.stories.tsx | 1213 ----------------- .../src/pages/ChatPage/ChatToolInvocation.tsx | 880 ------------ .../pages/ChatPage/LanguageModelSelector.tsx | 73 - site/src/router.tsx | 8 - 56 files changed, 16 insertions(+), 7505 deletions(-) delete mode 100644 coderd/ai/ai.go delete mode 100644 coderd/chat.go delete mode 100644 coderd/chat_test.go create mode 100644 coderd/database/migrations/000343_delete_chats.down.sql create mode 100644 coderd/database/migrations/000343_delete_chats.up.sql delete mode 100644 coderd/database/queries/chat.sql delete mode 100644 coderd/httpmw/chat.go delete mode 100644 coderd/httpmw/chat_test.go delete mode 100644 codersdk/chat.go delete mode 100644 docs/reference/api/chat.md delete mode 100644 site/src/api/queries/chats.ts delete mode 100644 site/src/contexts/useAgenticChat.ts delete mode 100644 site/src/pages/ChatPage/ChatLanding.tsx delete mode 100644 site/src/pages/ChatPage/ChatLayout.tsx delete mode 100644 site/src/pages/ChatPage/ChatMessages.tsx delete mode 100644 site/src/pages/ChatPage/ChatToolInvocation.stories.tsx delete mode 100644 site/src/pages/ChatPage/ChatToolInvocation.tsx delete mode 100644 site/src/pages/ChatPage/LanguageModelSelector.tsx diff --git a/cli/server.go b/cli/server.go index 9af9965210d72..bccaefd9b87bb 100644 --- a/cli/server.go +++ b/cli/server.go @@ -61,7 +61,6 @@ import ( "github.com/coder/serpent" "github.com/coder/wgtunnel/tunnelsdk" - "github.com/coder/coder/v2/coderd/ai" "github.com/coder/coder/v2/coderd/entitlements" "github.com/coder/coder/v2/coderd/notifications/reports" "github.com/coder/coder/v2/coderd/runtimeconfig" @@ -611,22 +610,6 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. ) } - aiProviders, err := ReadAIProvidersFromEnv(os.Environ()) - if err != nil { - return xerrors.Errorf("read ai providers from env: %w", err) - } - vals.AI.Value.Providers = append(vals.AI.Value.Providers, aiProviders...) - for _, provider := range aiProviders { - logger.Debug( - ctx, "loaded ai provider", - slog.F("type", provider.Type), - ) - } - languageModels, err := ai.ModelsFromConfig(ctx, vals.AI.Value.Providers) - if err != nil { - return xerrors.Errorf("create language models: %w", err) - } - realIPConfig, err := httpmw.ParseRealIPConfig(vals.ProxyTrustedHeaders, vals.ProxyTrustedOrigins) if err != nil { return xerrors.Errorf("parse real ip config: %w", err) @@ -657,7 +640,6 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. CacheDir: cacheDir, GoogleTokenValidator: googleTokenValidator, ExternalAuthConfigs: externalAuthConfigs, - LanguageModels: languageModels, RealIPConfig: realIPConfig, SSHKeygenAlgorithm: sshKeygenAlgorithm, TracerProvider: tracerProvider, diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index 8befccf3e320d..0e4cfa71a2fc6 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -526,9 +526,6 @@ client: # Support links to display in the top right drop down menu. # (default: , type: struct[[]codersdk.LinkConfig]) supportLinks: [] -# Configure AI providers. -# (default: , type: struct[codersdk.AIConfig]) -ai: {} # External Authentication providers. # (default: , type: struct[[]codersdk.ExternalAuthConfig]) externalAuthProviders: [] diff --git a/coderd/ai/ai.go b/coderd/ai/ai.go deleted file mode 100644 index 97c825ae44c06..0000000000000 --- a/coderd/ai/ai.go +++ /dev/null @@ -1,167 +0,0 @@ -package ai - -import ( - "context" - - "github.com/anthropics/anthropic-sdk-go" - anthropicoption "github.com/anthropics/anthropic-sdk-go/option" - "github.com/kylecarbs/aisdk-go" - "github.com/openai/openai-go" - openaioption "github.com/openai/openai-go/option" - "golang.org/x/xerrors" - "google.golang.org/genai" - - "github.com/coder/coder/v2/codersdk" -) - -type LanguageModel struct { - codersdk.LanguageModel - StreamFunc StreamFunc -} - -type StreamOptions struct { - SystemPrompt string - Model string - Messages []aisdk.Message - Thinking bool - Tools []aisdk.Tool -} - -type StreamFunc func(ctx context.Context, options StreamOptions) (aisdk.DataStream, error) - -// LanguageModels is a map of language model ID to language model. -type LanguageModels map[string]LanguageModel - -func ModelsFromConfig(ctx context.Context, configs []codersdk.AIProviderConfig) (LanguageModels, error) { - models := make(LanguageModels) - - for _, config := range configs { - var streamFunc StreamFunc - - switch config.Type { - case "openai": - opts := []openaioption.RequestOption{ - openaioption.WithAPIKey(config.APIKey), - } - if config.BaseURL != "" { - opts = append(opts, openaioption.WithBaseURL(config.BaseURL)) - } - client := openai.NewClient(opts...) - streamFunc = func(ctx context.Context, options StreamOptions) (aisdk.DataStream, error) { - openaiMessages, err := aisdk.MessagesToOpenAI(options.Messages) - if err != nil { - return nil, err - } - tools := aisdk.ToolsToOpenAI(options.Tools) - if options.SystemPrompt != "" { - openaiMessages = append([]openai.ChatCompletionMessageParamUnion{ - openai.SystemMessage(options.SystemPrompt), - }, openaiMessages...) - } - - return aisdk.OpenAIToDataStream(client.Chat.Completions.NewStreaming(ctx, openai.ChatCompletionNewParams{ - Messages: openaiMessages, - Model: options.Model, - Tools: tools, - MaxTokens: openai.Int(8192), - })), nil - } - if config.Models == nil { - models, err := client.Models.List(ctx) - if err != nil { - return nil, err - } - config.Models = make([]string, len(models.Data)) - for i, model := range models.Data { - config.Models[i] = model.ID - } - } - case "anthropic": - client := anthropic.NewClient(anthropicoption.WithAPIKey(config.APIKey)) - streamFunc = func(ctx context.Context, options StreamOptions) (aisdk.DataStream, error) { - anthropicMessages, systemMessage, err := aisdk.MessagesToAnthropic(options.Messages) - if err != nil { - return nil, err - } - if options.SystemPrompt != "" { - systemMessage = []anthropic.TextBlockParam{ - *anthropic.NewTextBlock(options.SystemPrompt).OfRequestTextBlock, - } - } - return aisdk.AnthropicToDataStream(client.Messages.NewStreaming(ctx, anthropic.MessageNewParams{ - Messages: anthropicMessages, - Model: options.Model, - System: systemMessage, - Tools: aisdk.ToolsToAnthropic(options.Tools), - MaxTokens: 8192, - })), nil - } - if config.Models == nil { - models, err := client.Models.List(ctx, anthropic.ModelListParams{}) - if err != nil { - return nil, err - } - config.Models = make([]string, len(models.Data)) - for i, model := range models.Data { - config.Models[i] = model.ID - } - } - case "google": - client, err := genai.NewClient(ctx, &genai.ClientConfig{ - APIKey: config.APIKey, - Backend: genai.BackendGeminiAPI, - }) - if err != nil { - return nil, err - } - streamFunc = func(ctx context.Context, options StreamOptions) (aisdk.DataStream, error) { - googleMessages, err := aisdk.MessagesToGoogle(options.Messages) - if err != nil { - return nil, err - } - tools, err := aisdk.ToolsToGoogle(options.Tools) - if err != nil { - return nil, err - } - var systemInstruction *genai.Content - if options.SystemPrompt != "" { - systemInstruction = &genai.Content{ - Parts: []*genai.Part{ - genai.NewPartFromText(options.SystemPrompt), - }, - Role: "model", - } - } - return aisdk.GoogleToDataStream(client.Models.GenerateContentStream(ctx, options.Model, googleMessages, &genai.GenerateContentConfig{ - SystemInstruction: systemInstruction, - Tools: tools, - })), nil - } - if config.Models == nil { - models, err := client.Models.List(ctx, &genai.ListModelsConfig{}) - if err != nil { - return nil, err - } - config.Models = make([]string, len(models.Items)) - for i, model := range models.Items { - config.Models[i] = model.Name - } - } - default: - return nil, xerrors.Errorf("unsupported model type: %s", config.Type) - } - - for _, model := range config.Models { - models[model] = LanguageModel{ - LanguageModel: codersdk.LanguageModel{ - ID: model, - DisplayName: model, - Provider: config.Type, - }, - StreamFunc: streamFunc, - } - } - } - - return models, nil -} diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 647a49e646a88..f08b7dfd7fc4d 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -343,173 +343,6 @@ const docTemplate = `{ } } }, - "/chats": { - "get": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Chat" - ], - "summary": "List chats", - "operationId": "list-chats", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.Chat" - } - } - } - } - }, - "post": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Chat" - ], - "summary": "Create a chat", - "operationId": "create-a-chat", - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/codersdk.Chat" - } - } - } - } - }, - "/chats/{chat}": { - "get": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Chat" - ], - "summary": "Get a chat", - "operationId": "get-a-chat", - "parameters": [ - { - "type": "string", - "description": "Chat ID", - "name": "chat", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.Chat" - } - } - } - } - }, - "/chats/{chat}/messages": { - "get": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Chat" - ], - "summary": "Get chat messages", - "operationId": "get-chat-messages", - "parameters": [ - { - "type": "string", - "description": "Chat ID", - "name": "chat", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/aisdk.Message" - } - } - } - } - }, - "post": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Chat" - ], - "summary": "Create a chat message", - "operationId": "create-a-chat-message", - "parameters": [ - { - "type": "string", - "description": "Chat ID", - "name": "chat", - "in": "path", - "required": true - }, - { - "description": "Request body", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/codersdk.CreateChatMessageRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": {} - } - } - } - } - }, "/csp/reports": { "post": { "security": [ @@ -826,31 +659,6 @@ const docTemplate = `{ } } }, - "/deployment/llms": { - "get": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "General" - ], - "summary": "Get language models", - "operationId": "get-language-models", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.LanguageModelConfig" - } - } - } - } - }, "/deployment/ssh": { "get": { "security": [ @@ -10617,190 +10425,6 @@ const docTemplate = `{ "ReinitializeReasonPrebuildClaimed" ] }, - "aisdk.Attachment": { - "type": "object", - "properties": { - "contentType": { - "type": "string" - }, - "name": { - "type": "string" - }, - "url": { - "type": "string" - } - } - }, - "aisdk.Message": { - "type": "object", - "properties": { - "annotations": { - "type": "array", - "items": {} - }, - "content": { - "type": "string" - }, - "createdAt": { - "type": "array", - "items": { - "type": "integer" - } - }, - "experimental_attachments": { - "type": "array", - "items": { - "$ref": "#/definitions/aisdk.Attachment" - } - }, - "id": { - "type": "string" - }, - "parts": { - "type": "array", - "items": { - "$ref": "#/definitions/aisdk.Part" - } - }, - "role": { - "type": "string" - } - } - }, - "aisdk.Part": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "integer" - } - }, - "details": { - "type": "array", - "items": { - "$ref": "#/definitions/aisdk.ReasoningDetail" - } - }, - "mimeType": { - "description": "Type: \"file\"", - "type": "string" - }, - "reasoning": { - "description": "Type: \"reasoning\"", - "type": "string" - }, - "source": { - "description": "Type: \"source\"", - "allOf": [ - { - "$ref": "#/definitions/aisdk.SourceInfo" - } - ] - }, - "text": { - "description": "Type: \"text\"", - "type": "string" - }, - "toolInvocation": { - "description": "Type: \"tool-invocation\"", - "allOf": [ - { - "$ref": "#/definitions/aisdk.ToolInvocation" - } - ] - }, - "type": { - "$ref": "#/definitions/aisdk.PartType" - } - } - }, - "aisdk.PartType": { - "type": "string", - "enum": [ - "text", - "reasoning", - "tool-invocation", - "source", - "file", - "step-start" - ], - "x-enum-varnames": [ - "PartTypeText", - "PartTypeReasoning", - "PartTypeToolInvocation", - "PartTypeSource", - "PartTypeFile", - "PartTypeStepStart" - ] - }, - "aisdk.ReasoningDetail": { - "type": "object", - "properties": { - "data": { - "type": "string" - }, - "signature": { - "type": "string" - }, - "text": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "aisdk.SourceInfo": { - "type": "object", - "properties": { - "contentType": { - "type": "string" - }, - "data": { - "type": "string" - }, - "metadata": { - "type": "object", - "additionalProperties": {} - }, - "uri": { - "type": "string" - } - } - }, - "aisdk.ToolInvocation": { - "type": "object", - "properties": { - "args": {}, - "result": {}, - "state": { - "$ref": "#/definitions/aisdk.ToolInvocationState" - }, - "step": { - "type": "integer" - }, - "toolCallId": { - "type": "string" - }, - "toolName": { - "type": "string" - } - } - }, - "aisdk.ToolInvocationState": { - "type": "string", - "enum": [ - "call", - "partial-call", - "result" - ], - "x-enum-varnames": [ - "ToolInvocationStateCall", - "ToolInvocationStatePartialCall", - "ToolInvocationStateResult" - ] - }, "coderd.SCIMUser": { "type": "object", "properties": { @@ -10892,37 +10516,6 @@ const docTemplate = `{ } } }, - "codersdk.AIConfig": { - "type": "object", - "properties": { - "providers": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.AIProviderConfig" - } - } - } - }, - "codersdk.AIProviderConfig": { - "type": "object", - "properties": { - "base_url": { - "description": "BaseURL is the base URL to use for the API provider.", - "type": "string" - }, - "models": { - "description": "Models is the list of models to use for the API provider.", - "type": "array", - "items": { - "type": "string" - } - }, - "type": { - "description": "Type is the type of the API provider.", - "type": "string" - } - } - }, "codersdk.APIKey": { "type": "object", "required": [ @@ -11508,62 +11101,6 @@ const docTemplate = `{ } } }, - "codersdk.Chat": { - "type": "object", - "properties": { - "created_at": { - "type": "string", - "format": "date-time" - }, - "id": { - "type": "string", - "format": "uuid" - }, - "title": { - "type": "string" - }, - "updated_at": { - "type": "string", - "format": "date-time" - } - } - }, - "codersdk.ChatMessage": { - "type": "object", - "properties": { - "annotations": { - "type": "array", - "items": {} - }, - "content": { - "type": "string" - }, - "createdAt": { - "type": "array", - "items": { - "type": "integer" - } - }, - "experimental_attachments": { - "type": "array", - "items": { - "$ref": "#/definitions/aisdk.Attachment" - } - }, - "id": { - "type": "string" - }, - "parts": { - "type": "array", - "items": { - "$ref": "#/definitions/aisdk.Part" - } - }, - "role": { - "type": "string" - } - } - }, "codersdk.ConnectionLatency": { "type": "object", "properties": { @@ -11597,20 +11134,6 @@ const docTemplate = `{ } } }, - "codersdk.CreateChatMessageRequest": { - "type": "object", - "properties": { - "message": { - "$ref": "#/definitions/codersdk.ChatMessage" - }, - "model": { - "type": "string" - }, - "thinking": { - "type": "boolean" - } - } - }, "codersdk.CreateFirstUserRequest": { "type": "object", "required": [ @@ -11898,73 +11421,7 @@ const docTemplate = `{ } }, "codersdk.CreateTestAuditLogRequest": { - "type": "object", - "properties": { - "action": { - "enum": [ - "create", - "write", - "delete", - "start", - "stop" - ], - "allOf": [ - { - "$ref": "#/definitions/codersdk.AuditAction" - } - ] - }, - "additional_fields": { - "type": "array", - "items": { - "type": "integer" - } - }, - "build_reason": { - "enum": [ - "autostart", - "autostop", - "initiator" - ], - "allOf": [ - { - "$ref": "#/definitions/codersdk.BuildReason" - } - ] - }, - "organization_id": { - "type": "string", - "format": "uuid" - }, - "request_id": { - "type": "string", - "format": "uuid" - }, - "resource_id": { - "type": "string", - "format": "uuid" - }, - "resource_type": { - "enum": [ - "template", - "template_version", - "user", - "workspace", - "workspace_build", - "git_ssh_key", - "auditable_group" - ], - "allOf": [ - { - "$ref": "#/definitions/codersdk.ResourceType" - } - ] - }, - "time": { - "type": "string", - "format": "date-time" - } - } + "type": "object" }, "codersdk.CreateTokenRequest": { "type": "object", @@ -12410,9 +11867,6 @@ const docTemplate = `{ "agent_stat_refresh_interval": { "type": "integer" }, - "ai": { - "$ref": "#/definitions/serpent.Struct-codersdk_AIConfig" - }, "allow_workspace_renames": { "type": "boolean" }, @@ -12741,11 +12195,9 @@ const docTemplate = `{ "notifications", "workspace-usage", "web-push", - "workspace-prebuilds", - "agentic-chat" + "workspace-prebuilds" ], "x-enum-comments": { - "ExperimentAgenticChat": "Enables the new agentic AI chat feature.", "ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.", "ExperimentExample": "This isn't used for anything.", "ExperimentNotifications": "Sends notifications via SMTP and webhooks following certain events.", @@ -12759,8 +12211,7 @@ const docTemplate = `{ "ExperimentNotifications", "ExperimentWorkspaceUsage", "ExperimentWebPush", - "ExperimentWorkspacePrebuilds", - "ExperimentAgenticChat" + "ExperimentWorkspacePrebuilds" ] }, "codersdk.ExternalAuth": { @@ -13288,33 +12739,6 @@ const docTemplate = `{ "RequiredTemplateVariables" ] }, - "codersdk.LanguageModel": { - "type": "object", - "properties": { - "display_name": { - "type": "string" - }, - "id": { - "description": "ID is used by the provider to identify the LLM.", - "type": "string" - }, - "provider": { - "description": "Provider is the provider of the LLM. e.g. openai, anthropic, etc.", - "type": "string" - } - } - }, - "codersdk.LanguageModelConfig": { - "type": "object", - "properties": { - "models": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.LanguageModel" - } - } - } - }, "codersdk.License": { "type": "object", "properties": { @@ -15233,7 +14657,6 @@ const docTemplate = `{ "assign_org_role", "assign_role", "audit_log", - "chat", "crypto_key", "debug_info", "deployment_config", @@ -15273,7 +14696,6 @@ const docTemplate = `{ "ResourceAssignOrgRole", "ResourceAssignRole", "ResourceAuditLog", - "ResourceChat", "ResourceCryptoKey", "ResourceDebugInfo", "ResourceDeploymentConfig", @@ -19342,14 +18764,6 @@ const docTemplate = `{ } } }, - "serpent.Struct-codersdk_AIConfig": { - "type": "object", - "properties": { - "value": { - "$ref": "#/definitions/codersdk.AIConfig" - } - } - }, "serpent.URL": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index a80d07a165b01..8317f9ed90503 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -291,151 +291,6 @@ } } }, - "/chats": { - "get": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "produces": ["application/json"], - "tags": ["Chat"], - "summary": "List chats", - "operationId": "list-chats", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.Chat" - } - } - } - } - }, - "post": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "produces": ["application/json"], - "tags": ["Chat"], - "summary": "Create a chat", - "operationId": "create-a-chat", - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/codersdk.Chat" - } - } - } - } - }, - "/chats/{chat}": { - "get": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "produces": ["application/json"], - "tags": ["Chat"], - "summary": "Get a chat", - "operationId": "get-a-chat", - "parameters": [ - { - "type": "string", - "description": "Chat ID", - "name": "chat", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.Chat" - } - } - } - } - }, - "/chats/{chat}/messages": { - "get": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "produces": ["application/json"], - "tags": ["Chat"], - "summary": "Get chat messages", - "operationId": "get-chat-messages", - "parameters": [ - { - "type": "string", - "description": "Chat ID", - "name": "chat", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/aisdk.Message" - } - } - } - } - }, - "post": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Chat"], - "summary": "Create a chat message", - "operationId": "create-a-chat-message", - "parameters": [ - { - "type": "string", - "description": "Chat ID", - "name": "chat", - "in": "path", - "required": true - }, - { - "description": "Request body", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/codersdk.CreateChatMessageRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": {} - } - } - } - } - }, "/csp/reports": { "post": { "security": [ @@ -708,27 +563,6 @@ } } }, - "/deployment/llms": { - "get": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "produces": ["application/json"], - "tags": ["General"], - "summary": "Get language models", - "operationId": "get-language-models", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.LanguageModelConfig" - } - } - } - } - }, "/deployment/ssh": { "get": { "security": [ @@ -9410,186 +9244,6 @@ "enum": ["prebuild_claimed"], "x-enum-varnames": ["ReinitializeReasonPrebuildClaimed"] }, - "aisdk.Attachment": { - "type": "object", - "properties": { - "contentType": { - "type": "string" - }, - "name": { - "type": "string" - }, - "url": { - "type": "string" - } - } - }, - "aisdk.Message": { - "type": "object", - "properties": { - "annotations": { - "type": "array", - "items": {} - }, - "content": { - "type": "string" - }, - "createdAt": { - "type": "array", - "items": { - "type": "integer" - } - }, - "experimental_attachments": { - "type": "array", - "items": { - "$ref": "#/definitions/aisdk.Attachment" - } - }, - "id": { - "type": "string" - }, - "parts": { - "type": "array", - "items": { - "$ref": "#/definitions/aisdk.Part" - } - }, - "role": { - "type": "string" - } - } - }, - "aisdk.Part": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "integer" - } - }, - "details": { - "type": "array", - "items": { - "$ref": "#/definitions/aisdk.ReasoningDetail" - } - }, - "mimeType": { - "description": "Type: \"file\"", - "type": "string" - }, - "reasoning": { - "description": "Type: \"reasoning\"", - "type": "string" - }, - "source": { - "description": "Type: \"source\"", - "allOf": [ - { - "$ref": "#/definitions/aisdk.SourceInfo" - } - ] - }, - "text": { - "description": "Type: \"text\"", - "type": "string" - }, - "toolInvocation": { - "description": "Type: \"tool-invocation\"", - "allOf": [ - { - "$ref": "#/definitions/aisdk.ToolInvocation" - } - ] - }, - "type": { - "$ref": "#/definitions/aisdk.PartType" - } - } - }, - "aisdk.PartType": { - "type": "string", - "enum": [ - "text", - "reasoning", - "tool-invocation", - "source", - "file", - "step-start" - ], - "x-enum-varnames": [ - "PartTypeText", - "PartTypeReasoning", - "PartTypeToolInvocation", - "PartTypeSource", - "PartTypeFile", - "PartTypeStepStart" - ] - }, - "aisdk.ReasoningDetail": { - "type": "object", - "properties": { - "data": { - "type": "string" - }, - "signature": { - "type": "string" - }, - "text": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "aisdk.SourceInfo": { - "type": "object", - "properties": { - "contentType": { - "type": "string" - }, - "data": { - "type": "string" - }, - "metadata": { - "type": "object", - "additionalProperties": {} - }, - "uri": { - "type": "string" - } - } - }, - "aisdk.ToolInvocation": { - "type": "object", - "properties": { - "args": {}, - "result": {}, - "state": { - "$ref": "#/definitions/aisdk.ToolInvocationState" - }, - "step": { - "type": "integer" - }, - "toolCallId": { - "type": "string" - }, - "toolName": { - "type": "string" - } - } - }, - "aisdk.ToolInvocationState": { - "type": "string", - "enum": ["call", "partial-call", "result"], - "x-enum-varnames": [ - "ToolInvocationStateCall", - "ToolInvocationStatePartialCall", - "ToolInvocationStateResult" - ] - }, "coderd.SCIMUser": { "type": "object", "properties": { @@ -9681,37 +9335,6 @@ } } }, - "codersdk.AIConfig": { - "type": "object", - "properties": { - "providers": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.AIProviderConfig" - } - } - } - }, - "codersdk.AIProviderConfig": { - "type": "object", - "properties": { - "base_url": { - "description": "BaseURL is the base URL to use for the API provider.", - "type": "string" - }, - "models": { - "description": "Models is the list of models to use for the API provider.", - "type": "array", - "items": { - "type": "string" - } - }, - "type": { - "description": "Type is the type of the API provider.", - "type": "string" - } - } - }, "codersdk.APIKey": { "type": "object", "required": [ @@ -10258,62 +9881,6 @@ } } }, - "codersdk.Chat": { - "type": "object", - "properties": { - "created_at": { - "type": "string", - "format": "date-time" - }, - "id": { - "type": "string", - "format": "uuid" - }, - "title": { - "type": "string" - }, - "updated_at": { - "type": "string", - "format": "date-time" - } - } - }, - "codersdk.ChatMessage": { - "type": "object", - "properties": { - "annotations": { - "type": "array", - "items": {} - }, - "content": { - "type": "string" - }, - "createdAt": { - "type": "array", - "items": { - "type": "integer" - } - }, - "experimental_attachments": { - "type": "array", - "items": { - "$ref": "#/definitions/aisdk.Attachment" - } - }, - "id": { - "type": "string" - }, - "parts": { - "type": "array", - "items": { - "$ref": "#/definitions/aisdk.Part" - } - }, - "role": { - "type": "string" - } - } - }, "codersdk.ConnectionLatency": { "type": "object", "properties": { @@ -10344,20 +9911,6 @@ } } }, - "codersdk.CreateChatMessageRequest": { - "type": "object", - "properties": { - "message": { - "$ref": "#/definitions/codersdk.ChatMessage" - }, - "model": { - "type": "string" - }, - "thinking": { - "type": "boolean" - } - } - }, "codersdk.CreateFirstUserRequest": { "type": "object", "required": ["email", "password", "username"], @@ -10626,63 +10179,7 @@ } }, "codersdk.CreateTestAuditLogRequest": { - "type": "object", - "properties": { - "action": { - "enum": ["create", "write", "delete", "start", "stop"], - "allOf": [ - { - "$ref": "#/definitions/codersdk.AuditAction" - } - ] - }, - "additional_fields": { - "type": "array", - "items": { - "type": "integer" - } - }, - "build_reason": { - "enum": ["autostart", "autostop", "initiator"], - "allOf": [ - { - "$ref": "#/definitions/codersdk.BuildReason" - } - ] - }, - "organization_id": { - "type": "string", - "format": "uuid" - }, - "request_id": { - "type": "string", - "format": "uuid" - }, - "resource_id": { - "type": "string", - "format": "uuid" - }, - "resource_type": { - "enum": [ - "template", - "template_version", - "user", - "workspace", - "workspace_build", - "git_ssh_key", - "auditable_group" - ], - "allOf": [ - { - "$ref": "#/definitions/codersdk.ResourceType" - } - ] - }, - "time": { - "type": "string", - "format": "date-time" - } - } + "type": "object" }, "codersdk.CreateTokenRequest": { "type": "object", @@ -11110,9 +10607,6 @@ "agent_stat_refresh_interval": { "type": "integer" }, - "ai": { - "$ref": "#/definitions/serpent.Struct-codersdk_AIConfig" - }, "allow_workspace_renames": { "type": "boolean" }, @@ -11434,11 +10928,9 @@ "notifications", "workspace-usage", "web-push", - "workspace-prebuilds", - "agentic-chat" + "workspace-prebuilds" ], "x-enum-comments": { - "ExperimentAgenticChat": "Enables the new agentic AI chat feature.", "ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.", "ExperimentExample": "This isn't used for anything.", "ExperimentNotifications": "Sends notifications via SMTP and webhooks following certain events.", @@ -11452,8 +10944,7 @@ "ExperimentNotifications", "ExperimentWorkspaceUsage", "ExperimentWebPush", - "ExperimentWorkspacePrebuilds", - "ExperimentAgenticChat" + "ExperimentWorkspacePrebuilds" ] }, "codersdk.ExternalAuth": { @@ -11965,33 +11456,6 @@ "enum": ["REQUIRED_TEMPLATE_VARIABLES"], "x-enum-varnames": ["RequiredTemplateVariables"] }, - "codersdk.LanguageModel": { - "type": "object", - "properties": { - "display_name": { - "type": "string" - }, - "id": { - "description": "ID is used by the provider to identify the LLM.", - "type": "string" - }, - "provider": { - "description": "Provider is the provider of the LLM. e.g. openai, anthropic, etc.", - "type": "string" - } - } - }, - "codersdk.LanguageModelConfig": { - "type": "object", - "properties": { - "models": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.LanguageModel" - } - } - } - }, "codersdk.License": { "type": "object", "properties": { @@ -13825,7 +13289,6 @@ "assign_org_role", "assign_role", "audit_log", - "chat", "crypto_key", "debug_info", "deployment_config", @@ -13865,7 +13328,6 @@ "ResourceAssignOrgRole", "ResourceAssignRole", "ResourceAuditLog", - "ResourceChat", "ResourceCryptoKey", "ResourceDebugInfo", "ResourceDeploymentConfig", @@ -17720,14 +17182,6 @@ } } }, - "serpent.Struct-codersdk_AIConfig": { - "type": "object", - "properties": { - "value": { - "$ref": "#/definitions/codersdk.AIConfig" - } - } - }, "serpent.URL": { "type": "object", "properties": { diff --git a/coderd/chat.go b/coderd/chat.go deleted file mode 100644 index b10211075cfe6..0000000000000 --- a/coderd/chat.go +++ /dev/null @@ -1,366 +0,0 @@ -package coderd - -import ( - "encoding/json" - "io" - "net/http" - "time" - - "github.com/kylecarbs/aisdk-go" - - "github.com/coder/coder/v2/coderd/ai" - "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/coderd/database/db2sdk" - "github.com/coder/coder/v2/coderd/database/dbtime" - "github.com/coder/coder/v2/coderd/httpapi" - "github.com/coder/coder/v2/coderd/httpmw" - "github.com/coder/coder/v2/coderd/util/strings" - "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/codersdk/toolsdk" -) - -// postChats creates a new chat. -// -// @Summary Create a chat -// @ID create-a-chat -// @Security CoderSessionToken -// @Produce json -// @Tags Chat -// @Success 201 {object} codersdk.Chat -// @Router /chats [post] -func (api *API) postChats(w http.ResponseWriter, r *http.Request) { - apiKey := httpmw.APIKey(r) - ctx := r.Context() - - chat, err := api.Database.InsertChat(ctx, database.InsertChatParams{ - OwnerID: apiKey.UserID, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Title: "New Chat", - }) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to create chat", - Detail: err.Error(), - }) - return - } - - httpapi.Write(ctx, w, http.StatusCreated, db2sdk.Chat(chat)) -} - -// listChats lists all chats for a user. -// -// @Summary List chats -// @ID list-chats -// @Security CoderSessionToken -// @Produce json -// @Tags Chat -// @Success 200 {array} codersdk.Chat -// @Router /chats [get] -func (api *API) listChats(w http.ResponseWriter, r *http.Request) { - apiKey := httpmw.APIKey(r) - ctx := r.Context() - - chats, err := api.Database.GetChatsByOwnerID(ctx, apiKey.UserID) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to list chats", - Detail: err.Error(), - }) - return - } - - httpapi.Write(ctx, w, http.StatusOK, db2sdk.Chats(chats)) -} - -// chat returns a chat by ID. -// -// @Summary Get a chat -// @ID get-a-chat -// @Security CoderSessionToken -// @Produce json -// @Tags Chat -// @Param chat path string true "Chat ID" -// @Success 200 {object} codersdk.Chat -// @Router /chats/{chat} [get] -func (*API) chat(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - chat := httpmw.ChatParam(r) - httpapi.Write(ctx, w, http.StatusOK, db2sdk.Chat(chat)) -} - -// chatMessages returns the messages of a chat. -// -// @Summary Get chat messages -// @ID get-chat-messages -// @Security CoderSessionToken -// @Produce json -// @Tags Chat -// @Param chat path string true "Chat ID" -// @Success 200 {array} aisdk.Message -// @Router /chats/{chat}/messages [get] -func (api *API) chatMessages(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - chat := httpmw.ChatParam(r) - rawMessages, err := api.Database.GetChatMessagesByChatID(ctx, chat.ID) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to get chat messages", - Detail: err.Error(), - }) - return - } - messages := make([]aisdk.Message, len(rawMessages)) - for i, message := range rawMessages { - var msg aisdk.Message - err = json.Unmarshal(message.Content, &msg) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to unmarshal chat message", - Detail: err.Error(), - }) - return - } - messages[i] = msg - } - - httpapi.Write(ctx, w, http.StatusOK, messages) -} - -// postChatMessages creates a new chat message and streams the response. -// -// @Summary Create a chat message -// @ID create-a-chat-message -// @Security CoderSessionToken -// @Accept json -// @Produce json -// @Tags Chat -// @Param chat path string true "Chat ID" -// @Param request body codersdk.CreateChatMessageRequest true "Request body" -// @Success 200 {array} aisdk.DataStreamPart -// @Router /chats/{chat}/messages [post] -func (api *API) postChatMessages(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - chat := httpmw.ChatParam(r) - var req codersdk.CreateChatMessageRequest - err := json.NewDecoder(r.Body).Decode(&req) - if err != nil { - httpapi.Write(ctx, w, http.StatusBadRequest, codersdk.Response{ - Message: "Failed to decode chat message", - Detail: err.Error(), - }) - return - } - - dbMessages, err := api.Database.GetChatMessagesByChatID(ctx, chat.ID) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to get chat messages", - Detail: err.Error(), - }) - return - } - - messages := make([]codersdk.ChatMessage, 0) - for _, dbMsg := range dbMessages { - var msg codersdk.ChatMessage - err = json.Unmarshal(dbMsg.Content, &msg) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to unmarshal chat message", - Detail: err.Error(), - }) - return - } - messages = append(messages, msg) - } - messages = append(messages, req.Message) - - client := codersdk.New(api.AccessURL) - client.SetSessionToken(httpmw.APITokenFromRequest(r)) - - tools := make([]aisdk.Tool, 0) - handlers := map[string]toolsdk.GenericHandlerFunc{} - for _, tool := range toolsdk.All { - if tool.Name == "coder_report_task" { - continue // This tool requires an agent to run. - } - tools = append(tools, tool.Tool) - handlers[tool.Tool.Name] = tool.Handler - } - - provider, ok := api.LanguageModels[req.Model] - if !ok { - httpapi.Write(ctx, w, http.StatusBadRequest, codersdk.Response{ - Message: "Model not found", - }) - return - } - - // If it's the user's first message, generate a title for the chat. - if len(messages) == 1 { - var acc aisdk.DataStreamAccumulator - stream, err := provider.StreamFunc(ctx, ai.StreamOptions{ - Model: req.Model, - SystemPrompt: `- You will generate a short title based on the user's message. -- It should be maximum of 40 characters. -- Do not use quotes, colons, special characters, or emojis.`, - Messages: messages, - Tools: []aisdk.Tool{}, // This initial stream doesn't use tools. - }) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to create stream", - Detail: err.Error(), - }) - return - } - stream = stream.WithAccumulator(&acc) - err = stream.Pipe(io.Discard) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to pipe stream", - Detail: err.Error(), - }) - return - } - var newTitle string - accMessages := acc.Messages() - // If for some reason the stream didn't return any messages, use the - // original message as the title. - if len(accMessages) == 0 { - newTitle = strings.Truncate(messages[0].Content, 40) - } else { - newTitle = strings.Truncate(accMessages[0].Content, 40) - } - err = api.Database.UpdateChatByID(ctx, database.UpdateChatByIDParams{ - ID: chat.ID, - Title: newTitle, - UpdatedAt: dbtime.Now(), - }) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to update chat title", - Detail: err.Error(), - }) - return - } - } - - // Write headers for the data stream! - aisdk.WriteDataStreamHeaders(w) - - // Insert the user-requested message into the database! - raw, err := json.Marshal([]aisdk.Message{req.Message}) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to marshal chat message", - Detail: err.Error(), - }) - return - } - _, err = api.Database.InsertChatMessages(ctx, database.InsertChatMessagesParams{ - ChatID: chat.ID, - CreatedAt: dbtime.Now(), - Model: req.Model, - Provider: provider.Provider, - Content: raw, - }) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to insert chat messages", - Detail: err.Error(), - }) - return - } - - deps, err := toolsdk.NewDeps(client) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to create tool dependencies", - Detail: err.Error(), - }) - return - } - - for { - var acc aisdk.DataStreamAccumulator - stream, err := provider.StreamFunc(ctx, ai.StreamOptions{ - Model: req.Model, - Messages: messages, - Tools: tools, - SystemPrompt: `You are a chat assistant for Coder - an open-source platform for creating and managing cloud development environments on any infrastructure. You are expected to be precise, concise, and helpful. - -You are running as an agent - please keep going until the user's query is completely resolved, before ending your turn and yielding back to the user. Only terminate your turn when you are sure that the problem is solved. Do NOT guess or make up an answer.`, - }) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to create stream", - Detail: err.Error(), - }) - return - } - stream = stream.WithToolCalling(func(toolCall aisdk.ToolCall) aisdk.ToolCallResult { - tool, ok := handlers[toolCall.Name] - if !ok { - return nil - } - toolArgs, err := json.Marshal(toolCall.Args) - if err != nil { - return nil - } - result, err := tool(ctx, deps, toolArgs) - if err != nil { - return map[string]any{ - "error": err.Error(), - } - } - return result - }).WithAccumulator(&acc) - - err = stream.Pipe(w) - if err != nil { - // The client disppeared! - api.Logger.Error(ctx, "stream pipe error", "error", err) - return - } - - // acc.Messages() may sometimes return nil. Serializing this - // will cause a pq error: "cannot extract elements from a scalar". - newMessages := append([]aisdk.Message{}, acc.Messages()...) - if len(newMessages) > 0 { - raw, err := json.Marshal(newMessages) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to marshal chat message", - Detail: err.Error(), - }) - return - } - messages = append(messages, newMessages...) - - // Insert these messages into the database! - _, err = api.Database.InsertChatMessages(ctx, database.InsertChatMessagesParams{ - ChatID: chat.ID, - CreatedAt: dbtime.Now(), - Model: req.Model, - Provider: provider.Provider, - Content: raw, - }) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to insert chat messages", - Detail: err.Error(), - }) - return - } - } - - if acc.FinishReason() == aisdk.FinishReasonToolCalls { - continue - } - - break - } -} diff --git a/coderd/chat_test.go b/coderd/chat_test.go deleted file mode 100644 index 71e7b99ab3720..0000000000000 --- a/coderd/chat_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package coderd_test - -import ( - "net/http" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/coder/coder/v2/coderd/coderdtest" - "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/coderd/database/dbgen" - "github.com/coder/coder/v2/coderd/database/dbtime" - "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/testutil" -) - -func TestChat(t *testing.T) { - t.Parallel() - - t.Run("ExperimentAgenticChatDisabled", func(t *testing.T) { - t.Parallel() - - client, _ := coderdtest.NewWithDatabase(t, nil) - owner := coderdtest.CreateFirstUser(t, client) - memberClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) - - // Hit the endpoint to get the chat. It should return a 404. - ctx := testutil.Context(t, testutil.WaitShort) - _, err := memberClient.ListChats(ctx) - require.Error(t, err, "list chats should fail") - var sdkErr *codersdk.Error - require.ErrorAs(t, err, &sdkErr, "request should fail with an SDK error") - require.Equal(t, http.StatusForbidden, sdkErr.StatusCode()) - }) - - t.Run("ChatCRUD", func(t *testing.T) { - t.Parallel() - - dv := coderdtest.DeploymentValues(t) - dv.Experiments = []string{string(codersdk.ExperimentAgenticChat)} - dv.AI.Value = codersdk.AIConfig{ - Providers: []codersdk.AIProviderConfig{ - { - Type: "fake", - APIKey: "", - BaseURL: "http://localhost", - Models: []string{"fake-model"}, - }, - }, - } - client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{ - DeploymentValues: dv, - }) - owner := coderdtest.CreateFirstUser(t, client) - memberClient, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) - - // Seed the database with some data. - dbChat := dbgen.Chat(t, db, database.Chat{ - OwnerID: memberUser.ID, - CreatedAt: dbtime.Now().Add(-time.Hour), - UpdatedAt: dbtime.Now().Add(-time.Hour), - Title: "This is a test chat", - }) - _ = dbgen.ChatMessage(t, db, database.ChatMessage{ - ChatID: dbChat.ID, - CreatedAt: dbtime.Now().Add(-time.Hour), - Content: []byte(`[{"content": "Hello world"}]`), - Model: "fake model", - Provider: "fake", - }) - - ctx := testutil.Context(t, testutil.WaitShort) - - // Listing chats should return the chat we just inserted. - chats, err := memberClient.ListChats(ctx) - require.NoError(t, err, "list chats should succeed") - require.Len(t, chats, 1, "response should have one chat") - require.Equal(t, dbChat.ID, chats[0].ID, "unexpected chat ID") - require.Equal(t, dbChat.Title, chats[0].Title, "unexpected chat title") - require.Equal(t, dbChat.CreatedAt.UTC(), chats[0].CreatedAt.UTC(), "unexpected chat created at") - require.Equal(t, dbChat.UpdatedAt.UTC(), chats[0].UpdatedAt.UTC(), "unexpected chat updated at") - - // Fetching a single chat by ID should return the same chat. - chat, err := memberClient.Chat(ctx, dbChat.ID) - require.NoError(t, err, "get chat should succeed") - require.Equal(t, chats[0], chat, "get chat should return the same chat") - - // Listing chat messages should return the message we just inserted. - messages, err := memberClient.ChatMessages(ctx, dbChat.ID) - require.NoError(t, err, "list chat messages should succeed") - require.Len(t, messages, 1, "response should have one message") - require.Equal(t, "Hello world", messages[0].Content, "response should have the correct message content") - - // Creating a new chat will fail because the model does not exist. - // TODO: Test the message streaming functionality with a mock model. - // Inserting a chat message will fail due to the model not existing. - _, err = memberClient.CreateChatMessage(ctx, dbChat.ID, codersdk.CreateChatMessageRequest{ - Model: "echo", - Message: codersdk.ChatMessage{ - Role: "user", - Content: "Hello world", - }, - Thinking: false, - }) - require.Error(t, err, "create chat message should fail") - var sdkErr *codersdk.Error - require.ErrorAs(t, err, &sdkErr, "create chat should fail with an SDK error") - require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode(), "create chat should fail with a 400 when model does not exist") - - // Creating a new chat message with malformed content should fail. - res, err := memberClient.Request(ctx, http.MethodPost, "/api/v2/chats/"+dbChat.ID.String()+"/messages", strings.NewReader(`{malformed json}`)) - require.NoError(t, err) - defer res.Body.Close() - apiErr := codersdk.ReadBodyAsError(res) - require.Contains(t, apiErr.Error(), "Failed to decode chat message") - - _, err = memberClient.CreateChat(ctx) - require.NoError(t, err, "create chat should succeed") - chats, err = memberClient.ListChats(ctx) - require.NoError(t, err, "list chats should succeed") - require.Len(t, chats, 2, "response should have two chats") - }) -} diff --git a/coderd/coderd.go b/coderd/coderd.go index 97e38047a3d50..9618e5e751931 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -45,7 +45,6 @@ import ( "github.com/coder/coder/v2/codersdk/drpcsdk" - "github.com/coder/coder/v2/coderd/ai" "github.com/coder/coder/v2/coderd/cryptokeys" "github.com/coder/coder/v2/coderd/entitlements" "github.com/coder/coder/v2/coderd/files" @@ -160,7 +159,6 @@ type Options struct { Authorizer rbac.Authorizer AzureCertificates x509.VerifyOptions GoogleTokenValidator *idtoken.Validator - LanguageModels ai.LanguageModels GithubOAuth2Config *GithubOAuth2Config OIDCConfig *OIDCConfig PrometheusRegistry *prometheus.Registry @@ -976,7 +974,6 @@ func New(options *Options) *API { r.Get("/config", api.deploymentValues) r.Get("/stats", api.deploymentStats) r.Get("/ssh", api.sshConfig) - r.Get("/llms", api.deploymentLLMs) }) r.Route("/experiments", func(r chi.Router) { r.Use(apiKeyMiddleware) @@ -1019,21 +1016,6 @@ func New(options *Options) *API { r.Get("/{fileID}", api.fileByID) r.Post("/", api.postFile) }) - // Chats are an experimental feature - r.Route("/chats", func(r chi.Router) { - r.Use( - apiKeyMiddleware, - httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentAgenticChat), - ) - r.Get("/", api.listChats) - r.Post("/", api.postChats) - r.Route("/{chat}", func(r chi.Router) { - r.Use(httpmw.ExtractChatParam(options.Database)) - r.Get("/", api.chat) - r.Get("/messages", api.chatMessages) - r.Post("/messages", api.postChatMessages) - }) - }) r.Route("/external-auth", func(r chi.Router) { r.Use( apiKeyMiddleware, diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index e926844d2b156..c74e63bb86f59 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -16,6 +16,8 @@ import ( "golang.org/x/xerrors" "tailscale.com/tailcfg" + previewtypes "github.com/coder/preview/types" + agentproto "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/rbac" @@ -26,7 +28,6 @@ import ( "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/provisionersdk/proto" "github.com/coder/coder/v2/tailnet" - previewtypes "github.com/coder/preview/types" ) // List is a helper function to reduce boilerplate when converting slices of @@ -803,19 +804,6 @@ func AgentProtoConnectionActionToAuditAction(action database.AuditAction) (agent } } -func Chat(chat database.Chat) codersdk.Chat { - return codersdk.Chat{ - ID: chat.ID, - Title: chat.Title, - CreatedAt: chat.CreatedAt, - UpdatedAt: chat.UpdatedAt, - } -} - -func Chats(chats []database.Chat) []codersdk.Chat { - return List(chats, Chat) -} - func PreviewParameter(param previewtypes.Parameter) codersdk.PreviewParameter { return codersdk.PreviewParameter{ PreviewParameterData: codersdk.PreviewParameterData{ diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index f714a53fd6675..d63e049abf8ee 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1373,10 +1373,6 @@ func (q *querier) DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, u return q.db.DeleteApplicationConnectAPIKeysByUserID(ctx, userID) } -func (q *querier) DeleteChat(ctx context.Context, id uuid.UUID) error { - return deleteQ(q.log, q.auth, q.db.GetChatByID, q.db.DeleteChat)(ctx, id) -} - func (q *querier) DeleteCoordinator(ctx context.Context, id uuid.UUID) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceTailnetCoordinator); err != nil { return err @@ -1814,22 +1810,6 @@ func (q *querier) GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUI return q.db.GetAuthorizationUserRoles(ctx, userID) } -func (q *querier) GetChatByID(ctx context.Context, id uuid.UUID) (database.Chat, error) { - return fetch(q.log, q.auth, q.db.GetChatByID)(ctx, id) -} - -func (q *querier) GetChatMessagesByChatID(ctx context.Context, chatID uuid.UUID) ([]database.ChatMessage, error) { - c, err := q.GetChatByID(ctx, chatID) - if err != nil { - return nil, err - } - return q.db.GetChatMessagesByChatID(ctx, c.ID) -} - -func (q *querier) GetChatsByOwnerID(ctx context.Context, ownerID uuid.UUID) ([]database.Chat, error) { - return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetChatsByOwnerID)(ctx, ownerID) -} - func (q *querier) GetCoordinatorResumeTokenSigningKey(ctx context.Context) (string, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return "", err @@ -3525,21 +3505,6 @@ func (q *querier) InsertAuditLog(ctx context.Context, arg database.InsertAuditLo return insert(q.log, q.auth, rbac.ResourceAuditLog, q.db.InsertAuditLog)(ctx, arg) } -func (q *querier) InsertChat(ctx context.Context, arg database.InsertChatParams) (database.Chat, error) { - return insert(q.log, q.auth, rbac.ResourceChat.WithOwner(arg.OwnerID.String()), q.db.InsertChat)(ctx, arg) -} - -func (q *querier) InsertChatMessages(ctx context.Context, arg database.InsertChatMessagesParams) ([]database.ChatMessage, error) { - c, err := q.db.GetChatByID(ctx, arg.ChatID) - if err != nil { - return nil, err - } - if err := q.authorizeContext(ctx, policy.ActionUpdate, c); err != nil { - return nil, err - } - return q.db.InsertChatMessages(ctx, arg) -} - func (q *querier) InsertCryptoKey(ctx context.Context, arg database.InsertCryptoKeyParams) (database.CryptoKey, error) { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceCryptoKey); err != nil { return database.CryptoKey{}, err @@ -4201,13 +4166,6 @@ func (q *querier) UpdateAPIKeyByID(ctx context.Context, arg database.UpdateAPIKe return update(q.log, q.auth, fetch, q.db.UpdateAPIKeyByID)(ctx, arg) } -func (q *querier) UpdateChatByID(ctx context.Context, arg database.UpdateChatByIDParams) error { - fetch := func(ctx context.Context, arg database.UpdateChatByIDParams) (database.Chat, error) { - return q.db.GetChatByID(ctx, arg.ID) - } - return update(q.log, q.auth, fetch, q.db.UpdateChatByID)(ctx, arg) -} - func (q *querier) UpdateCryptoKeyDeletesAt(ctx context.Context, arg database.UpdateCryptoKeyDeletesAtParams) (database.CryptoKey, error) { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceCryptoKey); err != nil { return database.CryptoKey{}, err diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index df4e1c94c311c..0361bf796cb55 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -5549,80 +5549,6 @@ func (s *MethodTestSuite) TestResourcesProvisionerdserver() { })) } -func (s *MethodTestSuite) TestChat() { - createChat := func(t *testing.T, db database.Store) (database.User, database.Chat, database.ChatMessage) { - t.Helper() - - usr := dbgen.User(t, db, database.User{}) - chat := dbgen.Chat(s.T(), db, database.Chat{ - OwnerID: usr.ID, - }) - msg := dbgen.ChatMessage(s.T(), db, database.ChatMessage{ - ChatID: chat.ID, - }) - - return usr, chat, msg - } - - s.Run("DeleteChat", s.Subtest(func(db database.Store, check *expects) { - _, c, _ := createChat(s.T(), db) - check.Args(c.ID).Asserts(c, policy.ActionDelete) - })) - - s.Run("GetChatByID", s.Subtest(func(db database.Store, check *expects) { - _, c, _ := createChat(s.T(), db) - check.Args(c.ID).Asserts(c, policy.ActionRead).Returns(c) - })) - - s.Run("GetChatMessagesByChatID", s.Subtest(func(db database.Store, check *expects) { - _, c, m := createChat(s.T(), db) - check.Args(c.ID).Asserts(c, policy.ActionRead).Returns([]database.ChatMessage{m}) - })) - - s.Run("GetChatsByOwnerID", s.Subtest(func(db database.Store, check *expects) { - u1, u1c1, _ := createChat(s.T(), db) - u1c2 := dbgen.Chat(s.T(), db, database.Chat{ - OwnerID: u1.ID, - CreatedAt: u1c1.CreatedAt.Add(time.Hour), - }) - _, _, _ = createChat(s.T(), db) // other user's chat - check.Args(u1.ID).Asserts(u1c2, policy.ActionRead, u1c1, policy.ActionRead).Returns([]database.Chat{u1c2, u1c1}) - })) - - s.Run("InsertChat", s.Subtest(func(db database.Store, check *expects) { - usr := dbgen.User(s.T(), db, database.User{}) - check.Args(database.InsertChatParams{ - OwnerID: usr.ID, - Title: "test chat", - CreatedAt: dbtime.Now(), - UpdatedAt: dbtime.Now(), - }).Asserts(rbac.ResourceChat.WithOwner(usr.ID.String()), policy.ActionCreate) - })) - - s.Run("InsertChatMessages", s.Subtest(func(db database.Store, check *expects) { - usr := dbgen.User(s.T(), db, database.User{}) - chat := dbgen.Chat(s.T(), db, database.Chat{ - OwnerID: usr.ID, - }) - check.Args(database.InsertChatMessagesParams{ - ChatID: chat.ID, - CreatedAt: dbtime.Now(), - Model: "test-model", - Provider: "test-provider", - Content: []byte(`[]`), - }).Asserts(chat, policy.ActionUpdate) - })) - - s.Run("UpdateChatByID", s.Subtest(func(db database.Store, check *expects) { - _, c, _ := createChat(s.T(), db) - check.Args(database.UpdateChatByIDParams{ - ID: c.ID, - Title: "new title", - UpdatedAt: dbtime.Now(), - }).Asserts(c, policy.ActionUpdate) - })) -} - func (s *MethodTestSuite) TestAuthorizePrebuiltWorkspace() { s.Run("PrebuildDelete/InsertWorkspaceBuild", s.Subtest(func(db database.Store, check *expects) { u := dbgen.User(s.T(), db, database.User{}) diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index 2be9c28f1cc47..fb3adc9e6f057 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -143,30 +143,6 @@ func APIKey(t testing.TB, db database.Store, seed database.APIKey) (key database return key, fmt.Sprintf("%s-%s", key.ID, secret) } -func Chat(t testing.TB, db database.Store, seed database.Chat) database.Chat { - chat, err := db.InsertChat(genCtx, database.InsertChatParams{ - OwnerID: takeFirst(seed.OwnerID, uuid.New()), - CreatedAt: takeFirst(seed.CreatedAt, dbtime.Now()), - UpdatedAt: takeFirst(seed.UpdatedAt, dbtime.Now()), - Title: takeFirst(seed.Title, "Test Chat"), - }) - require.NoError(t, err, "insert chat") - return chat -} - -func ChatMessage(t testing.TB, db database.Store, seed database.ChatMessage) database.ChatMessage { - msg, err := db.InsertChatMessages(genCtx, database.InsertChatMessagesParams{ - CreatedAt: takeFirst(seed.CreatedAt, dbtime.Now()), - ChatID: takeFirst(seed.ChatID, uuid.New()), - Model: takeFirst(seed.Model, "train"), - Provider: takeFirst(seed.Provider, "thomas"), - Content: takeFirstSlice(seed.Content, []byte(`[{"text": "Choo choo!"}]`)), - }) - require.NoError(t, err, "insert chat message") - require.Len(t, msg, 1, "insert one chat message did not return exactly one message") - return msg[0] -} - func WorkspaceAgentPortShare(t testing.TB, db database.Store, orig database.WorkspaceAgentPortShare) database.WorkspaceAgentPortShare { ps, err := db.UpsertWorkspaceAgentPortShare(genCtx, database.UpsertWorkspaceAgentPortShareParams{ WorkspaceID: takeFirst(orig.WorkspaceID, uuid.New()), diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index cfcbc060e90a4..e5604d440073b 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -215,8 +215,6 @@ type data struct { // New tables auditLogs []database.AuditLog - chats []database.Chat - chatMessages []database.ChatMessage cryptoKeys []database.CryptoKey dbcryptKeys []database.DBCryptKey files []database.File @@ -1909,19 +1907,6 @@ func (q *FakeQuerier) DeleteApplicationConnectAPIKeysByUserID(_ context.Context, return nil } -func (q *FakeQuerier) DeleteChat(ctx context.Context, id uuid.UUID) error { - q.mutex.Lock() - defer q.mutex.Unlock() - - for i, chat := range q.chats { - if chat.ID == id { - q.chats = append(q.chats[:i], q.chats[i+1:]...) - return nil - } - } - return sql.ErrNoRows -} - func (*FakeQuerier) DeleteCoordinator(context.Context, uuid.UUID) error { return ErrUnimplemented } @@ -2955,47 +2940,6 @@ func (q *FakeQuerier) GetAuthorizationUserRoles(_ context.Context, userID uuid.U }, nil } -func (q *FakeQuerier) GetChatByID(ctx context.Context, id uuid.UUID) (database.Chat, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - for _, chat := range q.chats { - if chat.ID == id { - return chat, nil - } - } - return database.Chat{}, sql.ErrNoRows -} - -func (q *FakeQuerier) GetChatMessagesByChatID(ctx context.Context, chatID uuid.UUID) ([]database.ChatMessage, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - messages := []database.ChatMessage{} - for _, chatMessage := range q.chatMessages { - if chatMessage.ChatID == chatID { - messages = append(messages, chatMessage) - } - } - return messages, nil -} - -func (q *FakeQuerier) GetChatsByOwnerID(ctx context.Context, ownerID uuid.UUID) ([]database.Chat, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - chats := []database.Chat{} - for _, chat := range q.chats { - if chat.OwnerID == ownerID { - chats = append(chats, chat) - } - } - sort.Slice(chats, func(i, j int) bool { - return chats[i].CreatedAt.After(chats[j].CreatedAt) - }) - return chats, nil -} - func (q *FakeQuerier) GetCoordinatorResumeTokenSigningKey(_ context.Context) (string, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -8630,66 +8574,6 @@ func (q *FakeQuerier) InsertAuditLog(_ context.Context, arg database.InsertAudit return alog, nil } -func (q *FakeQuerier) InsertChat(ctx context.Context, arg database.InsertChatParams) (database.Chat, error) { - err := validateDatabaseType(arg) - if err != nil { - return database.Chat{}, err - } - - q.mutex.Lock() - defer q.mutex.Unlock() - - chat := database.Chat{ - ID: uuid.New(), - CreatedAt: arg.CreatedAt, - UpdatedAt: arg.UpdatedAt, - OwnerID: arg.OwnerID, - Title: arg.Title, - } - q.chats = append(q.chats, chat) - - return chat, nil -} - -func (q *FakeQuerier) InsertChatMessages(ctx context.Context, arg database.InsertChatMessagesParams) ([]database.ChatMessage, error) { - err := validateDatabaseType(arg) - if err != nil { - return nil, err - } - - q.mutex.Lock() - defer q.mutex.Unlock() - - id := int64(0) - if len(q.chatMessages) > 0 { - id = q.chatMessages[len(q.chatMessages)-1].ID - } - - messages := make([]database.ChatMessage, 0) - - rawMessages := make([]json.RawMessage, 0) - err = json.Unmarshal(arg.Content, &rawMessages) - if err != nil { - return nil, err - } - - for _, content := range rawMessages { - id++ - _ = content - messages = append(messages, database.ChatMessage{ - ID: id, - ChatID: arg.ChatID, - CreatedAt: arg.CreatedAt, - Model: arg.Model, - Provider: arg.Provider, - Content: content, - }) - } - - q.chatMessages = append(q.chatMessages, messages...) - return messages, nil -} - func (q *FakeQuerier) InsertCryptoKey(_ context.Context, arg database.InsertCryptoKeyParams) (database.CryptoKey, error) { err := validateDatabaseType(arg) if err != nil { @@ -10638,27 +10522,6 @@ func (q *FakeQuerier) UpdateAPIKeyByID(_ context.Context, arg database.UpdateAPI return sql.ErrNoRows } -func (q *FakeQuerier) UpdateChatByID(ctx context.Context, arg database.UpdateChatByIDParams) error { - err := validateDatabaseType(arg) - if err != nil { - return err - } - - q.mutex.Lock() - defer q.mutex.Unlock() - - for i, chat := range q.chats { - if chat.ID == arg.ID { - q.chats[i].Title = arg.Title - q.chats[i].UpdatedAt = arg.UpdatedAt - q.chats[i] = chat - return nil - } - } - - return sql.ErrNoRows -} - func (q *FakeQuerier) UpdateCryptoKeyDeletesAt(_ context.Context, arg database.UpdateCryptoKeyDeletesAtParams) (database.CryptoKey, error) { err := validateDatabaseType(arg) if err != nil { diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 38e662ce444ac..0d68d0c15e1be 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -249,13 +249,6 @@ func (m queryMetricsStore) DeleteApplicationConnectAPIKeysByUserID(ctx context.C return err } -func (m queryMetricsStore) DeleteChat(ctx context.Context, id uuid.UUID) error { - start := time.Now() - r0 := m.s.DeleteChat(ctx, id) - m.queryLatencies.WithLabelValues("DeleteChat").Observe(time.Since(start).Seconds()) - return r0 -} - func (m queryMetricsStore) DeleteCoordinator(ctx context.Context, id uuid.UUID) error { start := time.Now() r0 := m.s.DeleteCoordinator(ctx, id) @@ -648,27 +641,6 @@ func (m queryMetricsStore) GetAuthorizationUserRoles(ctx context.Context, userID return row, err } -func (m queryMetricsStore) GetChatByID(ctx context.Context, id uuid.UUID) (database.Chat, error) { - start := time.Now() - r0, r1 := m.s.GetChatByID(ctx, id) - m.queryLatencies.WithLabelValues("GetChatByID").Observe(time.Since(start).Seconds()) - return r0, r1 -} - -func (m queryMetricsStore) GetChatMessagesByChatID(ctx context.Context, chatID uuid.UUID) ([]database.ChatMessage, error) { - start := time.Now() - r0, r1 := m.s.GetChatMessagesByChatID(ctx, chatID) - m.queryLatencies.WithLabelValues("GetChatMessagesByChatID").Observe(time.Since(start).Seconds()) - return r0, r1 -} - -func (m queryMetricsStore) GetChatsByOwnerID(ctx context.Context, ownerID uuid.UUID) ([]database.Chat, error) { - start := time.Now() - r0, r1 := m.s.GetChatsByOwnerID(ctx, ownerID) - m.queryLatencies.WithLabelValues("GetChatsByOwnerID").Observe(time.Since(start).Seconds()) - return r0, r1 -} - func (m queryMetricsStore) GetCoordinatorResumeTokenSigningKey(ctx context.Context) (string, error) { start := time.Now() r0, r1 := m.s.GetCoordinatorResumeTokenSigningKey(ctx) @@ -2083,20 +2055,6 @@ func (m queryMetricsStore) InsertAuditLog(ctx context.Context, arg database.Inse return log, err } -func (m queryMetricsStore) InsertChat(ctx context.Context, arg database.InsertChatParams) (database.Chat, error) { - start := time.Now() - r0, r1 := m.s.InsertChat(ctx, arg) - m.queryLatencies.WithLabelValues("InsertChat").Observe(time.Since(start).Seconds()) - return r0, r1 -} - -func (m queryMetricsStore) InsertChatMessages(ctx context.Context, arg database.InsertChatMessagesParams) ([]database.ChatMessage, error) { - start := time.Now() - r0, r1 := m.s.InsertChatMessages(ctx, arg) - m.queryLatencies.WithLabelValues("InsertChatMessages").Observe(time.Since(start).Seconds()) - return r0, r1 -} - func (m queryMetricsStore) InsertCryptoKey(ctx context.Context, arg database.InsertCryptoKeyParams) (database.CryptoKey, error) { start := time.Now() key, err := m.s.InsertCryptoKey(ctx, arg) @@ -2622,13 +2580,6 @@ func (m queryMetricsStore) UpdateAPIKeyByID(ctx context.Context, arg database.Up return err } -func (m queryMetricsStore) UpdateChatByID(ctx context.Context, arg database.UpdateChatByIDParams) error { - start := time.Now() - r0 := m.s.UpdateChatByID(ctx, arg) - m.queryLatencies.WithLabelValues("UpdateChatByID").Observe(time.Since(start).Seconds()) - return r0 -} - func (m queryMetricsStore) UpdateCryptoKeyDeletesAt(ctx context.Context, arg database.UpdateCryptoKeyDeletesAtParams) (database.CryptoKey, error) { start := time.Now() key, err := m.s.UpdateCryptoKeyDeletesAt(ctx, arg) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 586ebf9f60fab..03222782a5d68 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -376,20 +376,6 @@ func (mr *MockStoreMockRecorder) DeleteApplicationConnectAPIKeysByUserID(ctx, us return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteApplicationConnectAPIKeysByUserID", reflect.TypeOf((*MockStore)(nil).DeleteApplicationConnectAPIKeysByUserID), ctx, userID) } -// DeleteChat mocks base method. -func (m *MockStore) DeleteChat(ctx context.Context, id uuid.UUID) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteChat", ctx, id) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteChat indicates an expected call of DeleteChat. -func (mr *MockStoreMockRecorder) DeleteChat(ctx, id any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteChat", reflect.TypeOf((*MockStore)(nil).DeleteChat), ctx, id) -} - // DeleteCoordinator mocks base method. func (m *MockStore) DeleteCoordinator(ctx context.Context, id uuid.UUID) error { m.ctrl.T.Helper() @@ -1292,51 +1278,6 @@ func (mr *MockStoreMockRecorder) GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizedWorkspacesAndAgentsByOwnerID", reflect.TypeOf((*MockStore)(nil).GetAuthorizedWorkspacesAndAgentsByOwnerID), ctx, ownerID, prepared) } -// GetChatByID mocks base method. -func (m *MockStore) GetChatByID(ctx context.Context, id uuid.UUID) (database.Chat, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChatByID", ctx, id) - ret0, _ := ret[0].(database.Chat) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetChatByID indicates an expected call of GetChatByID. -func (mr *MockStoreMockRecorder) GetChatByID(ctx, id any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatByID", reflect.TypeOf((*MockStore)(nil).GetChatByID), ctx, id) -} - -// GetChatMessagesByChatID mocks base method. -func (m *MockStore) GetChatMessagesByChatID(ctx context.Context, chatID uuid.UUID) ([]database.ChatMessage, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChatMessagesByChatID", ctx, chatID) - ret0, _ := ret[0].([]database.ChatMessage) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetChatMessagesByChatID indicates an expected call of GetChatMessagesByChatID. -func (mr *MockStoreMockRecorder) GetChatMessagesByChatID(ctx, chatID any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatMessagesByChatID", reflect.TypeOf((*MockStore)(nil).GetChatMessagesByChatID), ctx, chatID) -} - -// GetChatsByOwnerID mocks base method. -func (m *MockStore) GetChatsByOwnerID(ctx context.Context, ownerID uuid.UUID) ([]database.Chat, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChatsByOwnerID", ctx, ownerID) - ret0, _ := ret[0].([]database.Chat) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetChatsByOwnerID indicates an expected call of GetChatsByOwnerID. -func (mr *MockStoreMockRecorder) GetChatsByOwnerID(ctx, ownerID any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatsByOwnerID", reflect.TypeOf((*MockStore)(nil).GetChatsByOwnerID), ctx, ownerID) -} - // GetCoordinatorResumeTokenSigningKey mocks base method. func (m *MockStore) GetCoordinatorResumeTokenSigningKey(ctx context.Context) (string, error) { m.ctrl.T.Helper() @@ -4411,36 +4352,6 @@ func (mr *MockStoreMockRecorder) InsertAuditLog(ctx, arg any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAuditLog", reflect.TypeOf((*MockStore)(nil).InsertAuditLog), ctx, arg) } -// InsertChat mocks base method. -func (m *MockStore) InsertChat(ctx context.Context, arg database.InsertChatParams) (database.Chat, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InsertChat", ctx, arg) - ret0, _ := ret[0].(database.Chat) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// InsertChat indicates an expected call of InsertChat. -func (mr *MockStoreMockRecorder) InsertChat(ctx, arg any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertChat", reflect.TypeOf((*MockStore)(nil).InsertChat), ctx, arg) -} - -// InsertChatMessages mocks base method. -func (m *MockStore) InsertChatMessages(ctx context.Context, arg database.InsertChatMessagesParams) ([]database.ChatMessage, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InsertChatMessages", ctx, arg) - ret0, _ := ret[0].([]database.ChatMessage) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// InsertChatMessages indicates an expected call of InsertChatMessages. -func (mr *MockStoreMockRecorder) InsertChatMessages(ctx, arg any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertChatMessages", reflect.TypeOf((*MockStore)(nil).InsertChatMessages), ctx, arg) -} - // InsertCryptoKey mocks base method. func (m *MockStore) InsertCryptoKey(ctx context.Context, arg database.InsertCryptoKeyParams) (database.CryptoKey, error) { m.ctrl.T.Helper() @@ -5575,20 +5486,6 @@ func (mr *MockStoreMockRecorder) UpdateAPIKeyByID(ctx, arg any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAPIKeyByID", reflect.TypeOf((*MockStore)(nil).UpdateAPIKeyByID), ctx, arg) } -// UpdateChatByID mocks base method. -func (m *MockStore) UpdateChatByID(ctx context.Context, arg database.UpdateChatByIDParams) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateChatByID", ctx, arg) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpdateChatByID indicates an expected call of UpdateChatByID. -func (mr *MockStoreMockRecorder) UpdateChatByID(ctx, arg any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatByID", reflect.TypeOf((*MockStore)(nil).UpdateChatByID), ctx, arg) -} - // UpdateCryptoKeyDeletesAt mocks base method. func (m *MockStore) UpdateCryptoKeyDeletesAt(ctx context.Context, arg database.UpdateCryptoKeyDeletesAtParams) (database.CryptoKey, error) { m.ctrl.T.Helper() diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 522f24537e576..480780c5fb556 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -822,32 +822,6 @@ CREATE TABLE audit_logs ( resource_icon text NOT NULL ); -CREATE TABLE chat_messages ( - id bigint NOT NULL, - chat_id uuid NOT NULL, - created_at timestamp with time zone DEFAULT now() NOT NULL, - model text NOT NULL, - provider text NOT NULL, - content jsonb NOT NULL -); - -CREATE SEQUENCE chat_messages_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - -ALTER SEQUENCE chat_messages_id_seq OWNED BY chat_messages.id; - -CREATE TABLE chats ( - id uuid DEFAULT gen_random_uuid() NOT NULL, - owner_id uuid NOT NULL, - created_at timestamp with time zone DEFAULT now() NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL, - title text NOT NULL -); - CREATE TABLE crypto_keys ( feature crypto_key_feature NOT NULL, sequence integer NOT NULL, @@ -2342,8 +2316,6 @@ CREATE VIEW workspaces_expanded AS COMMENT ON VIEW workspaces_expanded IS 'Joins in the display name information such as username, avatar, and organization name.'; -ALTER TABLE ONLY chat_messages ALTER COLUMN id SET DEFAULT nextval('chat_messages_id_seq'::regclass); - ALTER TABLE ONLY licenses ALTER COLUMN id SET DEFAULT nextval('licenses_id_seq'::regclass); ALTER TABLE ONLY provisioner_job_logs ALTER COLUMN id SET DEFAULT nextval('provisioner_job_logs_id_seq'::regclass); @@ -2365,12 +2337,6 @@ ALTER TABLE ONLY api_keys ALTER TABLE ONLY audit_logs ADD CONSTRAINT audit_logs_pkey PRIMARY KEY (id); -ALTER TABLE ONLY chat_messages - ADD CONSTRAINT chat_messages_pkey PRIMARY KEY (id); - -ALTER TABLE ONLY chats - ADD CONSTRAINT chats_pkey PRIMARY KEY (id); - ALTER TABLE ONLY crypto_keys ADD CONSTRAINT crypto_keys_pkey PRIMARY KEY (feature, sequence); @@ -2867,12 +2833,6 @@ forward without requiring a migration to clean up historical data.'; ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; -ALTER TABLE ONLY chat_messages - ADD CONSTRAINT chat_messages_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; - -ALTER TABLE ONLY chats - ADD CONSTRAINT chats_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE; - ALTER TABLE ONLY crypto_keys ADD CONSTRAINT crypto_keys_secret_key_id_fkey FOREIGN KEY (secret_key_id) REFERENCES dbcrypt_keys(active_key_digest); diff --git a/coderd/database/foreign_key_constraint.go b/coderd/database/foreign_key_constraint.go index c0720bcb5fcdd..5be75d07288e6 100644 --- a/coderd/database/foreign_key_constraint.go +++ b/coderd/database/foreign_key_constraint.go @@ -7,8 +7,6 @@ type ForeignKeyConstraint string // ForeignKeyConstraint enums. const ( ForeignKeyAPIKeysUserIDUUID ForeignKeyConstraint = "api_keys_user_id_uuid_fkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyChatMessagesChatID ForeignKeyConstraint = "chat_messages_chat_id_fkey" // ALTER TABLE ONLY chat_messages ADD CONSTRAINT chat_messages_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE; - ForeignKeyChatsOwnerID ForeignKeyConstraint = "chats_owner_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE; ForeignKeyCryptoKeysSecretKeyID ForeignKeyConstraint = "crypto_keys_secret_key_id_fkey" // ALTER TABLE ONLY crypto_keys ADD CONSTRAINT crypto_keys_secret_key_id_fkey FOREIGN KEY (secret_key_id) REFERENCES dbcrypt_keys(active_key_digest); ForeignKeyGitAuthLinksOauthAccessTokenKeyID ForeignKeyConstraint = "git_auth_links_oauth_access_token_key_id_fkey" // ALTER TABLE ONLY external_auth_links ADD CONSTRAINT git_auth_links_oauth_access_token_key_id_fkey FOREIGN KEY (oauth_access_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); ForeignKeyGitAuthLinksOauthRefreshTokenKeyID ForeignKeyConstraint = "git_auth_links_oauth_refresh_token_key_id_fkey" // ALTER TABLE ONLY external_auth_links ADD CONSTRAINT git_auth_links_oauth_refresh_token_key_id_fkey FOREIGN KEY (oauth_refresh_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); diff --git a/coderd/database/migrations/000343_delete_chats.down.sql b/coderd/database/migrations/000343_delete_chats.down.sql new file mode 100644 index 0000000000000..1fcd659ca64af --- /dev/null +++ b/coderd/database/migrations/000343_delete_chats.down.sql @@ -0,0 +1 @@ +-- noop diff --git a/coderd/database/migrations/000343_delete_chats.up.sql b/coderd/database/migrations/000343_delete_chats.up.sql new file mode 100644 index 0000000000000..53453647d583f --- /dev/null +++ b/coderd/database/migrations/000343_delete_chats.up.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS chat_messages; +DROP TABLE IF EXISTS chats; diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index 725e45c268d72..f4ddd906823a8 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -611,8 +611,3 @@ func (m WorkspaceAgentVolumeResourceMonitor) Debounce( return m.DebouncedUntil, false } - -func (c Chat) RBACObject() rbac.Object { - return rbac.ResourceChat.WithID(c.ID). - WithOwner(c.OwnerID.String()) -} diff --git a/coderd/database/models.go b/coderd/database/models.go index d964b0e2e57ad..634b5dcd4116d 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -2781,23 +2781,6 @@ type AuditLog struct { ResourceIcon string `db:"resource_icon" json:"resource_icon"` } -type Chat struct { - ID uuid.UUID `db:"id" json:"id"` - OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` - Title string `db:"title" json:"title"` -} - -type ChatMessage struct { - ID int64 `db:"id" json:"id"` - ChatID uuid.UUID `db:"chat_id" json:"chat_id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - Model string `db:"model" json:"model"` - Provider string `db:"provider" json:"provider"` - Content json.RawMessage `db:"content" json:"content"` -} - type CryptoKey struct { Feature CryptoKeyFeature `db:"feature" json:"feature"` Sequence int32 `db:"sequence" json:"sequence"` diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 2300402c27d07..b1c13d31ceb6d 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -79,7 +79,6 @@ type sqlcQuerier interface { // be recreated. DeleteAllWebpushSubscriptions(ctx context.Context) error DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error - DeleteChat(ctx context.Context, id uuid.UUID) error DeleteCoordinator(ctx context.Context, id uuid.UUID) error DeleteCryptoKey(ctx context.Context, arg DeleteCryptoKeyParams) (CryptoKey, error) DeleteCustomRole(ctx context.Context, arg DeleteCustomRoleParams) error @@ -154,9 +153,6 @@ type sqlcQuerier interface { // This function returns roles for authorization purposes. Implied member roles // are included. GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUID) (GetAuthorizationUserRolesRow, error) - GetChatByID(ctx context.Context, id uuid.UUID) (Chat, error) - GetChatMessagesByChatID(ctx context.Context, chatID uuid.UUID) ([]ChatMessage, error) - GetChatsByOwnerID(ctx context.Context, ownerID uuid.UUID) ([]Chat, error) GetCoordinatorResumeTokenSigningKey(ctx context.Context) (string, error) GetCryptoKeyByFeatureAndSequence(ctx context.Context, arg GetCryptoKeyByFeatureAndSequenceParams) (CryptoKey, error) GetCryptoKeys(ctx context.Context) ([]CryptoKey, error) @@ -472,8 +468,6 @@ type sqlcQuerier interface { // every member of the org. InsertAllUsersGroup(ctx context.Context, organizationID uuid.UUID) (Group, error) InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) (AuditLog, error) - InsertChat(ctx context.Context, arg InsertChatParams) (Chat, error) - InsertChatMessages(ctx context.Context, arg InsertChatMessagesParams) ([]ChatMessage, error) InsertCryptoKey(ctx context.Context, arg InsertCryptoKeyParams) (CryptoKey, error) InsertCustomRole(ctx context.Context, arg InsertCustomRoleParams) (CustomRole, error) InsertDBCryptKey(ctx context.Context, arg InsertDBCryptKeyParams) error @@ -567,7 +561,6 @@ type sqlcQuerier interface { UnarchiveTemplateVersion(ctx context.Context, arg UnarchiveTemplateVersionParams) error UnfavoriteWorkspace(ctx context.Context, id uuid.UUID) error UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error - UpdateChatByID(ctx context.Context, arg UpdateChatByIDParams) error UpdateCryptoKeyDeletesAt(ctx context.Context, arg UpdateCryptoKeyDeletesAtParams) (CryptoKey, error) UpdateCustomRole(ctx context.Context, arg UpdateCustomRoleParams) (CustomRole, error) UpdateExternalAuthLink(ctx context.Context, arg UpdateExternalAuthLinkParams) (ExternalAuthLink, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index cf98b03245099..733b42db7a461 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -766,207 +766,6 @@ func (q *sqlQuerier) InsertAuditLog(ctx context.Context, arg InsertAuditLogParam return i, err } -const deleteChat = `-- name: DeleteChat :exec -DELETE FROM chats WHERE id = $1 -` - -func (q *sqlQuerier) DeleteChat(ctx context.Context, id uuid.UUID) error { - _, err := q.db.ExecContext(ctx, deleteChat, id) - return err -} - -const getChatByID = `-- name: GetChatByID :one -SELECT id, owner_id, created_at, updated_at, title FROM chats -WHERE id = $1 -` - -func (q *sqlQuerier) GetChatByID(ctx context.Context, id uuid.UUID) (Chat, error) { - row := q.db.QueryRowContext(ctx, getChatByID, id) - var i Chat - err := row.Scan( - &i.ID, - &i.OwnerID, - &i.CreatedAt, - &i.UpdatedAt, - &i.Title, - ) - return i, err -} - -const getChatMessagesByChatID = `-- name: GetChatMessagesByChatID :many -SELECT id, chat_id, created_at, model, provider, content FROM chat_messages -WHERE chat_id = $1 -ORDER BY created_at ASC -` - -func (q *sqlQuerier) GetChatMessagesByChatID(ctx context.Context, chatID uuid.UUID) ([]ChatMessage, error) { - rows, err := q.db.QueryContext(ctx, getChatMessagesByChatID, chatID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []ChatMessage - for rows.Next() { - var i ChatMessage - if err := rows.Scan( - &i.ID, - &i.ChatID, - &i.CreatedAt, - &i.Model, - &i.Provider, - &i.Content, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const getChatsByOwnerID = `-- name: GetChatsByOwnerID :many -SELECT id, owner_id, created_at, updated_at, title FROM chats -WHERE owner_id = $1 -ORDER BY created_at DESC -` - -func (q *sqlQuerier) GetChatsByOwnerID(ctx context.Context, ownerID uuid.UUID) ([]Chat, error) { - rows, err := q.db.QueryContext(ctx, getChatsByOwnerID, ownerID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Chat - for rows.Next() { - var i Chat - if err := rows.Scan( - &i.ID, - &i.OwnerID, - &i.CreatedAt, - &i.UpdatedAt, - &i.Title, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const insertChat = `-- name: InsertChat :one -INSERT INTO chats (owner_id, created_at, updated_at, title) -VALUES ($1, $2, $3, $4) -RETURNING id, owner_id, created_at, updated_at, title -` - -type InsertChatParams struct { - OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` - Title string `db:"title" json:"title"` -} - -func (q *sqlQuerier) InsertChat(ctx context.Context, arg InsertChatParams) (Chat, error) { - row := q.db.QueryRowContext(ctx, insertChat, - arg.OwnerID, - arg.CreatedAt, - arg.UpdatedAt, - arg.Title, - ) - var i Chat - err := row.Scan( - &i.ID, - &i.OwnerID, - &i.CreatedAt, - &i.UpdatedAt, - &i.Title, - ) - return i, err -} - -const insertChatMessages = `-- name: InsertChatMessages :many -INSERT INTO chat_messages (chat_id, created_at, model, provider, content) -SELECT - $1 :: uuid AS chat_id, - $2 :: timestamptz AS created_at, - $3 :: VARCHAR(127) AS model, - $4 :: VARCHAR(127) AS provider, - jsonb_array_elements($5 :: jsonb) AS content -RETURNING chat_messages.id, chat_messages.chat_id, chat_messages.created_at, chat_messages.model, chat_messages.provider, chat_messages.content -` - -type InsertChatMessagesParams struct { - ChatID uuid.UUID `db:"chat_id" json:"chat_id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - Model string `db:"model" json:"model"` - Provider string `db:"provider" json:"provider"` - Content json.RawMessage `db:"content" json:"content"` -} - -func (q *sqlQuerier) InsertChatMessages(ctx context.Context, arg InsertChatMessagesParams) ([]ChatMessage, error) { - rows, err := q.db.QueryContext(ctx, insertChatMessages, - arg.ChatID, - arg.CreatedAt, - arg.Model, - arg.Provider, - arg.Content, - ) - if err != nil { - return nil, err - } - defer rows.Close() - var items []ChatMessage - for rows.Next() { - var i ChatMessage - if err := rows.Scan( - &i.ID, - &i.ChatID, - &i.CreatedAt, - &i.Model, - &i.Provider, - &i.Content, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const updateChatByID = `-- name: UpdateChatByID :exec -UPDATE chats -SET title = $2, updated_at = $3 -WHERE id = $1 -` - -type UpdateChatByIDParams struct { - ID uuid.UUID `db:"id" json:"id"` - Title string `db:"title" json:"title"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` -} - -func (q *sqlQuerier) UpdateChatByID(ctx context.Context, arg UpdateChatByIDParams) error { - _, err := q.db.ExecContext(ctx, updateChatByID, arg.ID, arg.Title, arg.UpdatedAt) - return err -} - const deleteCryptoKey = `-- name: DeleteCryptoKey :one UPDATE crypto_keys SET secret = NULL, secret_key_id = NULL diff --git a/coderd/database/queries/chat.sql b/coderd/database/queries/chat.sql deleted file mode 100644 index 68f662d8a886b..0000000000000 --- a/coderd/database/queries/chat.sql +++ /dev/null @@ -1,36 +0,0 @@ --- name: InsertChat :one -INSERT INTO chats (owner_id, created_at, updated_at, title) -VALUES ($1, $2, $3, $4) -RETURNING *; - --- name: UpdateChatByID :exec -UPDATE chats -SET title = $2, updated_at = $3 -WHERE id = $1; - --- name: GetChatsByOwnerID :many -SELECT * FROM chats -WHERE owner_id = $1 -ORDER BY created_at DESC; - --- name: GetChatByID :one -SELECT * FROM chats -WHERE id = $1; - --- name: InsertChatMessages :many -INSERT INTO chat_messages (chat_id, created_at, model, provider, content) -SELECT - @chat_id :: uuid AS chat_id, - @created_at :: timestamptz AS created_at, - @model :: VARCHAR(127) AS model, - @provider :: VARCHAR(127) AS provider, - jsonb_array_elements(@content :: jsonb) AS content -RETURNING chat_messages.*; - --- name: GetChatMessagesByChatID :many -SELECT * FROM chat_messages -WHERE chat_id = $1 -ORDER BY created_at ASC; - --- name: DeleteChat :exec -DELETE FROM chats WHERE id = $1; diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index f8a4821fa7640..8377c630a6d92 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -9,8 +9,6 @@ const ( UniqueAgentStatsPkey UniqueConstraint = "agent_stats_pkey" // ALTER TABLE ONLY workspace_agent_stats ADD CONSTRAINT agent_stats_pkey PRIMARY KEY (id); UniqueAPIKeysPkey UniqueConstraint = "api_keys_pkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_pkey PRIMARY KEY (id); UniqueAuditLogsPkey UniqueConstraint = "audit_logs_pkey" // ALTER TABLE ONLY audit_logs ADD CONSTRAINT audit_logs_pkey PRIMARY KEY (id); - UniqueChatMessagesPkey UniqueConstraint = "chat_messages_pkey" // ALTER TABLE ONLY chat_messages ADD CONSTRAINT chat_messages_pkey PRIMARY KEY (id); - UniqueChatsPkey UniqueConstraint = "chats_pkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_pkey PRIMARY KEY (id); UniqueCryptoKeysPkey UniqueConstraint = "crypto_keys_pkey" // ALTER TABLE ONLY crypto_keys ADD CONSTRAINT crypto_keys_pkey PRIMARY KEY (feature, sequence); UniqueCustomRolesUniqueKey UniqueConstraint = "custom_roles_unique_key" // ALTER TABLE ONLY custom_roles ADD CONSTRAINT custom_roles_unique_key UNIQUE (name, organization_id); UniqueDbcryptKeysActiveKeyDigestKey UniqueConstraint = "dbcrypt_keys_active_key_digest_key" // ALTER TABLE ONLY dbcrypt_keys ADD CONSTRAINT dbcrypt_keys_active_key_digest_key UNIQUE (active_key_digest); diff --git a/coderd/deployment.go b/coderd/deployment.go index 60988aeb2ce5a..4c78563a80456 100644 --- a/coderd/deployment.go +++ b/coderd/deployment.go @@ -1,11 +1,8 @@ package coderd import ( - "context" "net/http" - "github.com/kylecarbs/aisdk-go" - "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" @@ -87,25 +84,3 @@ func buildInfoHandler(resp codersdk.BuildInfoResponse) http.HandlerFunc { func (api *API) sshConfig(rw http.ResponseWriter, r *http.Request) { httpapi.Write(r.Context(), rw, http.StatusOK, api.SSHConfig) } - -type LanguageModel struct { - codersdk.LanguageModel - Provider func(ctx context.Context, messages []aisdk.Message, thinking bool) (aisdk.DataStream, error) -} - -// @Summary Get language models -// @ID get-language-models -// @Security CoderSessionToken -// @Produce json -// @Tags General -// @Success 200 {object} codersdk.LanguageModelConfig -// @Router /deployment/llms [get] -func (api *API) deploymentLLMs(rw http.ResponseWriter, r *http.Request) { - models := make([]codersdk.LanguageModel, 0, len(api.LanguageModels)) - for _, model := range api.LanguageModels { - models = append(models, model.LanguageModel) - } - httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.LanguageModelConfig{ - Models: models, - }) -} diff --git a/coderd/httpmw/chat.go b/coderd/httpmw/chat.go deleted file mode 100644 index c92fa5038ab22..0000000000000 --- a/coderd/httpmw/chat.go +++ /dev/null @@ -1,59 +0,0 @@ -package httpmw - -import ( - "context" - "net/http" - - "github.com/go-chi/chi/v5" - "github.com/google/uuid" - - "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/coderd/httpapi" - "github.com/coder/coder/v2/codersdk" -) - -type chatContextKey struct{} - -func ChatParam(r *http.Request) database.Chat { - chat, ok := r.Context().Value(chatContextKey{}).(database.Chat) - if !ok { - panic("developer error: chat param middleware not provided") - } - return chat -} - -func ExtractChatParam(db database.Store) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() - arg := chi.URLParam(r, "chat") - if arg == "" { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "\"chat\" must be provided.", - }) - return - } - chatID, err := uuid.Parse(arg) - if err != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Invalid chat ID.", - }) - return - } - chat, err := db.GetChatByID(ctx, chatID) - if httpapi.Is404Error(err) { - httpapi.ResourceNotFound(rw) - return - } - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to get chat.", - Detail: err.Error(), - }) - return - } - ctx = context.WithValue(ctx, chatContextKey{}, chat) - next.ServeHTTP(rw, r.WithContext(ctx)) - }) - } -} diff --git a/coderd/httpmw/chat_test.go b/coderd/httpmw/chat_test.go deleted file mode 100644 index 3acc2db8b9877..0000000000000 --- a/coderd/httpmw/chat_test.go +++ /dev/null @@ -1,150 +0,0 @@ -package httpmw_test - -import ( - "context" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/go-chi/chi/v5" - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/coderd/database/dbgen" - "github.com/coder/coder/v2/coderd/database/dbtestutil" - "github.com/coder/coder/v2/coderd/database/dbtime" - "github.com/coder/coder/v2/coderd/httpmw" - "github.com/coder/coder/v2/codersdk" -) - -func TestExtractChat(t *testing.T) { - t.Parallel() - - setupAuthentication := func(db database.Store) (*http.Request, database.User) { - r := httptest.NewRequest("GET", "/", nil) - - user := dbgen.User(t, db, database.User{ - ID: uuid.New(), - }) - _, token := dbgen.APIKey(t, db, database.APIKey{ - UserID: user.ID, - }) - r.Header.Set(codersdk.SessionTokenHeader, token) - r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, chi.NewRouteContext())) - return r, user - } - - t.Run("None", func(t *testing.T) { - t.Parallel() - var ( - db, _ = dbtestutil.NewDB(t) - rw = httptest.NewRecorder() - r, _ = setupAuthentication(db) - rtr = chi.NewRouter() - ) - rtr.Use( - httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{ - DB: db, - RedirectToLogin: false, - }), - httpmw.ExtractChatParam(db), - ) - rtr.Get("/", nil) - rtr.ServeHTTP(rw, r) - res := rw.Result() - defer res.Body.Close() - require.Equal(t, http.StatusBadRequest, res.StatusCode) - }) - - t.Run("InvalidUUID", func(t *testing.T) { - t.Parallel() - var ( - db, _ = dbtestutil.NewDB(t) - rw = httptest.NewRecorder() - r, _ = setupAuthentication(db) - rtr = chi.NewRouter() - ) - chi.RouteContext(r.Context()).URLParams.Add("chat", "not-a-uuid") - rtr.Use( - httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{ - DB: db, - RedirectToLogin: false, - }), - httpmw.ExtractChatParam(db), - ) - rtr.Get("/", nil) - rtr.ServeHTTP(rw, r) - res := rw.Result() - defer res.Body.Close() - require.Equal(t, http.StatusBadRequest, res.StatusCode) // Changed from NotFound in org test to BadRequest as per chat.go - }) - - t.Run("NotFound", func(t *testing.T) { - t.Parallel() - var ( - db, _ = dbtestutil.NewDB(t) - rw = httptest.NewRecorder() - r, _ = setupAuthentication(db) - rtr = chi.NewRouter() - ) - chi.RouteContext(r.Context()).URLParams.Add("chat", uuid.NewString()) - rtr.Use( - httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{ - DB: db, - RedirectToLogin: false, - }), - httpmw.ExtractChatParam(db), - ) - rtr.Get("/", nil) - rtr.ServeHTTP(rw, r) - res := rw.Result() - defer res.Body.Close() - require.Equal(t, http.StatusNotFound, res.StatusCode) - }) - - t.Run("Success", func(t *testing.T) { - t.Parallel() - var ( - db, _ = dbtestutil.NewDB(t) - rw = httptest.NewRecorder() - r, user = setupAuthentication(db) - rtr = chi.NewRouter() - ) - - // Create a test chat - testChat := dbgen.Chat(t, db, database.Chat{ - ID: uuid.New(), - OwnerID: user.ID, - CreatedAt: dbtime.Now(), - UpdatedAt: dbtime.Now(), - Title: "Test Chat", - }) - - rtr.Use( - httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{ - DB: db, - RedirectToLogin: false, - }), - httpmw.ExtractChatParam(db), - ) - rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) { - chat := httpmw.ChatParam(r) - require.NotZero(t, chat) - assert.Equal(t, testChat.ID, chat.ID) - assert.WithinDuration(t, testChat.CreatedAt, chat.CreatedAt, time.Second) - assert.WithinDuration(t, testChat.UpdatedAt, chat.UpdatedAt, time.Second) - assert.Equal(t, testChat.Title, chat.Title) - rw.WriteHeader(http.StatusOK) - }) - - // Try by ID - chi.RouteContext(r.Context()).URLParams.Add("chat", testChat.ID.String()) - rtr.ServeHTTP(rw, r) - res := rw.Result() - defer res.Body.Close() - require.Equal(t, http.StatusOK, res.StatusCode, "by id") - }) -} diff --git a/coderd/rbac/object_gen.go b/coderd/rbac/object_gen.go index a5c696fb2a491..d0d5dc4aab0fe 100644 --- a/coderd/rbac/object_gen.go +++ b/coderd/rbac/object_gen.go @@ -54,16 +54,6 @@ var ( Type: "audit_log", } - // ResourceChat - // Valid Actions - // - "ActionCreate" :: create a chat - // - "ActionDelete" :: delete a chat - // - "ActionRead" :: read a chat - // - "ActionUpdate" :: update a chat - ResourceChat = Object{ - Type: "chat", - } - // ResourceCryptoKey // Valid Actions // - "ActionCreate" :: create crypto keys @@ -378,7 +368,6 @@ func AllResources() []Objecter { ResourceAssignOrgRole, ResourceAssignRole, ResourceAuditLog, - ResourceChat, ResourceCryptoKey, ResourceDebugInfo, ResourceDeploymentConfig, diff --git a/coderd/rbac/policy/policy.go b/coderd/rbac/policy/policy.go index 733a70bcafd0e..a3ad614439c9a 100644 --- a/coderd/rbac/policy/policy.go +++ b/coderd/rbac/policy/policy.go @@ -124,14 +124,6 @@ var RBACPermissions = map[string]PermissionDefinition{ ActionRead: actDef("read and use a workspace proxy"), }, }, - "chat": { - Actions: map[Action]ActionDefinition{ - ActionCreate: actDef("create a chat"), - ActionRead: actDef("read a chat"), - ActionDelete: actDef("delete a chat"), - ActionUpdate: actDef("update a chat"), - }, - }, "license": { Actions: map[Action]ActionDefinition{ ActionCreate: actDef("create a license"), diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index 8acdf7486ddd2..9075e7e711adc 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -305,8 +305,6 @@ func ReloadBuiltinRoles(opts *RoleOptions) { ResourceOrganizationMember.Type: {policy.ActionRead}, // Users can create provisioner daemons scoped to themselves. ResourceProvisionerDaemon.Type: {policy.ActionRead, policy.ActionCreate, policy.ActionRead, policy.ActionUpdate}, - // Users can create, read, update, and delete their own agentic chat messages. - ResourceChat.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, })..., ), }.withCachedRegoValue() diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go index 7d6e9d67c923c..3e6f7d1e330d5 100644 --- a/coderd/rbac/roles_test.go +++ b/coderd/rbac/roles_test.go @@ -849,37 +849,6 @@ func TestRolePermissions(t *testing.T) { }, }, }, - // Members may read their own chats. - { - Name: "CreateReadUpdateDeleteMyChats", - Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, - Resource: rbac.ResourceChat.WithOwner(currentUser.String()), - AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {memberMe, orgMemberMe, owner}, - false: { - userAdmin, orgUserAdmin, templateAdmin, - orgAuditor, orgTemplateAdmin, - otherOrgMember, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin, - orgAdmin, otherOrgAdmin, - }, - }, - }, - // Only owners can create, read, update, and delete other users' chats. - { - Name: "CreateReadUpdateDeleteOtherUserChats", - Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, - Resource: rbac.ResourceChat.WithOwner(uuid.NewString()), // some other user - AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner}, - false: { - memberMe, orgMemberMe, - userAdmin, orgUserAdmin, templateAdmin, - orgAuditor, orgTemplateAdmin, - otherOrgMember, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin, - orgAdmin, otherOrgAdmin, - }, - }, - }, } // We expect every permission to be tested above. diff --git a/codersdk/chat.go b/codersdk/chat.go deleted file mode 100644 index 2093adaff95e8..0000000000000 --- a/codersdk/chat.go +++ /dev/null @@ -1,153 +0,0 @@ -package codersdk - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "time" - - "github.com/google/uuid" - "github.com/kylecarbs/aisdk-go" - "golang.org/x/xerrors" -) - -// CreateChat creates a new chat. -func (c *Client) CreateChat(ctx context.Context) (Chat, error) { - res, err := c.Request(ctx, http.MethodPost, "/api/v2/chats", nil) - if err != nil { - return Chat{}, xerrors.Errorf("execute request: %w", err) - } - if res.StatusCode != http.StatusCreated { - return Chat{}, ReadBodyAsError(res) - } - defer res.Body.Close() - var chat Chat - return chat, json.NewDecoder(res.Body).Decode(&chat) -} - -type Chat struct { - ID uuid.UUID `json:"id" format:"uuid"` - CreatedAt time.Time `json:"created_at" format:"date-time"` - UpdatedAt time.Time `json:"updated_at" format:"date-time"` - Title string `json:"title"` -} - -// ListChats lists all chats. -func (c *Client) ListChats(ctx context.Context) ([]Chat, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/v2/chats", nil) - if err != nil { - return nil, xerrors.Errorf("execute request: %w", err) - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, ReadBodyAsError(res) - } - - var chats []Chat - return chats, json.NewDecoder(res.Body).Decode(&chats) -} - -// Chat returns a chat by ID. -func (c *Client) Chat(ctx context.Context, id uuid.UUID) (Chat, error) { - res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/chats/%s", id), nil) - if err != nil { - return Chat{}, xerrors.Errorf("execute request: %w", err) - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return Chat{}, ReadBodyAsError(res) - } - var chat Chat - return chat, json.NewDecoder(res.Body).Decode(&chat) -} - -// ChatMessages returns the messages of a chat. -func (c *Client) ChatMessages(ctx context.Context, id uuid.UUID) ([]ChatMessage, error) { - res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/chats/%s/messages", id), nil) - if err != nil { - return nil, xerrors.Errorf("execute request: %w", err) - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, ReadBodyAsError(res) - } - var messages []ChatMessage - return messages, json.NewDecoder(res.Body).Decode(&messages) -} - -type ChatMessage = aisdk.Message - -type CreateChatMessageRequest struct { - Model string `json:"model"` - Message ChatMessage `json:"message"` - Thinking bool `json:"thinking"` -} - -// CreateChatMessage creates a new chat message and streams the response. -// If the provided message has a conflicting ID with an existing message, -// it will be overwritten. -func (c *Client) CreateChatMessage(ctx context.Context, id uuid.UUID, req CreateChatMessageRequest) (<-chan aisdk.DataStreamPart, error) { - res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/chats/%s/messages", id), req) - defer func() { - if res != nil && res.Body != nil { - _ = res.Body.Close() - } - }() - if err != nil { - return nil, xerrors.Errorf("execute request: %w", err) - } - if res.StatusCode != http.StatusOK { - return nil, ReadBodyAsError(res) - } - nextEvent := ServerSentEventReader(ctx, res.Body) - - wc := make(chan aisdk.DataStreamPart, 256) - go func() { - defer close(wc) - defer res.Body.Close() - - for { - select { - case <-ctx.Done(): - return - default: - sse, err := nextEvent() - if err != nil { - return - } - if sse.Type != ServerSentEventTypeData { - continue - } - var part aisdk.DataStreamPart - b, ok := sse.Data.([]byte) - if !ok { - return - } - err = json.Unmarshal(b, &part) - if err != nil { - return - } - select { - case <-ctx.Done(): - return - case wc <- part: - } - } - } - }() - - return wc, nil -} - -func (c *Client) DeleteChat(ctx context.Context, id uuid.UUID) error { - res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/chats/%s", id), nil) - if err != nil { - return xerrors.Errorf("execute request: %w", err) - } - defer res.Body.Close() - if res.StatusCode != http.StatusNoContent { - return ReadBodyAsError(res) - } - return nil -} diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 19ec16c02cb22..26c50b555dbfe 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -383,7 +383,6 @@ type DeploymentValues struct { DisablePasswordAuth serpent.Bool `json:"disable_password_auth,omitempty" typescript:",notnull"` Support SupportConfig `json:"support,omitempty" typescript:",notnull"` ExternalAuthConfigs serpent.Struct[[]ExternalAuthConfig] `json:"external_auth,omitempty" typescript:",notnull"` - AI serpent.Struct[AIConfig] `json:"ai,omitempty" typescript:",notnull"` SSHConfig SSHConfig `json:"config_ssh,omitempty" typescript:",notnull"` WgtunnelHost serpent.String `json:"wgtunnel_host,omitempty" typescript:",notnull"` DisableOwnerWorkspaceExec serpent.Bool `json:"disable_owner_workspace_exec,omitempty" typescript:",notnull"` @@ -2681,15 +2680,6 @@ Write out the current server config as YAML to stdout.`, Value: &c.Support.Links, Hidden: false, }, - { - // Env handling is done in cli.ReadAIProvidersFromEnv - Name: "AI", - Description: "Configure AI providers.", - YAML: "ai", - Value: &c.AI, - // Hidden because this is experimental. - Hidden: true, - }, { // Env handling is done in cli.ReadGitAuthFromEnvironment Name: "External Auth Providers", @@ -3368,7 +3358,6 @@ const ( ExperimentWorkspaceUsage Experiment = "workspace-usage" // Enables the new workspace usage tracking. ExperimentWebPush Experiment = "web-push" // Enables web push notifications through the browser. ExperimentWorkspacePrebuilds Experiment = "workspace-prebuilds" // Enables the new workspace prebuilds feature. - ExperimentAgenticChat Experiment = "agentic-chat" // Enables the new agentic AI chat feature. ) // ExperimentsKnown should include all experiments defined above. @@ -3379,7 +3368,6 @@ var ExperimentsKnown = Experiments{ ExperimentWorkspaceUsage, ExperimentWebPush, ExperimentWorkspacePrebuilds, - ExperimentAgenticChat, } // ExperimentsSafe should include all experiments that are safe for @@ -3597,32 +3585,6 @@ func (c *Client) SSHConfiguration(ctx context.Context) (SSHConfigResponse, error return sshConfig, json.NewDecoder(res.Body).Decode(&sshConfig) } -type LanguageModelConfig struct { - Models []LanguageModel `json:"models"` -} - -// LanguageModel is a language model that can be used for chat. -type LanguageModel struct { - // ID is used by the provider to identify the LLM. - ID string `json:"id"` - DisplayName string `json:"display_name"` - // Provider is the provider of the LLM. e.g. openai, anthropic, etc. - Provider string `json:"provider"` -} - -func (c *Client) LanguageModelConfig(ctx context.Context) (LanguageModelConfig, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/v2/deployment/llms", nil) - if err != nil { - return LanguageModelConfig{}, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return LanguageModelConfig{}, ReadBodyAsError(res) - } - var llms LanguageModelConfig - return llms, json.NewDecoder(res.Body).Decode(&llms) -} - type CryptoKeyFeature string const ( diff --git a/codersdk/rbacresources_gen.go b/codersdk/rbacresources_gen.go index 1304218ad7bea..5ffcfed6b4c35 100644 --- a/codersdk/rbacresources_gen.go +++ b/codersdk/rbacresources_gen.go @@ -9,7 +9,6 @@ const ( ResourceAssignOrgRole RBACResource = "assign_org_role" ResourceAssignRole RBACResource = "assign_role" ResourceAuditLog RBACResource = "audit_log" - ResourceChat RBACResource = "chat" ResourceCryptoKey RBACResource = "crypto_key" ResourceDebugInfo RBACResource = "debug_info" ResourceDeploymentConfig RBACResource = "deployment_config" @@ -73,7 +72,6 @@ var RBACResourceActions = map[RBACResource][]RBACAction{ ResourceAssignOrgRole: {ActionAssign, ActionCreate, ActionDelete, ActionRead, ActionUnassign, ActionUpdate}, ResourceAssignRole: {ActionAssign, ActionRead, ActionUnassign}, ResourceAuditLog: {ActionCreate, ActionRead}, - ResourceChat: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, ResourceCryptoKey: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, ResourceDebugInfo: {ActionRead}, ResourceDeploymentConfig: {ActionRead, ActionUpdate}, diff --git a/docs/reference/api/chat.md b/docs/reference/api/chat.md deleted file mode 100644 index 4b5ad8c23adae..0000000000000 --- a/docs/reference/api/chat.md +++ /dev/null @@ -1,372 +0,0 @@ -# Chat - -## List chats - -### Code samples - -```shell -# Example request using curl -curl -X GET http://coder-server:8080/api/v2/chats \ - -H 'Accept: application/json' \ - -H 'Coder-Session-Token: API_KEY' -``` - -`GET /chats` - -### Example responses - -> 200 Response - -```json -[ - { - "created_at": "2019-08-24T14:15:22Z", - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "title": "string", - "updated_at": "2019-08-24T14:15:22Z" - } -] -``` - -### Responses - -| Status | Meaning | Description | Schema | -|--------|---------------------------------------------------------|-------------|---------------------------------------------------| -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.Chat](schemas.md#codersdkchat) | - -

Response Schema

- -Status Code **200** - -| Name | Type | Required | Restrictions | Description | -|----------------|-------------------|----------|--------------|-------------| -| `[array item]` | array | false | | | -| `» created_at` | string(date-time) | false | | | -| `» id` | string(uuid) | false | | | -| `» title` | string | false | | | -| `» updated_at` | string(date-time) | false | | | - -To perform this operation, you must be authenticated. [Learn more](authentication.md). - -## Create a chat - -### Code samples - -```shell -# Example request using curl -curl -X POST http://coder-server:8080/api/v2/chats \ - -H 'Accept: application/json' \ - -H 'Coder-Session-Token: API_KEY' -``` - -`POST /chats` - -### Example responses - -> 201 Response - -```json -{ - "created_at": "2019-08-24T14:15:22Z", - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "title": "string", - "updated_at": "2019-08-24T14:15:22Z" -} -``` - -### Responses - -| Status | Meaning | Description | Schema | -|--------|--------------------------------------------------------------|-------------|------------------------------------------| -| 201 | [Created](https://tools.ietf.org/html/rfc7231#section-6.3.2) | Created | [codersdk.Chat](schemas.md#codersdkchat) | - -To perform this operation, you must be authenticated. [Learn more](authentication.md). - -## Get a chat - -### Code samples - -```shell -# Example request using curl -curl -X GET http://coder-server:8080/api/v2/chats/{chat} \ - -H 'Accept: application/json' \ - -H 'Coder-Session-Token: API_KEY' -``` - -`GET /chats/{chat}` - -### Parameters - -| Name | In | Type | Required | Description | -|--------|------|--------|----------|-------------| -| `chat` | path | string | true | Chat ID | - -### Example responses - -> 200 Response - -```json -{ - "created_at": "2019-08-24T14:15:22Z", - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "title": "string", - "updated_at": "2019-08-24T14:15:22Z" -} -``` - -### Responses - -| Status | Meaning | Description | Schema | -|--------|---------------------------------------------------------|-------------|------------------------------------------| -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.Chat](schemas.md#codersdkchat) | - -To perform this operation, you must be authenticated. [Learn more](authentication.md). - -## Get chat messages - -### Code samples - -```shell -# Example request using curl -curl -X GET http://coder-server:8080/api/v2/chats/{chat}/messages \ - -H 'Accept: application/json' \ - -H 'Coder-Session-Token: API_KEY' -``` - -`GET /chats/{chat}/messages` - -### Parameters - -| Name | In | Type | Required | Description | -|--------|------|--------|----------|-------------| -| `chat` | path | string | true | Chat ID | - -### Example responses - -> 200 Response - -```json -[ - { - "annotations": [ - null - ], - "content": "string", - "createdAt": [ - 0 - ], - "experimental_attachments": [ - { - "contentType": "string", - "name": "string", - "url": "string" - } - ], - "id": "string", - "parts": [ - { - "data": [ - 0 - ], - "details": [ - { - "data": "string", - "signature": "string", - "text": "string", - "type": "string" - } - ], - "mimeType": "string", - "reasoning": "string", - "source": { - "contentType": "string", - "data": "string", - "metadata": { - "property1": null, - "property2": null - }, - "uri": "string" - }, - "text": "string", - "toolInvocation": { - "args": null, - "result": null, - "state": "call", - "step": 0, - "toolCallId": "string", - "toolName": "string" - }, - "type": "text" - } - ], - "role": "string" - } -] -``` - -### Responses - -| Status | Meaning | Description | Schema | -|--------|---------------------------------------------------------|-------------|---------------------------------------------------| -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [aisdk.Message](schemas.md#aisdkmessage) | - -

Response Schema

- -Status Code **200** - -| Name | Type | Required | Restrictions | Description | -|------------------------------|------------------------------------------------------------------|----------|--------------|-------------------------| -| `[array item]` | array | false | | | -| `» annotations` | array | false | | | -| `» content` | string | false | | | -| `» createdAt` | array | false | | | -| `» experimental_attachments` | array | false | | | -| `»» contentType` | string | false | | | -| `»» name` | string | false | | | -| `»» url` | string | false | | | -| `» id` | string | false | | | -| `» parts` | array | false | | | -| `»» data` | array | false | | | -| `»» details` | array | false | | | -| `»»» data` | string | false | | | -| `»»» signature` | string | false | | | -| `»»» text` | string | false | | | -| `»»» type` | string | false | | | -| `»» mimeType` | string | false | | Type: "file" | -| `»» reasoning` | string | false | | Type: "reasoning" | -| `»» source` | [aisdk.SourceInfo](schemas.md#aisdksourceinfo) | false | | Type: "source" | -| `»»» contentType` | string | false | | | -| `»»» data` | string | false | | | -| `»»» metadata` | object | false | | | -| `»»»» [any property]` | any | false | | | -| `»»» uri` | string | false | | | -| `»» text` | string | false | | Type: "text" | -| `»» toolInvocation` | [aisdk.ToolInvocation](schemas.md#aisdktoolinvocation) | false | | Type: "tool-invocation" | -| `»»» args` | any | false | | | -| `»»» result` | any | false | | | -| `»»» state` | [aisdk.ToolInvocationState](schemas.md#aisdktoolinvocationstate) | false | | | -| `»»» step` | integer | false | | | -| `»»» toolCallId` | string | false | | | -| `»»» toolName` | string | false | | | -| `»» type` | [aisdk.PartType](schemas.md#aisdkparttype) | false | | | -| `» role` | string | false | | | - -#### Enumerated Values - -| Property | Value | -|----------|-------------------| -| `state` | `call` | -| `state` | `partial-call` | -| `state` | `result` | -| `type` | `text` | -| `type` | `reasoning` | -| `type` | `tool-invocation` | -| `type` | `source` | -| `type` | `file` | -| `type` | `step-start` | - -To perform this operation, you must be authenticated. [Learn more](authentication.md). - -## Create a chat message - -### Code samples - -```shell -# Example request using curl -curl -X POST http://coder-server:8080/api/v2/chats/{chat}/messages \ - -H 'Content-Type: application/json' \ - -H 'Accept: application/json' \ - -H 'Coder-Session-Token: API_KEY' -``` - -`POST /chats/{chat}/messages` - -> Body parameter - -```json -{ - "message": { - "annotations": [ - null - ], - "content": "string", - "createdAt": [ - 0 - ], - "experimental_attachments": [ - { - "contentType": "string", - "name": "string", - "url": "string" - } - ], - "id": "string", - "parts": [ - { - "data": [ - 0 - ], - "details": [ - { - "data": "string", - "signature": "string", - "text": "string", - "type": "string" - } - ], - "mimeType": "string", - "reasoning": "string", - "source": { - "contentType": "string", - "data": "string", - "metadata": { - "property1": null, - "property2": null - }, - "uri": "string" - }, - "text": "string", - "toolInvocation": { - "args": null, - "result": null, - "state": "call", - "step": 0, - "toolCallId": "string", - "toolName": "string" - }, - "type": "text" - } - ], - "role": "string" - }, - "model": "string", - "thinking": true -} -``` - -### Parameters - -| Name | In | Type | Required | Description | -|--------|------|----------------------------------------------------------------------------------|----------|--------------| -| `chat` | path | string | true | Chat ID | -| `body` | body | [codersdk.CreateChatMessageRequest](schemas.md#codersdkcreatechatmessagerequest) | true | Request body | - -### Example responses - -> 200 Response - -```json -[ - null -] -``` - -### Responses - -| Status | Meaning | Description | Schema | -|--------|---------------------------------------------------------|-------------|--------------------| -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of undefined | - -

Response Schema

- -To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/reference/api/general.md b/docs/reference/api/general.md index 92ee1c60b554b..8f440c55b42d6 100644 --- a/docs/reference/api/general.md +++ b/docs/reference/api/general.md @@ -161,19 +161,6 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "user": {} }, "agent_stat_refresh_interval": 0, - "ai": { - "value": { - "providers": [ - { - "base_url": "string", - "models": [ - "string" - ], - "type": "string" - } - ] - } - }, "allow_workspace_renames": true, "autobuild_poll_interval": 0, "browser_only": true, @@ -586,43 +573,6 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ To perform this operation, you must be authenticated. [Learn more](authentication.md). -## Get language models - -### Code samples - -```shell -# Example request using curl -curl -X GET http://coder-server:8080/api/v2/deployment/llms \ - -H 'Accept: application/json' \ - -H 'Coder-Session-Token: API_KEY' -``` - -`GET /deployment/llms` - -### Example responses - -> 200 Response - -```json -{ - "models": [ - { - "display_name": "string", - "id": "string", - "provider": "string" - } - ] -} -``` - -### Responses - -| Status | Meaning | Description | Schema | -|--------|---------------------------------------------------------|-------------|------------------------------------------------------------------------| -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.LanguageModelConfig](schemas.md#codersdklanguagemodelconfig) | - -To perform this operation, you must be authenticated. [Learn more](authentication.md). - ## SSH Config ### Code samples diff --git a/docs/reference/api/members.md b/docs/reference/api/members.md index 40921e40b70ee..b19c859aa10c1 100644 --- a/docs/reference/api/members.md +++ b/docs/reference/api/members.md @@ -187,7 +187,6 @@ Status Code **200** | `resource_type` | `assign_org_role` | | `resource_type` | `assign_role` | | `resource_type` | `audit_log` | -| `resource_type` | `chat` | | `resource_type` | `crypto_key` | | `resource_type` | `debug_info` | | `resource_type` | `deployment_config` | @@ -357,7 +356,6 @@ Status Code **200** | `resource_type` | `assign_org_role` | | `resource_type` | `assign_role` | | `resource_type` | `audit_log` | -| `resource_type` | `chat` | | `resource_type` | `crypto_key` | | `resource_type` | `debug_info` | | `resource_type` | `deployment_config` | @@ -527,7 +525,6 @@ Status Code **200** | `resource_type` | `assign_org_role` | | `resource_type` | `assign_role` | | `resource_type` | `audit_log` | -| `resource_type` | `chat` | | `resource_type` | `crypto_key` | | `resource_type` | `debug_info` | | `resource_type` | `deployment_config` | @@ -666,7 +663,6 @@ Status Code **200** | `resource_type` | `assign_org_role` | | `resource_type` | `assign_role` | | `resource_type` | `audit_log` | -| `resource_type` | `chat` | | `resource_type` | `crypto_key` | | `resource_type` | `debug_info` | | `resource_type` | `deployment_config` | @@ -1027,7 +1023,6 @@ Status Code **200** | `resource_type` | `assign_org_role` | | `resource_type` | `assign_role` | | `resource_type` | `audit_log` | -| `resource_type` | `chat` | | `resource_type` | `crypto_key` | | `resource_type` | `debug_info` | | `resource_type` | `deployment_config` | diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 04075bd574d1a..e19c9d15da413 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -212,250 +212,6 @@ |--------------------| | `prebuild_claimed` | -## aisdk.Attachment - -```json -{ - "contentType": "string", - "name": "string", - "url": "string" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -|---------------|--------|----------|--------------|-------------| -| `contentType` | string | false | | | -| `name` | string | false | | | -| `url` | string | false | | | - -## aisdk.Message - -```json -{ - "annotations": [ - null - ], - "content": "string", - "createdAt": [ - 0 - ], - "experimental_attachments": [ - { - "contentType": "string", - "name": "string", - "url": "string" - } - ], - "id": "string", - "parts": [ - { - "data": [ - 0 - ], - "details": [ - { - "data": "string", - "signature": "string", - "text": "string", - "type": "string" - } - ], - "mimeType": "string", - "reasoning": "string", - "source": { - "contentType": "string", - "data": "string", - "metadata": { - "property1": null, - "property2": null - }, - "uri": "string" - }, - "text": "string", - "toolInvocation": { - "args": null, - "result": null, - "state": "call", - "step": 0, - "toolCallId": "string", - "toolName": "string" - }, - "type": "text" - } - ], - "role": "string" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -|----------------------------|-----------------------------------------------|----------|--------------|-------------| -| `annotations` | array of undefined | false | | | -| `content` | string | false | | | -| `createdAt` | array of integer | false | | | -| `experimental_attachments` | array of [aisdk.Attachment](#aisdkattachment) | false | | | -| `id` | string | false | | | -| `parts` | array of [aisdk.Part](#aisdkpart) | false | | | -| `role` | string | false | | | - -## aisdk.Part - -```json -{ - "data": [ - 0 - ], - "details": [ - { - "data": "string", - "signature": "string", - "text": "string", - "type": "string" - } - ], - "mimeType": "string", - "reasoning": "string", - "source": { - "contentType": "string", - "data": "string", - "metadata": { - "property1": null, - "property2": null - }, - "uri": "string" - }, - "text": "string", - "toolInvocation": { - "args": null, - "result": null, - "state": "call", - "step": 0, - "toolCallId": "string", - "toolName": "string" - }, - "type": "text" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -|------------------|---------------------------------------------------------|----------|--------------|-------------------------| -| `data` | array of integer | false | | | -| `details` | array of [aisdk.ReasoningDetail](#aisdkreasoningdetail) | false | | | -| `mimeType` | string | false | | Type: "file" | -| `reasoning` | string | false | | Type: "reasoning" | -| `source` | [aisdk.SourceInfo](#aisdksourceinfo) | false | | Type: "source" | -| `text` | string | false | | Type: "text" | -| `toolInvocation` | [aisdk.ToolInvocation](#aisdktoolinvocation) | false | | Type: "tool-invocation" | -| `type` | [aisdk.PartType](#aisdkparttype) | false | | | - -## aisdk.PartType - -```json -"text" -``` - -### Properties - -#### Enumerated Values - -| Value | -|-------------------| -| `text` | -| `reasoning` | -| `tool-invocation` | -| `source` | -| `file` | -| `step-start` | - -## aisdk.ReasoningDetail - -```json -{ - "data": "string", - "signature": "string", - "text": "string", - "type": "string" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -|-------------|--------|----------|--------------|-------------| -| `data` | string | false | | | -| `signature` | string | false | | | -| `text` | string | false | | | -| `type` | string | false | | | - -## aisdk.SourceInfo - -```json -{ - "contentType": "string", - "data": "string", - "metadata": { - "property1": null, - "property2": null - }, - "uri": "string" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -|--------------------|--------|----------|--------------|-------------| -| `contentType` | string | false | | | -| `data` | string | false | | | -| `metadata` | object | false | | | -| » `[any property]` | any | false | | | -| `uri` | string | false | | | - -## aisdk.ToolInvocation - -```json -{ - "args": null, - "result": null, - "state": "call", - "step": 0, - "toolCallId": "string", - "toolName": "string" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -|--------------|--------------------------------------------------------|----------|--------------|-------------| -| `args` | any | false | | | -| `result` | any | false | | | -| `state` | [aisdk.ToolInvocationState](#aisdktoolinvocationstate) | false | | | -| `step` | integer | false | | | -| `toolCallId` | string | false | | | -| `toolName` | string | false | | | - -## aisdk.ToolInvocationState - -```json -"call" -``` - -### Properties - -#### Enumerated Values - -| Value | -|----------------| -| `call` | -| `partial-call` | -| `result` | - ## coderd.SCIMUser ```json @@ -579,48 +335,6 @@ | `groups` | array of [codersdk.Group](#codersdkgroup) | false | | | | `users` | array of [codersdk.ReducedUser](#codersdkreduceduser) | false | | | -## codersdk.AIConfig - -```json -{ - "providers": [ - { - "base_url": "string", - "models": [ - "string" - ], - "type": "string" - } - ] -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -|-------------|-----------------------------------------------------------------|----------|--------------|-------------| -| `providers` | array of [codersdk.AIProviderConfig](#codersdkaiproviderconfig) | false | | | - -## codersdk.AIProviderConfig - -```json -{ - "base_url": "string", - "models": [ - "string" - ], - "type": "string" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -|------------|-----------------|----------|--------------|-----------------------------------------------------------| -| `base_url` | string | false | | Base URL is the base URL to use for the API provider. | -| `models` | array of string | false | | Models is the list of models to use for the API provider. | -| `type` | string | false | | Type is the type of the API provider. | - ## codersdk.APIKey ```json @@ -1354,97 +1068,6 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `one_time_passcode` | string | true | | | | `password` | string | true | | | -## codersdk.Chat - -```json -{ - "created_at": "2019-08-24T14:15:22Z", - "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", - "title": "string", - "updated_at": "2019-08-24T14:15:22Z" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -|--------------|--------|----------|--------------|-------------| -| `created_at` | string | false | | | -| `id` | string | false | | | -| `title` | string | false | | | -| `updated_at` | string | false | | | - -## codersdk.ChatMessage - -```json -{ - "annotations": [ - null - ], - "content": "string", - "createdAt": [ - 0 - ], - "experimental_attachments": [ - { - "contentType": "string", - "name": "string", - "url": "string" - } - ], - "id": "string", - "parts": [ - { - "data": [ - 0 - ], - "details": [ - { - "data": "string", - "signature": "string", - "text": "string", - "type": "string" - } - ], - "mimeType": "string", - "reasoning": "string", - "source": { - "contentType": "string", - "data": "string", - "metadata": { - "property1": null, - "property2": null - }, - "uri": "string" - }, - "text": "string", - "toolInvocation": { - "args": null, - "result": null, - "state": "call", - "step": 0, - "toolCallId": "string", - "toolName": "string" - }, - "type": "text" - } - ], - "role": "string" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -|----------------------------|-----------------------------------------------|----------|--------------|-------------| -| `annotations` | array of undefined | false | | | -| `content` | string | false | | | -| `createdAt` | array of integer | false | | | -| `experimental_attachments` | array of [aisdk.Attachment](#aisdkattachment) | false | | | -| `id` | string | false | | | -| `parts` | array of [aisdk.Part](#aisdkpart) | false | | | -| `role` | string | false | | | - ## codersdk.ConnectionLatency ```json @@ -1477,77 +1100,6 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `password` | string | true | | | | `to_type` | [codersdk.LoginType](#codersdklogintype) | true | | To type is the login type to convert to. | -## codersdk.CreateChatMessageRequest - -```json -{ - "message": { - "annotations": [ - null - ], - "content": "string", - "createdAt": [ - 0 - ], - "experimental_attachments": [ - { - "contentType": "string", - "name": "string", - "url": "string" - } - ], - "id": "string", - "parts": [ - { - "data": [ - 0 - ], - "details": [ - { - "data": "string", - "signature": "string", - "text": "string", - "type": "string" - } - ], - "mimeType": "string", - "reasoning": "string", - "source": { - "contentType": "string", - "data": "string", - "metadata": { - "property1": null, - "property2": null - }, - "uri": "string" - }, - "text": "string", - "toolInvocation": { - "args": null, - "result": null, - "state": "call", - "step": 0, - "toolCallId": "string", - "toolName": "string" - }, - "type": "text" - } - ], - "role": "string" - }, - "model": "string", - "thinking": true -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -|------------|----------------------------------------------|----------|--------------|-------------| -| `message` | [codersdk.ChatMessage](#codersdkchatmessage) | false | | | -| `model` | string | false | | | -| `thinking` | boolean | false | | | - ## codersdk.CreateFirstUserRequest ```json @@ -1812,52 +1364,12 @@ This is required on creation to enable a user-flow of validating a template work ## codersdk.CreateTestAuditLogRequest ```json -{ - "action": "create", - "additional_fields": [ - 0 - ], - "build_reason": "autostart", - "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", - "request_id": "266ea41d-adf5-480b-af50-15b940c2b846", - "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", - "resource_type": "template", - "time": "2019-08-24T14:15:22Z" -} +{} ``` ### Properties -| Name | Type | Required | Restrictions | Description | -|---------------------|------------------------------------------------|----------|--------------|-------------| -| `action` | [codersdk.AuditAction](#codersdkauditaction) | false | | | -| `additional_fields` | array of integer | false | | | -| `build_reason` | [codersdk.BuildReason](#codersdkbuildreason) | false | | | -| `organization_id` | string | false | | | -| `request_id` | string | false | | | -| `resource_id` | string | false | | | -| `resource_type` | [codersdk.ResourceType](#codersdkresourcetype) | false | | | -| `time` | string | false | | | - -#### Enumerated Values - -| Property | Value | -|-----------------|--------------------| -| `action` | `create` | -| `action` | `write` | -| `action` | `delete` | -| `action` | `start` | -| `action` | `stop` | -| `build_reason` | `autostart` | -| `build_reason` | `autostop` | -| `build_reason` | `initiator` | -| `resource_type` | `template` | -| `resource_type` | `template_version` | -| `resource_type` | `user` | -| `resource_type` | `workspace` | -| `resource_type` | `workspace_build` | -| `resource_type` | `git_ssh_key` | -| `resource_type` | `auditable_group` | +None ## codersdk.CreateTokenRequest @@ -2328,19 +1840,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "user": {} }, "agent_stat_refresh_interval": 0, - "ai": { - "value": { - "providers": [ - { - "base_url": "string", - "models": [ - "string" - ], - "type": "string" - } - ] - } - }, "allow_workspace_renames": true, "autobuild_poll_interval": 0, "browser_only": true, @@ -2829,19 +2328,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "user": {} }, "agent_stat_refresh_interval": 0, - "ai": { - "value": { - "providers": [ - { - "base_url": "string", - "models": [ - "string" - ], - "type": "string" - } - ] - } - }, "allow_workspace_renames": true, "autobuild_poll_interval": 0, "browser_only": true, @@ -3221,7 +2707,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `address` | [serpent.HostPort](#serpenthostport) | false | | Deprecated: Use HTTPAddress or TLS.Address instead. | | `agent_fallback_troubleshooting_url` | [serpent.URL](#serpenturl) | false | | | | `agent_stat_refresh_interval` | integer | false | | | -| `ai` | [serpent.Struct-codersdk_AIConfig](#serpentstruct-codersdk_aiconfig) | false | | | | `allow_workspace_renames` | boolean | false | | | | `autobuild_poll_interval` | integer | false | | | | `browser_only` | boolean | false | | | @@ -3512,7 +2997,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `workspace-usage` | | `web-push` | | `workspace-prebuilds` | -| `agentic-chat` | ## codersdk.ExternalAuth @@ -4152,44 +3636,6 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith |-------------------------------| | `REQUIRED_TEMPLATE_VARIABLES` | -## codersdk.LanguageModel - -```json -{ - "display_name": "string", - "id": "string", - "provider": "string" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -|----------------|--------|----------|--------------|-------------------------------------------------------------------| -| `display_name` | string | false | | | -| `id` | string | false | | ID is used by the provider to identify the LLM. | -| `provider` | string | false | | Provider is the provider of the LLM. e.g. openai, anthropic, etc. | - -## codersdk.LanguageModelConfig - -```json -{ - "models": [ - { - "display_name": "string", - "id": "string", - "provider": "string" - } - ] -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -|----------|-----------------------------------------------------------|----------|--------------|-------------| -| `models` | array of [codersdk.LanguageModel](#codersdklanguagemodel) | false | | | - ## codersdk.License ```json @@ -6307,7 +5753,6 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | `assign_org_role` | | `assign_role` | | `audit_log` | -| `chat` | | `crypto_key` | | `debug_info` | | `deployment_config` | @@ -12269,30 +11714,6 @@ None |---------|-----------------------------------------------------|----------|--------------|-------------| | `value` | array of [codersdk.LinkConfig](#codersdklinkconfig) | false | | | -## serpent.Struct-codersdk_AIConfig - -```json -{ - "value": { - "providers": [ - { - "base_url": "string", - "models": [ - "string" - ], - "type": "string" - } - ] - } -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -|---------|----------------------------------------|----------|--------------|-------------| -| `value` | [codersdk.AIConfig](#codersdkaiconfig) | false | | | - ## serpent.URL ```json diff --git a/go.mod b/go.mod index 12deb9bab3745..22fb1cd7f3aa6 100644 --- a/go.mod +++ b/go.mod @@ -481,14 +481,11 @@ require ( ) require ( - github.com/anthropics/anthropic-sdk-go v0.2.0-beta.3 github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225 github.com/coder/preview v1.0.1 github.com/fsnotify/fsnotify v1.9.0 github.com/kylecarbs/aisdk-go v0.0.8 github.com/mark3labs/mcp-go v0.32.0 - github.com/openai/openai-go v0.1.0-beta.10 - google.golang.org/genai v0.7.0 ) require ( @@ -505,6 +502,7 @@ require ( github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0 // indirect github.com/Masterminds/semver/v3 v3.3.1 // indirect + github.com/anthropics/anthropic-sdk-go v0.2.0-beta.3 // indirect github.com/aquasecurity/go-version v0.0.1 // indirect github.com/aquasecurity/trivy v0.58.2 // indirect github.com/aws/aws-sdk-go v1.55.7 // indirect @@ -522,6 +520,7 @@ require ( github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect + github.com/openai/openai-go v0.1.0-beta.10 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect github.com/samber/lo v1.50.0 // indirect @@ -536,5 +535,6 @@ require ( go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect + google.golang.org/genai v0.7.0 // indirect k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect ) diff --git a/site/package.json b/site/package.json index 7f63035231d69..a3d06d1d44842 100644 --- a/site/package.json +++ b/site/package.json @@ -34,8 +34,6 @@ "update-emojis": "cp -rf ./node_modules/emoji-datasource-apple/img/apple/64/* ./static/emojis" }, "dependencies": { - "@ai-sdk/provider-utils": "2.2.6", - "@ai-sdk/react": "1.2.6", "@emoji-mart/data": "1.2.1", "@emoji-mart/react": "1.1.1", "@emotion/cache": "11.14.0", @@ -110,7 +108,6 @@ "react-virtualized-auto-sizer": "1.0.24", "react-window": "1.8.11", "recharts": "2.15.0", - "rehype-raw": "7.0.0", "remark-gfm": "4.0.0", "resize-observer-polyfill": "1.5.1", "semver": "7.6.2", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index e626209d2c754..7a6f81b402621 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -16,12 +16,6 @@ importers: .: dependencies: - '@ai-sdk/provider-utils': - specifier: 2.2.6 - version: 2.2.6(zod@3.24.3) - '@ai-sdk/react': - specifier: 1.2.6 - version: 1.2.6(react@18.3.1)(zod@3.24.3) '@emoji-mart/data': specifier: 1.2.1 version: 1.2.1 @@ -244,9 +238,6 @@ importers: recharts: specifier: 2.15.0 version: 2.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - rehype-raw: - specifier: 7.0.0 - version: 7.0.0 remark-gfm: specifier: 4.0.0 version: 4.0.0 @@ -492,42 +483,6 @@ packages: '@adobe/css-tools@4.4.1': resolution: {integrity: sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ==, tarball: https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.1.tgz} - '@ai-sdk/provider-utils@2.2.4': - resolution: {integrity: sha512-13sEGBxB6kgaMPGOgCLYibF6r8iv8mgjhuToFrOTU09bBxbFQd8ZoARarCfJN6VomCUbUvMKwjTBLb1vQnN+WA==, tarball: https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.4.tgz} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.23.8 - - '@ai-sdk/provider-utils@2.2.6': - resolution: {integrity: sha512-sUlZ7Gnq84DCGWMQRIK8XVbkzIBnvPR1diV4v6JwPgpn5armnLI/j+rqn62MpLrU5ZCQZlDKl/Lw6ed3ulYqaA==, tarball: https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.6.tgz} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.23.8 - - '@ai-sdk/provider@1.1.0': - resolution: {integrity: sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew==, tarball: https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.0.tgz} - engines: {node: '>=18'} - - '@ai-sdk/provider@1.1.2': - resolution: {integrity: sha512-ITdgNilJZwLKR7X5TnUr1BsQW6UTX5yFp0h66Nfx8XjBYkWD9W3yugr50GOz3CnE9m/U/Cd5OyEbTMI0rgi6ZQ==, tarball: https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.2.tgz} - engines: {node: '>=18'} - - '@ai-sdk/react@1.2.6': - resolution: {integrity: sha512-5BFChNbcYtcY9MBStcDev7WZRHf0NpTrk8yfSoedWctB3jfWkFd1HECBvdc8w3mUQshF2MumLHtAhRO7IFtGGQ==, tarball: https://registry.npmjs.org/@ai-sdk/react/-/react-1.2.6.tgz} - engines: {node: '>=18'} - peerDependencies: - react: ^18 || ^19 || ^19.0.0-rc - zod: ^3.23.8 - peerDependenciesMeta: - zod: - optional: true - - '@ai-sdk/ui-utils@1.2.5': - resolution: {integrity: sha512-XDgqnJcaCkDez7qolvk+PDbs/ceJvgkNkxkOlc9uDWqxfDJxtvCZ+14MP/1qr4IBwGIgKVHzMDYDXvqVhSWLzg==, tarball: https://registry.npmjs.org/@ai-sdk/ui-utils/-/ui-utils-1.2.5.tgz} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.23.8 - '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==, tarball: https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz} engines: {node: '>=10'} @@ -4030,33 +3985,18 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==, tarball: https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz} engines: {node: '>= 0.4'} - hast-util-from-parse5@8.0.3: - resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==, tarball: https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz} - hast-util-parse-selector@2.2.5: resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==, tarball: https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz} - hast-util-parse-selector@4.0.0: - resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==, tarball: https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz} - - hast-util-raw@9.1.0: - resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==, tarball: https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz} - hast-util-to-jsx-runtime@2.3.2: resolution: {integrity: sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==, tarball: https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.2.tgz} - hast-util-to-parse5@8.0.0: - resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==, tarball: https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz} - hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==, tarball: https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz} hastscript@6.0.0: resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==, tarball: https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz} - hastscript@9.0.1: - resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==, tarball: https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz} - headers-polyfill@4.0.3: resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==, tarball: https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz} @@ -4079,9 +4019,6 @@ packages: html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==, tarball: https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz} - html-void-elements@3.0.0: - resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==, tarball: https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz} - http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==, tarball: https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz} engines: {node: '>= 0.8'} @@ -4585,9 +4522,6 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==, tarball: https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz} - json-schema@0.4.0: - resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==, tarball: https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz} - json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==, tarball: https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz} @@ -5348,9 +5282,6 @@ packages: property-information@6.5.0: resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==, tarball: https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz} - property-information@7.0.0: - resolution: {integrity: sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==, tarball: https://registry.npmjs.org/property-information/-/property-information-7.0.0.tgz} - protobufjs@7.4.0: resolution: {integrity: sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==, tarball: https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz} engines: {node: '>=12.0.0'} @@ -5611,9 +5542,6 @@ packages: resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==, tarball: https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz} engines: {node: '>= 0.4'} - rehype-raw@7.0.0: - resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==, tarball: https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz} - remark-gfm@4.0.0: resolution: {integrity: sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==, tarball: https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz} @@ -5718,9 +5646,6 @@ packages: scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==, tarball: https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz} - secure-json-parse@2.7.0: - resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==, tarball: https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz} - semver@7.6.2: resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==, tarball: https://registry.npmjs.org/semver/-/semver-7.6.2.tgz} engines: {node: '>=10'} @@ -5958,11 +5883,6 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==, tarball: https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz} engines: {node: '>= 0.4'} - swr@2.3.3: - resolution: {integrity: sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==, tarball: https://registry.npmjs.org/swr/-/swr-2.3.3.tgz} - peerDependencies: - react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==, tarball: https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz} @@ -6000,10 +5920,6 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==, tarball: https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz} - throttleit@2.1.0: - resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==, tarball: https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz} - engines: {node: '>=18'} - tiny-case@1.0.3: resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==, tarball: https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz} @@ -6309,9 +6225,6 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==, tarball: https://registry.npmjs.org/vary/-/vary-1.1.2.tgz} engines: {node: '>= 0.8'} - vfile-location@5.0.3: - resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==, tarball: https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz} - vfile-message@4.0.2: resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==, tarball: https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz} @@ -6411,9 +6324,6 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==, tarball: https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz} - web-namespaces@2.0.1: - resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==, tarball: https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz} - webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==, tarball: https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz} engines: {node: '>=12'} @@ -6545,11 +6455,6 @@ packages: yup@1.6.1: resolution: {integrity: sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==, tarball: https://registry.npmjs.org/yup/-/yup-1.6.1.tgz} - zod-to-json-schema@3.24.5: - resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==, tarball: https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz} - peerDependencies: - zod: ^3.24.1 - zod-validation-error@3.4.0: resolution: {integrity: sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==, tarball: https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.4.0.tgz} engines: {node: '>=18.0.0'} @@ -6569,45 +6474,6 @@ snapshots: '@adobe/css-tools@4.4.1': {} - '@ai-sdk/provider-utils@2.2.4(zod@3.24.3)': - dependencies: - '@ai-sdk/provider': 1.1.0 - nanoid: 3.3.8 - secure-json-parse: 2.7.0 - zod: 3.24.3 - - '@ai-sdk/provider-utils@2.2.6(zod@3.24.3)': - dependencies: - '@ai-sdk/provider': 1.1.2 - nanoid: 3.3.8 - secure-json-parse: 2.7.0 - zod: 3.24.3 - - '@ai-sdk/provider@1.1.0': - dependencies: - json-schema: 0.4.0 - - '@ai-sdk/provider@1.1.2': - dependencies: - json-schema: 0.4.0 - - '@ai-sdk/react@1.2.6(react@18.3.1)(zod@3.24.3)': - dependencies: - '@ai-sdk/provider-utils': 2.2.4(zod@3.24.3) - '@ai-sdk/ui-utils': 1.2.5(zod@3.24.3) - react: 18.3.1 - swr: 2.3.3(react@18.3.1) - throttleit: 2.1.0 - optionalDependencies: - zod: 3.24.3 - - '@ai-sdk/ui-utils@1.2.5(zod@3.24.3)': - dependencies: - '@ai-sdk/provider': 1.1.0 - '@ai-sdk/provider-utils': 2.2.4(zod@3.24.3) - zod: 3.24.3 - zod-to-json-schema: 3.24.5(zod@3.24.3) - '@alloc/quick-lru@5.2.0': {} '@ampproject/remapping@2.3.0': @@ -10430,39 +10296,8 @@ snapshots: dependencies: function-bind: 1.1.2 - hast-util-from-parse5@8.0.3: - dependencies: - '@types/hast': 3.0.4 - '@types/unist': 3.0.3 - devlop: 1.1.0 - hastscript: 9.0.1 - property-information: 7.0.0 - vfile: 6.0.3 - vfile-location: 5.0.3 - web-namespaces: 2.0.1 - hast-util-parse-selector@2.2.5: {} - hast-util-parse-selector@4.0.0: - dependencies: - '@types/hast': 3.0.4 - - hast-util-raw@9.1.0: - dependencies: - '@types/hast': 3.0.4 - '@types/unist': 3.0.3 - '@ungap/structured-clone': 1.3.0 - hast-util-from-parse5: 8.0.3 - hast-util-to-parse5: 8.0.0 - html-void-elements: 3.0.0 - mdast-util-to-hast: 13.2.0 - parse5: 7.1.2 - unist-util-position: 5.0.0 - unist-util-visit: 5.0.0 - vfile: 6.0.3 - web-namespaces: 2.0.1 - zwitch: 2.0.4 - hast-util-to-jsx-runtime@2.3.2: dependencies: '@types/estree': 1.0.6 @@ -10483,16 +10318,6 @@ snapshots: transitivePeerDependencies: - supports-color - hast-util-to-parse5@8.0.0: - dependencies: - '@types/hast': 3.0.4 - comma-separated-tokens: 2.0.3 - devlop: 1.1.0 - property-information: 6.5.0 - space-separated-tokens: 2.0.2 - web-namespaces: 2.0.1 - zwitch: 2.0.4 - hast-util-whitespace@3.0.0: dependencies: '@types/hast': 3.0.4 @@ -10505,14 +10330,6 @@ snapshots: property-information: 5.6.0 space-separated-tokens: 1.1.5 - hastscript@9.0.1: - dependencies: - '@types/hast': 3.0.4 - comma-separated-tokens: 2.0.3 - hast-util-parse-selector: 4.0.0 - property-information: 7.0.0 - space-separated-tokens: 2.0.2 - headers-polyfill@4.0.3: {} highlight.js@10.7.3: {} @@ -10531,8 +10348,6 @@ snapshots: html-url-attributes@3.0.1: {} - html-void-elements@3.0.0: {} - http-errors@2.0.0: dependencies: depd: 2.0.0 @@ -11260,8 +11075,6 @@ snapshots: json-schema-traverse@0.4.1: optional: true - json-schema@0.4.0: {} - json-stable-stringify-without-jsonify@1.0.1: optional: true @@ -12295,8 +12108,6 @@ snapshots: property-information@6.5.0: {} - property-information@7.0.0: {} - protobufjs@7.4.0: dependencies: '@protobufjs/aspromise': 1.1.2 @@ -12620,12 +12431,6 @@ snapshots: define-properties: 1.2.1 set-function-name: 2.0.1 - rehype-raw@7.0.0: - dependencies: - '@types/hast': 3.0.4 - hast-util-raw: 9.1.0 - vfile: 6.0.3 - remark-gfm@4.0.0: dependencies: '@types/mdast': 4.0.3 @@ -12763,8 +12568,6 @@ snapshots: dependencies: loose-envify: 1.4.0 - secure-json-parse@2.7.0: {} - semver@7.6.2: {} send@0.19.0: @@ -13014,12 +12817,6 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - swr@2.3.3(react@18.3.1): - dependencies: - dequal: 2.0.3 - react: 18.3.1 - use-sync-external-store: 1.4.0(react@18.3.1) - symbol-tree@3.2.4: {} tailwind-merge@2.6.0: {} @@ -13078,8 +12875,6 @@ snapshots: dependencies: any-promise: 1.3.0 - throttleit@2.1.0: {} - tiny-case@1.0.3: {} tiny-invariant@1.3.3: {} @@ -13376,11 +13171,6 @@ snapshots: vary@1.1.2: {} - vfile-location@5.0.3: - dependencies: - '@types/unist': 3.0.3 - vfile: 6.0.3 - vfile-message@4.0.2: dependencies: '@types/unist': 3.0.3 @@ -13456,8 +13246,6 @@ snapshots: dependencies: defaults: 1.0.4 - web-namespaces@2.0.1: {} - webidl-conversions@7.0.0: {} webpack-sources@3.2.3: {} @@ -13572,10 +13360,6 @@ snapshots: toposort: 2.0.2 type-fest: 2.19.0 - zod-to-json-schema@3.24.5(zod@3.24.3): - dependencies: - zod: 3.24.3 - zod-validation-error@3.4.0(zod@3.24.3): dependencies: zod: 3.24.3 diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 35e60de6aeca5..458e93b32cdbe 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -818,13 +818,6 @@ class ApiMethods { return response.data; }; - getDeploymentLLMs = async (): Promise => { - const response = await this.axios.get( - "/api/v2/deployment/llms", - ); - return response.data; - }; - getOrganizationIdpSyncClaimFieldValues = async ( organization: string, field: string, @@ -2584,23 +2577,6 @@ class ApiMethods { markAllInboxNotificationsAsRead = async () => { await this.axios.put("/api/v2/notifications/inbox/mark-all-as-read"); }; - - createChat = async () => { - const res = await this.axios.post("/api/v2/chats"); - return res.data; - }; - - getChats = async () => { - const res = await this.axios.get("/api/v2/chats"); - return res.data; - }; - - getChatMessages = async (chatId: string) => { - const res = await this.axios.get( - `/api/v2/chats/${chatId}/messages`, - ); - return res.data; - }; } // Experimental API methods call endpoints under the /api/experimental/ prefix. diff --git a/site/src/api/queries/chats.ts b/site/src/api/queries/chats.ts deleted file mode 100644 index d23f672f9cfaf..0000000000000 --- a/site/src/api/queries/chats.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { API } from "api/api"; -import type { QueryClient } from "react-query"; - -export const createChat = (queryClient: QueryClient) => { - return { - mutationFn: API.createChat, - onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: ["chats"] }); - }, - }; -}; - -export const getChats = () => { - return { - queryKey: ["chats"], - queryFn: API.getChats, - }; -}; - -export const getChatMessages = (chatID: string) => { - return { - queryKey: ["chatMessages", chatID], - queryFn: () => API.getChatMessages(chatID), - }; -}; diff --git a/site/src/api/queries/deployment.ts b/site/src/api/queries/deployment.ts index 4d1610bd57b46..17777bf09c4ec 100644 --- a/site/src/api/queries/deployment.ts +++ b/site/src/api/queries/deployment.ts @@ -39,10 +39,3 @@ export const deploymentIdpSyncFieldValues = (field: string) => { queryFn: () => API.getDeploymentIdpSyncFieldValues(field), }; }; - -export const deploymentLanguageModels = () => { - return { - queryKey: ["deployment", "llms"], - queryFn: API.getDeploymentLLMs, - }; -}; diff --git a/site/src/api/rbacresourcesGenerated.ts b/site/src/api/rbacresourcesGenerated.ts index 3ec6a3accee32..de09b245ff049 100644 --- a/site/src/api/rbacresourcesGenerated.ts +++ b/site/src/api/rbacresourcesGenerated.ts @@ -31,12 +31,6 @@ export const RBACResourceActions: Partial< create: "create new audit log entries", read: "read audit logs", }, - chat: { - create: "create a chat", - delete: "delete a chat", - read: "read a chat", - update: "update a chat", - }, crypto_key: { create: "create crypto keys", delete: "delete crypto keys", diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 4a7280849df18..730b9c54ac2b7 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -311,28 +311,6 @@ export interface ChangePasswordWithOneTimePasscodeRequest { readonly one_time_passcode: string; } -// From codersdk/chat.go -export interface Chat { - readonly id: string; - readonly created_at: string; - readonly updated_at: string; - readonly title: string; -} - -// From codersdk/chat.go -export interface ChatMessage { - readonly id: string; - readonly createdAt?: Record; - readonly content: string; - readonly role: string; - // external type "github.com/kylecarbs/aisdk-go.Part", to include this type the package must be explicitly included in the parsing - readonly parts?: readonly unknown[]; - // empty interface{} type, falling back to unknown - readonly annotations?: readonly unknown[]; - // external type "github.com/kylecarbs/aisdk-go.Attachment", to include this type the package must be explicitly included in the parsing - readonly experimental_attachments?: readonly unknown[]; -} - // From codersdk/client.go export const CoderDesktopTelemetryHeader = "Coder-Desktop-Telemetry"; @@ -354,14 +332,6 @@ export interface ConvertLoginRequest { readonly password: string; } -// From codersdk/chat.go -export interface CreateChatMessageRequest { - readonly model: string; - // external type "github.com/kylecarbs/aisdk-go.Message", to include this type the package must be explicitly included in the parsing - readonly message: unknown; - readonly thinking: boolean; -} - // From codersdk/users.go export interface CreateFirstUserRequest { readonly email: string; @@ -726,7 +696,6 @@ export interface DeploymentValues { readonly disable_password_auth?: boolean; readonly support?: SupportConfig; readonly external_auth?: SerpentStruct; - readonly ai?: SerpentStruct; readonly config_ssh?: SSHConfig; readonly wgtunnel_host?: string; readonly disable_owner_workspace_exec?: boolean; @@ -834,7 +803,6 @@ export const EntitlementsWarningHeader = "X-Coder-Entitlements-Warning"; // From codersdk/deployment.go export type Experiment = - | "agentic-chat" | "auto-fill-parameters" | "example" | "notifications" @@ -843,7 +811,6 @@ export type Experiment = | "workspace-usage"; export const Experiments: Experiment[] = [ - "agentic-chat", "auto-fill-parameters", "example", "notifications", @@ -1259,18 +1226,6 @@ export type JobErrorCode = "REQUIRED_TEMPLATE_VARIABLES"; export const JobErrorCodes: JobErrorCode[] = ["REQUIRED_TEMPLATE_VARIABLES"]; -// From codersdk/deployment.go -export interface LanguageModel { - readonly id: string; - readonly display_name: string; - readonly provider: string; -} - -// From codersdk/deployment.go -export interface LanguageModelConfig { - readonly models: readonly LanguageModel[]; -} - // From codersdk/licenses.go export interface License { readonly id: number; @@ -2186,7 +2141,6 @@ export type RBACResource = | "assign_org_role" | "assign_role" | "audit_log" - | "chat" | "crypto_key" | "debug_info" | "deployment_config" @@ -2226,7 +2180,6 @@ export const RBACResources: RBACResource[] = [ "assign_org_role", "assign_role", "audit_log", - "chat", "crypto_key", "debug_info", "deployment_config", diff --git a/site/src/contexts/useAgenticChat.ts b/site/src/contexts/useAgenticChat.ts deleted file mode 100644 index 97194b4512340..0000000000000 --- a/site/src/contexts/useAgenticChat.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { experiments } from "api/queries/experiments"; - -import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; -import { useQuery } from "react-query"; - -interface AgenticChat { - readonly enabled: boolean; -} - -export const useAgenticChat = (): AgenticChat => { - const { metadata } = useEmbeddedMetadata(); - const enabledExperimentsQuery = useQuery(experiments(metadata.experiments)); - return { - enabled: enabledExperimentsQuery.data?.includes("agentic-chat") ?? false, - }; -}; diff --git a/site/src/modules/dashboard/Navbar/NavbarView.tsx b/site/src/modules/dashboard/Navbar/NavbarView.tsx index 8ef245cb13182..d83b0e8b694a4 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.tsx @@ -4,7 +4,6 @@ import { Button } from "components/Button/Button"; import { ExternalImage } from "components/ExternalImage/ExternalImage"; import { CoderIcon } from "components/Icons/CoderIcon"; import type { ProxyContextValue } from "contexts/ProxyContext"; -import { useAgenticChat } from "contexts/useAgenticChat"; import { useWebpushNotifications } from "contexts/useWebpushNotifications"; import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; import { NotificationsInbox } from "modules/notifications/NotificationsInbox/NotificationsInbox"; @@ -141,7 +140,6 @@ interface NavItemsProps { const NavItems: FC = ({ className }) => { const location = useLocation(); - const agenticChat = useAgenticChat(); const { metadata } = useEmbeddedMetadata(); return ( @@ -165,16 +163,6 @@ const NavItems: FC = ({ className }) => { > Templates - {agenticChat.enabled && ( - { - return cn(linkStyles.default, isActive ? linkStyles.active : ""); - }} - to="/chat" - > - Chat - - )} {metadata["tasks-tab-visible"].value && ( { diff --git a/site/src/pages/ChatPage/ChatLanding.tsx b/site/src/pages/ChatPage/ChatLanding.tsx deleted file mode 100644 index 2902ae8663da5..0000000000000 --- a/site/src/pages/ChatPage/ChatLanding.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { useTheme } from "@emotion/react"; -import IconButton from "@mui/material/IconButton"; -import Paper from "@mui/material/Paper"; -import Stack from "@mui/material/Stack"; -import TextField from "@mui/material/TextField"; -import { createChat } from "api/queries/chats"; -import type { Chat } from "api/typesGenerated"; -import { Button } from "components/Button/Button"; -import { Margins } from "components/Margins/Margins"; -import { useAuthenticated } from "hooks"; -import { SendIcon } from "lucide-react"; -import { type FC, type FormEvent, useState } from "react"; -import { useMutation, useQueryClient } from "react-query"; -import { useNavigate } from "react-router-dom"; -import { LanguageModelSelector } from "./LanguageModelSelector"; - -export interface ChatLandingLocationState { - chat: Chat; - message: string; -} - -const ChatLanding: FC = () => { - const { user } = useAuthenticated(); - const theme = useTheme(); - const [input, setInput] = useState(""); - const navigate = useNavigate(); - const queryClient = useQueryClient(); - const createChatMutation = useMutation(createChat(queryClient)); - - return ( - -
- {/* Initial Welcome Message Area */} -
-

- Good evening, {(user.name ?? user.username).split(" ")[0]} -

-

- How can I help you today? -

-
- - {/* Input Form and Suggestions - Always Visible */} -
- - - - - - - ) => { - e.preventDefault(); - setInput(""); - const chat = await createChatMutation.mutateAsync(); - navigate(`/chat/${chat.id}`, { - state: { - chat, - message: input, - }, - }); - }} - elevation={2} - css={{ - padding: "16px", - display: "flex", - alignItems: "center", - width: "100%", - borderRadius: "12px", - border: `1px solid ${theme.palette.divider}`, - }} - > - ) => { - setInput(event.target.value); - }} - placeholder="Ask Coder..." - required - fullWidth - variant="outlined" - multiline - maxRows={5} - css={{ - marginRight: theme.spacing(1), - "& .MuiOutlinedInput-root": { - borderRadius: "8px", - padding: "10px 14px", - }, - }} - autoFocus - /> - - - - -
-
-
- ); -}; - -export default ChatLanding; diff --git a/site/src/pages/ChatPage/ChatLayout.tsx b/site/src/pages/ChatPage/ChatLayout.tsx deleted file mode 100644 index 9e252764ea234..0000000000000 --- a/site/src/pages/ChatPage/ChatLayout.tsx +++ /dev/null @@ -1,242 +0,0 @@ -import { useTheme } from "@emotion/react"; -import List from "@mui/material/List"; -import ListItem from "@mui/material/ListItem"; -import ListItemButton from "@mui/material/ListItemButton"; -import ListItemText from "@mui/material/ListItemText"; -import Paper from "@mui/material/Paper"; -import { createChat, getChats } from "api/queries/chats"; -import { deploymentLanguageModels } from "api/queries/deployment"; -import type { LanguageModelConfig } from "api/typesGenerated"; -import { ErrorAlert } from "components/Alert/ErrorAlert"; -import { Button } from "components/Button/Button"; -import { Loader } from "components/Loader/Loader"; -import { Margins } from "components/Margins/Margins"; -import { useAgenticChat } from "contexts/useAgenticChat"; -import { PlusIcon } from "lucide-react"; -import { - type FC, - type PropsWithChildren, - createContext, - useContext, - useEffect, - useState, -} from "react"; -import { useMutation, useQuery, useQueryClient } from "react-query"; -import { Link, Outlet, useNavigate, useParams } from "react-router-dom"; - -interface ChatContext { - selectedModel: string; - modelConfig: LanguageModelConfig; - - setSelectedModel: (model: string) => void; -} -export const useChatContext = (): ChatContext => { - const context = useContext(ChatContext); - if (!context) { - throw new Error("useChatContext must be used within a ChatProvider"); - } - return context; -}; - -const ChatContext = createContext(undefined); - -const SELECTED_MODEL_KEY = "coder_chat_selected_model"; - -const ChatProvider: FC = ({ children }) => { - const [selectedModel, setSelectedModel] = useState(() => { - const savedModel = localStorage.getItem(SELECTED_MODEL_KEY); - return savedModel || ""; - }); - const modelConfigQuery = useQuery(deploymentLanguageModels()); - useEffect(() => { - if (!modelConfigQuery.data) { - return; - } - if (selectedModel === "") { - const firstModel = modelConfigQuery.data.models[0]?.id; // Handle empty models array - if (firstModel) { - setSelectedModel(firstModel); - localStorage.setItem(SELECTED_MODEL_KEY, firstModel); - } - } - }, [modelConfigQuery.data, selectedModel]); - - if (modelConfigQuery.error) { - return ; - } - - if (!modelConfigQuery.data) { - return ; - } - - const handleSetSelectedModel = (model: string) => { - setSelectedModel(model); - localStorage.setItem(SELECTED_MODEL_KEY, model); - }; - - return ( - - {children} - - ); -}; - -export const ChatLayout: FC = () => { - const agenticChat = useAgenticChat(); - const queryClient = useQueryClient(); - const { data: chats, isLoading: chatsLoading } = useQuery(getChats()); - const createChatMutation = useMutation(createChat(queryClient)); - const theme = useTheme(); - const navigate = useNavigate(); - const { chatID } = useParams<{ chatID?: string }>(); - - const handleNewChat = () => { - navigate("/chat"); - }; - - if (!agenticChat.enabled) { - return ( - -
-

Agentic Chat is not enabled

-

- Agentic Chat is an experimental feature and is not enabled by - default. Please contact your administrator for more information. -

-
-
- ); - } - - return ( - // Outermost container: controls height and prevents page scroll -
- {/* Sidebar Container (using Paper for background/border) */} - - {/* Sidebar Header */} -
- {/* Replaced Typography with div + styling */} -
- Chats -
- -
- {/* Sidebar Scrollable List Area */} -
- {chatsLoading ? ( - - ) : chats && chats.length > 0 ? ( - - {chats.map((chat) => ( - - - - - - ))} - - ) : ( - // Replaced Typography with div + styling -
- No chats yet. Start a new one! -
- )} -
-
- - {/* Main Content Area Container */} -
- - {/* Outlet renders ChatMessages, which should have its own internal scroll */} - - -
-
- ); -}; diff --git a/site/src/pages/ChatPage/ChatMessages.tsx b/site/src/pages/ChatPage/ChatMessages.tsx deleted file mode 100644 index 1ee75948d0976..0000000000000 --- a/site/src/pages/ChatPage/ChatMessages.tsx +++ /dev/null @@ -1,491 +0,0 @@ -import { type Message, useChat } from "@ai-sdk/react"; -import { type Theme, keyframes, useTheme } from "@emotion/react"; -import IconButton from "@mui/material/IconButton"; -import Paper from "@mui/material/Paper"; -import TextField from "@mui/material/TextField"; -import { getChatMessages } from "api/queries/chats"; -import type { ChatMessage, CreateChatMessageRequest } from "api/typesGenerated"; -import { ErrorAlert } from "components/Alert/ErrorAlert"; -import { Loader } from "components/Loader/Loader"; -import { SendIcon } from "lucide-react"; -import { - type FC, - type KeyboardEvent, - memo, - useCallback, - useEffect, - useRef, -} from "react"; -import ReactMarkdown from "react-markdown"; -import { useQuery } from "react-query"; -import { useLocation, useParams } from "react-router-dom"; -import rehypeRaw from "rehype-raw"; -import remarkGfm from "remark-gfm"; -import type { ChatLandingLocationState } from "./ChatLanding"; -import { useChatContext } from "./ChatLayout"; -import { ChatToolInvocation } from "./ChatToolInvocation"; -import { LanguageModelSelector } from "./LanguageModelSelector"; - -const fadeIn = keyframes` - from { - opacity: 0; - transform: translateY(5px); - } - to { - opacity: 1; - transform: translateY(0); - } -`; - -const renderReasoning = (reasoning: string, theme: Theme) => ( -
-
- 💭 Reasoning: -
-
- {reasoning} -
-
-); - -interface MessageBubbleProps { - message: Message; -} - -const MessageBubble: FC = memo(({ message }) => { - const theme = useTheme(); - const isUser = message.role === "user"; - - return ( -
- code)": { - backgroundColor: isUser - ? theme.palette.grey[700] - : theme.palette.action.hover, - color: isUser ? theme.palette.grey[50] : theme.palette.text.primary, - padding: theme.spacing(0.25, 0.75), - borderRadius: "4px", - fontSize: "0.875em", - fontFamily: "monospace", - }, - "& pre": { - backgroundColor: isUser - ? theme.palette.common.black - : theme.palette.grey[100], - color: isUser - ? theme.palette.grey[100] - : theme.palette.text.primary, - padding: theme.spacing(1.5), - borderRadius: "8px", - overflowX: "auto", - margin: theme.spacing(1.5, 0), - width: "100%", - "& code": { - backgroundColor: "transparent", - padding: 0, - fontSize: "0.875em", - fontFamily: "monospace", - color: "inherit", - }, - }, - "& a": { - color: isUser - ? theme.palette.grey[100] - : theme.palette.primary.main, - textDecoration: "underline", - fontWeight: 500, - "&:hover": { - textDecoration: "none", - color: isUser - ? theme.palette.grey[300] - : theme.palette.primary.dark, - }, - }, - }} - > - {message.role === "assistant" && message.parts ? ( -
- {message.parts.map((part) => { - switch (part.type) { - case "text": - return ( - - {part.text} - - ); - case "tool-invocation": - return ( -
- -
- ); - case "reasoning": - return ( -
- {renderReasoning(part.reasoning, theme)} -
- ); - default: - return null; - } - })} -
- ) : ( - - {message.content} - - )} -
-
- ); -}); - -interface ChatViewProps { - messages: Message[]; - input: string; - handleInputChange: React.ChangeEventHandler< - HTMLInputElement | HTMLTextAreaElement - >; - handleSubmit: (e?: React.FormEvent) => void; - isLoading: boolean; - chatID: string; -} - -const ChatView: FC = ({ - messages, - input, - handleInputChange, - handleSubmit, - isLoading, -}) => { - const theme = useTheme(); - const messagesEndRef = useRef(null); - const inputRef = useRef(null); - const chatContext = useChatContext(); - - useEffect(() => { - const timer = setTimeout(() => { - messagesEndRef.current?.scrollIntoView({ - behavior: "smooth", - block: "end", - }); - }, 50); - return () => clearTimeout(timer); - }, []); - - useEffect(() => { - inputRef.current?.focus(); - }, []); - - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === "Enter" && !event.shiftKey) { - event.preventDefault(); - handleSubmit(); - } - }; - - return ( -
-
-
- {messages.map((message) => ( - - ))} -
-
-
- -
- -
- -
- - - - -
-
-
- ); -}; - -export const ChatMessages: FC = () => { - const { chatID } = useParams(); - if (!chatID) { - throw new Error("Chat ID is required in URL path /chat/:chatID"); - } - - const { state } = useLocation(); - const transferredState = state as ChatLandingLocationState | undefined; - - const messagesQuery = useQuery(getChatMessages(chatID)); - - const chatContext = useChatContext(); - - const { - messages, - input, - handleInputChange, - handleSubmit: originalHandleSubmit, - isLoading, - setInput, - setMessages, - } = useChat({ - id: chatID, - api: `/api/v2/chats/${chatID}/messages`, - experimental_prepareRequestBody: (options): CreateChatMessageRequest => { - const userMessages = options.messages.filter( - (message) => message.role === "user", - ); - const mostRecentUserMessage = userMessages.at(-1); - return { - model: chatContext.selectedModel, - message: mostRecentUserMessage, - thinking: false, - }; - }, - initialInput: transferredState?.message, - initialMessages: messagesQuery.data as Message[] | undefined, - }); - - // Update messages from query data when it loads - useEffect(() => { - if (messagesQuery.data && messages.length === 0) { - setMessages(messagesQuery.data as Message[]); - } - }, [messagesQuery.data, messages.length, setMessages]); - - const handleSubmitCallback = useCallback( - (e?: React.FormEvent) => { - if (e) e.preventDefault(); - if (!input.trim()) return; - originalHandleSubmit(); - setInput(""); // Clear input after submit - }, - [input, originalHandleSubmit, setInput], - ); - - // Clear input and potentially submit on initial load with message - useEffect(() => { - if (transferredState?.message && input === transferredState.message) { - // Prevent submitting if messages already exist (e.g., browser back/forward) - if (messages.length === (messagesQuery.data?.length ?? 0)) { - handleSubmitCallback(); // Use the correct callback name - } - // Clear the state to prevent re-submission on subsequent renders/navigation - window.history.replaceState({}, document.title); - } - }, [ - transferredState?.message, - input, - handleSubmitCallback, - messages.length, - messagesQuery.data?.length, - ]); // Use the correct callback name - - useEffect(() => { - if (transferredState?.message) { - // Logic potentially related to transferredState can go here if needed, - } - }, [transferredState?.message]); - - if (messagesQuery.error) { - return ; - } - - if (messagesQuery.isLoading && messages.length === 0) { - return ; - } - - return ( - - ); -}; diff --git a/site/src/pages/ChatPage/ChatToolInvocation.stories.tsx b/site/src/pages/ChatPage/ChatToolInvocation.stories.tsx deleted file mode 100644 index a05cdd1843354..0000000000000 --- a/site/src/pages/ChatPage/ChatToolInvocation.stories.tsx +++ /dev/null @@ -1,1213 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { - MockStartingWorkspace, - MockStoppedWorkspace, - MockStoppingWorkspace, - MockTemplate, - MockTemplateVersion, - MockUserMember, - MockWorkspace, - MockWorkspaceBuild, -} from "testHelpers/entities"; -import { ChatToolInvocation } from "./ChatToolInvocation"; - -const meta: Meta = { - title: "pages/ChatPage/ChatToolInvocation", - component: ChatToolInvocation, -}; - -export default meta; -type Story = StoryObj; - -export const GetWorkspace: Story = { - render: () => - renderInvocations( - "coder_get_workspace", - { - workspace_id: MockWorkspace.id, - }, - MockWorkspace, - ), -}; - -export const CreateWorkspace: Story = { - render: () => - renderInvocations( - "coder_create_workspace", - { - name: MockWorkspace.name, - rich_parameters: {}, - template_version_id: MockWorkspace.template_active_version_id, - user: MockWorkspace.owner_name, - }, - MockWorkspace, - ), -}; - -export const ListWorkspaces: Story = { - render: () => - renderInvocations( - "coder_list_workspaces", - { - owner: "me", - }, - [ - MockWorkspace, - MockStoppedWorkspace, - MockStoppingWorkspace, - MockStartingWorkspace, - ], - ), -}; - -export const ListTemplates: Story = { - render: () => - renderInvocations("coder_list_templates", {}, [ - { - id: MockTemplate.id, - name: MockTemplate.name, - description: MockTemplate.description, - active_version_id: MockTemplate.active_version_id, - active_user_count: MockTemplate.active_user_count, - }, - { - id: "another-template", - name: "Another Template", - description: "A different template for testing purposes.", - active_version_id: "v2.0", - active_user_count: 5, - }, - ]), -}; - -export const TemplateVersionParameters: Story = { - render: () => - renderInvocations( - "coder_template_version_parameters", - { - template_version_id: MockTemplateVersion.id, - }, - [ - { - name: "region", - display_name: "Region", - description: "Select the deployment region.", - description_plaintext: "Select the deployment region.", - type: "string", - form_type: "radio", - mutable: false, - default_value: "us-west-1", - icon: "", - options: [ - { name: "US West", description: "", value: "us-west-1", icon: "" }, - { name: "US East", description: "", value: "us-east-1", icon: "" }, - ], - required: true, - ephemeral: false, - }, - { - name: "cpu_cores", - display_name: "CPU Cores", - description: "Number of CPU cores.", - description_plaintext: "Number of CPU cores.", - type: "number", - form_type: "input", - mutable: true, - default_value: "4", - icon: "", - options: [], - required: false, - ephemeral: false, - }, - ], - ), -}; - -export const GetAuthenticatedUser: Story = { - render: () => - renderInvocations("coder_get_authenticated_user", {}, MockUserMember), -}; - -export const CreateWorkspaceBuild: Story = { - render: () => - renderInvocations( - "coder_create_workspace_build", - { - workspace_id: MockWorkspace.id, - transition: "start", - }, - MockWorkspaceBuild, - ), -}; - -export const CreateTemplateVersion: Story = { - render: () => - renderInvocations( - "coder_create_template_version", - { - template_id: MockTemplate.id, - file_id: "file-123", - }, - MockTemplateVersion, - ), -}; - -const mockLogs = [ - "[INFO] Starting build process...", - "[DEBUG] Reading configuration file.", - "[WARN] Deprecated setting detected.", - "[INFO] Applying changes...", - "[ERROR] Failed to connect to database.", -]; - -export const GetWorkspaceAgentLogs: Story = { - render: () => - renderInvocations( - "coder_get_workspace_agent_logs", - { - workspace_agent_id: "agent-456", - }, - mockLogs, - ), -}; - -export const GetWorkspaceBuildLogs: Story = { - render: () => - renderInvocations( - "coder_get_workspace_build_logs", - { - workspace_build_id: MockWorkspaceBuild.id, - }, - mockLogs, - ), -}; - -export const GetTemplateVersionLogs: Story = { - render: () => - renderInvocations( - "coder_get_template_version_logs", - { - template_version_id: MockTemplateVersion.id, - }, - mockLogs, - ), -}; - -export const UpdateTemplateActiveVersion: Story = { - render: () => - renderInvocations( - "coder_update_template_active_version", - { - template_id: MockTemplate.id, - template_version_id: MockTemplateVersion.id, - }, - `Successfully updated active version for template ${MockTemplate.name}.`, - ), -}; - -export const UploadTarFile: Story = { - render: () => - renderInvocations( - "coder_upload_tar_file", - { - files: { "main.tf": templateTerraform, Dockerfile: templateDockerfile }, - }, - { - hash: "sha256:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2", - }, - ), -}; - -export const CreateTemplate: Story = { - render: () => - renderInvocations( - "coder_create_template", - { - name: "new-template", - }, - MockTemplate, - ), -}; - -export const DeleteTemplate: Story = { - render: () => - renderInvocations( - "coder_delete_template", - { - template_id: MockTemplate.id, - }, - `Successfully deleted template ${MockTemplate.name}.`, - ), -}; - -export const GetTemplateVersion: Story = { - render: () => - renderInvocations( - "coder_get_template_version", - { - template_version_id: MockTemplateVersion.id, - }, - MockTemplateVersion, - ), -}; - -export const DownloadTarFile: Story = { - render: () => - renderInvocations( - "coder_download_tar_file", - { - file_id: "file-789", - }, - { "main.tf": templateTerraform, "README.md": "# My Template\n" }, - ), -}; - -const renderInvocations = ( - toolName: T, - args: Extract["args"], - result: Extract< - ChatToolInvocation, - { toolName: T; state: "result" } - >["result"], - error?: string, -) => { - return ( - <> - - - - - - ); -}; - -const templateDockerfile = `FROM rust:slim@sha256:9abf10cc84dfad6ace1b0aae3951dc5200f467c593394288c11db1e17bb4d349 AS rust-utils -# Install rust helper programs -# ENV CARGO_NET_GIT_FETCH_WITH_CLI=true -ENV CARGO_INSTALL_ROOT=/tmp/ -RUN cargo install typos-cli watchexec-cli && \ - # Reduce image size. - rm -rf /usr/local/cargo/registry - -FROM ubuntu:jammy@sha256:0e5e4a57c2499249aafc3b40fcd541e9a456aab7296681a3994d631587203f97 AS go - -# Install Go manually, so that we can control the version -ARG GO_VERSION=1.24.1 - -# Boring Go is needed to build FIPS-compliant binaries. -RUN apt-get update && \ - apt-get install --yes curl && \ - curl --silent --show-error --location \ - "https://go.dev/dl/go\${GO_VERSION}.linux-amd64.tar.gz" \ - -o /usr/local/go.tar.gz && \ - rm -rf /var/lib/apt/lists/* - -ENV PATH=$PATH:/usr/local/go/bin -ARG GOPATH="/tmp/" -# Install Go utilities. -RUN apt-get update && \ - apt-get install --yes gcc && \ - mkdir --parents /usr/local/go && \ - tar --extract --gzip --directory=/usr/local/go --file=/usr/local/go.tar.gz --strip-components=1 && \ - mkdir --parents "$GOPATH" && \ - # moq for Go tests. - go install github.com/matryer/moq@v0.2.3 && \ - # swag for Swagger doc generation - go install github.com/swaggo/swag/cmd/swag@v1.7.4 && \ - # go-swagger tool to generate the go coder api client - go install github.com/go-swagger/go-swagger/cmd/swagger@v0.28.0 && \ - # goimports for updating imports - go install golang.org/x/tools/cmd/goimports@v0.31.0 && \ - # protoc-gen-go is needed to build sysbox from source - go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30 && \ - # drpc support for v2 - go install storj.io/drpc/cmd/protoc-gen-go-drpc@v0.0.34 && \ - # migrate for migration support for v2 - go install github.com/golang-migrate/migrate/v4/cmd/migrate@v4.15.1 && \ - # goreleaser for compiling v2 binaries - go install github.com/goreleaser/goreleaser@v1.6.1 && \ - # Install the latest version of gopls for editors that support - # the language server protocol - go install golang.org/x/tools/gopls@v0.18.1 && \ - # gotestsum makes test output more readable - go install gotest.tools/gotestsum@v1.9.0 && \ - # goveralls collects code coverage metrics from tests - # and sends to Coveralls - go install github.com/mattn/goveralls@v0.0.11 && \ - # kind for running Kubernetes-in-Docker, needed for tests - go install sigs.k8s.io/kind@v0.10.0 && \ - # helm-docs generates our Helm README based on a template and the - # charts and values files - go install github.com/norwoodj/helm-docs/cmd/helm-docs@v1.5.0 && \ - # sqlc for Go code generation - (CGO_ENABLED=1 go install github.com/sqlc-dev/sqlc/cmd/sqlc@v1.27.0) && \ - # gcr-cleaner-cli used by CI to prune unused images - go install github.com/sethvargo/gcr-cleaner/cmd/gcr-cleaner-cli@v0.5.1 && \ - # ruleguard for checking custom rules, without needing to run all of - # golangci-lint. Check the go.mod in the release of golangci-lint that - # we're using for the version of go-critic that it embeds, then check - # the version of ruleguard in go-critic for that tag. - go install github.com/quasilyte/go-ruleguard/cmd/ruleguard@v0.3.13 && \ - # go-releaser for building 'fat binaries' that work cross-platform - go install github.com/goreleaser/goreleaser@v1.6.1 && \ - go install mvdan.cc/sh/v3/cmd/shfmt@v3.7.0 && \ - # nfpm is used with \`make build\` to make release packages - go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.35.1 && \ - # yq v4 is used to process yaml files in coder v2. Conflicts with - # yq v3 used in v1. - go install github.com/mikefarah/yq/v4@v4.44.3 && \ - mv /tmp/bin/yq /tmp/bin/yq4 && \ - go install go.uber.org/mock/mockgen@v0.5.0 && \ - # Reduce image size. - apt-get remove --yes gcc && \ - apt-get autoremove --yes && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* && \ - rm -rf /usr/local/go && \ - rm -rf /tmp/go/pkg && \ - rm -rf /tmp/go/src - -# alpine:3.18 -FROM us-docker.pkg.dev/coder-v2-images-public/public/alpine@sha256:fd032399cd767f310a1d1274e81cab9f0fd8a49b3589eba2c3420228cd45b6a7 AS proto -WORKDIR /tmp -RUN apk add curl unzip -RUN curl -L -o protoc.zip https://github.com/protocolbuffers/protobuf/releases/download/v23.4/protoc-23.4-linux-x86_64.zip && \ - unzip protoc.zip && \ - rm protoc.zip - -FROM ubuntu:jammy@sha256:0e5e4a57c2499249aafc3b40fcd541e9a456aab7296681a3994d631587203f97 - -SHELL ["/bin/bash", "-c"] - -# Install packages from apt repositories -ARG DEBIAN_FRONTEND="noninteractive" - -# Updated certificates are necessary to use the teraswitch mirror. -# This must be ran before copying in configuration since the config replaces -# the default mirror with teraswitch. -# Also enable the en_US.UTF-8 locale so that we don't generate multiple locales -# and unminimize to include man pages. -RUN apt-get update && \ - apt-get install --yes ca-certificates locales && \ - echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen && \ - locale-gen && \ - yes | unminimize - -COPY files / - -# We used to copy /etc/sudoers.d/* in from files/ but this causes issues with -# permissions and layer caching. Instead, create the file directly. -RUN mkdir -p /etc/sudoers.d && \ - echo 'coder ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/nopasswd && \ - chmod 750 /etc/sudoers.d/ && \ - chmod 640 /etc/sudoers.d/nopasswd - -RUN apt-get update --quiet && apt-get install --yes \ - ansible \ - apt-transport-https \ - apt-utils \ - asciinema \ - bash \ - bash-completion \ - bat \ - bats \ - bind9-dnsutils \ - build-essential \ - ca-certificates \ - cargo \ - cmake \ - containerd.io \ - crypto-policies \ - curl \ - docker-ce \ - docker-ce-cli \ - docker-compose-plugin \ - exa \ - fd-find \ - file \ - fish \ - gettext-base \ - git \ - gnupg \ - google-cloud-sdk \ - google-cloud-sdk-datastore-emulator \ - graphviz \ - helix \ - htop \ - httpie \ - inetutils-tools \ - iproute2 \ - iputils-ping \ - iputils-tracepath \ - jq \ - kubectl \ - language-pack-en \ - less \ - libgbm-dev \ - libssl-dev \ - lsb-release \ - lsof \ - man \ - meld \ - ncdu \ - neovim \ - net-tools \ - openjdk-11-jdk-headless \ - openssh-server \ - openssl \ - packer \ - pkg-config \ - postgresql-16 \ - python3 \ - python3-pip \ - ripgrep \ - rsync \ - screen \ - shellcheck \ - strace \ - sudo \ - tcptraceroute \ - termshark \ - traceroute \ - unzip \ - vim \ - wget \ - xauth \ - zip \ - zsh \ - zstd && \ - # Delete package cache to avoid consuming space in layer - apt-get clean && \ - # Configure FIPS-compliant policies - update-crypto-policies --set FIPS - -# NOTE: In scripts/Dockerfile.base we specifically install Terraform version 1.11.3. -# Installing the same version here to match. -RUN wget -O /tmp/terraform.zip "https://releases.hashicorp.com/terraform/1.11.3/terraform_1.11.3_linux_amd64.zip" && \ - unzip /tmp/terraform.zip -d /usr/local/bin && \ - rm -f /tmp/terraform.zip && \ - chmod +x /usr/local/bin/terraform && \ - terraform --version - -# Install the docker buildx component. -RUN DOCKER_BUILDX_VERSION=$(curl -s "https://api.github.com/repos/docker/buildx/releases/latest" | grep '"tag_name":' | sed -E 's/.*"(v[^"]+)".*/\\1/') && \ - mkdir -p /usr/local/lib/docker/cli-plugins && \ - curl -Lo /usr/local/lib/docker/cli-plugins/docker-buildx "https://github.com/docker/buildx/releases/download/\${DOCKER_BUILDX_VERSION}/buildx-\${DOCKER_BUILDX_VERSION}.linux-amd64" && \ - chmod a+x /usr/local/lib/docker/cli-plugins/docker-buildx - -# See https://github.com/cli/cli/issues/6175#issuecomment-1235984381 for proof -# the apt repository is unreliable -RUN GH_CLI_VERSION=$(curl -s "https://api.github.com/repos/cli/cli/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\\1/') && \ - curl -L https://github.com/cli/cli/releases/download/v\${GH_CLI_VERSION}/gh_\${GH_CLI_VERSION}_linux_amd64.deb -o gh.deb && \ - dpkg -i gh.deb && \ - rm gh.deb - -# Install Lazygit -# See https://github.com/jesseduffield/lazygit#ubuntu -RUN LAZYGIT_VERSION=$(curl -s "https://api.github.com/repos/jesseduffield/lazygit/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v*([^"]+)".*/\\1/') && \ - curl -Lo lazygit.tar.gz "https://github.com/jesseduffield/lazygit/releases/latest/download/lazygit_\${LAZYGIT_VERSION}_Linux_x86_64.tar.gz" && \ - tar xf lazygit.tar.gz -C /usr/local/bin lazygit && \ - rm lazygit.tar.gz - -# Install doctl -# See https://docs.digitalocean.com/reference/doctl/how-to/install -RUN DOCTL_VERSION=$(curl -s "https://api.github.com/repos/digitalocean/doctl/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\\1/') && \ - curl -L https://github.com/digitalocean/doctl/releases/download/v\${DOCTL_VERSION}/doctl-\${DOCTL_VERSION}-linux-amd64.tar.gz -o doctl.tar.gz && \ - tar xf doctl.tar.gz -C /usr/local/bin doctl && \ - rm doctl.tar.gz - -ARG NVM_INSTALL_SHA=bdea8c52186c4dd12657e77e7515509cda5bf9fa5a2f0046bce749e62645076d -# Install frontend utilities -ENV NVM_DIR=/usr/local/nvm -ENV NODE_VERSION=20.16.0 -RUN mkdir -p $NVM_DIR -RUN curl -o nvm_install.sh https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh && \ - echo "\${NVM_INSTALL_SHA} nvm_install.sh" | sha256sum -c && \ - bash nvm_install.sh && \ - rm nvm_install.sh -RUN source $NVM_DIR/nvm.sh && \ - nvm install $NODE_VERSION && \ - nvm use $NODE_VERSION -ENV PATH=$NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH -# Allow patch updates for npm and pnpm -RUN npm install -g npm@10.8.1 --integrity=sha512-Dp1C6SvSMYQI7YHq/y2l94uvI+59Eqbu1EpuKQHQ8p16txXRuRit5gH3Lnaagk2aXDIjg/Iru9pd05bnneKgdw== -RUN npm install -g pnpm@9.15.1 --integrity=sha512-GstWXmGT7769p3JwKVBGkVDPErzHZCYudYfnHRncmKQj3/lTblfqRMSb33kP9pToPCe+X6oj1n4MAztYO+S/zw== - -RUN pnpx playwright@1.47.0 install --with-deps chromium - -# Ensure PostgreSQL binaries are in the users $PATH. -RUN update-alternatives --install /usr/local/bin/initdb initdb /usr/lib/postgresql/16/bin/initdb 100 && \ - update-alternatives --install /usr/local/bin/postgres postgres /usr/lib/postgresql/16/bin/postgres 100 - -# Create links for injected dependencies -RUN ln --symbolic /var/tmp/coder/coder-cli/coder /usr/local/bin/coder && \ - ln --symbolic /var/tmp/coder/code-server/bin/code-server /usr/local/bin/code-server - -# Disable the PostgreSQL systemd service. -# Coder uses a custom timescale container to test the database instead. -RUN systemctl disable \ - postgresql - -# Configure systemd services for CVMs -RUN systemctl enable \ - docker \ - ssh && \ - # Workaround for envbuilder cache probing not working unless the filesystem is modified. - touch /tmp/.envbuilder-systemctl-enable-docker-ssh-workaround - -# Install tools with published releases, where that is the -# preferred/recommended installation method. -ARG CLOUD_SQL_PROXY_VERSION=2.2.0 \ - DIVE_VERSION=0.10.0 \ - DOCKER_GCR_VERSION=2.1.8 \ - GOLANGCI_LINT_VERSION=1.64.8 \ - GRYPE_VERSION=0.61.1 \ - HELM_VERSION=3.12.0 \ - KUBE_LINTER_VERSION=0.6.3 \ - KUBECTX_VERSION=0.9.4 \ - STRIPE_VERSION=1.14.5 \ - TERRAGRUNT_VERSION=0.45.11 \ - TRIVY_VERSION=0.41.0 \ - SYFT_VERSION=1.20.0 \ - COSIGN_VERSION=2.4.3 - -# cloud_sql_proxy, for connecting to cloudsql instances -# the upstream go.mod prevents this from being installed with go install -RUN curl --silent --show-error --location --output /usr/local/bin/cloud_sql_proxy "https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v\${CLOUD_SQL_PROXY_VERSION}/cloud-sql-proxy.linux.amd64" && \ - chmod a=rx /usr/local/bin/cloud_sql_proxy && \ - # dive for scanning image layer utilization metrics in CI - curl --silent --show-error --location "https://github.com/wagoodman/dive/releases/download/v\${DIVE_VERSION}/dive_\${DIVE_VERSION}_linux_amd64.tar.gz" | \ - tar --extract --gzip --directory=/usr/local/bin --file=- dive && \ - # docker-credential-gcr is a Docker credential helper for pushing/pulling - # images from Google Container Registry and Artifact Registry - curl --silent --show-error --location "https://github.com/GoogleCloudPlatform/docker-credential-gcr/releases/download/v\${DOCKER_GCR_VERSION}/docker-credential-gcr_linux_amd64-\${DOCKER_GCR_VERSION}.tar.gz" | \ - tar --extract --gzip --directory=/usr/local/bin --file=- docker-credential-gcr && \ - # golangci-lint performs static code analysis for our Go code - curl --silent --show-error --location "https://github.com/golangci/golangci-lint/releases/download/v\${GOLANGCI_LINT_VERSION}/golangci-lint-\${GOLANGCI_LINT_VERSION}-linux-amd64.tar.gz" | \ - tar --extract --gzip --directory=/usr/local/bin --file=- --strip-components=1 "golangci-lint-\${GOLANGCI_LINT_VERSION}-linux-amd64/golangci-lint" && \ - # Anchore Grype for scanning container images for security issues - curl --silent --show-error --location "https://github.com/anchore/grype/releases/download/v\${GRYPE_VERSION}/grype_\${GRYPE_VERSION}_linux_amd64.tar.gz" | \ - tar --extract --gzip --directory=/usr/local/bin --file=- grype && \ - # Helm is necessary for deploying Coder - curl --silent --show-error --location "https://get.helm.sh/helm-v\${HELM_VERSION}-linux-amd64.tar.gz" | \ - tar --extract --gzip --directory=/usr/local/bin --file=- --strip-components=1 linux-amd64/helm && \ - # kube-linter for linting Kubernetes objects, including those - # that Helm generates from our charts - curl --silent --show-error --location "https://github.com/stackrox/kube-linter/releases/download/\${KUBE_LINTER_VERSION}/kube-linter-linux" --output /usr/local/bin/kube-linter && \ - # kubens and kubectx for managing Kubernetes namespaces and contexts - curl --silent --show-error --location "https://github.com/ahmetb/kubectx/releases/download/v\${KUBECTX_VERSION}/kubectx_v\${KUBECTX_VERSION}_linux_x86_64.tar.gz" | \ - tar --extract --gzip --directory=/usr/local/bin --file=- kubectx && \ - curl --silent --show-error --location "https://github.com/ahmetb/kubectx/releases/download/v\${KUBECTX_VERSION}/kubens_v\${KUBECTX_VERSION}_linux_x86_64.tar.gz" | \ - tar --extract --gzip --directory=/usr/local/bin --file=- kubens && \ - # stripe for coder.com billing API - curl --silent --show-error --location "https://github.com/stripe/stripe-cli/releases/download/v\${STRIPE_VERSION}/stripe_\${STRIPE_VERSION}_linux_x86_64.tar.gz" | \ - tar --extract --gzip --directory=/usr/local/bin --file=- stripe && \ - # terragrunt for running Terraform and Terragrunt files - curl --silent --show-error --location --output /usr/local/bin/terragrunt "https://github.com/gruntwork-io/terragrunt/releases/download/v\${TERRAGRUNT_VERSION}/terragrunt_linux_amd64" && \ - chmod a=rx /usr/local/bin/terragrunt && \ - # AquaSec Trivy for scanning container images for security issues - curl --silent --show-error --location "https://github.com/aquasecurity/trivy/releases/download/v\${TRIVY_VERSION}/trivy_\${TRIVY_VERSION}_Linux-64bit.tar.gz" | \ - tar --extract --gzip --directory=/usr/local/bin --file=- trivy && \ - # Anchore Syft for SBOM generation - curl --silent --show-error --location "https://github.com/anchore/syft/releases/download/v\${SYFT_VERSION}/syft_\${SYFT_VERSION}_linux_amd64.tar.gz" | \ - tar --extract --gzip --directory=/usr/local/bin --file=- syft && \ - # Sigstore Cosign for artifact signing and attestation - curl --silent --show-error --location --output /usr/local/bin/cosign "https://github.com/sigstore/cosign/releases/download/v\${COSIGN_VERSION}/cosign-linux-amd64" && \ - chmod a=rx /usr/local/bin/cosign - -# We use yq during "make deploy" to manually substitute out fields in -# our helm values.yaml file. See https://github.com/helm/helm/issues/3141 -# -# TODO: update to 4.x, we can't do this now because it included breaking -# changes (yq w doesn't work anymore) -# RUN curl --silent --show-error --location "https://github.com/mikefarah/yq/releases/download/v4.9.0/yq_linux_amd64.tar.gz" | \ -# tar --extract --gzip --directory=/usr/local/bin --file=- ./yq_linux_amd64 && \ -# mv /usr/local/bin/yq_linux_amd64 /usr/local/bin/yq - -RUN curl --silent --show-error --location --output /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/3.3.0/yq_linux_amd64" && \ - chmod a=rx /usr/local/bin/yq - -# Install GoLand. -RUN mkdir --parents /usr/local/goland && \ - curl --silent --show-error --location "https://download.jetbrains.com/go/goland-2021.2.tar.gz" | \ - tar --extract --gzip --directory=/usr/local/goland --file=- --strip-components=1 && \ - ln --symbolic /usr/local/goland/bin/goland.sh /usr/local/bin/goland - -# Install Antlrv4, needed to generate paramlang lexer/parser -RUN curl --silent --show-error --location --output /usr/local/lib/antlr-4.9.2-complete.jar "https://www.antlr.org/download/antlr-4.9.2-complete.jar" -ENV CLASSPATH="/usr/local/lib/antlr-4.9.2-complete.jar:\${PATH}" - -# Add coder user and allow use of docker/sudo -RUN useradd coder \ - --create-home \ - --shell=/bin/bash \ - --groups=docker \ - --uid=1000 \ - --user-group - -# Adjust OpenSSH config -RUN echo "PermitUserEnvironment yes" >>/etc/ssh/sshd_config && \ - echo "X11Forwarding yes" >>/etc/ssh/sshd_config && \ - echo "X11UseLocalhost no" >>/etc/ssh/sshd_config - -# We avoid copying the extracted directory since COPY slows to minutes when there -# are a lot of small files. -COPY --from=go /usr/local/go.tar.gz /usr/local/go.tar.gz -RUN mkdir /usr/local/go && \ - tar --extract --gzip --directory=/usr/local/go --file=/usr/local/go.tar.gz --strip-components=1 - -ENV PATH=$PATH:/usr/local/go/bin - -RUN update-alternatives --install /usr/local/bin/gofmt gofmt /usr/local/go/bin/gofmt 100 - -COPY --from=go /tmp/bin /usr/local/bin -COPY --from=rust-utils /tmp/bin /usr/local/bin -COPY --from=proto /tmp/bin /usr/local/bin -COPY --from=proto /tmp/include /usr/local/bin/include - -USER coder - -# Ensure go bins are in the 'coder' user's path. Note that no go bins are -# installed in this docker file, as they'd be mounted over by the persistent -# home volume. -ENV PATH="/home/coder/go/bin:\${PATH}" - -# This setting prevents Go from using the public checksum database for -# our module path prefixes. It is required because these are in private -# repositories that require authentication. -# -# For details, see: https://golang.org/ref/mod#private-modules -ENV GOPRIVATE="coder.com,cdr.dev,go.coder.com,github.com/cdr,github.com/coder" - -# Increase memory allocation to NodeJS -ENV NODE_OPTIONS="--max-old-space-size=8192" -`; - -const templateTerraform = `terraform { - required_providers { - coder = { - source = "coder/coder" - version = "2.2.0-pre0" - } - docker = { - source = "kreuzwerker/docker" - version = "~> 3.0.0" - } - } -} - -locals { - // These are cluster service addresses mapped to Tailscale nodes. Ask Dean or - // Kyle for help. - docker_host = { - "" = "tcp://dogfood-ts-cdr-dev.tailscale.svc.cluster.local:2375" - "us-pittsburgh" = "tcp://dogfood-ts-cdr-dev.tailscale.svc.cluster.local:2375" - // For legacy reasons, this host is labelled \`eu-helsinki\` but it's - // actually in Germany now. - "eu-helsinki" = "tcp://katerose-fsn-cdr-dev.tailscale.svc.cluster.local:2375" - "ap-sydney" = "tcp://wolfgang-syd-cdr-dev.tailscale.svc.cluster.local:2375" - "sa-saopaulo" = "tcp://oberstein-sao-cdr-dev.tailscale.svc.cluster.local:2375" - "za-cpt" = "tcp://schonkopf-cpt-cdr-dev.tailscale.svc.cluster.local:2375" - } - - repo_base_dir = data.coder_parameter.repo_base_dir.value == "~" ? "/home/coder" : replace(data.coder_parameter.repo_base_dir.value, "/^~\\//", "/home/coder/") - repo_dir = replace(try(module.git-clone[0].repo_dir, ""), "/^~\\//", "/home/coder/") - container_name = "coder-\${data.coder_workspace_owner.me.name}-\${lower(data.coder_workspace.me.name)}" -} - -data "coder_parameter" "repo_base_dir" { - type = "string" - name = "Coder Repository Base Directory" - default = "~" - description = "The directory specified will be created (if missing) and [coder/coder](https://github.com/coder/coder) will be automatically cloned into [base directory]/coder 🪄." - mutable = true -} - -data "coder_parameter" "image_type" { - type = "string" - name = "Coder Image" - default = "codercom/oss-dogfood:latest" - description = "The Docker image used to run your workspace. Choose between nix and non-nix images." - option { - icon = "/icon/coder.svg" - name = "Dogfood (Default)" - value = "codercom/oss-dogfood:latest" - } - option { - icon = "/icon/nix.svg" - name = "Dogfood Nix (Experimental)" - value = "codercom/oss-dogfood-nix:latest" - } -} - -data "coder_parameter" "region" { - type = "string" - name = "Region" - icon = "/emojis/1f30e.png" - default = "us-pittsburgh" - option { - icon = "/emojis/1f1fa-1f1f8.png" - name = "Pittsburgh" - value = "us-pittsburgh" - } - option { - icon = "/emojis/1f1e9-1f1ea.png" - name = "Falkenstein" - // For legacy reasons, this host is labelled \`eu-helsinki\` but it's - // actually in Germany now. - value = "eu-helsinki" - } - option { - icon = "/emojis/1f1e6-1f1fa.png" - name = "Sydney" - value = "ap-sydney" - } - option { - icon = "/emojis/1f1e7-1f1f7.png" - name = "São Paulo" - value = "sa-saopaulo" - } - option { - icon = "/emojis/1f1ff-1f1e6.png" - name = "Cape Town" - value = "za-cpt" - } -} - -data "coder_parameter" "res_mon_memory_threshold" { - type = "number" - name = "Memory usage threshold" - default = 80 - description = "The memory usage threshold used in resources monitoring to trigger notifications." - mutable = true - validation { - min = 0 - max = 100 - } -} - -data "coder_parameter" "res_mon_volume_threshold" { - type = "number" - name = "Volume usage threshold" - default = 90 - description = "The volume usage threshold used in resources monitoring to trigger notifications." - mutable = true - validation { - min = 0 - max = 100 - } -} - -data "coder_parameter" "res_mon_volume_path" { - type = "string" - name = "Volume path" - default = "/home/coder" - description = "The path monitored in resources monitoring to trigger notifications." - mutable = true -} - -provider "docker" { - host = lookup(local.docker_host, data.coder_parameter.region.value) -} - -provider "coder" {} - -data "coder_external_auth" "github" { - id = "github" -} - -data "coder_workspace" "me" {} -data "coder_workspace_owner" "me" {} -data "coder_workspace_tags" "tags" { - tags = { - "cluster" : "dogfood-v2" - "env" : "gke" - } -} - -module "slackme" { - count = data.coder_workspace.me.start_count - source = "dev.registry.coder.com/modules/slackme/coder" - version = ">= 1.0.0" - agent_id = coder_agent.dev.id - auth_provider_id = "slack" -} - -module "dotfiles" { - count = data.coder_workspace.me.start_count - source = "dev.registry.coder.com/modules/dotfiles/coder" - version = ">= 1.0.0" - agent_id = coder_agent.dev.id -} - -module "git-clone" { - count = data.coder_workspace.me.start_count - source = "dev.registry.coder.com/modules/git-clone/coder" - version = ">= 1.0.0" - agent_id = coder_agent.dev.id - url = "https://github.com/coder/coder" - base_dir = local.repo_base_dir -} - -module "personalize" { - count = data.coder_workspace.me.start_count - source = "dev.registry.coder.com/modules/personalize/coder" - version = ">= 1.0.0" - agent_id = coder_agent.dev.id -} - -module "code-server" { - count = data.coder_workspace.me.start_count - source = "dev.registry.coder.com/modules/code-server/coder" - version = ">= 1.0.0" - agent_id = coder_agent.dev.id - folder = local.repo_dir - auto_install_extensions = true -} - -module "vscode-web" { - count = data.coder_workspace.me.start_count - source = "registry.coder.com/modules/vscode-web/coder" - version = ">= 1.0.0" - agent_id = coder_agent.dev.id - folder = local.repo_dir - extensions = ["github.copilot"] - auto_install_extensions = true # will install extensions from the repos .vscode/extensions.json file - accept_license = true -} - -module "jetbrains_gateway" { - count = data.coder_workspace.me.start_count - source = "dev.registry.coder.com/modules/jetbrains-gateway/coder" - version = ">= 1.0.0" - agent_id = coder_agent.dev.id - agent_name = "dev" - folder = local.repo_dir - jetbrains_ides = ["GO", "WS"] - default = "GO" - latest = true -} - -module "filebrowser" { - count = data.coder_workspace.me.start_count - source = "dev.registry.coder.com/modules/filebrowser/coder" - version = ">= 1.0.0" - agent_id = coder_agent.dev.id - agent_name = "dev" -} - -module "coder-login" { - count = data.coder_workspace.me.start_count - source = "dev.registry.coder.com/modules/coder-login/coder" - version = ">= 1.0.0" - agent_id = coder_agent.dev.id -} - -module "cursor" { - count = data.coder_workspace.me.start_count - source = "dev.registry.coder.com/modules/cursor/coder" - version = ">= 1.0.0" - agent_id = coder_agent.dev.id - folder = local.repo_dir -} - -module "zed" { - count = data.coder_workspace.me.start_count - source = "./zed" - agent_id = coder_agent.dev.id - folder = local.repo_dir -} - -resource "coder_agent" "dev" { - arch = "amd64" - os = "linux" - dir = local.repo_dir - env = { - OIDC_TOKEN : data.coder_workspace_owner.me.oidc_access_token, - } - startup_script_behavior = "blocking" - - # The following metadata blocks are optional. They are used to display - # information about your workspace in the dashboard. You can remove them - # if you don't want to display any information. - metadata { - display_name = "CPU Usage" - key = "cpu_usage" - order = 0 - script = "coder stat cpu" - interval = 10 - timeout = 1 - } - - metadata { - display_name = "RAM Usage" - key = "ram_usage" - order = 1 - script = "coder stat mem" - interval = 10 - timeout = 1 - } - - metadata { - display_name = "CPU Usage (Host)" - key = "cpu_usage_host" - order = 2 - script = "coder stat cpu --host" - interval = 10 - timeout = 1 - } - - metadata { - display_name = "RAM Usage (Host)" - key = "ram_usage_host" - order = 3 - script = "coder stat mem --host" - interval = 10 - timeout = 1 - } - - metadata { - display_name = "Swap Usage (Host)" - key = "swap_usage_host" - order = 4 - script = <&1 | awk ' $0 ~ "Word of the Day: [A-z]+" { print $5; exit }' - EOT - interval = 86400 - timeout = 5 - } - - resources_monitoring { - memory { - enabled = true - threshold = data.coder_parameter.res_mon_memory_threshold.value - } - volume { - enabled = true - threshold = data.coder_parameter.res_mon_volume_threshold.value - path = data.coder_parameter.res_mon_volume_path.value - } - } - - startup_script = <<-EOT - #!/usr/bin/env bash - set -eux -o pipefail - - # Allow synchronization between scripts. - trap 'touch /tmp/.coder-startup-script.done' EXIT - - # Start Docker service - sudo service docker start - # Install playwright dependencies - # We want to use the playwright version from site/package.json - # Check if the directory exists At workspace creation as the coder_script runs in parallel so clone might not exist yet. - while ! [[ -f "\${local.repo_dir}/site/package.json" ]]; do - sleep 1 - done - cd "\${local.repo_dir}" && make clean - cd "\${local.repo_dir}/site" && pnpm install - EOT - - shutdown_script = <<-EOT - #!/usr/bin/env bash - set -eux -o pipefail - - # Stop the Docker service to prevent errors during workspace destroy. - sudo service docker stop - EOT -} - -# Add a cost so we get some quota usage in dev.coder.com -resource "coder_metadata" "home_volume" { - resource_id = docker_volume.home_volume.id - daily_cost = 1 -} - -resource "docker_volume" "home_volume" { - name = "coder-\${data.coder_workspace.me.id}-home" - # Protect the volume from being deleted due to changes in attributes. - lifecycle { - ignore_changes = all - } - # Add labels in Docker to keep track of orphan resources. - labels { - label = "coder.owner" - value = data.coder_workspace_owner.me.name - } - labels { - label = "coder.owner_id" - value = data.coder_workspace_owner.me.id - } - labels { - label = "coder.workspace_id" - value = data.coder_workspace.me.id - } - # This field becomes outdated if the workspace is renamed but can - # be useful for debugging or cleaning out dangling volumes. - labels { - label = "coder.workspace_name_at_creation" - value = data.coder_workspace.me.name - } -} - -data "docker_registry_image" "dogfood" { - name = data.coder_parameter.image_type.value -} - -resource "docker_image" "dogfood" { - name = "\${data.coder_parameter.image_type.value}@\${data.docker_registry_image.dogfood.sha256_digest}" - pull_triggers = [ - data.docker_registry_image.dogfood.sha256_digest, - sha1(join("", [for f in fileset(path.module, "files/*") : filesha1(f)])), - filesha1("Dockerfile"), - filesha1("nix.hash"), - ] - keep_locally = true -} - -resource "docker_container" "workspace" { - count = data.coder_workspace.me.start_count - image = docker_image.dogfood.name - name = local.container_name - # Hostname makes the shell more user friendly: coder@my-workspace:~$ - hostname = data.coder_workspace.me.name - # Use the docker gateway if the access URL is 127.0.0.1 - entrypoint = ["sh", "-c", coder_agent.dev.init_script] - # CPU limits are unnecessary since Docker will load balance automatically - memory = data.coder_workspace_owner.me.name == "code-asher" ? 65536 : 32768 - runtime = "sysbox-runc" - # Ensure the workspace is given time to execute shutdown scripts. - destroy_grace_seconds = 60 - stop_timeout = 60 - stop_signal = "SIGINT" - env = [ - "CODER_AGENT_TOKEN=\${coder_agent.dev.token}", - "USE_CAP_NET_ADMIN=true", - "CODER_PROC_PRIO_MGMT=1", - "CODER_PROC_OOM_SCORE=10", - "CODER_PROC_NICE_SCORE=1", - "CODER_AGENT_DEVCONTAINERS_ENABLE=1", - ] - host { - host = "host.docker.internal" - ip = "host-gateway" - } - volumes { - container_path = "/home/coder/" - volume_name = docker_volume.home_volume.name - read_only = false - } - capabilities { - add = ["CAP_NET_ADMIN", "CAP_SYS_NICE"] - } - # Add labels in Docker to keep track of orphan resources. - labels { - label = "coder.owner" - value = data.coder_workspace_owner.me.name - } - labels { - label = "coder.owner_id" - value = data.coder_workspace_owner.me.id - } - labels { - label = "coder.workspace_id" - value = data.coder_workspace.me.id - } - labels { - label = "coder.workspace_name" - value = data.coder_workspace.me.name - } -} - -resource "coder_metadata" "container_info" { - count = data.coder_workspace.me.start_count - resource_id = docker_container.workspace[0].id - item { - key = "memory" - value = docker_container.workspace[0].memory - } - item { - key = "runtime" - value = docker_container.workspace[0].runtime - } - item { - key = "region" - value = data.coder_parameter.region.option[index(data.coder_parameter.region.option.*.value, data.coder_parameter.region.value)].name - } -} -`; diff --git a/site/src/pages/ChatPage/ChatToolInvocation.tsx b/site/src/pages/ChatPage/ChatToolInvocation.tsx deleted file mode 100644 index 1f6b5556cb30c..0000000000000 --- a/site/src/pages/ChatPage/ChatToolInvocation.tsx +++ /dev/null @@ -1,880 +0,0 @@ -import type { ToolCall, ToolResult } from "@ai-sdk/provider-utils"; -import { useTheme } from "@emotion/react"; -import ArticleIcon from "@mui/icons-material/Article"; -import BuildIcon from "@mui/icons-material/Build"; -import CodeIcon from "@mui/icons-material/Code"; -import FileUploadIcon from "@mui/icons-material/FileUpload"; -import PersonIcon from "@mui/icons-material/Person"; -import SettingsIcon from "@mui/icons-material/Settings"; -import CircularProgress from "@mui/material/CircularProgress"; -import Tooltip from "@mui/material/Tooltip"; -import type * as TypesGen from "api/typesGenerated"; -import { Avatar } from "components/Avatar/Avatar"; -import { - CircleAlertIcon, - CircleCheckIcon, - InfoIcon, - TrashIcon, -} from "lucide-react"; -import type React from "react"; -import { type FC, memo, useMemo, useState } from "react"; -import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; -import { dracula } from "react-syntax-highlighter/dist/cjs/styles/prism"; -import { vscDarkPlus } from "react-syntax-highlighter/dist/cjs/styles/prism"; -import { TabLink, Tabs, TabsList } from "../../components/Tabs/Tabs"; - -interface ChatToolInvocationProps { - toolInvocation: ChatToolInvocation; -} - -export const ChatToolInvocation: FC = ({ - toolInvocation, -}) => { - const theme = useTheme(); - const friendlyName = useMemo(() => { - return toolInvocation.toolName - .replace("coder_", "") - .replace(/_/g, " ") - .replace(/\b\w/g, (char) => char.toUpperCase()); - }, [toolInvocation.toolName]); - - const hasError = useMemo(() => { - if (toolInvocation.state !== "result") { - return false; - } - return ( - typeof toolInvocation.result === "object" && - toolInvocation.result !== null && - "error" in toolInvocation.result - ); - }, [toolInvocation]); - const statusColor = useMemo(() => { - if (toolInvocation.state !== "result") { - return theme.palette.info.main; - } - return hasError ? theme.palette.error.main : theme.palette.success.main; - }, [toolInvocation, hasError, theme]); - const tooltipContent = useMemo(() => { - return ( - - {JSON.stringify(toolInvocation, null, 2)} - - ); - }, [toolInvocation, theme.shape.borderRadius, theme.spacing]); - - return ( -
-
- {toolInvocation.state !== "result" && ( - - )} - {toolInvocation.state === "result" ? ( - hasError ? ( - - ) : ( - - ) - ) : null} -
- {friendlyName} -
- - - -
- {toolInvocation.state === "result" ? ( - - ) : ( - - )} -
- ); -}; - -const ChatToolInvocationCallPreview: FC<{ - toolInvocation: Extract< - ChatToolInvocation, - { state: "call" | "partial-call" } - >; -}> = memo(({ toolInvocation }) => { - const theme = useTheme(); - - let content: React.ReactNode; - switch (toolInvocation.toolName) { - case "coder_upload_tar_file": - content = ( - - ); - break; - } - - if (!content) { - return null; - } - - return
{content}
; -}); - -const ChatToolInvocationResultPreview: FC<{ - toolInvocation: Extract; -}> = memo(({ toolInvocation }) => { - const theme = useTheme(); - - if (!toolInvocation.result) { - return null; - } - - if ( - typeof toolInvocation.result === "object" && - "error" in toolInvocation.result - ) { - return null; - } - - let content: React.ReactNode; - switch (toolInvocation.toolName) { - case "coder_get_workspace": - case "coder_create_workspace": - content = ( -
- {toolInvocation.result.template_icon && ( - {toolInvocation.result.template_display_name - )} -
-
- {toolInvocation.result.name} -
-
- {toolInvocation.result.template_display_name} -
-
-
- ); - break; - case "coder_list_workspaces": - content = ( -
- {toolInvocation.result.map((workspace) => ( -
- {workspace.template_icon && ( - {workspace.template_display_name - )} -
-
- {workspace.name} -
-
- {workspace.template_display_name} -
-
-
- ))} -
- ); - break; - case "coder_list_templates": { - const templates = toolInvocation.result; - content = ( -
- {templates.map((template) => ( -
- -
-
- {template.name} -
-
- {template.description} -
-
-
- ))} - {templates.length === 0 &&
No templates found.
} -
- ); - break; - } - case "coder_template_version_parameters": { - const params = toolInvocation.result; - content = ( -
- - {params.length > 0 - ? `${params.length} parameter(s)` - : "No parameters"} -
- ); - break; - } - case "coder_get_authenticated_user": { - const user = toolInvocation.result; - content = ( -
- - - -
-
- {user.username} -
-
- {user.email} -
-
-
- ); - break; - } - case "coder_create_workspace_build": { - const build = toolInvocation.result; - content = ( -
- - Build #{build.build_number} ({build.transition}) status:{" "} - {build.status} -
- ); - break; - } - case "coder_create_template_version": { - const version = toolInvocation.result; - content = ( -
- -
-
{version.name}
- {version.message && ( -
- {version.message} -
- )} -
-
- ); - break; - } - case "coder_get_workspace_agent_logs": - case "coder_get_workspace_build_logs": - case "coder_get_template_version_logs": { - const logs = toolInvocation.result; - const totalLines = logs.length; - const maxLinesToShow = 5; - const lastLogs = logs.slice(-maxLinesToShow); - const hiddenLines = totalLines - lastLogs.length; - - const totalLinesText = `${totalLines} log line${totalLines !== 1 ? "s" : ""}`; - const hiddenLinesText = - hiddenLines > 0 - ? `... hiding ${hiddenLines} more line${hiddenLines !== 1 ? "s" : ""} ...` - : null; - - const logsToShow = hiddenLinesText - ? [hiddenLinesText, ...lastLogs] - : lastLogs; - - content = ( -
-
- - Retrieved {totalLinesText}. -
- {logsToShow.length > 0 && ( - - {logsToShow.join("\n")} - - )} -
- ); - break; - } - case "coder_update_template_active_version": - content = ( -
- - {toolInvocation.result} -
- ); - break; - case "coder_upload_tar_file": - content = ( - - ); - break; - case "coder_create_template": { - const template = toolInvocation.result; - content = ( -
- {template.display_name -
-
- {template.name} -
-
- {template.display_name} -
-
-
- ); - break; - } - case "coder_delete_template": - content = ( -
- - {toolInvocation.result} -
- ); - break; - case "coder_get_template_version": { - const version = toolInvocation.result; - content = ( -
- -
-
{version.name}
- {version.message && ( -
- {version.message} -
- )} -
-
- ); - break; - } - case "coder_download_tar_file": { - const files = toolInvocation.result; - content = ; - break; - } - // Add default case or handle other tools if necessary - } - return ( -
- {content} -
- ); -}); - -// New component to preview files with tabs -const FilePreview: FC<{ files: Record; prefix?: string }> = - memo(({ files, prefix }) => { - const theme = useTheme(); - const [selectedTab, setSelectedTab] = useState(0); - const fileEntries = useMemo(() => Object.entries(files), [files]); - - if (fileEntries.length === 0) { - return null; - } - - const handleTabChange = (index: number) => { - setSelectedTab(index); - }; - - const getLanguage = (filename: string): string => { - if (filename.includes("Dockerfile")) { - return "dockerfile"; - } - const extension = filename.split(".").pop()?.toLowerCase(); - switch (extension) { - case "tf": - return "hcl"; - case "json": - return "json"; - case "yaml": - case "yml": - return "yaml"; - case "js": - case "jsx": - return "javascript"; - case "ts": - case "tsx": - return "typescript"; - case "py": - return "python"; - case "go": - return "go"; - case "rb": - return "ruby"; - case "java": - return "java"; - case "sh": - return "bash"; - case "md": - return "markdown"; - default: - return "plaintext"; - } - }; - - // Get filename and content based on the selectedTab index - const [selectedFilename, selectedContent] = fileEntries[selectedTab] ?? [ - "", - "", - ]; - - return ( -
- {prefix && ( -
- - {prefix} -
- )} - {/* Use custom Tabs component with active prop */} - - - {fileEntries.map(([filename], index) => ( - { - e.preventDefault(); // Prevent any potential default link behavior - handleTabChange(index); - }} - > - {filename} - - ))} - - - - {selectedContent} - -
- ); - }); - -// TODO: generate these from codersdk/toolsdk.go. -export type ChatToolInvocation = - | ToolInvocation< - "coder_get_workspace", - { - workspace_id: string; - }, - TypesGen.Workspace - > - | ToolInvocation< - "coder_create_workspace", - { - user: string; - template_version_id: string; - name: string; - rich_parameters: Record; - }, - TypesGen.Workspace - > - | ToolInvocation< - "coder_list_workspaces", - { - owner: string; - }, - Pick< - TypesGen.Workspace, - | "id" - | "name" - | "template_id" - | "template_name" - | "template_display_name" - | "template_icon" - | "template_active_version_id" - | "outdated" - >[] - > - | ToolInvocation< - "coder_list_templates", - Record, - Pick< - TypesGen.Template, - | "id" - | "name" - | "description" - | "active_version_id" - | "active_user_count" - >[] - > - | ToolInvocation< - "coder_template_version_parameters", - { - template_version_id: string; - }, - TypesGen.TemplateVersionParameter[] - > - | ToolInvocation< - "coder_get_authenticated_user", - Record, - TypesGen.User - > - | ToolInvocation< - "coder_create_workspace_build", - { - workspace_id: string; - template_version_id?: string; - transition: "start" | "stop" | "delete"; - }, - TypesGen.WorkspaceBuild - > - | ToolInvocation< - "coder_create_template_version", - { - template_id?: string; - file_id: string; - }, - TypesGen.TemplateVersion - > - | ToolInvocation< - "coder_get_workspace_agent_logs", - { - workspace_agent_id: string; - }, - string[] - > - | ToolInvocation< - "coder_get_workspace_build_logs", - { - workspace_build_id: string; - }, - string[] - > - | ToolInvocation< - "coder_get_template_version_logs", - { - template_version_id: string; - }, - string[] - > - | ToolInvocation< - "coder_get_template_version", - { - template_version_id: string; - }, - TypesGen.TemplateVersion - > - | ToolInvocation< - "coder_download_tar_file", - { - file_id: string; - }, - Record - > - | ToolInvocation< - "coder_update_template_active_version", - { - template_id: string; - template_version_id: string; - }, - string - > - | ToolInvocation< - "coder_upload_tar_file", - { - files: Record; - }, - TypesGen.UploadResponse - > - | ToolInvocation< - "coder_create_template", - { - name: string; - }, - TypesGen.Template - > - | ToolInvocation< - "coder_delete_template", - { - template_id: string; - }, - string - >; - -type ToolInvocation = - | ({ - state: "partial-call"; - step?: number; - } & ToolCall) - | ({ - state: "call"; - step?: number; - } & ToolCall) - | ({ - state: "result"; - step?: number; - } & ToolResult< - N, - A, - | R - | { - error: string; - } - >); diff --git a/site/src/pages/ChatPage/LanguageModelSelector.tsx b/site/src/pages/ChatPage/LanguageModelSelector.tsx deleted file mode 100644 index da56ad6839491..0000000000000 --- a/site/src/pages/ChatPage/LanguageModelSelector.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { useTheme } from "@emotion/react"; -import FormControl from "@mui/material/FormControl"; -import InputLabel from "@mui/material/InputLabel"; -import MenuItem from "@mui/material/MenuItem"; -import Select from "@mui/material/Select"; -import { deploymentLanguageModels } from "api/queries/deployment"; -import type { LanguageModel } from "api/typesGenerated"; // Assuming types live here based on project structure -import { Loader } from "components/Loader/Loader"; -import type { FC } from "react"; -import { useQuery } from "react-query"; -import { useChatContext } from "./ChatLayout"; - -export const LanguageModelSelector: FC = () => { - const theme = useTheme(); - const { setSelectedModel, modelConfig, selectedModel } = useChatContext(); - const { - data: languageModelConfig, - isLoading, - error, - } = useQuery(deploymentLanguageModels()); - - if (isLoading) { - return ; - } - - if (error || !languageModelConfig) { - console.error("Failed to load language models:", error); - return ( -
Error loading models.
- ); - } - - const models = Array.from(languageModelConfig.models).toSorted((a, b) => { - // Sort by provider first, then by display name - const compareProvider = a.provider.localeCompare(b.provider); - if (compareProvider !== 0) { - return compareProvider; - } - return a.display_name.localeCompare(b.display_name); - }); - - if (models.length === 0) { - return ( -
- No language models available. -
- ); - } - - return ( - - Model - - - ); -}; diff --git a/site/src/router.tsx b/site/src/router.tsx index 27163b63eb426..a45b96f1af01e 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -1,6 +1,4 @@ import { GlobalErrorBoundary } from "components/ErrorBoundary/GlobalErrorBoundary"; -import { ChatLayout } from "pages/ChatPage/ChatLayout"; -import { ChatMessages } from "pages/ChatPage/ChatMessages"; import { TemplateRedirectController } from "pages/TemplatePage/TemplateRedirectController"; import { Suspense, lazy } from "react"; import { @@ -33,7 +31,6 @@ const NotFoundPage = lazy(() => import("./pages/404Page/404Page")); const DeploymentSettingsLayout = lazy( () => import("./modules/management/DeploymentSettingsLayout"), ); -const ChatLanding = lazy(() => import("./pages/ChatPage/ChatLanding")); const DeploymentConfigProvider = lazy( () => import("./modules/management/DeploymentConfigProvider"), ); @@ -436,11 +433,6 @@ export const router = createBrowserRouter( } /> - }> - } /> - } /> - - } /> }> From 3c4d9206bc7001d657e447c7f32129a5322b10fc Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Wed, 25 Jun 2025 17:41:36 +0300 Subject: [PATCH 02/10] feat(agent/agentcontainers): add feature options as envs (#18576) --- agent/agentcontainers/api.go | 20 ++- agent/agentcontainers/api_test.go | 116 ++++++++++++++++++ agent/agentcontainers/devcontainercli.go | 46 +++++++ agent/agentcontainers/devcontainercli_test.go | 106 ++++++++++++++++ 4 files changed, 282 insertions(+), 6 deletions(-) diff --git a/agent/agentcontainers/api.go b/agent/agentcontainers/api.go index 7ba29e45df7f4..fba5babce5003 100644 --- a/agent/agentcontainers/api.go +++ b/agent/agentcontainers/api.go @@ -1302,6 +1302,7 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c } var ( + featureOptionsAsEnvs []string appsWithPossibleDuplicates []SubAgentApp workspaceFolder = DevcontainerDefaultContainerWorkspaceFolder ) @@ -1313,12 +1314,14 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c ) readConfig := func() (DevcontainerConfig, error) { - return api.dccli.ReadConfig(ctx, dc.WorkspaceFolder, dc.ConfigPath, []string{ - fmt.Sprintf("CODER_WORKSPACE_AGENT_NAME=%s", subAgentConfig.Name), - fmt.Sprintf("CODER_WORKSPACE_OWNER_NAME=%s", api.ownerName), - fmt.Sprintf("CODER_WORKSPACE_NAME=%s", api.workspaceName), - fmt.Sprintf("CODER_URL=%s", api.subAgentURL), - }) + return api.dccli.ReadConfig(ctx, dc.WorkspaceFolder, dc.ConfigPath, + append(featureOptionsAsEnvs, []string{ + fmt.Sprintf("CODER_WORKSPACE_AGENT_NAME=%s", subAgentConfig.Name), + fmt.Sprintf("CODER_WORKSPACE_OWNER_NAME=%s", api.ownerName), + fmt.Sprintf("CODER_WORKSPACE_NAME=%s", api.workspaceName), + fmt.Sprintf("CODER_URL=%s", api.subAgentURL), + }...), + ) } if config, err = readConfig(); err != nil { @@ -1334,6 +1337,11 @@ func (api *API) maybeInjectSubAgentIntoContainerLocked(ctx context.Context, dc c workspaceFolder = config.Workspace.WorkspaceFolder + featureOptionsAsEnvs = config.MergedConfiguration.Features.OptionsAsEnvs() + if len(featureOptionsAsEnvs) > 0 { + configOutdated = true + } + // NOTE(DanielleMaywood): // We only want to take an agent name specified in the root customization layer. // This restricts the ability for a feature to specify the agent name. We may revisit diff --git a/agent/agentcontainers/api_test.go b/agent/agentcontainers/api_test.go index 7c4988630e164..f61f4c24bfc2b 100644 --- a/agent/agentcontainers/api_test.go +++ b/agent/agentcontainers/api_test.go @@ -2060,6 +2060,122 @@ func TestAPI(t *testing.T) { require.Len(t, fSAC.created, 1) }) + t.Run("ReadConfigWithFeatureOptions", func(t *testing.T) { + t.Parallel() + + if runtime.GOOS == "windows" { + t.Skip("Dev Container tests are not supported on Windows (this test uses mocks but fails due to Windows paths)") + } + + var ( + ctx = testutil.Context(t, testutil.WaitMedium) + logger = testutil.Logger(t) + mClock = quartz.NewMock(t) + mCCLI = acmock.NewMockContainerCLI(gomock.NewController(t)) + fSAC = &fakeSubAgentClient{ + logger: logger.Named("fakeSubAgentClient"), + createErrC: make(chan error, 1), + } + fDCCLI = &fakeDevcontainerCLI{ + readConfig: agentcontainers.DevcontainerConfig{ + MergedConfiguration: agentcontainers.DevcontainerMergedConfiguration{ + Features: agentcontainers.DevcontainerFeatures{ + "./code-server": map[string]any{ + "port": 9090, + }, + "ghcr.io/devcontainers/features/docker-in-docker:2": map[string]any{ + "moby": "false", + }, + }, + }, + Workspace: agentcontainers.DevcontainerWorkspace{ + WorkspaceFolder: "/workspaces/coder", + }, + }, + readConfigErrC: make(chan func(envs []string) error, 2), + } + + testContainer = codersdk.WorkspaceAgentContainer{ + ID: "test-container-id", + FriendlyName: "test-container", + Image: "test-image", + Running: true, + CreatedAt: time.Now(), + Labels: map[string]string{ + agentcontainers.DevcontainerLocalFolderLabel: "/workspaces/coder", + agentcontainers.DevcontainerConfigFileLabel: "/workspaces/coder/.devcontainer/devcontainer.json", + }, + } + ) + + coderBin, err := os.Executable() + require.NoError(t, err) + + // Mock the `List` function to always return our test container. + mCCLI.EXPECT().List(gomock.Any()).Return(codersdk.WorkspaceAgentListContainersResponse{ + Containers: []codersdk.WorkspaceAgentContainer{testContainer}, + }, nil).AnyTimes() + + // Mock the steps used for injecting the coder agent. + gomock.InOrder( + mCCLI.EXPECT().DetectArchitecture(gomock.Any(), testContainer.ID).Return(runtime.GOARCH, nil), + mCCLI.EXPECT().ExecAs(gomock.Any(), testContainer.ID, "root", "mkdir", "-p", "/.coder-agent").Return(nil, nil), + mCCLI.EXPECT().Copy(gomock.Any(), testContainer.ID, coderBin, "/.coder-agent/coder").Return(nil), + mCCLI.EXPECT().ExecAs(gomock.Any(), testContainer.ID, "root", "chmod", "0755", "/.coder-agent", "/.coder-agent/coder").Return(nil, nil), + ) + + mClock.Set(time.Now()).MustWait(ctx) + tickerTrap := mClock.Trap().TickerFunc("updaterLoop") + + api := agentcontainers.NewAPI(logger, + agentcontainers.WithClock(mClock), + agentcontainers.WithContainerCLI(mCCLI), + agentcontainers.WithDevcontainerCLI(fDCCLI), + agentcontainers.WithSubAgentClient(fSAC), + agentcontainers.WithSubAgentURL("test-subagent-url"), + agentcontainers.WithWatcher(watcher.NewNoop()), + agentcontainers.WithManifestInfo("test-user", "test-workspace"), + ) + api.Init() + defer api.Close() + + // Close before api.Close() defer to avoid deadlock after test. + defer close(fSAC.createErrC) + defer close(fDCCLI.readConfigErrC) + + // Allow agent creation and injection to succeed. + testutil.RequireSend(ctx, t, fSAC.createErrC, nil) + + testutil.RequireSend(ctx, t, fDCCLI.readConfigErrC, func(envs []string) error { + assert.Contains(t, envs, "CODER_WORKSPACE_AGENT_NAME=coder") + assert.Contains(t, envs, "CODER_WORKSPACE_NAME=test-workspace") + assert.Contains(t, envs, "CODER_WORKSPACE_OWNER_NAME=test-user") + assert.Contains(t, envs, "CODER_URL=test-subagent-url") + // First call should not have feature envs. + assert.NotContains(t, envs, "FEATURE_CODE_SERVER_OPTION_PORT=9090") + assert.NotContains(t, envs, "FEATURE_DOCKER_IN_DOCKER_OPTION_MOBY=false") + return nil + }) + + testutil.RequireSend(ctx, t, fDCCLI.readConfigErrC, func(envs []string) error { + assert.Contains(t, envs, "CODER_WORKSPACE_AGENT_NAME=coder") + assert.Contains(t, envs, "CODER_WORKSPACE_NAME=test-workspace") + assert.Contains(t, envs, "CODER_WORKSPACE_OWNER_NAME=test-user") + assert.Contains(t, envs, "CODER_URL=test-subagent-url") + // Second call should have feature envs from the first config read. + assert.Contains(t, envs, "FEATURE_CODE_SERVER_OPTION_PORT=9090") + assert.Contains(t, envs, "FEATURE_DOCKER_IN_DOCKER_OPTION_MOBY=false") + return nil + }) + + // Wait until the ticker has been registered. + tickerTrap.MustWait(ctx).MustRelease(ctx) + tickerTrap.Close() + + // Verify agent was created successfully + require.Len(t, fSAC.created, 1) + }) + t.Run("CommandEnv", func(t *testing.T) { t.Parallel() diff --git a/agent/agentcontainers/devcontainercli.go b/agent/agentcontainers/devcontainercli.go index 5a9460c945187..4d3a93ae99f4e 100644 --- a/agent/agentcontainers/devcontainercli.go +++ b/agent/agentcontainers/devcontainercli.go @@ -6,7 +6,10 @@ import ( "context" "encoding/json" "errors" + "fmt" "io" + "slices" + "strings" "golang.org/x/xerrors" @@ -26,12 +29,55 @@ type DevcontainerConfig struct { type DevcontainerMergedConfiguration struct { Customizations DevcontainerMergedCustomizations `json:"customizations,omitempty"` + Features DevcontainerFeatures `json:"features,omitempty"` } type DevcontainerMergedCustomizations struct { Coder []CoderCustomization `json:"coder,omitempty"` } +type DevcontainerFeatures map[string]any + +// OptionsAsEnvs converts the DevcontainerFeatures into a list of +// environment variables that can be used to set feature options. +// The format is FEATURE__OPTION_=. +// For example, if the feature is: +// +// "ghcr.io/coder/devcontainer-features/code-server:1": { +// "port": 9090, +// } +// +// It will produce: +// +// FEATURE_CODE_SERVER_OPTION_PORT=9090 +// +// Note that the feature name is derived from the last part of the key, +// so "ghcr.io/coder/devcontainer-features/code-server:1" becomes +// "CODE_SERVER". The version part (e.g. ":1") is removed, and dashes in +// the feature and option names are replaced with underscores. +func (f DevcontainerFeatures) OptionsAsEnvs() []string { + var env []string + for k, v := range f { + vv, ok := v.(map[string]any) + if !ok { + continue + } + // Take the last part of the key as the feature name/path. + k = k[strings.LastIndex(k, "/")+1:] + // Remove ":" and anything following it. + if idx := strings.Index(k, ":"); idx != -1 { + k = k[:idx] + } + k = strings.ReplaceAll(k, "-", "_") + for k2, v2 := range vv { + k2 = strings.ReplaceAll(k2, "-", "_") + env = append(env, fmt.Sprintf("FEATURE_%s_OPTION_%s=%s", strings.ToUpper(k), strings.ToUpper(k2), fmt.Sprintf("%v", v2))) + } + } + slices.Sort(env) + return env +} + type DevcontainerConfiguration struct { Customizations DevcontainerCustomizations `json:"customizations,omitempty"` } diff --git a/agent/agentcontainers/devcontainercli_test.go b/agent/agentcontainers/devcontainercli_test.go index ed600c6d1ee49..523b47d93563c 100644 --- a/agent/agentcontainers/devcontainercli_test.go +++ b/agent/agentcontainers/devcontainercli_test.go @@ -3,6 +3,7 @@ package agentcontainers_test import ( "bytes" "context" + "encoding/json" "errors" "flag" "fmt" @@ -13,6 +14,7 @@ import ( "strings" "testing" + "github.com/google/go-cmp/cmp" "github.com/ory/dockertest/v3" "github.com/ory/dockertest/v3/docker" "github.com/stretchr/testify/assert" @@ -637,3 +639,107 @@ func removeDevcontainerByID(t *testing.T, pool *dockertest.Pool, id string) { assert.NoError(t, err, "remove container failed") } } + +func TestDevcontainerFeatures_OptionsAsEnvs(t *testing.T) { + t.Parallel() + + realConfigJSON := `{ + "mergedConfiguration": { + "features": { + "./code-server": { + "port": 9090 + }, + "ghcr.io/devcontainers/features/docker-in-docker:2": { + "moby": "false" + } + } + } + }` + var realConfig agentcontainers.DevcontainerConfig + err := json.Unmarshal([]byte(realConfigJSON), &realConfig) + require.NoError(t, err, "unmarshal JSON payload") + + tests := []struct { + name string + features agentcontainers.DevcontainerFeatures + want []string + }{ + { + name: "code-server feature", + features: agentcontainers.DevcontainerFeatures{ + "./code-server": map[string]any{ + "port": 9090, + }, + }, + want: []string{ + "FEATURE_CODE_SERVER_OPTION_PORT=9090", + }, + }, + { + name: "docker-in-docker feature", + features: agentcontainers.DevcontainerFeatures{ + "ghcr.io/devcontainers/features/docker-in-docker:2": map[string]any{ + "moby": "false", + }, + }, + want: []string{ + "FEATURE_DOCKER_IN_DOCKER_OPTION_MOBY=false", + }, + }, + { + name: "multiple features with multiple options", + features: agentcontainers.DevcontainerFeatures{ + "./code-server": map[string]any{ + "port": 9090, + "password": "secret", + }, + "ghcr.io/devcontainers/features/docker-in-docker:2": map[string]any{ + "moby": "false", + "docker-dash-compose-version": "v2", + }, + }, + want: []string{ + "FEATURE_CODE_SERVER_OPTION_PASSWORD=secret", + "FEATURE_CODE_SERVER_OPTION_PORT=9090", + "FEATURE_DOCKER_IN_DOCKER_OPTION_DOCKER_DASH_COMPOSE_VERSION=v2", + "FEATURE_DOCKER_IN_DOCKER_OPTION_MOBY=false", + }, + }, + { + name: "feature with non-map value (should be ignored)", + features: agentcontainers.DevcontainerFeatures{ + "./code-server": map[string]any{ + "port": 9090, + }, + "./invalid-feature": "not-a-map", + }, + want: []string{ + "FEATURE_CODE_SERVER_OPTION_PORT=9090", + }, + }, + { + name: "real config example", + features: realConfig.MergedConfiguration.Features, + want: []string{ + "FEATURE_CODE_SERVER_OPTION_PORT=9090", + "FEATURE_DOCKER_IN_DOCKER_OPTION_MOBY=false", + }, + }, + { + name: "empty features", + features: agentcontainers.DevcontainerFeatures{}, + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got := tt.features.OptionsAsEnvs() + if diff := cmp.Diff(tt.want, got); diff != "" { + require.Failf(t, "OptionsAsEnvs() mismatch (-want +got):\n%s", diff) + } + }) + } +} From 072c81cd730b0d35afd540d10112915ca4d8462a Mon Sep 17 00:00:00 2001 From: Edward Angert Date: Wed, 25 Jun 2025 11:17:49 -0400 Subject: [PATCH 03/10] docs: remove nested alerts (#18580) hotfix removes nested gfm alerts, which is a known ~issue~ feature https://github.com/orgs/community/discussions/16925#discussioncomment-12043928 Co-authored-by: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> --- docs/admin/security/database-encryption.md | 9 ++++----- docs/admin/templates/creating-templates.md | 12 ++++-------- docs/admin/users/idp-sync.md | 7 +++---- docs/install/cli.md | 8 +++----- docs/install/index.md | 8 +++----- docs/start/local-deploy.md | 8 +++----- docs/tutorials/quickstart.md | 7 +++---- 7 files changed, 23 insertions(+), 36 deletions(-) diff --git a/docs/admin/security/database-encryption.md b/docs/admin/security/database-encryption.md index 289c18a7c11dd..ecdea90dba499 100644 --- a/docs/admin/security/database-encryption.md +++ b/docs/admin/security/database-encryption.md @@ -118,11 +118,10 @@ data: This command will re-encrypt all tokens with the specified new encryption key. We recommend performing this action during a maintenance window. - > [!IMPORTANT] - > This command requires direct access to the database. If you are using - > the built-in PostgreSQL database, you can run - > [`coder server postgres-builtin-url`](../../reference/cli/server_postgres-builtin-url.md) - > to get the connection URL. + This command requires direct access to the database. + If you are using the built-in PostgreSQL database, you can run + [`coder server postgres-builtin-url`](../../reference/cli/server_postgres-builtin-url.md) + to get the connection URL. - Once the above command completes successfully, remove the old encryption key from Coder's configuration and restart Coder once more. You can now safely diff --git a/docs/admin/templates/creating-templates.md b/docs/admin/templates/creating-templates.md index a0a6b54366948..6387cc0368c35 100644 --- a/docs/admin/templates/creating-templates.md +++ b/docs/admin/templates/creating-templates.md @@ -25,10 +25,8 @@ Give your template a name, description, and icon and press `Create template`. ![Name and icon](../../images/admin/templates/import-template.png) -> [!NOTE] -> If template creation fails, Coder is likely not authorized to -> deploy infrastructure in the given location. Learn how to configure -> [provisioner authentication](./extending-templates/provider-authentication.md). +If template creation fails, it's likely that Coder is not authorized to deploy infrastructure in the given location. +Learn how to configure [provisioner authentication](./extending-templates/provider-authentication.md). ### CLI @@ -65,10 +63,8 @@ Next, push it to Coder with the coder templates push ``` -> [!NOTE] -> If `template push` fails, Coder is likely not authorized to deploy -> infrastructure in the given location. Learn how to configure -> [provisioner authentication](../provisioners/index.md). +If `template push` fails, it's likely that Coder is not authorized to deploy infrastructure in the given location. +Learn how to configure [provisioner authentication](../provisioners/index.md). You can edit the metadata of the template such as the display name with the [`templates edit`](../../reference/cli/templates_edit.md) command: diff --git a/docs/admin/users/idp-sync.md b/docs/admin/users/idp-sync.md index b59431c5f0026..e893bf91bb8ef 100644 --- a/docs/admin/users/idp-sync.md +++ b/docs/admin/users/idp-sync.md @@ -107,10 +107,9 @@ Below is an example that uses the `groups` claim and maps all groups prefixed by } ``` -> [!IMPORTANT] -> You must specify Coder group IDs instead of group names. The fastest way to find -> the ID for a corresponding group is by visiting -> `https://coder.example.com/api/v2/groups`. +You must specify Coder group IDs instead of group names. +You can find the ID for a corresponding group by visiting +`https://coder.example.com/api/v2/groups`. Here is another example which maps `coder-admins` from the identity provider to two groups in Coder and `coder-users` from the identity provider to another diff --git a/docs/install/cli.md b/docs/install/cli.md index 9ee914a80f326..9193c9a103a19 100644 --- a/docs/install/cli.md +++ b/docs/install/cli.md @@ -22,11 +22,9 @@ alternate installation methods (e.g. standalone binaries, system packages). ## Windows -> [!IMPORTANT] -> If you plan to use the built-in PostgreSQL database, you will -> need to ensure that the -> [Visual C++ Runtime](https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist#latest-microsoft-visual-c-redistributable-version) -> is installed. +If you plan to use the built-in PostgreSQL database, ensure that the +[Visual C++ Runtime](https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist#latest-microsoft-visual-c-redistributable-version) +is installed. Use [GitHub releases](https://github.com/coder/coder/releases) to download the Windows installer (`.msi`) or standalone binary (`.exe`). diff --git a/docs/install/index.md b/docs/install/index.md index ae64dd2bf5915..c1d12dd779276 100644 --- a/docs/install/index.md +++ b/docs/install/index.md @@ -29,11 +29,9 @@ alternate installation methods (e.g. standalone binaries, system packages). ## Windows -> [!IMPORTANT] -> If you plan to use the built-in PostgreSQL database, you will -> need to ensure that the -> [Visual C++ Runtime](https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist#latest-microsoft-visual-c-redistributable-version) -> is installed. +If you plan to use the built-in PostgreSQL database, ensure that the +[Visual C++ Runtime](https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist#latest-microsoft-visual-c-redistributable-version) +is installed. Use [GitHub releases](https://github.com/coder/coder/releases) to download the Windows installer (`.msi`) or standalone binary (`.exe`). diff --git a/docs/start/local-deploy.md b/docs/start/local-deploy.md index 3fe501c02b8eb..eb3b2af131853 100644 --- a/docs/start/local-deploy.md +++ b/docs/start/local-deploy.md @@ -29,11 +29,9 @@ curl -L https://coder.com/install.sh | sh ## Windows -> [!IMPORTANT] -> If you plan to use the built-in PostgreSQL database, you will -> need to ensure that the -> [Visual C++ Runtime](https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist#latest-microsoft-visual-c-redistributable-version) -> is installed. +If you plan to use the built-in PostgreSQL database, ensure that the +[Visual C++ Runtime](https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist#latest-microsoft-visual-c-redistributable-version) +is installed. You can use the [`winget`](https://learn.microsoft.com/en-us/windows/package-manager/winget/#use-winget) diff --git a/docs/tutorials/quickstart.md b/docs/tutorials/quickstart.md index a09bb95d478b7..595414fd63ccd 100644 --- a/docs/tutorials/quickstart.md +++ b/docs/tutorials/quickstart.md @@ -57,10 +57,9 @@ persistent environment from your main device, a tablet, or your phone. ## Windows -> [!IMPORTANT] -> If you plan to use the built-in PostgreSQL database, ensure that the -> [Visual C++ Runtime](https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist#latest-microsoft-visual-c-redistributable-version) -> is installed. +If you plan to use the built-in PostgreSQL database, ensure that the +[Visual C++ Runtime](https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist#latest-microsoft-visual-c-redistributable-version) +is installed. 1. [Install Docker](https://docs.docker.com/desktop/install/windows-install/). From e396b06c25a1c741122b76f03c22fdf2b006d4a7 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 25 Jun 2025 10:41:53 -0500 Subject: [PATCH 04/10] feat: allow new immutable parameters for existing workspaces (#18579) Closes https://github.com/coder/coder/issues/18578 --- coderd/coderdtest/dynamicparameters.go | 23 +++++- coderd/dynamicparameters/rendermock/mock.go | 2 + .../rendermock/rendermock.go | 71 +++++++++++++++++++ coderd/dynamicparameters/resolver.go | 10 ++- coderd/dynamicparameters/resolver_test.go | 59 +++++++++++++++ enterprise/coderd/dynamicparameters_test.go | 51 +++++++++++++ .../testdata/parameters/immutable/main.tf | 16 +++++ .../coderd/testdata/parameters/none/main.tf | 10 +++ 8 files changed, 237 insertions(+), 5 deletions(-) create mode 100644 coderd/dynamicparameters/rendermock/mock.go create mode 100644 coderd/dynamicparameters/rendermock/rendermock.go create mode 100644 coderd/dynamicparameters/resolver_test.go create mode 100644 enterprise/coderd/testdata/parameters/immutable/main.tf create mode 100644 enterprise/coderd/testdata/parameters/none/main.tf diff --git a/coderd/coderdtest/dynamicparameters.go b/coderd/coderdtest/dynamicparameters.go index b5bb34a0e3468..cb295eeaae965 100644 --- a/coderd/coderdtest/dynamicparameters.go +++ b/coderd/coderdtest/dynamicparameters.go @@ -22,6 +22,9 @@ type DynamicParameterTemplateParams struct { // StaticParams is used if the provisioner daemon version does not support dynamic parameters. StaticParams []*proto.RichParameter + + // TemplateID is used to update an existing template instead of creating a new one. + TemplateID uuid.UUID } func DynamicParameterTemplate(t *testing.T, client *codersdk.Client, org uuid.UUID, args DynamicParameterTemplateParams) (codersdk.Template, codersdk.TemplateVersion) { @@ -40,16 +43,30 @@ func DynamicParameterTemplate(t *testing.T, client *codersdk.Client, org uuid.UU }, }} - version := CreateTemplateVersion(t, client, org, files) + version := CreateTemplateVersion(t, client, org, files, func(request *codersdk.CreateTemplateVersionRequest) { + if args.TemplateID != uuid.Nil { + request.TemplateID = args.TemplateID + } + }) AwaitTemplateVersionJobCompleted(t, client, version.ID) - tpl := CreateTemplate(t, client, org, version.ID) + + tplID := args.TemplateID + if args.TemplateID == uuid.Nil { + tpl := CreateTemplate(t, client, org, version.ID) + tplID = tpl.ID + } var err error - tpl, err = client.UpdateTemplateMeta(t.Context(), tpl.ID, codersdk.UpdateTemplateMeta{ + tpl, err := client.UpdateTemplateMeta(t.Context(), tplID, codersdk.UpdateTemplateMeta{ UseClassicParameterFlow: ptr.Ref(false), }) require.NoError(t, err) + err = client.UpdateActiveTemplateVersion(t.Context(), tpl.ID, codersdk.UpdateActiveTemplateVersion{ + ID: version.ID, + }) + require.NoError(t, err) + return tpl, version } diff --git a/coderd/dynamicparameters/rendermock/mock.go b/coderd/dynamicparameters/rendermock/mock.go new file mode 100644 index 0000000000000..ffb23780629f6 --- /dev/null +++ b/coderd/dynamicparameters/rendermock/mock.go @@ -0,0 +1,2 @@ +//go:generate mockgen -destination ./rendermock.go -package rendermock github.com/coder/coder/v2/coderd/dynamicparameters Renderer +package rendermock diff --git a/coderd/dynamicparameters/rendermock/rendermock.go b/coderd/dynamicparameters/rendermock/rendermock.go new file mode 100644 index 0000000000000..996b02a555b08 --- /dev/null +++ b/coderd/dynamicparameters/rendermock/rendermock.go @@ -0,0 +1,71 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/coder/coder/v2/coderd/dynamicparameters (interfaces: Renderer) +// +// Generated by this command: +// +// mockgen -destination ./rendermock.go -package rendermock github.com/coder/coder/v2/coderd/dynamicparameters Renderer +// + +// Package rendermock is a generated GoMock package. +package rendermock + +import ( + context "context" + reflect "reflect" + + preview "github.com/coder/preview" + uuid "github.com/google/uuid" + hcl "github.com/hashicorp/hcl/v2" + gomock "go.uber.org/mock/gomock" +) + +// MockRenderer is a mock of Renderer interface. +type MockRenderer struct { + ctrl *gomock.Controller + recorder *MockRendererMockRecorder + isgomock struct{} +} + +// MockRendererMockRecorder is the mock recorder for MockRenderer. +type MockRendererMockRecorder struct { + mock *MockRenderer +} + +// NewMockRenderer creates a new mock instance. +func NewMockRenderer(ctrl *gomock.Controller) *MockRenderer { + mock := &MockRenderer{ctrl: ctrl} + mock.recorder = &MockRendererMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRenderer) EXPECT() *MockRendererMockRecorder { + return m.recorder +} + +// Close mocks base method. +func (m *MockRenderer) Close() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Close") +} + +// Close indicates an expected call of Close. +func (mr *MockRendererMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockRenderer)(nil).Close)) +} + +// Render mocks base method. +func (m *MockRenderer) Render(ctx context.Context, ownerID uuid.UUID, values map[string]string) (*preview.Output, hcl.Diagnostics) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Render", ctx, ownerID, values) + ret0, _ := ret[0].(*preview.Output) + ret1, _ := ret[1].(hcl.Diagnostics) + return ret0, ret1 +} + +// Render indicates an expected call of Render. +func (mr *MockRendererMockRecorder) Render(ctx, ownerID, values any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Render", reflect.TypeOf((*MockRenderer)(nil).Render), ctx, ownerID, values) +} diff --git a/coderd/dynamicparameters/resolver.go b/coderd/dynamicparameters/resolver.go index f71b630858013..3cb9c59f286d6 100644 --- a/coderd/dynamicparameters/resolver.go +++ b/coderd/dynamicparameters/resolver.go @@ -169,9 +169,15 @@ func ResolveParameters( parameterNames[parameter.Name] = struct{}{} if !firstBuild && !parameter.Mutable { + originalValue, ok := originalValues[parameter.Name] // Immutable parameters should not be changed after the first build. - // They can match the original value though! - if parameter.Value.AsString() != originalValues[parameter.Name].Value { + // If the value matches the original value, that is fine. + // + // If the original value is not set, that means this is a new parameter. New + // immutable parameters are allowed. This is an opinionated choice to prevent + // workspaces failing to update or delete. Ideally we would block this, as + // immutable parameters should only be able to be set at creation time. + if ok && parameter.Value.AsString() != originalValue.Value { var src *hcl.Range if parameter.Source != nil { src = ¶meter.Source.HCLBlock().TypeRange diff --git a/coderd/dynamicparameters/resolver_test.go b/coderd/dynamicparameters/resolver_test.go new file mode 100644 index 0000000000000..ec5218613ff03 --- /dev/null +++ b/coderd/dynamicparameters/resolver_test.go @@ -0,0 +1,59 @@ +package dynamicparameters_test + +import ( + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/dynamicparameters" + "github.com/coder/coder/v2/coderd/dynamicparameters/rendermock" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/testutil" + "github.com/coder/preview" + previewtypes "github.com/coder/preview/types" + "github.com/coder/terraform-provider-coder/v2/provider" +) + +func TestResolveParameters(t *testing.T) { + t.Parallel() + + t.Run("NewImmutable", func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + render := rendermock.NewMockRenderer(ctrl) + + // A single immutable parameter with no previous value. + render.EXPECT(). + Render(gomock.Any(), gomock.Any(), gomock.Any()). + AnyTimes(). + Return(&preview.Output{ + Parameters: []previewtypes.Parameter{ + { + ParameterData: previewtypes.ParameterData{ + Name: "immutable", + Type: previewtypes.ParameterTypeString, + FormType: provider.ParameterFormTypeInput, + Mutable: false, + DefaultValue: previewtypes.StringLiteral("foo"), + Required: true, + }, + Value: previewtypes.StringLiteral("foo"), + Diagnostics: nil, + }, + }, + }, nil) + + ctx := testutil.Context(t, testutil.WaitShort) + values, err := dynamicparameters.ResolveParameters(ctx, uuid.New(), render, false, + []database.WorkspaceBuildParameter{}, // No previous values + []codersdk.WorkspaceBuildParameter{}, // No new build values + []database.TemplateVersionPresetParameter{}, // No preset values + ) + require.NoError(t, err) + require.Equal(t, map[string]string{"immutable": "foo"}, values) + }) +} diff --git a/enterprise/coderd/dynamicparameters_test.go b/enterprise/coderd/dynamicparameters_test.go index 8bbe5da470f42..87d115034f247 100644 --- a/enterprise/coderd/dynamicparameters_test.go +++ b/enterprise/coderd/dynamicparameters_test.go @@ -302,6 +302,57 @@ func TestDynamicParameterBuild(t *testing.T) { require.ErrorContains(t, err, "Number must be between 0 and 10") }) }) + + t.Run("ImmutableValidation", func(t *testing.T) { + t.Parallel() + + // NewImmutable tests the case where a new immutable parameter is added to a template + // after a workspace has been created with an older version of the template. + // The test tries to delete the workspace, which should succeed. + t.Run("NewImmutable", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + // Start with a new template that has 0 parameters + empty, _ := coderdtest.DynamicParameterTemplate(t, templateAdmin, orgID, coderdtest.DynamicParameterTemplateParams{ + MainTF: string(must(os.ReadFile("testdata/parameters/none/main.tf"))), + }) + + // Create the workspace with 0 parameters + wrk, err := templateAdmin.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{ + TemplateID: empty.ID, + Name: coderdtest.RandomUsername(t), + RichParameterValues: []codersdk.WorkspaceBuildParameter{}, + }) + require.NoError(t, err) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, templateAdmin, wrk.LatestBuild.ID) + + // Update the template with a new immutable parameter + _, immutable := coderdtest.DynamicParameterTemplate(t, templateAdmin, orgID, coderdtest.DynamicParameterTemplateParams{ + MainTF: string(must(os.ReadFile("testdata/parameters/immutable/main.tf"))), + TemplateID: empty.ID, + }) + + bld, err := templateAdmin.CreateWorkspaceBuild(ctx, wrk.ID, codersdk.CreateWorkspaceBuildRequest{ + TemplateVersionID: immutable.ID, // Use the new template version with the immutable parameter + Transition: codersdk.WorkspaceTransitionDelete, + DryRun: false, + }) + require.NoError(t, err) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, templateAdmin, bld.ID) + + // Verify the immutable parameter is set on the workspace build + params, err := templateAdmin.WorkspaceBuildParameters(ctx, bld.ID) + require.NoError(t, err) + require.Len(t, params, 1) + require.Equal(t, "Hello World", params[0].Value) + + // Verify the workspace is deleted + deleted, err := templateAdmin.DeletedWorkspace(ctx, wrk.ID) + require.NoError(t, err) + require.Equal(t, wrk.ID, deleted.ID, "workspace should be deleted") + }) + }) } // TestDynamicParameterTemplate uses a template with some dynamic elements, and diff --git a/enterprise/coderd/testdata/parameters/immutable/main.tf b/enterprise/coderd/testdata/parameters/immutable/main.tf new file mode 100644 index 0000000000000..84b8967ac305e --- /dev/null +++ b/enterprise/coderd/testdata/parameters/immutable/main.tf @@ -0,0 +1,16 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + } +} + +data "coder_workspace_owner" "me" {} + +data "coder_parameter" "immutable" { + name = "immutable" + type = "string" + mutable = false + default = "Hello World" +} diff --git a/enterprise/coderd/testdata/parameters/none/main.tf b/enterprise/coderd/testdata/parameters/none/main.tf new file mode 100644 index 0000000000000..74a83f752f4d8 --- /dev/null +++ b/enterprise/coderd/testdata/parameters/none/main.tf @@ -0,0 +1,10 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + } +} + +data "coder_workspace_owner" "me" {} + From 8e0b6f81574b8f8dc88d74b2ec6d8c5ad690dc77 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 25 Jun 2025 17:47:42 +0200 Subject: [PATCH 05/10] chore: upgrade aisdk-go lib, remove vestigial code (#18577) --- cli/server.go | 71 -------------------------------- codersdk/deployment.go | 15 ------- codersdk/toolsdk/toolsdk.go | 3 +- codersdk/toolsdk/toolsdk_test.go | 3 +- go.mod | 8 ++-- go.sum | 16 +++---- site/src/api/typesGenerated.ts | 12 ------ 7 files changed, 16 insertions(+), 112 deletions(-) diff --git a/cli/server.go b/cli/server.go index bccaefd9b87bb..d739863b9fa6f 100644 --- a/cli/server.go +++ b/cli/server.go @@ -2624,77 +2624,6 @@ func redirectHTTPToHTTPSDeprecation(ctx context.Context, logger slog.Logger, inv } } -func ReadAIProvidersFromEnv(environ []string) ([]codersdk.AIProviderConfig, error) { - // The index numbers must be in-order. - sort.Strings(environ) - - var providers []codersdk.AIProviderConfig - for _, v := range serpent.ParseEnviron(environ, "CODER_AI_PROVIDER_") { - tokens := strings.SplitN(v.Name, "_", 2) - if len(tokens) != 2 { - return nil, xerrors.Errorf("invalid env var: %s", v.Name) - } - - providerNum, err := strconv.Atoi(tokens[0]) - if err != nil { - return nil, xerrors.Errorf("parse number: %s", v.Name) - } - - var provider codersdk.AIProviderConfig - switch { - case len(providers) < providerNum: - return nil, xerrors.Errorf( - "provider num %v skipped: %s", - len(providers), - v.Name, - ) - case len(providers) == providerNum: - // At the next next provider. - providers = append(providers, provider) - case len(providers) == providerNum+1: - // At the current provider. - provider = providers[providerNum] - } - - key := tokens[1] - switch key { - case "TYPE": - provider.Type = v.Value - case "API_KEY": - provider.APIKey = v.Value - case "BASE_URL": - provider.BaseURL = v.Value - case "MODELS": - provider.Models = strings.Split(v.Value, ",") - } - providers[providerNum] = provider - } - for _, envVar := range environ { - tokens := strings.SplitN(envVar, "=", 2) - if len(tokens) != 2 { - continue - } - switch tokens[0] { - case "OPENAI_API_KEY": - providers = append(providers, codersdk.AIProviderConfig{ - Type: "openai", - APIKey: tokens[1], - }) - case "ANTHROPIC_API_KEY": - providers = append(providers, codersdk.AIProviderConfig{ - Type: "anthropic", - APIKey: tokens[1], - }) - case "GOOGLE_API_KEY": - providers = append(providers, codersdk.AIProviderConfig{ - Type: "google", - APIKey: tokens[1], - }) - } - } - return providers, nil -} - // ReadExternalAuthProvidersFromEnv is provided for compatibility purposes with // the viper CLI. func ReadExternalAuthProvidersFromEnv(environ []string) ([]codersdk.ExternalAuthConfig, error) { diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 26c50b555dbfe..ef0b4eaa07821 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -3122,21 +3122,6 @@ Write out the current server config as YAML to stdout.`, return opts } -type AIProviderConfig struct { - // Type is the type of the API provider. - Type string `json:"type" yaml:"type"` - // APIKey is the API key to use for the API provider. - APIKey string `json:"-" yaml:"api_key"` - // Models is the list of models to use for the API provider. - Models []string `json:"models" yaml:"models"` - // BaseURL is the base URL to use for the API provider. - BaseURL string `json:"base_url" yaml:"base_url"` -} - -type AIConfig struct { - Providers []AIProviderConfig `json:"providers,omitempty" yaml:"providers,omitempty"` -} - type SupportConfig struct { Links serpent.Struct[[]LinkConfig] `json:"links" typescript:",notnull"` } diff --git a/codersdk/toolsdk/toolsdk.go b/codersdk/toolsdk/toolsdk.go index 3b992124005ac..24433c1b2a6da 100644 --- a/codersdk/toolsdk/toolsdk.go +++ b/codersdk/toolsdk/toolsdk.go @@ -8,9 +8,10 @@ import ( "io" "github.com/google/uuid" - "github.com/kylecarbs/aisdk-go" "golang.org/x/xerrors" + "github.com/coder/aisdk-go" + "github.com/coder/coder/v2/codersdk" ) diff --git a/codersdk/toolsdk/toolsdk_test.go b/codersdk/toolsdk/toolsdk_test.go index e4c4239be51e2..d08191a614a99 100644 --- a/codersdk/toolsdk/toolsdk_test.go +++ b/codersdk/toolsdk/toolsdk_test.go @@ -10,11 +10,12 @@ import ( "time" "github.com/google/uuid" - "github.com/kylecarbs/aisdk-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/goleak" + "github.com/coder/aisdk-go" + "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbfake" diff --git a/go.mod b/go.mod index 22fb1cd7f3aa6..9e5df584af9e8 100644 --- a/go.mod +++ b/go.mod @@ -482,9 +482,9 @@ require ( require ( github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225 + github.com/coder/aisdk-go v0.0.9 github.com/coder/preview v1.0.1 github.com/fsnotify/fsnotify v1.9.0 - github.com/kylecarbs/aisdk-go v0.0.8 github.com/mark3labs/mcp-go v0.32.0 ) @@ -502,7 +502,7 @@ require ( github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0 // indirect github.com/Masterminds/semver/v3 v3.3.1 // indirect - github.com/anthropics/anthropic-sdk-go v0.2.0-beta.3 // indirect + github.com/anthropics/anthropic-sdk-go v1.4.0 // indirect github.com/aquasecurity/go-version v0.0.1 // indirect github.com/aquasecurity/trivy v0.58.2 // indirect github.com/aws/aws-sdk-go v1.55.7 // indirect @@ -520,7 +520,7 @@ require ( github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect - github.com/openai/openai-go v0.1.0-beta.10 // indirect + github.com/openai/openai-go v1.3.0 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect github.com/samber/lo v1.50.0 // indirect @@ -535,6 +535,6 @@ require ( go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect - google.golang.org/genai v0.7.0 // indirect + google.golang.org/genai v1.10.0 // indirect k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect ) diff --git a/go.sum b/go.sum index 7a996d81c6348..c5482e3477839 100644 --- a/go.sum +++ b/go.sum @@ -720,8 +720,8 @@ github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7X github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= -github.com/anthropics/anthropic-sdk-go v0.2.0-beta.3 h1:b5t1ZJMvV/l99y4jbz7kRFdUp3BSDkI8EhSlHczivtw= -github.com/anthropics/anthropic-sdk-go v0.2.0-beta.3/go.mod h1:AapDW22irxK2PSumZiQXYUFvsdQgkwIWlpESweWZI/c= +github.com/anthropics/anthropic-sdk-go v1.4.0 h1:fU1jKxYbQdQDiEXCxeW5XZRIOwKevn/PMg8Ay1nnUx0= +github.com/anthropics/anthropic-sdk-go v1.4.0/go.mod h1:AapDW22irxK2PSumZiQXYUFvsdQgkwIWlpESweWZI/c= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= @@ -897,6 +897,8 @@ github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73l github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225 h1:tRIViZ5JRmzdOEo5wUWngaGEFBG8OaE1o2GIHN5ujJ8= github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225/go.mod h1:rNLVpYgEVeu1Zk29K64z6Od8RBP9DwqCu9OfCzh8MR4= +github.com/coder/aisdk-go v0.0.9 h1:Vzo/k2qwVGLTR10ESDeP2Ecek1SdPfZlEjtTfMveiVo= +github.com/coder/aisdk-go v0.0.9/go.mod h1:KF6/Vkono0FJJOtWtveh5j7yfNrSctVTpwgweYWSp5M= github.com/coder/bubbletea v1.2.2-0.20241212190825-007a1cdb2c41 h1:SBN/DA63+ZHwuWwPHPYoCZ/KLAjHv5g4h2MS4f2/MTI= github.com/coder/bubbletea v1.2.2-0.20241212190825-007a1cdb2c41/go.mod h1:I9ULxr64UaOSUv7hcb3nX4kowodJCVS7vt7VVJk/kW4= github.com/coder/clistat v1.0.0 h1:MjiS7qQ1IobuSSgDnxcCSyBPESs44hExnh2TEqMcGnA= @@ -1470,8 +1472,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylecarbs/aisdk-go v0.0.8 h1:hnKVbLM6U8XqX3t5I26J8k5saXdra595bGt1HP0PvKA= -github.com/kylecarbs/aisdk-go v0.0.8/go.mod h1:3nAhClwRNo6ZfU44GrBZ8O2fCCrxJdaHb9JIz+P3LR8= github.com/kylecarbs/chroma/v2 v2.0.0-20240401211003-9e036e0631f3 h1:Z9/bo5PSeMutpdiKYNt/TTSfGM1Ll0naj3QzYX9VxTc= github.com/kylecarbs/chroma/v2 v2.0.0-20240401211003-9e036e0631f3/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk= github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= @@ -1613,8 +1613,8 @@ github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.120.1 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.120.1/go.mod h1:01TvyaK8x640crO2iFwW/6CFCZgNsOvOGH3B5J239m0= github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisticsamplerprocessor v0.120.1 h1:TCyOus9tym82PD1VYtthLKMVMlVyRwtDI4ck4SR2+Ok= github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisticsamplerprocessor v0.120.1/go.mod h1:Z/S1brD5gU2Ntht/bHxBVnGxXKTvZDr0dNv/riUzPmY= -github.com/openai/openai-go v0.1.0-beta.10 h1:CknhGXe8aXQMRuqg255PFnWzgRY9nEryMxoNIBBM9tU= -github.com/openai/openai-go v0.1.0-beta.10/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= +github.com/openai/openai-go v1.3.0 h1:lBpvgXxGHUufk9DNTguval40y2oK0GHZwgWQyUtjPIQ= +github.com/openai/openai-go v1.3.0/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -2495,8 +2495,8 @@ google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genai v0.7.0 h1:TINBYXnP+K+D8b16LfVyb6XR3kdtieXy6nJsGoEXcBc= -google.golang.org/genai v0.7.0/go.mod h1:TyfOKRz/QyCaj6f/ZDt505x+YreXnY40l2I6k8TvgqY= +google.golang.org/genai v1.10.0 h1:ETP0Yksn5KUSEn5+ihMOnP3IqjZ+7Z4i0LjJslEXatI= +google.golang.org/genai v1.10.0/go.mod h1:TyfOKRz/QyCaj6f/ZDt505x+YreXnY40l2I6k8TvgqY= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 730b9c54ac2b7..b2117cf15c987 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -6,18 +6,6 @@ export interface ACLAvailable { readonly groups: readonly Group[]; } -// From codersdk/deployment.go -export interface AIConfig { - readonly providers?: readonly AIProviderConfig[]; -} - -// From codersdk/deployment.go -export interface AIProviderConfig { - readonly type: string; - readonly models: readonly string[]; - readonly base_url: string; -} - // From codersdk/aitasks.go export const AITaskPromptParameterName = "AI Prompt"; From 9d2f8dc274ed47e63849acbea5b87599579fe78d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Jun 2025 16:01:29 +0000 Subject: [PATCH 06/10] chore: bump github.com/openai/openai-go from 0.1.0-beta.10 to 1.6.0 (#18493) [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/openai/openai-go&package-manager=go_modules&previous-version=0.1.0-beta.10&new-version=1.6.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9e5df584af9e8..adfbc951c4ff1 100644 --- a/go.mod +++ b/go.mod @@ -520,7 +520,7 @@ require ( github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect - github.com/openai/openai-go v1.3.0 // indirect + github.com/openai/openai-go v1.7.0 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect github.com/samber/lo v1.50.0 // indirect diff --git a/go.sum b/go.sum index c5482e3477839..b011e04d7bb3c 100644 --- a/go.sum +++ b/go.sum @@ -1613,8 +1613,8 @@ github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.120.1 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.120.1/go.mod h1:01TvyaK8x640crO2iFwW/6CFCZgNsOvOGH3B5J239m0= github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisticsamplerprocessor v0.120.1 h1:TCyOus9tym82PD1VYtthLKMVMlVyRwtDI4ck4SR2+Ok= github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisticsamplerprocessor v0.120.1/go.mod h1:Z/S1brD5gU2Ntht/bHxBVnGxXKTvZDr0dNv/riUzPmY= -github.com/openai/openai-go v1.3.0 h1:lBpvgXxGHUufk9DNTguval40y2oK0GHZwgWQyUtjPIQ= -github.com/openai/openai-go v1.3.0/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= +github.com/openai/openai-go v1.7.0 h1:M1JfDjQgo3d3PsLyZgpGUG0wUAaUAitqJPM4Rl56dCA= +github.com/openai/openai-go v1.7.0/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= From aef101ffd3a2fe86b54168a7e208ebd851460a4f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Jun 2025 16:01:53 +0000 Subject: [PATCH 07/10] chore: bump google.golang.org/genai from 0.7.0 to 1.12.0 (#18496) [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=google.golang.org/genai&package-manager=go_modules&previous-version=0.7.0&new-version=1.12.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index adfbc951c4ff1..5325b190b1380 100644 --- a/go.mod +++ b/go.mod @@ -535,6 +535,6 @@ require ( go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect - google.golang.org/genai v1.10.0 // indirect + google.golang.org/genai v1.12.0 // indirect k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect ) diff --git a/go.sum b/go.sum index b011e04d7bb3c..14b86b4d38b4a 100644 --- a/go.sum +++ b/go.sum @@ -2495,8 +2495,8 @@ google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genai v1.10.0 h1:ETP0Yksn5KUSEn5+ihMOnP3IqjZ+7Z4i0LjJslEXatI= -google.golang.org/genai v1.10.0/go.mod h1:TyfOKRz/QyCaj6f/ZDt505x+YreXnY40l2I6k8TvgqY= +google.golang.org/genai v1.12.0 h1:0JjAdwvEAha9ZpPH5hL6dVG8bpMnRbAMCgv2f2LDnz4= +google.golang.org/genai v1.12.0/go.mod h1:HFXR1zT3LCdLxd/NW6IOSCczOYyRAxwaShvYbgPSeVw= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= From 6c713d5c209dce8d4abad01f7b9be13c6942c840 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 25 Jun 2025 17:36:23 +0100 Subject: [PATCH 08/10] fix(coderd/agentapi): make sub agent slugs more unique (#18581) The incorrect assumption that slugs were unique per-agent was made when the subagent API was implemented. Whilst this PR doesn't completely enforce that, we instead compute a stable hash to prefix the slug that should provide a reasonable level of probability that the slug will be unique. --- coderd/agentapi/subagent.go | 13 ++++++++++++- coderd/agentapi/subagent_test.go | 30 +++++++++++++++--------------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/coderd/agentapi/subagent.go b/coderd/agentapi/subagent.go index 1868ad39bd362..1753f5b7d4093 100644 --- a/coderd/agentapi/subagent.go +++ b/coderd/agentapi/subagent.go @@ -2,7 +2,9 @@ package agentapi import ( "context" + "crypto/sha256" "database/sql" + "encoding/base32" "errors" "fmt" "strings" @@ -165,11 +167,20 @@ func (a *SubAgentAPI) CreateSubAgent(ctx context.Context, req *agentproto.Create } } + // NOTE(DanielleMaywood): + // Slugs must be unique PER workspace/template. As of 2025-06-25, + // there is no database-layer enforcement of this constraint. + // We can get around this by creating a slug that *should* be + // unique (at least highly probable). + slugHash := sha256.Sum256([]byte(subAgent.Name + "/" + app.Slug)) + slugHashEnc := base32.HexEncoding.WithPadding(base32.NoPadding).EncodeToString(slugHash[:]) + computedSlug := strings.ToLower(slugHashEnc[:8]) + "-" + app.Slug + _, err := a.Database.UpsertWorkspaceApp(ctx, database.UpsertWorkspaceAppParams{ ID: uuid.New(), // NOTE: we may need to maintain the app's ID here for stability, but for now we'll leave this as-is. CreatedAt: createdAt, AgentID: subAgent.ID, - Slug: app.Slug, + Slug: computedSlug, DisplayName: app.GetDisplayName(), Icon: app.GetIcon(), Command: sql.NullString{ diff --git a/coderd/agentapi/subagent_test.go b/coderd/agentapi/subagent_test.go index 3fa2bed1ead85..0a95a70e5216d 100644 --- a/coderd/agentapi/subagent_test.go +++ b/coderd/agentapi/subagent_test.go @@ -216,7 +216,7 @@ func TestSubAgentAPI(t *testing.T) { }, expectApps: []database.WorkspaceApp{ { - Slug: "code-server", + Slug: "fdqf0lpd-code-server", DisplayName: "VS Code", Icon: "/icon/code.svg", Command: sql.NullString{}, @@ -234,7 +234,7 @@ func TestSubAgentAPI(t *testing.T) { DisplayGroup: sql.NullString{}, }, { - Slug: "vim", + Slug: "547knu0f-vim", DisplayName: "Vim", Icon: "/icon/vim.svg", Command: sql.NullString{Valid: true, String: "vim"}, @@ -377,7 +377,7 @@ func TestSubAgentAPI(t *testing.T) { }, expectApps: []database.WorkspaceApp{ { - Slug: "valid-app", + Slug: "511ctirn-valid-app", DisplayName: "Valid App", SharingLevel: database.AppSharingLevelOwner, Health: database.WorkspaceAppHealthDisabled, @@ -410,19 +410,19 @@ func TestSubAgentAPI(t *testing.T) { }, expectApps: []database.WorkspaceApp{ { - Slug: "authenticated-app", + Slug: "atpt261l-authenticated-app", SharingLevel: database.AppSharingLevelAuthenticated, Health: database.WorkspaceAppHealthDisabled, OpenIn: database.WorkspaceAppOpenInSlimWindow, }, { - Slug: "owner-app", + Slug: "eh5gp1he-owner-app", SharingLevel: database.AppSharingLevelOwner, Health: database.WorkspaceAppHealthDisabled, OpenIn: database.WorkspaceAppOpenInSlimWindow, }, { - Slug: "public-app", + Slug: "oopjevf1-public-app", SharingLevel: database.AppSharingLevelPublic, Health: database.WorkspaceAppHealthDisabled, OpenIn: database.WorkspaceAppOpenInSlimWindow, @@ -443,13 +443,13 @@ func TestSubAgentAPI(t *testing.T) { }, expectApps: []database.WorkspaceApp{ { - Slug: "tab-app", + Slug: "ci9500rm-tab-app", SharingLevel: database.AppSharingLevelOwner, Health: database.WorkspaceAppHealthDisabled, OpenIn: database.WorkspaceAppOpenInTab, }, { - Slug: "window-app", + Slug: "p17s76re-window-app", SharingLevel: database.AppSharingLevelOwner, Health: database.WorkspaceAppHealthDisabled, OpenIn: database.WorkspaceAppOpenInSlimWindow, @@ -479,7 +479,7 @@ func TestSubAgentAPI(t *testing.T) { }, expectApps: []database.WorkspaceApp{ { - Slug: "full-app", + Slug: "0ccdbg39-full-app", Command: sql.NullString{Valid: true, String: "echo hello"}, DisplayName: "Full Featured App", External: true, @@ -507,7 +507,7 @@ func TestSubAgentAPI(t *testing.T) { }, expectApps: []database.WorkspaceApp{ { - Slug: "no-health-app", + Slug: "nphrhbh6-no-health-app", Health: database.WorkspaceAppHealthDisabled, SharingLevel: database.AppSharingLevelOwner, OpenIn: database.WorkspaceAppOpenInSlimWindow, @@ -531,7 +531,7 @@ func TestSubAgentAPI(t *testing.T) { }, expectApps: []database.WorkspaceApp{ { - Slug: "duplicate-app", + Slug: "uiklfckv-duplicate-app", DisplayName: "First App", SharingLevel: database.AppSharingLevelOwner, Health: database.WorkspaceAppHealthDisabled, @@ -568,14 +568,14 @@ func TestSubAgentAPI(t *testing.T) { }, expectApps: []database.WorkspaceApp{ { - Slug: "duplicate-app", + Slug: "uiklfckv-duplicate-app", DisplayName: "First Duplicate", SharingLevel: database.AppSharingLevelOwner, Health: database.WorkspaceAppHealthDisabled, OpenIn: database.WorkspaceAppOpenInSlimWindow, }, { - Slug: "valid-app", + Slug: "511ctirn-valid-app", DisplayName: "Valid App", SharingLevel: database.AppSharingLevelOwner, Health: database.WorkspaceAppHealthDisabled, @@ -754,7 +754,7 @@ func TestSubAgentAPI(t *testing.T) { apps, err := db.GetWorkspaceAppsByAgentID(dbauthz.AsSystemRestricted(ctx), agentID) //nolint:gocritic // this is a test. require.NoError(t, err) require.Len(t, apps, 1) - require.Equal(t, "duplicate-slug", apps[0].Slug) + require.Equal(t, "k5jd7a99-duplicate-slug", apps[0].Slug) require.Equal(t, "First Duplicate", apps[0].DisplayName) }) }) @@ -1128,7 +1128,7 @@ func TestSubAgentAPI(t *testing.T) { apps, err := api.Database.GetWorkspaceAppsByAgentID(dbauthz.AsSystemRestricted(ctx), agentID) //nolint:gocritic // this is a test. require.NoError(t, err) require.Len(t, apps, 1) - require.Equal(t, "custom-app", apps[0].Slug) + require.Equal(t, "v4qhkq17-custom-app", apps[0].Slug) require.Equal(t, "Custom App", apps[0].DisplayName) }) From 48bb534a51065adeba6788a8dfb83f2cf9fe5e14 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 25 Jun 2025 10:06:57 -0800 Subject: [PATCH 09/10] chore: fix idle state icon when disabled (#18554) When the workspace is off, we set a disabled text/stroke color, but for the idle icon that also needs a fill, this only changed the outline making it look weird. Instead, move the disabled logic into the component so we can apply a matching fill. I felt it looked too thick with both the outline and fill, so I also removed the outline. Really I think maybe the workspace status should be a separate column rather than disabling these icons, but this maintains the status quo. Before with mismatching stroke and fill color: ![screenshot](https://github.com/user-attachments/assets/961014b7-1e26-49f0-aa87-834f2f367618) After with disabled fill and stroke removal: ![screenshot](https://github.com/user-attachments/assets/205e4515-dc01-4437-87ec-a0f8a546da3b) Enabled fill and stroke removal: ![screenshot](https://github.com/user-attachments/assets/04f5914e-f96c-4c75-8873-e3fc9c854d95) --- site/src/modules/apps/AppStatusStateIcon.tsx | 11 ++++-- .../WorkspaceAppStatus.stories.tsx | 36 ++++++++++++++++++- .../WorkspaceAppStatus/WorkspaceAppStatus.tsx | 4 --- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/site/src/modules/apps/AppStatusStateIcon.tsx b/site/src/modules/apps/AppStatusStateIcon.tsx index 3497773952373..1800c81958c4e 100644 --- a/site/src/modules/apps/AppStatusStateIcon.tsx +++ b/site/src/modules/apps/AppStatusStateIcon.tsx @@ -24,16 +24,23 @@ export const AppStatusStateIcon: FC = ({ latest, className: customClassName, }) => { - const className = cn(["size-4 shrink-0", customClassName]); + const className = cn([ + "size-4 shrink-0", + customClassName, + disabled && "text-content-disabled", + ]); switch (state) { case "idle": + // The pause icon is outlined; add a fill since it is hard to see and + // remove the stroke so it is not overly thick. return ( ); diff --git a/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.stories.tsx b/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.stories.tsx index 0e229467b994b..9327ff6b46e98 100644 --- a/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.stories.tsx +++ b/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.stories.tsx @@ -69,9 +69,43 @@ export const LongMessage: Story = { }, }; -export const Disabled: Story = { +export const DisabledComplete: Story = { args: { status: MockWorkspaceAppStatus, disabled: true, }, }; + +export const DisabledFailure: Story = { + args: { + status: { + ...MockWorkspaceAppStatus, + state: "failure", + message: "Couldn't figure out how to start the dev server", + }, + disabled: true, + }, +}; + +export const DisabledWorking: Story = { + args: { + status: { + ...MockWorkspaceAppStatus, + state: "working", + message: "Starting dev server...", + uri: "", + }, + disabled: true, + }, +}; + +export const DisabledIdle: Story = { + args: { + status: { + ...MockWorkspaceAppStatus, + state: "idle", + message: "Done for now", + }, + disabled: true, + }, +}; diff --git a/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.tsx b/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.tsx index 587ae9f5b062f..633a9fcbc1ad8 100644 --- a/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.tsx +++ b/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.tsx @@ -7,7 +7,6 @@ import { } from "components/Tooltip/Tooltip"; import capitalize from "lodash/capitalize"; import { AppStatusStateIcon } from "modules/apps/AppStatusStateIcon"; -import { cn } from "utils/cn"; type WorkspaceAppStatusProps = { status: APIWorkspaceAppStatus | null; @@ -37,9 +36,6 @@ export const WorkspaceAppStatus = ({ latest disabled={disabled} state={status.state} - className={cn({ - "text-content-disabled": disabled, - })} /> {message} From 09e1a8ad99aa161d6cf6179fa162ef6f2b9538c8 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Wed, 25 Jun 2025 21:35:43 +0300 Subject: [PATCH 10/10] feat(.devcontainer): add code-server feature to `devcontainer.json` (#18589) --- .devcontainer/devcontainer.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 907287634c2c4..a9339f7e0cf7e 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,6 +6,10 @@ // See all possible options here https://github.com/devcontainers/features/tree/main/src/docker-in-docker "ghcr.io/devcontainers/features/docker-in-docker:2": { "moby": "false" + }, + "ghcr.io/coder/devcontainer-features/code-server:1": { + "auth": "none", + "port": 13337 } }, // SYS_PTRACE to enable go debugging