From 7cf1815834082d3c5e0dda8d0feae5124224f523 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 20 Aug 2025 12:03:37 +0000 Subject: [PATCH 1/6] refactor: generate task name fallback on coderd Instead of generating the fallback task name on the website, we instead generate it on coderd. --- coderd/aitasks.go | 3 ++- coderd/aitasks_test.go | 8 +------ coderd/taskname/taskname.go | 32 +++++++++++++++++++------- codersdk/aitasks.go | 1 - site/src/api/typesGenerated.ts | 1 - site/src/pages/TasksPage/TasksPage.tsx | 2 -- 6 files changed, 27 insertions(+), 20 deletions(-) diff --git a/coderd/aitasks.go b/coderd/aitasks.go index f5d72beaf3903..0bcbb9b46c337 100644 --- a/coderd/aitasks.go +++ b/coderd/aitasks.go @@ -107,7 +107,7 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) { return } - taskName := req.Name + taskName := taskname.GenerateFallback() if anthropicAPIKey := taskname.GetAnthropicAPIKeyFromEnv(); anthropicAPIKey != "" { anthropicModel := taskname.GetAnthropicModelFromEnv() @@ -118,6 +118,7 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) { taskName = generatedName } } + taskName += "-" + taskname.GenerateSuffix() createReq := codersdk.CreateWorkspaceRequest{ Name: taskName, diff --git a/coderd/aitasks_test.go b/coderd/aitasks_test.go index 8d12dd3a5ec95..d4fecd2145f6d 100644 --- a/coderd/aitasks_test.go +++ b/coderd/aitasks_test.go @@ -151,7 +151,6 @@ func TestTaskCreate(t *testing.T) { var ( ctx = testutil.Context(t, testutil.WaitShort) - taskName = "task-foo-bar-baz" taskPrompt = "Some task prompt" ) @@ -176,7 +175,6 @@ func TestTaskCreate(t *testing.T) { // When: We attempt to create a Task. workspace, err := expClient.CreateTask(ctx, "me", codersdk.CreateTaskRequest{ - Name: taskName, TemplateVersionID: template.ActiveVersionID, Prompt: taskPrompt, }) @@ -184,7 +182,7 @@ func TestTaskCreate(t *testing.T) { coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) // Then: We expect a workspace to have been created. - assert.Equal(t, taskName, workspace.Name) + assert.NotEmpty(t, workspace.Name) assert.Equal(t, template.ID, workspace.TemplateID) // And: We expect it to have the "AI Prompt" parameter correctly set. @@ -201,7 +199,6 @@ func TestTaskCreate(t *testing.T) { var ( ctx = testutil.Context(t, testutil.WaitShort) - taskName = "task-foo-bar-baz" taskPrompt = "Some task prompt" ) @@ -217,7 +214,6 @@ func TestTaskCreate(t *testing.T) { // When: We attempt to create a Task. _, err := expClient.CreateTask(ctx, "me", codersdk.CreateTaskRequest{ - Name: taskName, TemplateVersionID: template.ActiveVersionID, Prompt: taskPrompt, }) @@ -235,7 +231,6 @@ func TestTaskCreate(t *testing.T) { var ( ctx = testutil.Context(t, testutil.WaitShort) - taskName = "task-foo-bar-baz" taskPrompt = "Some task prompt" ) @@ -251,7 +246,6 @@ func TestTaskCreate(t *testing.T) { // When: We attempt to create a Task with an invalid template version ID. _, err := expClient.CreateTask(ctx, "me", codersdk.CreateTaskRequest{ - Name: taskName, TemplateVersionID: uuid.New(), Prompt: taskPrompt, }) diff --git a/coderd/taskname/taskname.go b/coderd/taskname/taskname.go index 970e5ad67b2a0..0d71049fe689b 100644 --- a/coderd/taskname/taskname.go +++ b/coderd/taskname/taskname.go @@ -2,11 +2,15 @@ package taskname import ( "context" + "fmt" "io" + "math/rand/v2" "os" + "strings" "github.com/anthropics/anthropic-sdk-go" anthropicoption "github.com/anthropics/anthropic-sdk-go/option" + "github.com/moby/moby/pkg/namesgenerator" "golang.org/x/xerrors" "github.com/coder/aisdk-go" @@ -20,19 +24,17 @@ const ( Requirements: - Only lowercase letters, numbers, and hyphens - Start with "task-" -- End with a random number between 0-99 -- Maximum 32 characters total +- Maximum 28 characters total - Descriptive of the main task Examples: -- "Help me debug a Python script" → "task-python-debug-12" -- "Create a React dashboard component" → "task-react-dashboard-93" -- "Analyze sales data from Q3" → "task-analyze-q3-sales-37" -- "Set up CI/CD pipeline" → "task-setup-cicd-44" +- "Help me debug a Python script" → "task-python-debug" +- "Create a React dashboard component" → "task-react-dashboard" +- "Analyze sales data from Q3" → "task-analyze-q3-sales" +- "Set up CI/CD pipeline" → "task-setup-cicd" If you cannot create a suitable name: -- Respond with "task-unnamed" -- Do not end with a random number` +- Respond with "task-unnamed"` ) var ( @@ -67,6 +69,20 @@ func GetAnthropicModelFromEnv() anthropic.Model { return anthropic.Model(os.Getenv("ANTHROPIC_MODEL")) } +// GenerateSuffix generates a random hex string between `100` and `fff`. +func GenerateSuffix() string { + numMin := 0x100 + numMax := 0x1000 + //nolint:gosec // We don't need a cryptographically secure random number generator for generating a task name suffix. + num := rand.IntN(numMax-numMin) + numMin + + return fmt.Sprintf("%x", num) +} + +func GenerateFallback() string { + return "task-" + strings.ReplaceAll(namesgenerator.GetRandomName(0), "_", "-") +} + func Generate(ctx context.Context, prompt string, opts ...Option) (string, error) { o := options{} for _, opt := range opts { diff --git a/codersdk/aitasks.go b/codersdk/aitasks.go index 49d89bf5e2656..56b43d43a0d19 100644 --- a/codersdk/aitasks.go +++ b/codersdk/aitasks.go @@ -47,7 +47,6 @@ func (c *ExperimentalClient) AITaskPrompts(ctx context.Context, buildIDs []uuid. } type CreateTaskRequest struct { - Name string `json:"name"` TemplateVersionID uuid.UUID `json:"template_version_id" format:"uuid"` TemplateVersionPresetID uuid.UUID `json:"template_version_preset_id,omitempty" format:"uuid"` Prompt string `json:"prompt"` diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 4f873fb7b7829..db840040687fc 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -478,7 +478,6 @@ export interface CreateProvisionerKeyResponse { // From codersdk/aitasks.go export interface CreateTaskRequest { - readonly name: string; readonly template_version_id: string; readonly template_version_preset_id?: string; readonly prompt: string; diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index 0e149f7943a61..b7b1d3f5998ef 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -53,7 +53,6 @@ import { useAuthenticated } from "hooks"; import { useExternalAuth } from "hooks/useExternalAuth"; import { RedoIcon, RotateCcwIcon, SendIcon } from "lucide-react"; import { AI_PROMPT_PARAMETER_NAME, type Task } from "modules/tasks/tasks"; -import { generateWorkspaceName } from "modules/workspaces/generateWorkspaceName"; import { WorkspaceAppStatus } from "modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus"; import { type FC, type ReactNode, useEffect, useState } from "react"; import { Helmet } from "react-helmet-async"; @@ -741,7 +740,6 @@ export const data = { } const workspace = await API.experimental.createTask(userId, { - name: `task-${generateWorkspaceName()}`, template_version_id: templateVersionId, template_version_preset_id: preset_id || undefined, prompt, From 620a01b14e0f4d5791a18c76e9133407fc12e7d9 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 20 Aug 2025 16:23:14 +0000 Subject: [PATCH 2/6] refactor: make `generateSuffix` internal --- coderd/aitasks.go | 1 - coderd/taskname/taskname.go | 10 ++++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/coderd/aitasks.go b/coderd/aitasks.go index 0bcbb9b46c337..9ba201f11c0d6 100644 --- a/coderd/aitasks.go +++ b/coderd/aitasks.go @@ -118,7 +118,6 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) { taskName = generatedName } } - taskName += "-" + taskname.GenerateSuffix() createReq := codersdk.CreateWorkspaceRequest{ Name: taskName, diff --git a/coderd/taskname/taskname.go b/coderd/taskname/taskname.go index 0d71049fe689b..7d630fbdd5ed3 100644 --- a/coderd/taskname/taskname.go +++ b/coderd/taskname/taskname.go @@ -69,8 +69,8 @@ func GetAnthropicModelFromEnv() anthropic.Model { return anthropic.Model(os.Getenv("ANTHROPIC_MODEL")) } -// GenerateSuffix generates a random hex string between `100` and `fff`. -func GenerateSuffix() string { +// generateSuffix generates a random hex string between `100` and `fff`. +func generateSuffix() string { numMin := 0x100 numMax := 0x1000 //nolint:gosec // We don't need a cryptographically secure random number generator for generating a task name suffix. @@ -80,7 +80,9 @@ func GenerateSuffix() string { } func GenerateFallback() string { - return "task-" + strings.ReplaceAll(namesgenerator.GetRandomName(0), "_", "-") + name := strings.ReplaceAll(namesgenerator.GetRandomName(0), "_", "-") + + return fmt.Sprintf("task-%s-%s", name, generateSuffix()) } func Generate(ctx context.Context, prompt string, opts ...Option) (string, error) { @@ -143,7 +145,7 @@ func Generate(ctx context.Context, prompt string, opts ...Option) (string, error return "", ErrNoNameGenerated } - return generatedName, nil + return fmt.Sprintf("%s-%s", generatedName, generateSuffix()), nil } func anthropicDataStream(ctx context.Context, client anthropic.Client, model anthropic.Model, input []aisdk.Message) (aisdk.DataStream, error) { From a9ccd7241b9fdcdf6fb8d1e11990c85bee42a119 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 21 Aug 2025 09:04:00 +0000 Subject: [PATCH 3/6] fix: ensure `GenerateFallback` cannot be an invalid workspace name --- coderd/taskname/taskname.go | 9 +++++++++ coderd/taskname/taskname_test.go | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/coderd/taskname/taskname.go b/coderd/taskname/taskname.go index 7d630fbdd5ed3..c0574f8392fb6 100644 --- a/coderd/taskname/taskname.go +++ b/coderd/taskname/taskname.go @@ -80,7 +80,16 @@ func generateSuffix() string { } func GenerateFallback() string { + // We have a 32 character limit for the name. + // We have a 5 character prefix `task-`. + // We have a 4 character suffix `-fff`. + // This leaves us with 23 characters for the middle. + // + // Unfortunately, `namesgenerator.GetRandomName(0)` will + // generate names that are longer than 23 characters, so + // we just trim these down to length. name := strings.ReplaceAll(namesgenerator.GetRandomName(0), "_", "-") + name = name[:min(len(name), 23)] return fmt.Sprintf("task-%s-%s", name, generateSuffix()) } diff --git a/coderd/taskname/taskname_test.go b/coderd/taskname/taskname_test.go index 0737621b8f4eb..3eb26ef1d4ac7 100644 --- a/coderd/taskname/taskname_test.go +++ b/coderd/taskname/taskname_test.go @@ -15,6 +15,14 @@ const ( anthropicEnvVar = "ANTHROPIC_API_KEY" ) +func TestGenerateFallback(t *testing.T) { + t.Parallel() + + name := taskname.GenerateFallback() + err := codersdk.NameValid(name) + require.NoErrorf(t, err, "expected fallback to be valid workspace name, instead found %s", name) +} + func TestGenerateTaskName(t *testing.T) { t.Parallel() From fb2b8f2a6107da73703b39ef8f7fef4f9c51803e Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 21 Aug 2025 09:23:39 +0000 Subject: [PATCH 4/6] fix: ensure we don't have `--` occur in fallback name --- coderd/taskname/taskname.go | 1 + 1 file changed, 1 insertion(+) diff --git a/coderd/taskname/taskname.go b/coderd/taskname/taskname.go index c0574f8392fb6..d776a65e3d12e 100644 --- a/coderd/taskname/taskname.go +++ b/coderd/taskname/taskname.go @@ -90,6 +90,7 @@ func GenerateFallback() string { // we just trim these down to length. name := strings.ReplaceAll(namesgenerator.GetRandomName(0), "_", "-") name = name[:min(len(name), 23)] + name = strings.TrimSuffix(name, "-") return fmt.Sprintf("task-%s-%s", name, generateSuffix()) } From 74bfabc0a4bca91bcbb828ea85c2f1e277e34298 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 21 Aug 2025 09:28:24 +0000 Subject: [PATCH 5/6] chore: increase range of generateSuffix --- coderd/taskname/taskname.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/coderd/taskname/taskname.go b/coderd/taskname/taskname.go index d776a65e3d12e..5a5b0c4a30eff 100644 --- a/coderd/taskname/taskname.go +++ b/coderd/taskname/taskname.go @@ -69,14 +69,14 @@ func GetAnthropicModelFromEnv() anthropic.Model { return anthropic.Model(os.Getenv("ANTHROPIC_MODEL")) } -// generateSuffix generates a random hex string between `100` and `fff`. +// generateSuffix generates a random hex string between `000` and `fff`. func generateSuffix() string { - numMin := 0x100 + numMin := 0x0000 numMax := 0x1000 //nolint:gosec // We don't need a cryptographically secure random number generator for generating a task name suffix. num := rand.IntN(numMax-numMin) + numMin - return fmt.Sprintf("%x", num) + return fmt.Sprintf("%03x", num) } func GenerateFallback() string { From a0e41c19262a65759774d3d6f268284024f6d1f3 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 21 Aug 2025 09:35:25 +0000 Subject: [PATCH 6/6] fix: increase suffix length to 4 chars to reduce collision risk --- coderd/taskname/taskname.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/coderd/taskname/taskname.go b/coderd/taskname/taskname.go index 5a5b0c4a30eff..dff57dfd0c7f5 100644 --- a/coderd/taskname/taskname.go +++ b/coderd/taskname/taskname.go @@ -69,27 +69,27 @@ func GetAnthropicModelFromEnv() anthropic.Model { return anthropic.Model(os.Getenv("ANTHROPIC_MODEL")) } -// generateSuffix generates a random hex string between `000` and `fff`. +// generateSuffix generates a random hex string between `0000` and `ffff`. func generateSuffix() string { - numMin := 0x0000 - numMax := 0x1000 + numMin := 0x00000 + numMax := 0x10000 //nolint:gosec // We don't need a cryptographically secure random number generator for generating a task name suffix. num := rand.IntN(numMax-numMin) + numMin - return fmt.Sprintf("%03x", num) + return fmt.Sprintf("%04x", num) } func GenerateFallback() string { // We have a 32 character limit for the name. // We have a 5 character prefix `task-`. - // We have a 4 character suffix `-fff`. - // This leaves us with 23 characters for the middle. + // We have a 5 character suffix `-ffff`. + // This leaves us with 22 characters for the middle. // // Unfortunately, `namesgenerator.GetRandomName(0)` will - // generate names that are longer than 23 characters, so + // generate names that are longer than 22 characters, so // we just trim these down to length. name := strings.ReplaceAll(namesgenerator.GetRandomName(0), "_", "-") - name = name[:min(len(name), 23)] + name = name[:min(len(name), 22)] name = strings.TrimSuffix(name, "-") return fmt.Sprintf("task-%s-%s", name, generateSuffix())