From 4cb1621ee45bf369ec0ec62ceeeb8c79c9f098af Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 25 Jun 2025 15:30:16 +0000 Subject: [PATCH 1/2] fix(coderd/agentapi): make sub agent slugs more unique 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 | 11 ++++++++++- coderd/agentapi/subagent_test.go | 30 +++++++++++++++--------------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/coderd/agentapi/subagent.go b/coderd/agentapi/subagent.go index 1868ad39bd362..0845af3ff5ccc 100644 --- a/coderd/agentapi/subagent.go +++ b/coderd/agentapi/subagent.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "fmt" + "hash/crc32" "strings" "github.com/google/uuid" @@ -165,11 +166,19 @@ 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 := fmt.Sprintf("%08x", crc32.ChecksumIEEE([]byte(subAgent.Name+"/"+app.Slug))) + computedSlug := slugHash + "-" + 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..82d065d1323db 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: "d2b537b8-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: "9c813024-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: "7f9e8fef-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: "51f592de-authenticated-app", SharingLevel: database.AppSharingLevelAuthenticated, Health: database.WorkspaceAppHealthDisabled, OpenIn: database.WorkspaceAppOpenInSlimWindow, }, { - Slug: "owner-app", + Slug: "cad4f019-owner-app", SharingLevel: database.AppSharingLevelOwner, Health: database.WorkspaceAppHealthDisabled, OpenIn: database.WorkspaceAppOpenInSlimWindow, }, { - Slug: "public-app", + Slug: "9e367a4c-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: "6cb5ae2b-tab-app", SharingLevel: database.AppSharingLevelOwner, Health: database.WorkspaceAppHealthDisabled, OpenIn: database.WorkspaceAppOpenInTab, }, { - Slug: "window-app", + Slug: "11675927-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: "1067629f-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: "3290f004-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: "6977f2fe-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: "6977f2fe-duplicate-app", DisplayName: "First Duplicate", SharingLevel: database.AppSharingLevelOwner, Health: database.WorkspaceAppHealthDisabled, OpenIn: database.WorkspaceAppOpenInSlimWindow, }, { - Slug: "valid-app", + Slug: "7f9e8fef-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, "b219bd99-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, "9cc545fe-custom-app", apps[0].Slug) require.Equal(t, "Custom App", apps[0].DisplayName) }) From 3e9f2f3631f74158d2febc7bb1070cd39e82209d Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 25 Jun 2025 16:24:18 +0000 Subject: [PATCH 2/2] chore: replace crc32 with sha256 + base32 + truncation --- coderd/agentapi/subagent.go | 8 +++++--- coderd/agentapi/subagent_test.go | 30 +++++++++++++++--------------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/coderd/agentapi/subagent.go b/coderd/agentapi/subagent.go index 0845af3ff5ccc..1753f5b7d4093 100644 --- a/coderd/agentapi/subagent.go +++ b/coderd/agentapi/subagent.go @@ -2,10 +2,11 @@ package agentapi import ( "context" + "crypto/sha256" "database/sql" + "encoding/base32" "errors" "fmt" - "hash/crc32" "strings" "github.com/google/uuid" @@ -171,8 +172,9 @@ func (a *SubAgentAPI) CreateSubAgent(ctx context.Context, req *agentproto.Create // 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 := fmt.Sprintf("%08x", crc32.ChecksumIEEE([]byte(subAgent.Name+"/"+app.Slug))) - computedSlug := slugHash + "-" + app.Slug + 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. diff --git a/coderd/agentapi/subagent_test.go b/coderd/agentapi/subagent_test.go index 82d065d1323db..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: "d2b537b8-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: "9c813024-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: "7f9e8fef-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: "51f592de-authenticated-app", + Slug: "atpt261l-authenticated-app", SharingLevel: database.AppSharingLevelAuthenticated, Health: database.WorkspaceAppHealthDisabled, OpenIn: database.WorkspaceAppOpenInSlimWindow, }, { - Slug: "cad4f019-owner-app", + Slug: "eh5gp1he-owner-app", SharingLevel: database.AppSharingLevelOwner, Health: database.WorkspaceAppHealthDisabled, OpenIn: database.WorkspaceAppOpenInSlimWindow, }, { - Slug: "9e367a4c-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: "6cb5ae2b-tab-app", + Slug: "ci9500rm-tab-app", SharingLevel: database.AppSharingLevelOwner, Health: database.WorkspaceAppHealthDisabled, OpenIn: database.WorkspaceAppOpenInTab, }, { - Slug: "11675927-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: "1067629f-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: "3290f004-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: "6977f2fe-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: "6977f2fe-duplicate-app", + Slug: "uiklfckv-duplicate-app", DisplayName: "First Duplicate", SharingLevel: database.AppSharingLevelOwner, Health: database.WorkspaceAppHealthDisabled, OpenIn: database.WorkspaceAppOpenInSlimWindow, }, { - Slug: "7f9e8fef-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, "b219bd99-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, "9cc545fe-custom-app", apps[0].Slug) + require.Equal(t, "v4qhkq17-custom-app", apps[0].Slug) require.Equal(t, "Custom App", apps[0].DisplayName) })