From c56714c35095dfc4739ebba35131f2e22d46e2d5 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 6 Aug 2025 17:17:18 +0100 Subject: [PATCH 1/4] chore: add failing test case --- .../provisionerdserver/provisionerdserver_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index ec26a2b92000f..2b2f1c35b0e46 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -2794,6 +2794,20 @@ func TestCompleteJob(t *testing.T) { }, expected: true, }, + { + name: "non-existing app", + input: &proto.CompletedJob_WorkspaceBuild{ + AiTasks: []*sdkproto.AITask{ + { + Id: uuid.NewString(), + SidebarApp: &sdkproto.AITaskSidebarApp{ + Id: uuid.NewString(), // Non-existing app ID. + }, + }, + }, + }, + expected: false, + }, } { t.Run(tc.name, func(t *testing.T) { t.Parallel() From ae1c3b5f88739853f50442d0b902974b9d9b87c2 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 7 Aug 2025 20:04:31 +0100 Subject: [PATCH 2/4] fix(coderd): remove workspace_builds.sidebar_app_id --- coderd/database/check_constraint.go | 13 +++-- coderd/database/dbauthz/dbauthz_test.go | 8 +-- coderd/database/dbgen/dbgen.go | 8 ++- coderd/database/dump.sql | 6 --- coderd/database/foreign_key_constraint.go | 1 - .../000358_remove_sidebar_app_id.down.sql | 52 +++++++++++++++++++ .../000358_remove_sidebar_app_id.up.sql | 43 +++++++++++++++ coderd/database/models.go | 2 - coderd/database/queries.sql.go | 49 ++++++----------- coderd/database/queries/workspacebuilds.sql | 1 - .../provisionerdserver/provisionerdserver.go | 23 ++------ .../provisionerdserver_test.go | 11 ++-- coderd/workspacebuilds.go | 6 +-- coderd/workspaces_test.go | 22 +++----- docs/admin/security/audit-logs.md | 2 +- enterprise/audit/table.go | 2 +- 16 files changed, 139 insertions(+), 110 deletions(-) create mode 100644 coderd/database/migrations/000358_remove_sidebar_app_id.down.sql create mode 100644 coderd/database/migrations/000358_remove_sidebar_app_id.up.sql diff --git a/coderd/database/check_constraint.go b/coderd/database/check_constraint.go index f9d54705a7cf5..d5472fa789e54 100644 --- a/coderd/database/check_constraint.go +++ b/coderd/database/check_constraint.go @@ -6,11 +6,10 @@ type CheckConstraint string // CheckConstraint enums. const ( - CheckOneTimePasscodeSet CheckConstraint = "one_time_passcode_set" // users - CheckMaxProvisionerLogsLength CheckConstraint = "max_provisioner_logs_length" // provisioner_jobs - CheckValidationMonotonicOrder CheckConstraint = "validation_monotonic_order" // template_version_parameters - CheckMaxLogsLength CheckConstraint = "max_logs_length" // workspace_agents - CheckSubsystemsNotNone CheckConstraint = "subsystems_not_none" // workspace_agents - CheckWorkspaceBuildsAiTaskSidebarAppIDRequired CheckConstraint = "workspace_builds_ai_task_sidebar_app_id_required" // workspace_builds - CheckWorkspaceBuildsDeadlineBelowMaxDeadline CheckConstraint = "workspace_builds_deadline_below_max_deadline" // workspace_builds + CheckOneTimePasscodeSet CheckConstraint = "one_time_passcode_set" // users + CheckMaxProvisionerLogsLength CheckConstraint = "max_provisioner_logs_length" // provisioner_jobs + CheckValidationMonotonicOrder CheckConstraint = "validation_monotonic_order" // template_version_parameters + CheckMaxLogsLength CheckConstraint = "max_logs_length" // workspace_agents + CheckSubsystemsNotNone CheckConstraint = "subsystems_not_none" // workspace_agents + CheckWorkspaceBuildsDeadlineBelowMaxDeadline CheckConstraint = "workspace_builds_deadline_below_max_deadline" // workspace_builds ) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 82b7b47c892b2..d2f36c1097d9e 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -3252,13 +3252,9 @@ func (s *MethodTestSuite) TestWorkspace() { WorkspaceID: w.ID, TemplateVersionID: tv.ID, }) - res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: b.JobID}) - agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) - app := dbgen.WorkspaceApp(s.T(), db, database.WorkspaceApp{AgentID: agt.ID}) check.Args(database.UpdateWorkspaceBuildAITaskByIDParams{ - HasAITask: sql.NullBool{Bool: true, Valid: true}, - SidebarAppID: uuid.NullUUID{UUID: app.ID, Valid: true}, - ID: b.ID, + HasAITask: sql.NullBool{Bool: true, Valid: true}, + ID: b.ID, }).Asserts(w, policy.ActionUpdate) })) s.Run("SoftDeleteWorkspaceByID", s.Subtest(func(db database.Store, check *expects) { diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index 11e02d0f651e9..1929c58c22ae2 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -436,7 +436,6 @@ func WorkspaceBuild(t testing.TB, db database.Store, orig database.WorkspaceBuil buildID := takeFirst(orig.ID, uuid.New()) jobID := takeFirst(orig.JobID, uuid.New()) hasAITask := takeFirst(orig.HasAITask, sql.NullBool{}) - sidebarAppID := takeFirst(orig.AITaskSidebarAppID, uuid.NullUUID{}) var build database.WorkspaceBuild err := db.InTx(func(db database.Store) error { err := db.InsertWorkspaceBuild(genCtx, database.InsertWorkspaceBuildParams{ @@ -472,10 +471,9 @@ func WorkspaceBuild(t testing.TB, db database.Store, orig database.WorkspaceBuil if hasAITask.Valid { require.NoError(t, db.UpdateWorkspaceBuildAITaskByID(genCtx, database.UpdateWorkspaceBuildAITaskByIDParams{ - HasAITask: hasAITask, - SidebarAppID: sidebarAppID, - UpdatedAt: dbtime.Now(), - ID: buildID, + HasAITask: hasAITask, + UpdatedAt: dbtime.Now(), + ID: buildID, })) } diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 7bea770248310..48c7d0411e96e 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -2235,8 +2235,6 @@ CREATE TABLE workspace_builds ( max_deadline timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL, template_version_preset_id uuid, has_ai_task boolean, - ai_task_sidebar_app_id uuid, - CONSTRAINT workspace_builds_ai_task_sidebar_app_id_required CHECK (((((has_ai_task IS NULL) OR (has_ai_task = false)) AND (ai_task_sidebar_app_id IS NULL)) OR ((has_ai_task = true) AND (ai_task_sidebar_app_id IS NOT NULL)))), CONSTRAINT workspace_builds_deadline_below_max_deadline CHECK ((((deadline <> '0001-01-01 00:00:00+00'::timestamp with time zone) AND (deadline <= max_deadline)) OR (max_deadline = '0001-01-01 00:00:00+00'::timestamp with time zone))) ); @@ -2257,7 +2255,6 @@ CREATE VIEW workspace_build_with_user AS workspace_builds.max_deadline, workspace_builds.template_version_preset_id, workspace_builds.has_ai_task, - workspace_builds.ai_task_sidebar_app_id, COALESCE(visible_users.avatar_url, ''::text) AS initiator_by_avatar_url, COALESCE(visible_users.username, ''::text) AS initiator_by_username, COALESCE(visible_users.name, ''::text) AS initiator_by_name @@ -3258,9 +3255,6 @@ ALTER TABLE ONLY workspace_apps ALTER TABLE ONLY workspace_build_parameters ADD CONSTRAINT workspace_build_parameters_workspace_build_id_fkey FOREIGN KEY (workspace_build_id) REFERENCES workspace_builds(id) ON DELETE CASCADE; -ALTER TABLE ONLY workspace_builds - ADD CONSTRAINT workspace_builds_ai_task_sidebar_app_id_fkey FOREIGN KEY (ai_task_sidebar_app_id) REFERENCES workspace_apps(id); - ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; diff --git a/coderd/database/foreign_key_constraint.go b/coderd/database/foreign_key_constraint.go index 33aa8edd69032..ee04ad64633ab 100644 --- a/coderd/database/foreign_key_constraint.go +++ b/coderd/database/foreign_key_constraint.go @@ -86,7 +86,6 @@ const ( ForeignKeyWorkspaceAppStatusesWorkspaceID ForeignKeyConstraint = "workspace_app_statuses_workspace_id_fkey" // ALTER TABLE ONLY workspace_app_statuses ADD CONSTRAINT workspace_app_statuses_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id); ForeignKeyWorkspaceAppsAgentID ForeignKeyConstraint = "workspace_apps_agent_id_fkey" // ALTER TABLE ONLY workspace_apps ADD CONSTRAINT workspace_apps_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; ForeignKeyWorkspaceBuildParametersWorkspaceBuildID ForeignKeyConstraint = "workspace_build_parameters_workspace_build_id_fkey" // ALTER TABLE ONLY workspace_build_parameters ADD CONSTRAINT workspace_build_parameters_workspace_build_id_fkey FOREIGN KEY (workspace_build_id) REFERENCES workspace_builds(id) ON DELETE CASCADE; - ForeignKeyWorkspaceBuildsAiTaskSidebarAppID ForeignKeyConstraint = "workspace_builds_ai_task_sidebar_app_id_fkey" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_ai_task_sidebar_app_id_fkey FOREIGN KEY (ai_task_sidebar_app_id) REFERENCES workspace_apps(id); ForeignKeyWorkspaceBuildsJobID ForeignKeyConstraint = "workspace_builds_job_id_fkey" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; ForeignKeyWorkspaceBuildsTemplateVersionID ForeignKeyConstraint = "workspace_builds_template_version_id_fkey" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; ForeignKeyWorkspaceBuildsTemplateVersionPresetID ForeignKeyConstraint = "workspace_builds_template_version_preset_id_fkey" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_template_version_preset_id_fkey FOREIGN KEY (template_version_preset_id) REFERENCES template_version_presets(id) ON DELETE SET NULL; diff --git a/coderd/database/migrations/000358_remove_sidebar_app_id.down.sql b/coderd/database/migrations/000358_remove_sidebar_app_id.down.sql new file mode 100644 index 0000000000000..96c3aa59fc06e --- /dev/null +++ b/coderd/database/migrations/000358_remove_sidebar_app_id.down.sql @@ -0,0 +1,52 @@ +DROP VIEW workspace_build_with_user; + +ALTER TABLE ONLY workspace_builds ADD COLUMN ai_task_sidebar_app_id UUID DEFAULT NULL; + +ALTER TABLE workspace_builds ADD CONSTRAINT workspace_builds_ai_task_sidebar_app_id_fkey FOREIGN KEY (ai_task_sidebar_app_id) REFERENCES workspace_apps(id); + +ALTER TABLE workspace_builds + ADD CONSTRAINT workspace_builds_ai_task_sidebar_app_id_required CHECK ( + ((has_ai_task IS NULL OR has_ai_task = false) AND ai_task_sidebar_app_id IS NULL) + OR (has_ai_task = true AND ai_task_sidebar_app_id IS NOT NULL) + ); + + +CREATE VIEW workspace_build_with_user AS +SELECT + workspace_builds.id, + workspace_builds.created_at, + workspace_builds.updated_at, + workspace_builds.workspace_id, + workspace_builds.template_version_id, + workspace_builds.build_number, + workspace_builds.transition, + workspace_builds.initiator_id, + workspace_builds.provisioner_state, + workspace_builds.job_id, + workspace_builds.deadline, + workspace_builds.reason, + workspace_builds.daily_cost, + workspace_builds.max_deadline, + workspace_builds.template_version_preset_id, + workspace_builds.has_ai_task, + workspace_builds.ai_task_sidebar_app_id, + COALESCE( + visible_users.avatar_url, + '' :: text + ) AS initiator_by_avatar_url, + COALESCE( + visible_users.username, + '' :: text + ) AS initiator_by_username, + COALESCE(visible_users.name, '' :: text) AS initiator_by_name +FROM + ( + workspace_builds + LEFT JOIN visible_users ON ( + ( + workspace_builds.initiator_id = visible_users.id + ) + ) + ); + +COMMENT ON VIEW workspace_build_with_user IS 'Joins in the username + avatar url of the initiated by user.'; diff --git a/coderd/database/migrations/000358_remove_sidebar_app_id.up.sql b/coderd/database/migrations/000358_remove_sidebar_app_id.up.sql new file mode 100644 index 0000000000000..47804d44b9123 --- /dev/null +++ b/coderd/database/migrations/000358_remove_sidebar_app_id.up.sql @@ -0,0 +1,43 @@ +DROP VIEW workspace_build_with_user; + +ALTER TABLE ONLY workspace_builds DROP CONSTRAINT workspace_builds_ai_task_sidebar_app_id_required; +ALTER TABLE ONLY workspace_builds DROP COLUMN ai_task_sidebar_app_id; + +CREATE VIEW workspace_build_with_user AS +SELECT + workspace_builds.id, + workspace_builds.created_at, + workspace_builds.updated_at, + workspace_builds.workspace_id, + workspace_builds.template_version_id, + workspace_builds.build_number, + workspace_builds.transition, + workspace_builds.initiator_id, + workspace_builds.provisioner_state, + workspace_builds.job_id, + workspace_builds.deadline, + workspace_builds.reason, + workspace_builds.daily_cost, + workspace_builds.max_deadline, + workspace_builds.template_version_preset_id, + workspace_builds.has_ai_task, + COALESCE( + visible_users.avatar_url, + '' :: text + ) AS initiator_by_avatar_url, + COALESCE( + visible_users.username, + '' :: text + ) AS initiator_by_username, + COALESCE(visible_users.name, '' :: text) AS initiator_by_name +FROM + ( + workspace_builds + LEFT JOIN visible_users ON ( + ( + workspace_builds.initiator_id = visible_users.id + ) + ) + ); + +COMMENT ON VIEW workspace_build_with_user IS 'Joins in the username + avatar url of the initiated by user.'; diff --git a/coderd/database/models.go b/coderd/database/models.go index 75d2b941dab3c..29ef4630185e3 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -4153,7 +4153,6 @@ type WorkspaceBuild struct { MaxDeadline time.Time `db:"max_deadline" json:"max_deadline"` TemplateVersionPresetID uuid.NullUUID `db:"template_version_preset_id" json:"template_version_preset_id"` HasAITask sql.NullBool `db:"has_ai_task" json:"has_ai_task"` - AITaskSidebarAppID uuid.NullUUID `db:"ai_task_sidebar_app_id" json:"ai_task_sidebar_app_id"` InitiatorByAvatarUrl string `db:"initiator_by_avatar_url" json:"initiator_by_avatar_url"` InitiatorByUsername string `db:"initiator_by_username" json:"initiator_by_username"` InitiatorByName string `db:"initiator_by_name" json:"initiator_by_name"` @@ -4184,7 +4183,6 @@ type WorkspaceBuildTable struct { MaxDeadline time.Time `db:"max_deadline" json:"max_deadline"` TemplateVersionPresetID uuid.NullUUID `db:"template_version_preset_id" json:"template_version_preset_id"` HasAITask sql.NullBool `db:"has_ai_task" json:"has_ai_task"` - AITaskSidebarAppID uuid.NullUUID `db:"ai_task_sidebar_app_id" json:"ai_task_sidebar_app_id"` } type WorkspaceLatestBuild struct { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 74cefd09359b0..f39f87cdfd2ac 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -15702,7 +15702,7 @@ const getWorkspaceAgentAndLatestBuildByAuthToken = `-- name: GetWorkspaceAgentAn SELECT workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.group_acl, workspaces.user_acl, workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted, - workspace_build_with_user.id, workspace_build_with_user.created_at, workspace_build_with_user.updated_at, workspace_build_with_user.workspace_id, workspace_build_with_user.template_version_id, workspace_build_with_user.build_number, workspace_build_with_user.transition, workspace_build_with_user.initiator_id, workspace_build_with_user.provisioner_state, workspace_build_with_user.job_id, workspace_build_with_user.deadline, workspace_build_with_user.reason, workspace_build_with_user.daily_cost, workspace_build_with_user.max_deadline, workspace_build_with_user.template_version_preset_id, workspace_build_with_user.has_ai_task, workspace_build_with_user.ai_task_sidebar_app_id, workspace_build_with_user.initiator_by_avatar_url, workspace_build_with_user.initiator_by_username, workspace_build_with_user.initiator_by_name + workspace_build_with_user.id, workspace_build_with_user.created_at, workspace_build_with_user.updated_at, workspace_build_with_user.workspace_id, workspace_build_with_user.template_version_id, workspace_build_with_user.build_number, workspace_build_with_user.transition, workspace_build_with_user.initiator_id, workspace_build_with_user.provisioner_state, workspace_build_with_user.job_id, workspace_build_with_user.deadline, workspace_build_with_user.reason, workspace_build_with_user.daily_cost, workspace_build_with_user.max_deadline, workspace_build_with_user.template_version_preset_id, workspace_build_with_user.has_ai_task, workspace_build_with_user.initiator_by_avatar_url, workspace_build_with_user.initiator_by_username, workspace_build_with_user.initiator_by_name FROM workspace_agents JOIN @@ -15814,7 +15814,6 @@ func (q *sqlQuerier) GetWorkspaceAgentAndLatestBuildByAuthToken(ctx context.Cont &i.WorkspaceBuild.MaxDeadline, &i.WorkspaceBuild.TemplateVersionPresetID, &i.WorkspaceBuild.HasAITask, - &i.WorkspaceBuild.AITaskSidebarAppID, &i.WorkspaceBuild.InitiatorByAvatarUrl, &i.WorkspaceBuild.InitiatorByUsername, &i.WorkspaceBuild.InitiatorByName, @@ -18469,7 +18468,7 @@ func (q *sqlQuerier) InsertWorkspaceBuildParameters(ctx context.Context, arg Ins } const getActiveWorkspaceBuildsByTemplateID = `-- name: GetActiveWorkspaceBuildsByTemplateID :many -SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.max_deadline, wb.template_version_preset_id, wb.has_ai_task, wb.ai_task_sidebar_app_id, wb.initiator_by_avatar_url, wb.initiator_by_username, wb.initiator_by_name +SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.max_deadline, wb.template_version_preset_id, wb.has_ai_task, wb.initiator_by_avatar_url, wb.initiator_by_username, wb.initiator_by_name FROM ( SELECT workspace_id, MAX(build_number) as max_build_number @@ -18525,7 +18524,6 @@ func (q *sqlQuerier) GetActiveWorkspaceBuildsByTemplateID(ctx context.Context, t &i.MaxDeadline, &i.TemplateVersionPresetID, &i.HasAITask, - &i.AITaskSidebarAppID, &i.InitiatorByAvatarUrl, &i.InitiatorByUsername, &i.InitiatorByName, @@ -18625,7 +18623,7 @@ func (q *sqlQuerier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, a const getLatestWorkspaceBuildByWorkspaceID = `-- name: GetLatestWorkspaceBuildByWorkspaceID :one SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, ai_task_sidebar_app_id, initiator_by_avatar_url, initiator_by_username, initiator_by_name + id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, initiator_by_avatar_url, initiator_by_username, initiator_by_name FROM workspace_build_with_user AS workspace_builds WHERE @@ -18656,7 +18654,6 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, w &i.MaxDeadline, &i.TemplateVersionPresetID, &i.HasAITask, - &i.AITaskSidebarAppID, &i.InitiatorByAvatarUrl, &i.InitiatorByUsername, &i.InitiatorByName, @@ -18665,7 +18662,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, w } const getLatestWorkspaceBuilds = `-- name: GetLatestWorkspaceBuilds :many -SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.max_deadline, wb.template_version_preset_id, wb.has_ai_task, wb.ai_task_sidebar_app_id, wb.initiator_by_avatar_url, wb.initiator_by_username, wb.initiator_by_name +SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.max_deadline, wb.template_version_preset_id, wb.has_ai_task, wb.initiator_by_avatar_url, wb.initiator_by_username, wb.initiator_by_name FROM ( SELECT workspace_id, MAX(build_number) as max_build_number @@ -18705,7 +18702,6 @@ func (q *sqlQuerier) GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceB &i.MaxDeadline, &i.TemplateVersionPresetID, &i.HasAITask, - &i.AITaskSidebarAppID, &i.InitiatorByAvatarUrl, &i.InitiatorByUsername, &i.InitiatorByName, @@ -18724,7 +18720,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceB } const getLatestWorkspaceBuildsByWorkspaceIDs = `-- name: GetLatestWorkspaceBuildsByWorkspaceIDs :many -SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.max_deadline, wb.template_version_preset_id, wb.has_ai_task, wb.ai_task_sidebar_app_id, wb.initiator_by_avatar_url, wb.initiator_by_username, wb.initiator_by_name +SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.max_deadline, wb.template_version_preset_id, wb.has_ai_task, wb.initiator_by_avatar_url, wb.initiator_by_username, wb.initiator_by_name FROM ( SELECT workspace_id, MAX(build_number) as max_build_number @@ -18766,7 +18762,6 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, &i.MaxDeadline, &i.TemplateVersionPresetID, &i.HasAITask, - &i.AITaskSidebarAppID, &i.InitiatorByAvatarUrl, &i.InitiatorByUsername, &i.InitiatorByName, @@ -18786,7 +18781,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, const getWorkspaceBuildByID = `-- name: GetWorkspaceBuildByID :one SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, ai_task_sidebar_app_id, initiator_by_avatar_url, initiator_by_username, initiator_by_name + id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, initiator_by_avatar_url, initiator_by_username, initiator_by_name FROM workspace_build_with_user AS workspace_builds WHERE @@ -18815,7 +18810,6 @@ func (q *sqlQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (W &i.MaxDeadline, &i.TemplateVersionPresetID, &i.HasAITask, - &i.AITaskSidebarAppID, &i.InitiatorByAvatarUrl, &i.InitiatorByUsername, &i.InitiatorByName, @@ -18825,7 +18819,7 @@ func (q *sqlQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (W const getWorkspaceBuildByJobID = `-- name: GetWorkspaceBuildByJobID :one SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, ai_task_sidebar_app_id, initiator_by_avatar_url, initiator_by_username, initiator_by_name + id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, initiator_by_avatar_url, initiator_by_username, initiator_by_name FROM workspace_build_with_user AS workspace_builds WHERE @@ -18854,7 +18848,6 @@ func (q *sqlQuerier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UU &i.MaxDeadline, &i.TemplateVersionPresetID, &i.HasAITask, - &i.AITaskSidebarAppID, &i.InitiatorByAvatarUrl, &i.InitiatorByUsername, &i.InitiatorByName, @@ -18864,7 +18857,7 @@ func (q *sqlQuerier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UU const getWorkspaceBuildByWorkspaceIDAndBuildNumber = `-- name: GetWorkspaceBuildByWorkspaceIDAndBuildNumber :one SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, ai_task_sidebar_app_id, initiator_by_avatar_url, initiator_by_username, initiator_by_name + id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, initiator_by_avatar_url, initiator_by_username, initiator_by_name FROM workspace_build_with_user AS workspace_builds WHERE @@ -18897,7 +18890,6 @@ func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Co &i.MaxDeadline, &i.TemplateVersionPresetID, &i.HasAITask, - &i.AITaskSidebarAppID, &i.InitiatorByAvatarUrl, &i.InitiatorByUsername, &i.InitiatorByName, @@ -18974,7 +18966,7 @@ func (q *sqlQuerier) GetWorkspaceBuildStatsByTemplates(ctx context.Context, sinc const getWorkspaceBuildsByWorkspaceID = `-- name: GetWorkspaceBuildsByWorkspaceID :many SELECT - id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, ai_task_sidebar_app_id, initiator_by_avatar_url, initiator_by_username, initiator_by_name + id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, initiator_by_avatar_url, initiator_by_username, initiator_by_name FROM workspace_build_with_user AS workspace_builds WHERE @@ -19046,7 +19038,6 @@ func (q *sqlQuerier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg Ge &i.MaxDeadline, &i.TemplateVersionPresetID, &i.HasAITask, - &i.AITaskSidebarAppID, &i.InitiatorByAvatarUrl, &i.InitiatorByUsername, &i.InitiatorByName, @@ -19065,7 +19056,7 @@ func (q *sqlQuerier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg Ge } const getWorkspaceBuildsCreatedAfter = `-- name: GetWorkspaceBuildsCreatedAfter :many -SELECT id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, ai_task_sidebar_app_id, initiator_by_avatar_url, initiator_by_username, initiator_by_name FROM workspace_build_with_user WHERE created_at > $1 +SELECT id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, initiator_by_avatar_url, initiator_by_username, initiator_by_name FROM workspace_build_with_user WHERE created_at > $1 ` func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceBuild, error) { @@ -19094,7 +19085,6 @@ func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, created &i.MaxDeadline, &i.TemplateVersionPresetID, &i.HasAITask, - &i.AITaskSidebarAppID, &i.InitiatorByAvatarUrl, &i.InitiatorByUsername, &i.InitiatorByName, @@ -19176,25 +19166,18 @@ UPDATE workspace_builds SET has_ai_task = $1, - ai_task_sidebar_app_id = $2, - updated_at = $3::timestamptz -WHERE id = $4::uuid + updated_at = $2::timestamptz +WHERE id = $3::uuid ` type UpdateWorkspaceBuildAITaskByIDParams struct { - HasAITask sql.NullBool `db:"has_ai_task" json:"has_ai_task"` - SidebarAppID uuid.NullUUID `db:"sidebar_app_id" json:"sidebar_app_id"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` - ID uuid.UUID `db:"id" json:"id"` + HasAITask sql.NullBool `db:"has_ai_task" json:"has_ai_task"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + ID uuid.UUID `db:"id" json:"id"` } func (q *sqlQuerier) UpdateWorkspaceBuildAITaskByID(ctx context.Context, arg UpdateWorkspaceBuildAITaskByIDParams) error { - _, err := q.db.ExecContext(ctx, updateWorkspaceBuildAITaskByID, - arg.HasAITask, - arg.SidebarAppID, - arg.UpdatedAt, - arg.ID, - ) + _, err := q.db.ExecContext(ctx, updateWorkspaceBuildAITaskByID, arg.HasAITask, arg.UpdatedAt, arg.ID) return err } diff --git a/coderd/database/queries/workspacebuilds.sql b/coderd/database/queries/workspacebuilds.sql index be76b6642df1f..f30c0619b7c67 100644 --- a/coderd/database/queries/workspacebuilds.sql +++ b/coderd/database/queries/workspacebuilds.sql @@ -156,7 +156,6 @@ UPDATE workspace_builds SET has_ai_task = @has_ai_task, - ai_task_sidebar_app_id = @sidebar_app_id, updated_at = @updated_at::timestamptz WHERE id = @id::uuid; diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index d1b03cbd68a27..7a359de3362f7 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -1944,32 +1944,17 @@ func (s *server) completeWorkspaceBuildJob(ctx context.Context, job database.Pro } } - var sidebarAppID uuid.NullUUID - hasAITask := len(jobType.WorkspaceBuild.AiTasks) == 1 - if hasAITask { - task := jobType.WorkspaceBuild.AiTasks[0] - if task.SidebarApp == nil { - return xerrors.Errorf("update ai task: sidebar app is nil") - } - - id, err := uuid.Parse(task.SidebarApp.Id) - if err != nil { - return xerrors.Errorf("parse sidebar app id: %w", err) - } - - sidebarAppID = uuid.NullUUID{UUID: id, Valid: true} - } - + // Generally a template _should_ only define zero or one coder_ai_task resources. This is enforced in the provider. + hasAITask := len(jobType.WorkspaceBuild.AiTasks) > 0 // Regardless of whether there is an AI task or not, update the field to indicate one way or the other since it - // always defaults to nil. ONLY if has_ai_task=true MUST ai_task_sidebar_app_id be set. + // always defaults to nil. err = db.UpdateWorkspaceBuildAITaskByID(ctx, database.UpdateWorkspaceBuildAITaskByIDParams{ ID: workspaceBuild.ID, HasAITask: sql.NullBool{ Bool: hasAITask, Valid: true, }, - SidebarAppID: sidebarAppID, - UpdatedAt: now, + UpdatedAt: now, }) if err != nil { return xerrors.Errorf("update workspace build ai tasks flag: %w", err) diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index 2b2f1c35b0e46..ad0a5da06a3c4 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -2794,19 +2794,20 @@ func TestCompleteJob(t *testing.T) { }, expected: true, }, - { + { // Checks regression for https://github.com/coder/coder/issues/18776 name: "non-existing app", input: &proto.CompletedJob_WorkspaceBuild{ AiTasks: []*sdkproto.AITask{ { Id: uuid.NewString(), SidebarApp: &sdkproto.AITaskSidebarApp{ - Id: uuid.NewString(), // Non-existing app ID. + // Non-existing app ID would previously trigger a FK violation. + Id: uuid.NewString(), }, }, }, }, - expected: false, + expected: true, }, } { t.Run(tc.name, func(t *testing.T) { @@ -2898,10 +2899,6 @@ func TestCompleteJob(t *testing.T) { require.NoError(t, err) require.True(t, build.HasAITask.Valid) // We ALWAYS expect a value to be set, therefore not nil, i.e. valid = true. require.Equal(t, tc.expected, build.HasAITask.Bool) - - if tc.expected { - require.Equal(t, sidebarAppID, build.AITaskSidebarAppID.UUID.String()) - } }) } }) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 583b9c4edaf21..70e700f82bba6 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -1152,10 +1152,6 @@ func (api *API) convertWorkspaceBuild( if build.HasAITask.Valid { hasAITask = &build.HasAITask.Bool } - var aiTasksSidebarAppID *uuid.UUID - if build.AITaskSidebarAppID.Valid { - aiTasksSidebarAppID = &build.AITaskSidebarAppID.UUID - } apiJob := convertProvisionerJob(job) transition := codersdk.WorkspaceTransition(build.Transition) @@ -1184,7 +1180,7 @@ func (api *API) convertWorkspaceBuild( MatchedProvisioners: &matchedProvisioners, TemplateVersionPresetID: presetID, HasAITask: hasAITask, - AITaskSidebarAppID: aiTasksSidebarAppID, + AITaskSidebarAppID: nil, }, nil } diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 96381043db0ab..d7f2ac8714d06 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -4584,23 +4584,13 @@ func TestWorkspaceFilterHasAITask(t *testing.T) { } job := dbgen.ProvisionerJob(t, db, pubsub, jobConfig) - res := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{JobID: job.ID}) - agnt := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ResourceID: res.ID}) - - var sidebarAppID uuid.UUID - if hasAITask.Bool { - sidebarApp := dbgen.WorkspaceApp(t, db, database.WorkspaceApp{AgentID: agnt.ID}) - sidebarAppID = sidebarApp.ID - } - build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ - WorkspaceID: ws.ID, - TemplateVersionID: version.ID, - InitiatorID: user.UserID, - JobID: job.ID, - BuildNumber: 1, - HasAITask: hasAITask, - AITaskSidebarAppID: uuid.NullUUID{UUID: sidebarAppID, Valid: sidebarAppID != uuid.Nil}, + WorkspaceID: ws.ID, + TemplateVersionID: version.ID, + InitiatorID: user.UserID, + JobID: job.ID, + BuildNumber: 1, + HasAITask: hasAITask, }) if aiTaskPrompt != nil { diff --git a/docs/admin/security/audit-logs.md b/docs/admin/security/audit-logs.md index 0232c3d45a0c2..70cc9e0c30947 100644 --- a/docs/admin/security/audit-logs.md +++ b/docs/admin/security/audit-logs.md @@ -35,7 +35,7 @@ We track the following resources: | Template
write, delete | |
FieldTracked
active_version_idtrue
activity_bumptrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
autostart_block_days_of_weektrue
autostop_requirement_days_of_weektrue
autostop_requirement_weekstrue
cors_behaviortrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_namefalse
created_by_usernamefalse
default_ttltrue
deletedfalse
deprecatedtrue
descriptiontrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
max_port_sharing_leveltrue
nametrue
organization_display_namefalse
organization_iconfalse
organization_idfalse
organization_namefalse
provisionertrue
require_active_versiontrue
time_til_dormanttrue
time_til_dormant_autodeletetrue
updated_atfalse
use_classic_parameter_flowtrue
user_acltrue
| | TemplateVersion
create, write | |
FieldTracked
archivedtrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_namefalse
created_by_usernamefalse
external_auth_providersfalse
has_ai_taskfalse
idtrue
job_idfalse
messagefalse
nametrue
organization_idfalse
readmetrue
source_example_idfalse
template_idtrue
updated_atfalse
| | User
create, write, delete | |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
github_com_user_idfalse
hashed_one_time_passcodefalse
hashed_passwordtrue
idtrue
is_systemtrue
last_seen_atfalse
login_typetrue
nametrue
one_time_passcode_expires_attrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
updated_atfalse
usernametrue
| -| WorkspaceBuild
start, stop | |
FieldTracked
ai_task_sidebar_app_idfalse
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
has_ai_taskfalse
idfalse
initiator_by_avatar_urlfalse
initiator_by_namefalse
initiator_by_usernamefalse
initiator_idfalse
job_idfalse
max_deadlinefalse
provisioner_statefalse
reasonfalse
template_version_idtrue
template_version_preset_idfalse
transitionfalse
updated_atfalse
workspace_idfalse
| +| WorkspaceBuild
start, stop | |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
has_ai_taskfalse
idfalse
initiator_by_avatar_urlfalse
initiator_by_namefalse
initiator_by_usernamefalse
initiator_idfalse
job_idfalse
max_deadlinefalse
provisioner_statefalse
reasonfalse
template_version_idtrue
template_version_preset_idfalse
transitionfalse
updated_atfalse
workspace_idfalse
| | WorkspaceProxy
| |
FieldTracked
created_attrue
deletedfalse
derp_enabledtrue
derp_onlytrue
display_nametrue
icontrue
idtrue
nametrue
region_idtrue
token_hashed_secrettrue
updated_atfalse
urltrue
versiontrue
wildcard_hostnametrue
| | WorkspaceTable
| |
FieldTracked
automatic_updatestrue
autostart_scheduletrue
created_atfalse
deletedfalse
deleting_attrue
dormant_attrue
favoritetrue
group_acltrue
idtrue
last_used_atfalse
nametrue
next_start_attrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
user_acltrue
| diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index 1ad76a1e44ca9..b4541441deefe 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -196,7 +196,7 @@ var auditableResourcesTypes = map[any]map[string]Action{ "initiator_by_name": ActionIgnore, "template_version_preset_id": ActionIgnore, // Never changes. "has_ai_task": ActionIgnore, // Never changes. - "ai_task_sidebar_app_id": ActionIgnore, // Never changes. + // "ai_task_sidebar_app_id": ActionIgnore, // Never changes. }, &database.AuditableGroup{}: { "id": ActionTrack, From 31e48b6ed8fb4c2f8816a9719d6f2828d85670cb Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 7 Aug 2025 20:42:12 +0100 Subject: [PATCH 3/4] chore(site): remove dependency on sidebar_app_id --- site/src/pages/TaskPage/TaskApps.tsx | 8 +-- site/src/pages/TaskPage/TaskPage.tsx | 79 ++++++++++++++++++++++++- site/src/pages/TaskPage/TaskSidebar.tsx | 65 +++----------------- 3 files changed, 86 insertions(+), 66 deletions(-) diff --git a/site/src/pages/TaskPage/TaskApps.tsx b/site/src/pages/TaskPage/TaskApps.tsx index 83cd01f37c004..7311079dc0aae 100644 --- a/site/src/pages/TaskPage/TaskApps.tsx +++ b/site/src/pages/TaskPage/TaskApps.tsx @@ -21,6 +21,7 @@ import { TaskAppIFrame } from "./TaskAppIframe"; type TaskAppsProps = { task: Task; + sidebarApp: WorkspaceApp | null; }; type AppWithAgent = { @@ -28,7 +29,7 @@ type AppWithAgent = { agent: WorkspaceAgent; }; -export const TaskApps: FC = ({ task }) => { +export const TaskApps: FC = ({ task, sidebarApp }) => { const agents = task.workspace.latest_build.resources .flatMap((r) => r.agents) .filter((a) => !!a); @@ -42,10 +43,7 @@ export const TaskApps: FC = ({ task }) => { agent, })), ) - .filter( - ({ app }) => - !!app && app.id !== task.workspace.latest_build.ai_task_sidebar_app_id, - ); + .filter(({ app }) => !!app && app.id !== sidebarApp?.id); const embeddedApps = apps.filter(({ app }) => !app.external); const externalApps = apps.filter(({ app }) => app.external); diff --git a/site/src/pages/TaskPage/TaskPage.tsx b/site/src/pages/TaskPage/TaskPage.tsx index 19e2c5aafdcd7..e045a7e8e4b24 100644 --- a/site/src/pages/TaskPage/TaskPage.tsx +++ b/site/src/pages/TaskPage/TaskPage.tsx @@ -1,7 +1,11 @@ import { API } from "api/api"; import { getErrorDetail, getErrorMessage } from "api/errors"; import { template as templateQueryOptions } from "api/queries/templates"; -import type { Workspace, WorkspaceStatus } from "api/typesGenerated"; +import type { + Workspace, + WorkspaceApp, + WorkspaceStatus, +} from "api/typesGenerated"; import { Button } from "components/Button/Button"; import { Loader } from "components/Loader/Loader"; import { Margins } from "components/Margins/Margins"; @@ -105,6 +109,8 @@ const TaskPage = () => { "stopping", ]; + const [sidebarApp, sidebarAppStatus] = getSidebarApp(task); + if (waitingStatuses.includes(task.workspace.latest_build.status)) { // If no template yet, use an indeterminate progress bar. const transition = (template && @@ -171,7 +177,7 @@ const TaskPage = () => { ); } else { - content = ; + content = ; } return ( @@ -181,7 +187,11 @@ const TaskPage = () => { - +
@@ -229,3 +239,66 @@ export const data = { } satisfies Task; }, }; + +const getSidebarApp = ( + task: Task, +): [WorkspaceApp | null, "error" | "loading" | "healthy"] => { + if (!task.workspace.latest_build.job.completed_at) { + // while the workspace build is running, we don't have a sidebar app yet + return [null, "loading"]; + } + + // Ensure all the agents are healthy before continuing. + const healthyAgents = task.workspace.latest_build.resources + .flatMap((res) => res.agents) + .filter((agt) => !!agt && agt.health.healthy); + if (!healthyAgents) { + return [null, "loading"]; + } + + // TODO(Cian): Improve logic for determining sidebar app. + // For now, we take the first workspace_app with at least one app_status. + const sidebarApps = healthyAgents + .flatMap((a) => a?.apps) + .filter((a) => !!a && a.statuses && a.statuses.length > 0); + + // At this point the workspace build is complete but no app has reported a status + // indicating that it is ready. Most well-behaved agentic AI applications will + // indicate their readiness status via MCP(coder_report_task). + // It's also possible that the application is just not ready yet. + // We return "loading" instead of "error" to avoid showing an error state if the app + // becomes available shortly after. The tradeoff is that users may see a loading state + // indefinitely if there's a genuine issue, but this is preferable to false error alerts. + if (!sidebarApps) { + return [null, "loading"]; + } + + const sidebarApp = sidebarApps[0]; + if (!sidebarApp) { + return [null, "loading"]; + } + + // "disabled" means that the health check is disabled, so we assume + // that the app is healthy + if (sidebarApp.health === "disabled") { + return [sidebarApp, "healthy"]; + } + if (sidebarApp.health === "healthy") { + return [sidebarApp, "healthy"]; + } + if (sidebarApp.health === "initializing") { + return [sidebarApp, "loading"]; + } + if (sidebarApp.health === "unhealthy") { + return [sidebarApp, "error"]; + } + + // exhaustiveness check + const _: never = sidebarApp.health; + // this should never happen + console.error( + "Task workspace has a finished build but the sidebar app is in an unknown health state", + task.workspace, + ); + return [null, "error"]; +}; diff --git a/site/src/pages/TaskPage/TaskSidebar.tsx b/site/src/pages/TaskPage/TaskSidebar.tsx index ca691bea08788..b194e49e52f42 100644 --- a/site/src/pages/TaskPage/TaskSidebar.tsx +++ b/site/src/pages/TaskPage/TaskSidebar.tsx @@ -22,66 +22,15 @@ import { TaskStatusLink } from "./TaskStatusLink"; type TaskSidebarProps = { task: Task; + sidebarApp: WorkspaceApp | null; + sidebarAppStatus: "error" | "loading" | "healthy"; }; -type SidebarAppStatus = "error" | "loading" | "healthy"; - -const getSidebarApp = (task: Task): [WorkspaceApp | null, SidebarAppStatus] => { - const sidebarAppId = task.workspace.latest_build.ai_task_sidebar_app_id; - // a task workspace with a finished build must have a sidebar app id - if (!sidebarAppId && task.workspace.latest_build.job.completed_at) { - console.error( - "Task workspace has a finished build but no sidebar app id", - task.workspace, - ); - return [null, "error"]; - } - - const sidebarApp = task.workspace.latest_build.resources - .flatMap((r) => r.agents) - .flatMap((a) => a?.apps) - .find((a) => a?.id === sidebarAppId); - - if (!task.workspace.latest_build.job.completed_at) { - // while the workspace build is running, we don't have a sidebar app yet - return [null, "loading"]; - } - if (!sidebarApp) { - // The workspace build is complete but the expected sidebar app wasn't found in the resources. - // This could happen due to timing issues or temporary inconsistencies in the data. - // We return "loading" instead of "error" to avoid showing an error state if the app - // becomes available shortly after. The tradeoff is that users may see a loading state - // indefinitely if there's a genuine issue, but this is preferable to false error alerts. - return [null, "loading"]; - } - // "disabled" means that the health check is disabled, so we assume - // that the app is healthy - if (sidebarApp.health === "disabled") { - return [sidebarApp, "healthy"]; - } - if (sidebarApp.health === "healthy") { - return [sidebarApp, "healthy"]; - } - if (sidebarApp.health === "initializing") { - return [sidebarApp, "loading"]; - } - if (sidebarApp.health === "unhealthy") { - return [sidebarApp, "error"]; - } - - // exhaustiveness check - const _: never = sidebarApp.health; - // this should never happen - console.error( - "Task workspace has a finished build but the sidebar app is in an unknown health state", - task.workspace, - ); - return [null, "error"]; -}; - -export const TaskSidebar: FC = ({ task }) => { - const [sidebarApp, sidebarAppStatus] = getSidebarApp(task); - +export const TaskSidebar: FC = ({ + task, + sidebarApp, + sidebarAppStatus, +}) => { return (