From 794c1237115a0027743a977f434463a2c00ad454 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 6 Aug 2026 21:36:17 +0000 Subject: [PATCH 1/7] feat: include agent metadata in workspace list responses Agent metadata could only be read by opening a watch stream per agent, so a consumer inspecting N workspaces made N+1 requests per pass for state coderd already stores. The workspaces list query now aggregates the requested keys as JSON when the new include_agent_metadata search key opts in, and the response attaches them to each agent as metadata. The expansion is key-scoped and opt-in because values can be 64KiB each; without it the response is unchanged and the aggregate subquery never runs. Closes #27933 --- coderd/apidoc/docs.go | 57 +++++++ coderd/apidoc/swagger.json | 57 +++++++ coderd/database/modelqueries.go | 2 + coderd/database/queries.sql.go | 168 ++++++++++++------ coderd/database/queries/workspaces.sql | 54 ++++++ coderd/searchquery/search.go | 3 + coderd/searchquery/search_test.go | 11 ++ coderd/workspaces.go | 44 +++++ coderd/workspaces_test.go | 76 +++++++++ codersdk/workspaceagents.go | 4 + codersdk/workspaces.go | 6 + docs/reference/api/agents.md | 17 ++ docs/reference/api/builds.md | 126 ++++++++++++++ docs/reference/api/schemas.md | 178 ++++++++++++++++++++ docs/reference/api/tasks.md | 34 ++++ docs/reference/api/templates.md | 58 +++++++ docs/reference/api/workspaces.md | 91 ++++++++++ docs/user-guides/workspace-management.md | 4 + site/src/api/typesGenerated.ts | 6 + site/src/pages/AgentsPage/AgentChatPage.tsx | 1 + 20 files changed, 942 insertions(+), 55 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 39e1a97f23ee7..602f7d7784de2 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -27254,6 +27254,13 @@ const docTemplate = `{ "logs_overflowed": { "type": "boolean" }, + "metadata": { + "description": "Metadata is only populated on the workspaces list endpoint when the\nrequest opts in with the include_agent_metadata search key, and it\nonly carries the requested keys.", + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.WorkspaceAgentMetadata" + } + }, "name": { "type": "string" }, @@ -27638,6 +27645,56 @@ const docTemplate = `{ } } }, + "codersdk.WorkspaceAgentMetadata": { + "type": "object", + "properties": { + "description": { + "$ref": "#/definitions/codersdk.WorkspaceAgentMetadataDescription" + }, + "result": { + "$ref": "#/definitions/codersdk.WorkspaceAgentMetadataResult" + } + } + }, + "codersdk.WorkspaceAgentMetadataDescription": { + "type": "object", + "properties": { + "display_name": { + "type": "string" + }, + "interval": { + "type": "integer" + }, + "key": { + "type": "string" + }, + "script": { + "type": "string" + }, + "timeout": { + "type": "integer" + } + } + }, + "codersdk.WorkspaceAgentMetadataResult": { + "type": "object", + "properties": { + "age": { + "description": "Age is the number of seconds since the metadata was collected.\nIt is provided in addition to CollectedAt to protect against clock skew.", + "type": "integer" + }, + "collected_at": { + "type": "string", + "format": "date-time" + }, + "error": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, "codersdk.WorkspaceAgentPortShare": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index e70e01bad930e..10f5d29ee60d9 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -25096,6 +25096,13 @@ "logs_overflowed": { "type": "boolean" }, + "metadata": { + "description": "Metadata is only populated on the workspaces list endpoint when the\nrequest opts in with the include_agent_metadata search key, and it\nonly carries the requested keys.", + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.WorkspaceAgentMetadata" + } + }, "name": { "type": "string" }, @@ -25477,6 +25484,56 @@ } } }, + "codersdk.WorkspaceAgentMetadata": { + "type": "object", + "properties": { + "description": { + "$ref": "#/definitions/codersdk.WorkspaceAgentMetadataDescription" + }, + "result": { + "$ref": "#/definitions/codersdk.WorkspaceAgentMetadataResult" + } + } + }, + "codersdk.WorkspaceAgentMetadataDescription": { + "type": "object", + "properties": { + "display_name": { + "type": "string" + }, + "interval": { + "type": "integer" + }, + "key": { + "type": "string" + }, + "script": { + "type": "string" + }, + "timeout": { + "type": "integer" + } + } + }, + "codersdk.WorkspaceAgentMetadataResult": { + "type": "object", + "properties": { + "age": { + "description": "Age is the number of seconds since the metadata was collected.\nIt is provided in addition to CollectedAt to protect against clock skew.", + "type": "integer" + }, + "collected_at": { + "type": "string", + "format": "date-time" + }, + "error": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, "codersdk.WorkspaceAgentPortShare": { "type": "object", "properties": { diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 79f1d91095ee5..3a73660a6020c 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -260,6 +260,7 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa // The name comment is for metric tracking query := fmt.Sprintf("-- name: GetAuthorizedWorkspaces :many\n%s", filtered) rows, err := q.db.QueryContext(ctx, query, + pq.Array(arg.IncludeAgentMetadata), pq.Array(arg.ParamNames), pq.Array(arg.ParamValues), arg.Deleted, @@ -336,6 +337,7 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa &i.LatestBuildTransition, &i.LatestBuildStatus, &i.LatestBuildHasExternalAgent, + &i.AgentMetadata, &i.Count, ); err != nil { return nil, err diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index e499234f558db..8a98a492a36d4 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -38510,8 +38510,8 @@ const getWorkspaces = `-- name: GetWorkspaces :many WITH build_params AS ( SELECT - LOWER(unnest($1 :: text[])) AS name, - LOWER(unnest($2 :: text[])) AS value + LOWER(unnest($2 :: text[])) AS name, + LOWER(unnest($3 :: text[])) AS value ), filtered_workspaces AS ( SELECT @@ -38572,32 +38572,32 @@ LEFT JOIN LATERAL ( ) template ON true WHERE -- Optionally include deleted workspaces - workspaces.deleted = $3 + workspaces.deleted = $4 AND CASE - WHEN $4 :: text != '' THEN + WHEN $5 :: text != '' THEN CASE -- Some workspace specific status refer to the transition -- type. By default, the standard provisioner job status -- search strings are supported. -- 'running' states - WHEN $4 = 'starting' THEN + WHEN $5 = 'starting' THEN latest_build.job_status = 'running'::provisioner_job_status AND latest_build.transition = 'start'::workspace_transition - WHEN $4 = 'stopping' THEN + WHEN $5 = 'stopping' THEN latest_build.job_status = 'running'::provisioner_job_status AND latest_build.transition = 'stop'::workspace_transition - WHEN $4 = 'deleting' THEN + WHEN $5 = 'deleting' THEN latest_build.job_status = 'running' AND latest_build.transition = 'delete'::workspace_transition -- 'succeeded' states - WHEN $4 = 'deleted' THEN + WHEN $5 = 'deleted' THEN latest_build.job_status = 'succeeded'::provisioner_job_status AND latest_build.transition = 'delete'::workspace_transition - WHEN $4 = 'stopped' THEN + WHEN $5 = 'stopped' THEN latest_build.job_status = 'succeeded'::provisioner_job_status AND latest_build.transition = 'stop'::workspace_transition - WHEN $4 = 'started' THEN + WHEN $5 = 'started' THEN latest_build.job_status = 'succeeded'::provisioner_job_status AND latest_build.transition = 'start'::workspace_transition @@ -38605,13 +38605,13 @@ WHERE -- differ. A workspace is "running" if the job is "succeeded" and -- the transition is "start". This is because a workspace starts -- running when a job is complete. - WHEN $4 = 'running' THEN + WHEN $5 = 'running' THEN latest_build.job_status = 'succeeded'::provisioner_job_status AND latest_build.transition = 'start'::workspace_transition - WHEN $4 != '' THEN + WHEN $5 != '' THEN -- By default just match the job status exactly - latest_build.job_status = $4::provisioner_job_status + latest_build.job_status = $5::provisioner_job_status ELSE true END @@ -38619,19 +38619,19 @@ WHERE END -- Filter by owner_id AND CASE - WHEN $5 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN - workspaces.owner_id = $5 + WHEN $6 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + workspaces.owner_id = $6 ELSE true END -- Filter by organization_id AND CASE - WHEN $6 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN - workspaces.organization_id = $6 + WHEN $7 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + workspaces.organization_id = $7 ELSE true END -- Filter by build parameter -- @has_param will match any build that includes the parameter. - AND CASE WHEN array_length($7 :: text[], 1) > 0 THEN + AND CASE WHEN array_length($8 :: text[], 1) > 0 THEN EXISTS ( SELECT 1 @@ -38640,14 +38640,14 @@ WHERE WHERE workspace_build_parameters.workspace_build_id = latest_build.id AND -- ILIKE is case insensitive - workspace_build_parameters.name ILIKE ANY($7) + workspace_build_parameters.name ILIKE ANY($8) ) ELSE true END -- @param_value will match param name an value. -- requires 2 arrays, @param_names and @param_values to be passed in. -- Array index must match between the 2 arrays for name=value - AND CASE WHEN array_length($1 :: text[], 1) > 0 THEN + AND CASE WHEN array_length($2 :: text[], 1) > 0 THEN EXISTS ( SELECT 1 @@ -38665,40 +38665,40 @@ WHERE -- Filter by owner_name AND CASE - WHEN $8 :: text != '' THEN - workspaces.owner_id = (SELECT id FROM users WHERE lower(users.username) = lower($8) AND deleted = false) + WHEN $9 :: text != '' THEN + workspaces.owner_id = (SELECT id FROM users WHERE lower(users.username) = lower($9) AND deleted = false) ELSE true END -- Filter by template_name -- There can be more than 1 template with the same name across organizations. -- Use the organization filter to restrict to 1 org if needed. AND CASE - WHEN $9 :: text != '' THEN - workspaces.template_id = ANY(SELECT id FROM templates WHERE lower(name) = lower($9) AND deleted = false) + WHEN $10 :: text != '' THEN + workspaces.template_id = ANY(SELECT id FROM templates WHERE lower(name) = lower($10) AND deleted = false) ELSE true END -- Filter by template_ids AND CASE - WHEN array_length($10 :: uuid[], 1) > 0 THEN - workspaces.template_id = ANY($10) + WHEN array_length($11 :: uuid[], 1) > 0 THEN + workspaces.template_id = ANY($11) ELSE true END -- Filter by workspace_ids AND CASE - WHEN array_length($11 :: uuid[], 1) > 0 THEN - workspaces.id = ANY($11) + WHEN array_length($12 :: uuid[], 1) > 0 THEN + workspaces.id = ANY($12) ELSE true END -- Filter by name, matching on substring AND CASE - WHEN $12 :: text != '' THEN - workspaces.name ILIKE '%' || $12 || '%' + WHEN $13 :: text != '' THEN + workspaces.name ILIKE '%' || $13 || '%' ELSE true END -- Filter by agent status -- has-agent: is only applicable for workspaces in "start" transition. Stopped and deleted workspaces don't have agents. AND CASE - WHEN array_length($13 :: text[], 1) > 0 THEN + WHEN array_length($14 :: text[], 1) > 0 THEN ( SELECT COUNT(*) FROM @@ -38723,43 +38723,43 @@ WHERE END WHEN workspace_agents.disconnected_at > workspace_agents.last_connected_at THEN 'disconnected' - WHEN NOW() - workspace_agents.last_connected_at > INTERVAL '1 second' * $14 :: bigint THEN + WHEN NOW() - workspace_agents.last_connected_at > INTERVAL '1 second' * $15 :: bigint THEN 'disconnected' WHEN workspace_agents.last_connected_at IS NOT NULL THEN 'connected' ELSE NULL END - ) = ANY($13 :: text[]) + ) = ANY($14 :: text[]) ) > 0 ELSE true END -- Filter by dormant workspaces. AND CASE - WHEN $15 :: boolean != 'false' THEN + WHEN $16 :: boolean != 'false' THEN dormant_at IS NOT NULL ELSE true END -- Filter by last_used AND CASE - WHEN $16 :: timestamp with time zone > '0001-01-01 00:00:00Z' THEN - workspaces.last_used_at <= $16 + WHEN $17 :: timestamp with time zone > '0001-01-01 00:00:00Z' THEN + workspaces.last_used_at <= $17 ELSE true END AND CASE - WHEN $17 :: timestamp with time zone > '0001-01-01 00:00:00Z' THEN - workspaces.last_used_at >= $17 + WHEN $18 :: timestamp with time zone > '0001-01-01 00:00:00Z' THEN + workspaces.last_used_at >= $18 ELSE true END AND CASE - WHEN $18 :: boolean IS NOT NULL THEN - (latest_build.template_version_id = template.active_version_id) = $18 :: boolean + WHEN $19 :: boolean IS NOT NULL THEN + (latest_build.template_version_id = template.active_version_id) = $19 :: boolean ELSE true END -- Filter by has_ai_task, checks if this is a task workspace. AND CASE - WHEN $19::boolean IS NOT NULL - THEN $19::boolean = EXISTS ( + WHEN $20::boolean IS NOT NULL + THEN $20::boolean = EXISTS ( SELECT 1 FROM @@ -38773,26 +38773,26 @@ WHERE END -- Filter by has_external_agent in latest build AND CASE - WHEN $20 :: boolean IS NOT NULL THEN - latest_build.has_external_agent = $20 :: boolean + WHEN $21 :: boolean IS NOT NULL THEN + latest_build.has_external_agent = $21 :: boolean ELSE true END -- Filter by shared status AND CASE - WHEN $21 :: boolean IS NOT NULL THEN - (workspaces.user_acl != '{}'::jsonb OR workspaces.group_acl != '{}'::jsonb) = $21 :: boolean + WHEN $22 :: boolean IS NOT NULL THEN + (workspaces.user_acl != '{}'::jsonb OR workspaces.group_acl != '{}'::jsonb) = $22 :: boolean ELSE true END -- Filter by shared_with_user_id AND CASE - WHEN $22 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN - workspaces.user_acl ? ($22 :: uuid) :: text + WHEN $23 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + workspaces.user_acl ? ($23 :: uuid) :: text ELSE true END -- Filter by shared_with_group_id AND CASE - WHEN $23 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN - workspaces.group_acl ? ($23 :: uuid) :: text + WHEN $24 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + workspaces.group_acl ? ($24 :: uuid) :: text ELSE true END @@ -38805,7 +38805,7 @@ WHERE filtered_workspaces fw ORDER BY -- To ensure that 'favorite' workspaces show up first in the list only for their owner. - CASE WHEN favorite AND owner_username = (SELECT users.username FROM users WHERE users.id = $24) THEN 0 ELSE 1 END ASC, + CASE WHEN favorite AND owner_username = (SELECT users.username FROM users WHERE users.id = $25) THEN 0 ELSE 1 END ASC, (latest_build_completed_at IS NOT NULL AND latest_build_canceled_at IS NULL AND latest_build_error IS NULL AND @@ -38814,11 +38814,11 @@ WHERE LOWER(name) ASC LIMIT CASE - WHEN $26 :: integer > 0 THEN - $26 + WHEN $27 :: integer > 0 THEN + $27 END OFFSET - $25 + $26 ), filtered_workspaces_order_with_summary AS ( SELECT fwo.id, fwo.created_at, fwo.updated_at, fwo.owner_id, fwo.organization_id, fwo.template_id, fwo.deleted, fwo.name, fwo.autostart_schedule, fwo.ttl, fwo.last_used_at, fwo.dormant_at, fwo.deleting_at, fwo.automatic_updates, fwo.favorite, fwo.next_start_at, fwo.group_acl, fwo.user_acl, fwo.owner_avatar_url, fwo.owner_username, fwo.owner_name, fwo.organization_name, fwo.organization_display_name, fwo.organization_icon, fwo.organization_description, fwo.template_name, fwo.template_display_name, fwo.template_icon, fwo.template_description, fwo.task_id, fwo.group_acl_display_info, fwo.user_acl_display_info, fwo.template_version_id, fwo.template_version_name, fwo.latest_build_completed_at, fwo.latest_build_canceled_at, fwo.latest_build_error, fwo.latest_build_transition, fwo.latest_build_status, fwo.latest_build_has_external_agent @@ -38870,7 +38870,7 @@ WHERE 'unknown'::provisioner_job_status, -- latest_build_status false -- latest_build_has_external_agent WHERE - $27 :: boolean = true + $28 :: boolean = true ), total_count AS ( SELECT count(*) AS count @@ -38879,6 +38879,60 @@ WHERE ) SELECT fwos.id, fwos.created_at, fwos.updated_at, fwos.owner_id, fwos.organization_id, fwos.template_id, fwos.deleted, fwos.name, fwos.autostart_schedule, fwos.ttl, fwos.last_used_at, fwos.dormant_at, fwos.deleting_at, fwos.automatic_updates, fwos.favorite, fwos.next_start_at, fwos.group_acl, fwos.user_acl, fwos.owner_avatar_url, fwos.owner_username, fwos.owner_name, fwos.organization_name, fwos.organization_display_name, fwos.organization_icon, fwos.organization_description, fwos.template_name, fwos.template_display_name, fwos.template_icon, fwos.template_description, fwos.task_id, fwos.group_acl_display_info, fwos.user_acl_display_info, fwos.template_version_id, fwos.template_version_name, fwos.latest_build_completed_at, fwos.latest_build_canceled_at, fwos.latest_build_error, fwos.latest_build_transition, fwos.latest_build_status, fwos.latest_build_has_external_agent, + -- agent_metadata expands the response with the requested agent + -- metadata keys for the latest build's agents. The CASE keeps the + -- subquery unevaluated for every caller that does not opt in, and + -- it only runs for the returned page. Each element carries the + -- workspace_agent_id so multi-agent workspaces can map values onto + -- the right agent. Keys match case-insensitively because search + -- queries are lowercased. + CASE WHEN cardinality($1 :: text[]) > 0 THEN + COALESCE(( + SELECT + jsonb_agg(jsonb_build_object( + 'workspace_agent_id', workspace_agents.id, + 'display_name', workspace_agent_metadata.display_name, + 'key', workspace_agent_metadata.key, + 'script', workspace_agent_metadata.script, + 'value', workspace_agent_metadata.value, + 'error', workspace_agent_metadata.error, + 'timeout', workspace_agent_metadata.timeout, + 'interval', workspace_agent_metadata.interval, + 'collected_at', workspace_agent_metadata.collected_at, + 'display_order', workspace_agent_metadata.display_order + )) + FROM + workspace_agents + JOIN + workspace_resources + ON + workspace_resources.id = workspace_agents.resource_id + JOIN + workspace_builds + ON + workspace_builds.job_id = workspace_resources.job_id + JOIN + workspace_agent_metadata + ON + workspace_agent_metadata.workspace_agent_id = workspace_agents.id + WHERE + workspace_builds.workspace_id = fwos.id + AND workspace_builds.build_number = ( + SELECT + max(build_number) + FROM + workspace_builds + WHERE + workspace_builds.workspace_id = fwos.id + ) + -- Filter out deleted sub agents. + AND workspace_agents.deleted = FALSE + AND LOWER(workspace_agent_metadata.key) = ANY($1 :: text[]) + ), '[]'::jsonb) + ELSE + -- Never NULL: lib/pq cannot scan NULL into json.RawMessage. + '[]'::jsonb + END :: jsonb AS agent_metadata, tc.count FROM filtered_workspaces_order_with_summary fwos @@ -38887,6 +38941,7 @@ CROSS JOIN ` type GetWorkspacesParams struct { + IncludeAgentMetadata []string `db:"include_agent_metadata" json:"include_agent_metadata"` ParamNames []string `db:"param_names" json:"param_names"` ParamValues []string `db:"param_values" json:"param_values"` Deleted bool `db:"deleted" json:"deleted"` @@ -38957,6 +39012,7 @@ type GetWorkspacesRow struct { LatestBuildTransition WorkspaceTransition `db:"latest_build_transition" json:"latest_build_transition"` LatestBuildStatus ProvisionerJobStatus `db:"latest_build_status" json:"latest_build_status"` LatestBuildHasExternalAgent sql.NullBool `db:"latest_build_has_external_agent" json:"latest_build_has_external_agent"` + AgentMetadata json.RawMessage `db:"agent_metadata" json:"agent_metadata"` Count int64 `db:"count" json:"count"` } @@ -38965,6 +39021,7 @@ type GetWorkspacesRow struct { // be used in a WHERE clause. func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error) { rows, err := q.db.QueryContext(ctx, getWorkspaces, + pq.Array(arg.IncludeAgentMetadata), pq.Array(arg.ParamNames), pq.Array(arg.ParamValues), arg.Deleted, @@ -39041,6 +39098,7 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) &i.LatestBuildTransition, &i.LatestBuildStatus, &i.LatestBuildHasExternalAgent, + &i.AgentMetadata, &i.Count, ); err != nil { return nil, err diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index e8b1885a2de5b..cdcf54fd629ea 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -473,6 +473,60 @@ WHERE ) SELECT fwos.*, + -- agent_metadata expands the response with the requested agent + -- metadata keys for the latest build's agents. The CASE keeps the + -- subquery unevaluated for every caller that does not opt in, and + -- it only runs for the returned page. Each element carries the + -- workspace_agent_id so multi-agent workspaces can map values onto + -- the right agent. Keys match case-insensitively because search + -- queries are lowercased. + CASE WHEN cardinality(@include_agent_metadata :: text[]) > 0 THEN + COALESCE(( + SELECT + jsonb_agg(jsonb_build_object( + 'workspace_agent_id', workspace_agents.id, + 'display_name', workspace_agent_metadata.display_name, + 'key', workspace_agent_metadata.key, + 'script', workspace_agent_metadata.script, + 'value', workspace_agent_metadata.value, + 'error', workspace_agent_metadata.error, + 'timeout', workspace_agent_metadata.timeout, + 'interval', workspace_agent_metadata.interval, + 'collected_at', workspace_agent_metadata.collected_at, + 'display_order', workspace_agent_metadata.display_order + )) + FROM + workspace_agents + JOIN + workspace_resources + ON + workspace_resources.id = workspace_agents.resource_id + JOIN + workspace_builds + ON + workspace_builds.job_id = workspace_resources.job_id + JOIN + workspace_agent_metadata + ON + workspace_agent_metadata.workspace_agent_id = workspace_agents.id + WHERE + workspace_builds.workspace_id = fwos.id + AND workspace_builds.build_number = ( + SELECT + max(build_number) + FROM + workspace_builds + WHERE + workspace_builds.workspace_id = fwos.id + ) + -- Filter out deleted sub agents. + AND workspace_agents.deleted = FALSE + AND LOWER(workspace_agent_metadata.key) = ANY(@include_agent_metadata :: text[]) + ), '[]'::jsonb) + ELSE + -- Never NULL: lib/pq cannot scan NULL into json.RawMessage. + '[]'::jsonb + END :: jsonb AS agent_metadata, tc.count FROM filtered_workspaces_order_with_summary fwos diff --git a/coderd/searchquery/search.go b/coderd/searchquery/search.go index 4849c55a39e64..dd829b65b536a 100644 --- a/coderd/searchquery/search.go +++ b/coderd/searchquery/search.go @@ -272,6 +272,9 @@ func Workspaces(ctx context.Context, db database.Store, query string, page coder } filter.HasAITask = parser.NullableBoolean(values, sql.NullBool{}, "has-ai-task") filter.HasExternalAgent = parser.NullableBoolean(values, sql.NullBool{}, "has_external_agent") + // include_agent_metadata expands the response with the named agent + // metadata keys; it does not filter the returned workspaces. + filter.IncludeAgentMetadata = parser.Strings(values, []string{}, "include_agent_metadata") filter.OrganizationID = parseOrganization(ctx, db, parser, values, "organization") filter.Shared = parser.NullableBoolean(values, sql.NullBool{}, "shared") filter.SharedWithUserID = parseUser(ctx, db, parser, values, "shared_with_user", actorID) diff --git a/coderd/searchquery/search_test.go b/coderd/searchquery/search_test.go index 67be2c8ec379c..752a966f566cf 100644 --- a/coderd/searchquery/search_test.go +++ b/coderd/searchquery/search_test.go @@ -341,6 +341,13 @@ func TestSearchWorkspace(t *testing.T) { HasAgentStatuses: []string{"connecting", "connected"}, }, }, + { + Name: "IncludeAgentMetadata", + Query: `include_agent_metadata:"byoc_status" include_agent_metadata:"cpu"`, + Expected: database.GetWorkspacesParams{ + IncludeAgentMetadata: []string{"byoc_status", "cpu"}, + }, + }, { Name: "SharedWithMe", Query: `shared_with_user:me`, @@ -541,6 +548,10 @@ func TestSearchWorkspace(t *testing.T) { // nil slice vs 0 len slice is equivalent for our purposes. c.Expected.HasAgentStatuses = values.HasAgentStatuses } + if len(c.Expected.IncludeAgentMetadata) == len(values.IncludeAgentMetadata) { + // nil slice vs 0 len slice is equivalent for our purposes. + c.Expected.IncludeAgentMetadata = values.IncludeAgentMetadata + } assert.Len(t, errs, 0, "expected no error") assert.Equal(t, c.Expected, values, "expected values") } diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 8d9c8bde7c096..9740dbdb9e940 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -248,6 +248,17 @@ func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) { return } + if len(filter.IncludeAgentMetadata) > 0 { + err = attachAgentMetadata(wss, workspaceRows) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error converting agent metadata.", + Detail: err.Error(), + }) + return + } + } + httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspacesResponse{ Workspaces: wss, Count: int(workspaceRows[0].Count), @@ -2758,6 +2769,39 @@ func (api *API) workspaceData(ctx context.Context, workspaces []database.Workspa }, nil } +// attachAgentMetadata maps the agent_metadata JSON the workspaces query +// aggregated per workspace onto the agents in the converted response. +// The aggregate elements have the database.WorkspaceAgentMetadatum JSON +// shape, each carrying its workspace_agent_id. +func attachAgentMetadata(workspaces []codersdk.Workspace, rows []database.GetWorkspacesRow) error { + byAgent := map[uuid.UUID][]database.WorkspaceAgentMetadatum{} + for _, row := range rows { + if len(row.AgentMetadata) == 0 { + continue + } + var data []database.WorkspaceAgentMetadatum + err := json.Unmarshal(row.AgentMetadata, &data) + if err != nil { + return xerrors.Errorf("unmarshal agent metadata for workspace %q: %w", row.ID, err) + } + for _, datum := range data { + byAgent[datum.WorkspaceAgentID] = append(byAgent[datum.WorkspaceAgentID], datum) + } + } + for wi := range workspaces { + resources := workspaces[wi].LatestBuild.Resources + for ri := range resources { + for ai := range resources[ri].Agents { + agent := &resources[ri].Agents[ai] + if metadata, ok := byAgent[agent.ID]; ok { + agent.Metadata = convertWorkspaceAgentMetadata(metadata) + } + } + } + } + return nil +} + func convertWorkspaces( ctx context.Context, logger slog.Logger, diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index e30f0e71c8f68..e81e537b748fc 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -2869,6 +2869,82 @@ func TestWorkspaceFilterManual(t *testing.T) { require.Equal(t, workspace.ID, res.Workspaces[0].ID) }) + t.Run("IncludeAgentMetadata", func(t *testing.T) { + t.Parallel() + + client, db := coderdtest.NewWithDatabase(t, nil) + user := coderdtest.CreateFirstUser(t, client) + + build := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: user.OrganizationID, + OwnerID: user.UserID, + }).WithAgent().Do() + require.Len(t, build.Agents, 1) + agentID := build.Agents[0].ID + + //nolint:gocritic // This is a test; only the agent API writes metadata. + ctx := dbauthz.AsSystemRestricted(context.Background()) + collectedAt := dbtime.Now() + for i, key := range []string{"byoc_status", "cpu", "unrequested"} { + err := db.InsertWorkspaceAgentMetadata(ctx, database.InsertWorkspaceAgentMetadataParams{ + WorkspaceAgentID: agentID, + DisplayName: key, + Key: key, + Script: "echo", + Timeout: int64(time.Second), + Interval: int64(time.Second), + // Reversed so the response order proves display_order + // sorting rather than insertion order. + DisplayOrder: int32(3 - i), //nolint:gosec // Tiny test constant. + }) + require.NoError(t, err) + err = db.UpdateWorkspaceAgentMetadata(ctx, database.UpdateWorkspaceAgentMetadataParams{ + WorkspaceAgentID: agentID, + Key: []string{key}, + Value: []string{"value-" + key}, + Error: []string{""}, + CollectedAt: []time.Time{collectedAt}, + }) + require.NoError(t, err) + } + + reqCtx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + findAgent := func(res codersdk.WorkspacesResponse) codersdk.WorkspaceAgent { + require.Len(t, res.Workspaces, 1) + require.Len(t, res.Workspaces[0].LatestBuild.Resources, 1) + require.Len(t, res.Workspaces[0].LatestBuild.Resources[0].Agents, 1) + return res.Workspaces[0].LatestBuild.Resources[0].Agents[0] + } + + // Without the opt-in the response carries no metadata. + res, err := client.Workspaces(reqCtx, codersdk.WorkspaceFilter{}) + require.NoError(t, err) + require.Empty(t, findAgent(res).Metadata) + + // Opting in returns exactly the requested keys, ordered by + // display_order, with their collected values. + res, err = client.Workspaces(reqCtx, codersdk.WorkspaceFilter{ + IncludeAgentMetadata: []string{"byoc_status", "cpu"}, + }) + require.NoError(t, err) + metadata := findAgent(res).Metadata + require.Len(t, metadata, 2) + require.Equal(t, "cpu", metadata[0].Description.Key) + require.Equal(t, "value-cpu", metadata[0].Result.Value) + require.Equal(t, "byoc_status", metadata[1].Description.Key) + require.Equal(t, "value-byoc_status", metadata[1].Result.Value) + require.WithinDuration(t, collectedAt, metadata[1].Result.CollectedAt, time.Second) + + // Unknown keys are not an error; the metadata is just absent. + res, err = client.Workspaces(reqCtx, codersdk.WorkspaceFilter{ + IncludeAgentMetadata: []string{"no_such_key"}, + }) + require.NoError(t, err) + require.Empty(t, findAgent(res).Metadata) + }) + t.Run("HealthyFilter", func(t *testing.T) { t.Parallel() diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index b03383ace9a18..1ceb4b41778f6 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -170,6 +170,10 @@ type WorkspaceAgent struct { DisplayApps []DisplayApp `json:"display_apps"` LogSources []WorkspaceAgentLogSource `json:"log_sources"` Scripts []WorkspaceAgentScript `json:"scripts"` + // Metadata is only populated on the workspaces list endpoint when the + // request opts in with the include_agent_metadata search key, and it + // only carries the requested keys. + Metadata []WorkspaceAgentMetadata `json:"metadata,omitempty"` // StartupScriptBehavior is a legacy field that is deprecated in favor // of the `coder_script` resource. It's only referenced by old clients. diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index b2f6256c080d6..6a78ecd7b364d 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -560,6 +560,9 @@ type WorkspaceFilter struct { SharedWithUser string `json:"shared_with_user,omitempty" typescript:"-"` // SharedWithGroup is the group name, group ID, or / of the group that the workspace is shared with SharedWithGroup string `json:"shared_with_group,omitempty" typescript:"-"` + // IncludeAgentMetadata expands each agent in the response with the + // named metadata keys. It does not filter the returned workspaces. + IncludeAgentMetadata []string `json:"include_agent_metadata,omitempty" typescript:"-"` // FilterQuery supports a raw filter query string FilterQuery string `json:"q,omitempty"` } @@ -595,6 +598,9 @@ func (f WorkspaceFilter) asRequestOption() RequestOption { if f.SharedWithGroup != "" { params = append(params, fmt.Sprintf("shared_with_group:%q", f.SharedWithGroup)) } + for _, key := range f.IncludeAgentMetadata { + params = append(params, fmt.Sprintf("include_agent_metadata:%q", key)) + } if f.FilterQuery != "" { // If custom stuff is added, just add it on here. params = append(params, f.FilterQuery) diff --git a/docs/reference/api/agents.md b/docs/reference/api/agents.md index 75eb4ea60be3c..d241d8247cb2a 100644 --- a/docs/reference/api/agents.md +++ b/docs/reference/api/agents.md @@ -624,6 +624,23 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent} \ ], "logs_length": 0, "logs_overflowed": true, + "metadata": [ + { + "description": { + "display_name": "string", + "interval": 0, + "key": "string", + "script": "string", + "timeout": 0 + }, + "result": { + "age": 0, + "collected_at": "2019-08-24T14:15:22Z", + "error": "string", + "value": "string" + } + } + ], "name": "string", "operating_system": "string", "parent_id": { diff --git a/docs/reference/api/builds.md b/docs/reference/api/builds.md index cbd793431440d..7f618d3269189 100644 --- a/docs/reference/api/builds.md +++ b/docs/reference/api/builds.md @@ -170,6 +170,23 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam ], "logs_length": 0, "logs_overflowed": true, + "metadata": [ + { + "description": { + "display_name": "string", + "interval": 0, + "key": "string", + "script": "string", + "timeout": 0 + }, + "result": { + "age": 0, + "collected_at": "2019-08-24T14:15:22Z", + "error": "string", + "value": "string" + } + } + ], "name": "string", "operating_system": "string", "parent_id": { @@ -413,6 +430,23 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild} \ ], "logs_length": 0, "logs_overflowed": true, + "metadata": [ + { + "description": { + "display_name": "string", + "interval": 0, + "key": "string", + "script": "string", + "timeout": 0 + }, + "result": { + "age": 0, + "collected_at": "2019-08-24T14:15:22Z", + "error": "string", + "value": "string" + } + } + ], "name": "string", "operating_system": "string", "parent_id": { @@ -774,6 +808,23 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/res ], "logs_length": 0, "logs_overflowed": true, + "metadata": [ + { + "description": { + "display_name": "string", + "interval": 0, + "key": "string", + "script": "string", + "timeout": 0 + }, + "result": { + "age": 0, + "collected_at": "2019-08-24T14:15:22Z", + "error": "string", + "value": "string" + } + } + ], "name": "string", "operating_system": "string", "parent_id": { @@ -904,6 +955,18 @@ Status Code **200** | `»»» workspace_agent_id` | string(uuid) | false | | | | `»» logs_length` | integer | false | | | | `»» logs_overflowed` | boolean | false | | | +| `»» metadata` | array | false | | Metadata is only populated on the workspaces list endpoint when the request opts in with the include_agent_metadata search key, and it only carries the requested keys. | +| `»»» description` | [codersdk.WorkspaceAgentMetadataDescription](schemas.md#codersdkworkspaceagentmetadatadescription) | false | | | +| `»»»» display_name` | string | false | | | +| `»»»» interval` | integer | false | | | +| `»»»» key` | string | false | | | +| `»»»» script` | string | false | | | +| `»»»» timeout` | integer | false | | | +| `»»» result` | [codersdk.WorkspaceAgentMetadataResult](schemas.md#codersdkworkspaceagentmetadataresult) | false | | | +| `»»»» age` | integer | false | | Age is the number of seconds since the metadata was collected. It is provided in addition to CollectedAt to protect against clock skew. | +| `»»»» collected_at` | string(date-time) | false | | | +| `»»»» error` | string | false | | | +| `»»»» value` | string | false | | | | `»» name` | string | false | | | | `»» operating_system` | string | false | | | | `»» parent_id` | [uuid.NullUUID](schemas.md#uuidnulluuid) | false | | | @@ -1128,6 +1191,23 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/sta ], "logs_length": 0, "logs_overflowed": true, + "metadata": [ + { + "description": { + "display_name": "string", + "interval": 0, + "key": "string", + "script": "string", + "timeout": 0 + }, + "result": { + "age": 0, + "collected_at": "2019-08-24T14:15:22Z", + "error": "string", + "value": "string" + } + } + ], "name": "string", "operating_system": "string", "parent_id": { @@ -1482,6 +1562,23 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ ], "logs_length": 0, "logs_overflowed": true, + "metadata": [ + { + "description": { + "display_name": "string", + "interval": 0, + "key": "string", + "script": "string", + "timeout": 0 + }, + "result": { + "age": 0, + "collected_at": "2019-08-24T14:15:22Z", + "error": "string", + "value": "string" + } + } + ], "name": "string", "operating_system": "string", "parent_id": { @@ -1675,6 +1772,18 @@ Status Code **200** | `»»»» workspace_agent_id` | string(uuid) | false | | | | `»»» logs_length` | integer | false | | | | `»»» logs_overflowed` | boolean | false | | | +| `»»» metadata` | array | false | | Metadata is only populated on the workspaces list endpoint when the request opts in with the include_agent_metadata search key, and it only carries the requested keys. | +| `»»»» description` | [codersdk.WorkspaceAgentMetadataDescription](schemas.md#codersdkworkspaceagentmetadatadescription) | false | | | +| `»»»»» display_name` | string | false | | | +| `»»»»» interval` | integer | false | | | +| `»»»»» key` | string | false | | | +| `»»»»» script` | string | false | | | +| `»»»»» timeout` | integer | false | | | +| `»»»» result` | [codersdk.WorkspaceAgentMetadataResult](schemas.md#codersdkworkspaceagentmetadataresult) | false | | | +| `»»»»» age` | integer | false | | Age is the number of seconds since the metadata was collected. It is provided in addition to CollectedAt to protect against clock skew. | +| `»»»»» collected_at` | string(date-time) | false | | | +| `»»»»» error` | string | false | | | +| `»»»»» value` | string | false | | | | `»»» name` | string | false | | | | `»»» operating_system` | string | false | | | | `»»» parent_id` | [uuid.NullUUID](schemas.md#uuidnulluuid) | false | | | @@ -1951,6 +2060,23 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ ], "logs_length": 0, "logs_overflowed": true, + "metadata": [ + { + "description": { + "display_name": "string", + "interval": 0, + "key": "string", + "script": "string", + "timeout": 0 + }, + "result": { + "age": 0, + "collected_at": "2019-08-24T14:15:22Z", + "error": "string", + "value": "string" + } + } + ], "name": "string", "operating_system": "string", "parent_id": { diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 1dc9283ebea18..23b1cbdd18254 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -10192,6 +10192,23 @@ Only certain features set these fields: - FeatureManagedAgentLimit - FeatureAgen ], "logs_length": 0, "logs_overflowed": true, + "metadata": [ + { + "description": { + "display_name": "string", + "interval": 0, + "key": "string", + "script": "string", + "timeout": 0 + }, + "result": { + "age": 0, + "collected_at": "2019-08-24T14:15:22Z", + "error": "string", + "value": "string" + } + } + ], "name": "string", "operating_system": "string", "parent_id": { @@ -11544,6 +11561,23 @@ Only certain features set these fields: - FeatureManagedAgentLimit - FeatureAgen ], "logs_length": 0, "logs_overflowed": true, + "metadata": [ + { + "description": { + "display_name": "string", + "interval": 0, + "key": "string", + "script": "string", + "timeout": 0 + }, + "result": { + "age": 0, + "collected_at": "2019-08-24T14:15:22Z", + "error": "string", + "value": "string" + } + } + ], "name": "string", "operating_system": "string", "parent_id": { @@ -15243,6 +15277,23 @@ If the schedule is empty, the user will be updated to use the default schedule.| ], "logs_length": 0, "logs_overflowed": true, + "metadata": [ + { + "description": { + "display_name": "string", + "interval": 0, + "key": "string", + "script": "string", + "timeout": 0 + }, + "result": { + "age": 0, + "collected_at": "2019-08-24T14:15:22Z", + "error": "string", + "value": "string" + } + } + ], "name": "string", "operating_system": "string", "parent_id": { @@ -15528,6 +15579,23 @@ If the schedule is empty, the user will be updated to use the default schedule.| ], "logs_length": 0, "logs_overflowed": true, + "metadata": [ + { + "description": { + "display_name": "string", + "interval": 0, + "key": "string", + "script": "string", + "timeout": 0 + }, + "result": { + "age": 0, + "collected_at": "2019-08-24T14:15:22Z", + "error": "string", + "value": "string" + } + } + ], "name": "string", "operating_system": "string", "parent_id": { @@ -15590,6 +15658,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `log_sources` | array of [codersdk.WorkspaceAgentLogSource](#codersdkworkspaceagentlogsource) | false | | | | `logs_length` | integer | false | | | | `logs_overflowed` | boolean | false | | | +| `metadata` | array of [codersdk.WorkspaceAgentMetadata](#codersdkworkspaceagentmetadata) | false | | Metadata is only populated on the workspaces list endpoint when the request opts in with the include_agent_metadata search key, and it only carries the requested keys. | | `name` | string | false | | | | `operating_system` | string | false | | | | `parent_id` | [uuid.NullUUID](#uuidnulluuid) | false | | | @@ -16005,6 +16074,75 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `id` | string | false | | | | `workspace_agent_id` | string | false | | | +## codersdk.WorkspaceAgentMetadata + +```json +{ + "description": { + "display_name": "string", + "interval": 0, + "key": "string", + "script": "string", + "timeout": 0 + }, + "result": { + "age": 0, + "collected_at": "2019-08-24T14:15:22Z", + "error": "string", + "value": "string" + } +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|---------------|------------------------------------------------------------------------------------------|----------|--------------|-------------| +| `description` | [codersdk.WorkspaceAgentMetadataDescription](#codersdkworkspaceagentmetadatadescription) | false | | | +| `result` | [codersdk.WorkspaceAgentMetadataResult](#codersdkworkspaceagentmetadataresult) | false | | | + +## codersdk.WorkspaceAgentMetadataDescription + +```json +{ + "display_name": "string", + "interval": 0, + "key": "string", + "script": "string", + "timeout": 0 +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|----------------|---------|----------|--------------|-------------| +| `display_name` | string | false | | | +| `interval` | integer | false | | | +| `key` | string | false | | | +| `script` | string | false | | | +| `timeout` | integer | false | | | + +## codersdk.WorkspaceAgentMetadataResult + +```json +{ + "age": 0, + "collected_at": "2019-08-24T14:15:22Z", + "error": "string", + "value": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|----------------|---------|----------|--------------|-----------------------------------------------------------------------------------------------------------------------------------------| +| `age` | integer | false | | Age is the number of seconds since the metadata was collected. It is provided in addition to CollectedAt to protect against clock skew. | +| `collected_at` | string | false | | | +| `error` | string | false | | | +| `value` | string | false | | | + ## codersdk.WorkspaceAgentPortShare ```json @@ -16488,6 +16626,23 @@ If the schedule is empty, the user will be updated to use the default schedule.| ], "logs_length": 0, "logs_overflowed": true, + "metadata": [ + { + "description": { + "display_name": "string", + "interval": 0, + "key": "string", + "script": "string", + "timeout": 0 + }, + "result": { + "age": 0, + "collected_at": "2019-08-24T14:15:22Z", + "error": "string", + "value": "string" + } + } + ], "name": "string", "operating_system": "string", "parent_id": { @@ -16958,6 +17113,23 @@ If the schedule is empty, the user will be updated to use the default schedule.| ], "logs_length": 0, "logs_overflowed": true, + "metadata": [ + { + "description": { + "display_name": "string", + "interval": 0, + "key": "string", + "script": "string", + "timeout": 0 + }, + "result": { + "age": 0, + "collected_at": "2019-08-24T14:15:22Z", + "error": "string", + "value": "string" + } + } + ], "name": "string", "operating_system": "string", "parent_id": { @@ -17308,6 +17480,12 @@ If the schedule is empty, the user will be updated to use the default schedule.| ], "logs_length": 0, "logs_overflowed": true, + "metadata": [ + { + "description": {}, + "result": {} + } + ], "name": "string", "operating_system": "string", "parent_id": { diff --git a/docs/reference/api/tasks.md b/docs/reference/api/tasks.md index a50b4b3cbec49..5de76291cef5a 100644 --- a/docs/reference/api/tasks.md +++ b/docs/reference/api/tasks.md @@ -535,6 +535,23 @@ curl -X POST http://coder-server:8080/api/v2/tasks/{user}/{task}/pause \ ], "logs_length": 0, "logs_overflowed": true, + "metadata": [ + { + "description": { + "display_name": "string", + "interval": 0, + "key": "string", + "script": "string", + "timeout": 0 + }, + "result": { + "age": 0, + "collected_at": "2019-08-24T14:15:22Z", + "error": "string", + "value": "string" + } + } + ], "name": "string", "operating_system": "string", "parent_id": { @@ -781,6 +798,23 @@ curl -X POST http://coder-server:8080/api/v2/tasks/{user}/{task}/resume \ ], "logs_length": 0, "logs_overflowed": true, + "metadata": [ + { + "description": { + "display_name": "string", + "interval": 0, + "key": "string", + "script": "string", + "timeout": 0 + }, + "result": { + "age": 0, + "collected_at": "2019-08-24T14:15:22Z", + "error": "string", + "value": "string" + } + } + ], "name": "string", "operating_system": "string", "parent_id": { diff --git a/docs/reference/api/templates.md b/docs/reference/api/templates.md index 668b9ce8eea18..754870c491a25 100644 --- a/docs/reference/api/templates.md +++ b/docs/reference/api/templates.md @@ -2502,6 +2502,23 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/d ], "logs_length": 0, "logs_overflowed": true, + "metadata": [ + { + "description": { + "display_name": "string", + "interval": 0, + "key": "string", + "script": "string", + "timeout": 0 + }, + "result": { + "age": 0, + "collected_at": "2019-08-24T14:15:22Z", + "error": "string", + "value": "string" + } + } + ], "name": "string", "operating_system": "string", "parent_id": { @@ -2632,6 +2649,18 @@ Status Code **200** | `»»» workspace_agent_id` | string(uuid) | false | | | | `»» logs_length` | integer | false | | | | `»» logs_overflowed` | boolean | false | | | +| `»» metadata` | array | false | | Metadata is only populated on the workspaces list endpoint when the request opts in with the include_agent_metadata search key, and it only carries the requested keys. | +| `»»» description` | [codersdk.WorkspaceAgentMetadataDescription](schemas.md#codersdkworkspaceagentmetadatadescription) | false | | | +| `»»»» display_name` | string | false | | | +| `»»»» interval` | integer | false | | | +| `»»»» key` | string | false | | | +| `»»»» script` | string | false | | | +| `»»»» timeout` | integer | false | | | +| `»»» result` | [codersdk.WorkspaceAgentMetadataResult](schemas.md#codersdkworkspaceagentmetadataresult) | false | | | +| `»»»» age` | integer | false | | Age is the number of seconds since the metadata was collected. It is provided in addition to CollectedAt to protect against clock skew. | +| `»»»» collected_at` | string(date-time) | false | | | +| `»»»» error` | string | false | | | +| `»»»» value` | string | false | | | | `»» name` | string | false | | | | `»» operating_system` | string | false | | | | `»» parent_id` | [uuid.NullUUID](schemas.md#uuidnulluuid) | false | | | @@ -3173,6 +3202,23 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/r ], "logs_length": 0, "logs_overflowed": true, + "metadata": [ + { + "description": { + "display_name": "string", + "interval": 0, + "key": "string", + "script": "string", + "timeout": 0 + }, + "result": { + "age": 0, + "collected_at": "2019-08-24T14:15:22Z", + "error": "string", + "value": "string" + } + } + ], "name": "string", "operating_system": "string", "parent_id": { @@ -3303,6 +3349,18 @@ Status Code **200** | `»»» workspace_agent_id` | string(uuid) | false | | | | `»» logs_length` | integer | false | | | | `»» logs_overflowed` | boolean | false | | | +| `»» metadata` | array | false | | Metadata is only populated on the workspaces list endpoint when the request opts in with the include_agent_metadata search key, and it only carries the requested keys. | +| `»»» description` | [codersdk.WorkspaceAgentMetadataDescription](schemas.md#codersdkworkspaceagentmetadatadescription) | false | | | +| `»»»» display_name` | string | false | | | +| `»»»» interval` | integer | false | | | +| `»»»» key` | string | false | | | +| `»»»» script` | string | false | | | +| `»»»» timeout` | integer | false | | | +| `»»» result` | [codersdk.WorkspaceAgentMetadataResult](schemas.md#codersdkworkspaceagentmetadataresult) | false | | | +| `»»»» age` | integer | false | | Age is the number of seconds since the metadata was collected. It is provided in addition to CollectedAt to protect against clock skew. | +| `»»»» collected_at` | string(date-time) | false | | | +| `»»»» error` | string | false | | | +| `»»»» value` | string | false | | | | `»» name` | string | false | | | | `»» operating_system` | string | false | | | | `»» parent_id` | [uuid.NullUUID](schemas.md#uuidnulluuid) | false | | | diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md index 80e7959ff3d2e..5f163082873c2 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -225,6 +225,23 @@ of the template will be used. ], "logs_length": 0, "logs_overflowed": true, + "metadata": [ + { + "description": { + "display_name": "string", + "interval": 0, + "key": "string", + "script": "string", + "timeout": 0 + }, + "result": { + "age": 0, + "collected_at": "2019-08-24T14:15:22Z", + "error": "string", + "value": "string" + } + } + ], "name": "string", "operating_system": "string", "parent_id": { @@ -591,6 +608,23 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam ], "logs_length": 0, "logs_overflowed": true, + "metadata": [ + { + "description": { + "display_name": "string", + "interval": 0, + "key": "string", + "script": "string", + "timeout": 0 + }, + "result": { + "age": 0, + "collected_at": "2019-08-24T14:15:22Z", + "error": "string", + "value": "string" + } + } + ], "name": "string", "operating_system": "string", "parent_id": { @@ -924,6 +958,23 @@ of the template will be used. ], "logs_length": 0, "logs_overflowed": true, + "metadata": [ + { + "description": { + "display_name": "string", + "interval": 0, + "key": "string", + "script": "string", + "timeout": 0 + }, + "result": { + "age": 0, + "collected_at": "2019-08-24T14:15:22Z", + "error": "string", + "value": "string" + } + } + ], "name": "string", "operating_system": "string", "parent_id": { @@ -1218,6 +1269,12 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \ ], "logs_length": 0, "logs_overflowed": true, + "metadata": [ + { + "description": {}, + "result": {} + } + ], "name": "string", "operating_system": "string", "parent_id": { @@ -1527,6 +1584,23 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \ ], "logs_length": 0, "logs_overflowed": true, + "metadata": [ + { + "description": { + "display_name": "string", + "interval": 0, + "key": "string", + "script": "string", + "timeout": 0 + }, + "result": { + "age": 0, + "collected_at": "2019-08-24T14:15:22Z", + "error": "string", + "value": "string" + } + } + ], "name": "string", "operating_system": "string", "parent_id": { @@ -2146,6 +2220,23 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \ ], "logs_length": 0, "logs_overflowed": true, + "metadata": [ + { + "description": { + "display_name": "string", + "interval": 0, + "key": "string", + "script": "string", + "timeout": 0 + }, + "result": { + "age": 0, + "collected_at": "2019-08-24T14:15:22Z", + "error": "string", + "value": "string" + } + } + ], "name": "string", "operating_system": "string", "parent_id": { diff --git a/docs/user-guides/workspace-management.md b/docs/user-guides/workspace-management.md index 57a5f4ee502ba..013b0a29ab808 100644 --- a/docs/user-guides/workspace-management.md +++ b/docs/user-guides/workspace-management.md @@ -69,6 +69,10 @@ The following filters are supported: `connecting|connected|timeout|disconnected`, e.g, `has-agent:connecting` - `id` - Workspace UUID - `healthy` - Only applicable for workspaces in "start" transition. `healthy:false` is an alias for `has-agent:timeout,disconnected`, `healthy:true` is an alias for `has-agent:connected`. +- `include_agent_metadata` - Not a filter: expands each agent in the API + response with the named agent metadata keys, e.g, + `include_agent_metadata:cpu_usage`. Repeat the key to request multiple + metadata items. Keys match case-insensitively. ## Updating workspaces diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index ba98c91351f5d..083d1628b9857 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -10661,6 +10661,12 @@ export interface WorkspaceAgent { readonly display_apps: readonly DisplayApp[]; readonly log_sources: readonly WorkspaceAgentLogSource[]; readonly scripts: readonly WorkspaceAgentScript[]; + /** + * Metadata is only populated on the workspaces list endpoint when the + * request opts in with the include_agent_metadata search key, and it + * only carries the requested keys. + */ + readonly metadata?: readonly WorkspaceAgentMetadata[]; /** * StartupScriptBehavior is a legacy field that is deprecated in favor * of the `coder_script` resource. It's only referenced by old clients. diff --git a/site/src/pages/AgentsPage/AgentChatPage.tsx b/site/src/pages/AgentsPage/AgentChatPage.tsx index 27d2ff021d8fb..05c79d5ac2b2a 100644 --- a/site/src/pages/AgentsPage/AgentChatPage.tsx +++ b/site/src/pages/AgentsPage/AgentChatPage.tsx @@ -811,6 +811,7 @@ type _UncoveredAgentFields = Omit< | "display_apps" | "log_sources" | "scripts" + | "metadata" | "startup_script_behavior" >; // If this errors, a new field was added to WorkspaceAgent. From d9b065a249db31174ee6a7a5037b5d9e800e28e6 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 6 Aug 2026 22:00:12 +0000 Subject: [PATCH 2/7] fix(coderd): omit the collection script from list-embedded agent metadata The script is the collection command, not collected state; it can be long and list consumers want values. The description's script field is always empty on the workspaces list endpoint. --- coderd/apidoc/docs.go | 2 +- coderd/apidoc/swagger.json | 2 +- coderd/database/queries.sql.go | 3 +- coderd/database/queries/workspaces.sql | 3 +- coderd/workspaces_test.go | 3 + codersdk/workspaceagents.go | 3 +- docs/reference/api/builds.md | 590 ++++++++++++------------- docs/reference/api/schemas.md | 76 ++-- docs/reference/api/templates.md | 468 ++++++++++---------- site/src/api/typesGenerated.ts | 3 +- 10 files changed, 580 insertions(+), 573 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 602f7d7784de2..25d6b51be261b 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -27255,7 +27255,7 @@ const docTemplate = `{ "type": "boolean" }, "metadata": { - "description": "Metadata is only populated on the workspaces list endpoint when the\nrequest opts in with the include_agent_metadata search key, and it\nonly carries the requested keys.", + "description": "Metadata is only populated on the workspaces list endpoint when the\nrequest opts in with the include_agent_metadata search key, and it\nonly carries the requested keys. The description's script is always\nempty here: it can be long, and list consumers want values.", "type": "array", "items": { "$ref": "#/definitions/codersdk.WorkspaceAgentMetadata" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 10f5d29ee60d9..0a8aee377ef70 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -25097,7 +25097,7 @@ "type": "boolean" }, "metadata": { - "description": "Metadata is only populated on the workspaces list endpoint when the\nrequest opts in with the include_agent_metadata search key, and it\nonly carries the requested keys.", + "description": "Metadata is only populated on the workspaces list endpoint when the\nrequest opts in with the include_agent_metadata search key, and it\nonly carries the requested keys. The description's script is always\nempty here: it can be long, and list consumers want values.", "type": "array", "items": { "$ref": "#/definitions/codersdk.WorkspaceAgentMetadata" diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 8a98a492a36d4..3672469fcd410 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -38893,7 +38893,8 @@ SELECT 'workspace_agent_id', workspace_agents.id, 'display_name', workspace_agent_metadata.display_name, 'key', workspace_agent_metadata.key, - 'script', workspace_agent_metadata.script, + -- script is deliberately omitted: it can be long and + -- list consumers want values, not collection commands. 'value', workspace_agent_metadata.value, 'error', workspace_agent_metadata.error, 'timeout', workspace_agent_metadata.timeout, diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index cdcf54fd629ea..b8705277c87e8 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -487,7 +487,8 @@ SELECT 'workspace_agent_id', workspace_agents.id, 'display_name', workspace_agent_metadata.display_name, 'key', workspace_agent_metadata.key, - 'script', workspace_agent_metadata.script, + -- script is deliberately omitted: it can be long and + -- list consumers want values, not collection commands. 'value', workspace_agent_metadata.value, 'error', workspace_agent_metadata.error, 'timeout', workspace_agent_metadata.timeout, diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index e81e537b748fc..37b6bc8bcafaf 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -2933,6 +2933,9 @@ func TestWorkspaceFilterManual(t *testing.T) { require.Len(t, metadata, 2) require.Equal(t, "cpu", metadata[0].Description.Key) require.Equal(t, "value-cpu", metadata[0].Result.Value) + // The collection script is deliberately not exposed on the list + // endpoint; it can be long. + require.Empty(t, metadata[0].Description.Script) require.Equal(t, "byoc_status", metadata[1].Description.Key) require.Equal(t, "value-byoc_status", metadata[1].Result.Value) require.WithinDuration(t, collectedAt, metadata[1].Result.CollectedAt, time.Second) diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index 1ceb4b41778f6..653919c2fc540 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -172,7 +172,8 @@ type WorkspaceAgent struct { Scripts []WorkspaceAgentScript `json:"scripts"` // Metadata is only populated on the workspaces list endpoint when the // request opts in with the include_agent_metadata search key, and it - // only carries the requested keys. + // only carries the requested keys. The description's script is always + // empty here: it can be long, and list consumers want values. Metadata []WorkspaceAgentMetadata `json:"metadata,omitempty"` // StartupScriptBehavior is a legacy field that is deprecated in favor diff --git a/docs/reference/api/builds.md b/docs/reference/api/builds.md index 7f618d3269189..bdf1c8683c4ff 100644 --- a/docs/reference/api/builds.md +++ b/docs/reference/api/builds.md @@ -890,123 +890,123 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/res Status Code **200** -| Name | Type | Required | Restrictions | Description | -|---------------------------------|--------------------------------------------------------------------------------------------------------|----------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `[array item]` | array | false | | | -| `» agents` | array | false | | | -| `»» api_version` | string | false | | | -| `»» apps` | array | false | | | -| `»»» command` | string | false | | | -| `»»» display_name` | string | false | | Display name is a friendly name for the app. | -| `»»» external` | boolean | false | | External specifies whether the URL should be opened externally on the client or not. | -| `»»» group` | string | false | | | -| `»»» health` | [codersdk.WorkspaceAppHealth](schemas.md#codersdkworkspaceapphealth) | false | | | -| `»»» healthcheck` | [codersdk.Healthcheck](schemas.md#codersdkhealthcheck) | false | | Healthcheck specifies the configuration for checking app health. | -| `»»»» interval` | integer | false | | Interval specifies the seconds between each health check. | -| `»»»» threshold` | integer | false | | Threshold specifies the number of consecutive failed health checks before returning "unhealthy". | -| `»»»» url` | string | false | | URL specifies the endpoint to check for the app health. | -| `»»» hidden` | boolean | false | | | -| `»»» icon` | string | false | | Icon is a relative path or external URL that specifies an icon to be displayed in the dashboard. | -| `»»» id` | string(uuid) | false | | | -| `»»» open_in` | [codersdk.WorkspaceAppOpenIn](schemas.md#codersdkworkspaceappopenin) | false | | | -| `»»» sharing_level` | [codersdk.WorkspaceAppSharingLevel](schemas.md#codersdkworkspaceappsharinglevel) | false | | | -| `»»» slug` | string | false | | Slug is a unique identifier within the agent. | -| `»»» statuses` | array | false | | Statuses is a list of statuses for the app. | -| `»»»» agent_id` | string(uuid) | false | | | -| `»»»» app_id` | string(uuid) | false | | | -| `»»»» created_at` | string(date-time) | false | | | -| `»»»» icon` | string | false | | Deprecated: This field is unused and will be removed in a future version. Icon is an external URL to an icon that will be rendered in the UI. | -| `»»»» id` | string(uuid) | false | | | -| `»»»» message` | string | false | | | -| `»»»» needs_user_attention` | boolean | false | | Deprecated: This field is unused and will be removed in a future version. NeedsUserAttention specifies whether the status needs user attention. | -| `»»»» state` | [codersdk.WorkspaceAppStatusState](schemas.md#codersdkworkspaceappstatusstate) | false | | | -| `»»»» uri` | string | false | | Uri is the URI of the resource that the status is for. e.g. https://github.com/org/repo/pull/123 e.g. file:///path/to/file | -| `»»»» workspace_id` | string(uuid) | false | | | -| `»»» subdomain` | boolean | false | | Subdomain denotes whether the app should be accessed via a path on the `coder server` or via a hostname-based dev URL. If this is set to true and there is no app wildcard configured on the server, the app will not be accessible in the UI. | -| `»»» subdomain_name` | string | false | | Subdomain name is the application domain exposed on the `coder server`. | -| `»»» tooltip` | string | false | | Tooltip is an optional markdown supported field that is displayed when hovering over workspace apps in the UI. | -| `»»» url` | string | false | | URL is the address being proxied to inside the workspace. If external is specified, this will be opened on the client. | -| `»» architecture` | string | false | | | -| `»» connection_timeout_seconds` | integer | false | | | -| `»» created_at` | string(date-time) | false | | | -| `»» directory` | string | false | | | -| `»» disconnected_at` | string(date-time) | false | | | -| `»» display_apps` | array | false | | | -| `»» environment_variables` | object | false | | | -| `»»» [any property]` | string | false | | | -| `»» expanded_directory` | string | false | | | -| `»» first_connected_at` | string(date-time) | false | | | -| `»» health` | [codersdk.WorkspaceAgentHealth](schemas.md#codersdkworkspaceagenthealth) | false | | Health reports the health of the agent. | -| `»»» healthy` | boolean | false | | Healthy is true if the agent is healthy. | -| `»»» reason` | string | false | | Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true. | -| `»» id` | string(uuid) | false | | | -| `»» instance_id` | string | false | | | -| `»» last_connected_at` | string(date-time) | false | | | -| `»» latency` | object | false | | Latency is mapped by region name (e.g. "New York City", "Seattle"). | -| `»»» [any property]` | [codersdk.DERPRegion](schemas.md#codersdkderpregion) | false | | | -| `»»»» latency_ms` | number | false | | | -| `»»»» preferred` | boolean | false | | | -| `»» lifecycle_state` | [codersdk.WorkspaceAgentLifecycle](schemas.md#codersdkworkspaceagentlifecycle) | false | | | -| `»» log_sources` | array | false | | | -| `»»» created_at` | string(date-time) | false | | | -| `»»» display_name` | string | false | | | -| `»»» icon` | string | false | | | -| `»»» id` | string(uuid) | false | | | -| `»»» workspace_agent_id` | string(uuid) | false | | | -| `»» logs_length` | integer | false | | | -| `»» logs_overflowed` | boolean | false | | | -| `»» metadata` | array | false | | Metadata is only populated on the workspaces list endpoint when the request opts in with the include_agent_metadata search key, and it only carries the requested keys. | -| `»»» description` | [codersdk.WorkspaceAgentMetadataDescription](schemas.md#codersdkworkspaceagentmetadatadescription) | false | | | -| `»»»» display_name` | string | false | | | -| `»»»» interval` | integer | false | | | -| `»»»» key` | string | false | | | -| `»»»» script` | string | false | | | -| `»»»» timeout` | integer | false | | | -| `»»» result` | [codersdk.WorkspaceAgentMetadataResult](schemas.md#codersdkworkspaceagentmetadataresult) | false | | | -| `»»»» age` | integer | false | | Age is the number of seconds since the metadata was collected. It is provided in addition to CollectedAt to protect against clock skew. | -| `»»»» collected_at` | string(date-time) | false | | | -| `»»»» error` | string | false | | | -| `»»»» value` | string | false | | | -| `»» name` | string | false | | | -| `»» operating_system` | string | false | | | -| `»» parent_id` | [uuid.NullUUID](schemas.md#uuidnulluuid) | false | | | -| `»»» uuid` | string | false | | | -| `»»» valid` | boolean | false | | Valid is true if UUID is not NULL | -| `»» ready_at` | string(date-time) | false | | | -| `»» resource_id` | string(uuid) | false | | | -| `»» scripts` | array | false | | | -| `»»» cron` | string | false | | | -| `»»» display_name` | string | false | | | -| `»»» exit_code` | integer | false | | | -| `»»» id` | string(uuid) | false | | | -| `»»» log_path` | string | false | | | -| `»»» log_source_id` | string(uuid) | false | | | -| `»»» run_on_start` | boolean | false | | | -| `»»» run_on_stop` | boolean | false | | | -| `»»» script` | string | false | | | -| `»»» start_blocks_login` | boolean | false | | | -| `»»» status` | [codersdk.WorkspaceAgentScriptStatus](schemas.md#codersdkworkspaceagentscriptstatus) | false | | | -| `»»» timeout` | integer | false | | | -| `»» started_at` | string(date-time) | false | | | -| `»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | false | | Startup script behavior is a legacy field that is deprecated in favor of the `coder_script` resource. It's only referenced by old clients. Deprecated: Remove in the future! | -| `»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | | -| `»» subsystems` | array | false | | | -| `»» troubleshooting_url` | string | false | | | -| `»» updated_at` | string(date-time) | false | | | -| `»» version` | string | false | | | -| `» created_at` | string(date-time) | false | | | -| `» daily_cost` | integer | false | | | -| `» hide` | boolean | false | | | -| `» icon` | string | false | | | -| `» id` | string(uuid) | false | | | -| `» job_id` | string(uuid) | false | | | -| `» metadata` | array | false | | | -| `»» key` | string | false | | | -| `»» sensitive` | boolean | false | | | -| `»» value` | string | false | | | -| `» name` | string | false | | | -| `» type` | string | false | | | -| `» workspace_transition` | [codersdk.WorkspaceTransition](schemas.md#codersdkworkspacetransition) | false | | | +| Name | Type | Required | Restrictions | Description | +|---------------------------------|--------------------------------------------------------------------------------------------------------|----------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `[array item]` | array | false | | | +| `» agents` | array | false | | | +| `»» api_version` | string | false | | | +| `»» apps` | array | false | | | +| `»»» command` | string | false | | | +| `»»» display_name` | string | false | | Display name is a friendly name for the app. | +| `»»» external` | boolean | false | | External specifies whether the URL should be opened externally on the client or not. | +| `»»» group` | string | false | | | +| `»»» health` | [codersdk.WorkspaceAppHealth](schemas.md#codersdkworkspaceapphealth) | false | | | +| `»»» healthcheck` | [codersdk.Healthcheck](schemas.md#codersdkhealthcheck) | false | | Healthcheck specifies the configuration for checking app health. | +| `»»»» interval` | integer | false | | Interval specifies the seconds between each health check. | +| `»»»» threshold` | integer | false | | Threshold specifies the number of consecutive failed health checks before returning "unhealthy". | +| `»»»» url` | string | false | | URL specifies the endpoint to check for the app health. | +| `»»» hidden` | boolean | false | | | +| `»»» icon` | string | false | | Icon is a relative path or external URL that specifies an icon to be displayed in the dashboard. | +| `»»» id` | string(uuid) | false | | | +| `»»» open_in` | [codersdk.WorkspaceAppOpenIn](schemas.md#codersdkworkspaceappopenin) | false | | | +| `»»» sharing_level` | [codersdk.WorkspaceAppSharingLevel](schemas.md#codersdkworkspaceappsharinglevel) | false | | | +| `»»» slug` | string | false | | Slug is a unique identifier within the agent. | +| `»»» statuses` | array | false | | Statuses is a list of statuses for the app. | +| `»»»» agent_id` | string(uuid) | false | | | +| `»»»» app_id` | string(uuid) | false | | | +| `»»»» created_at` | string(date-time) | false | | | +| `»»»» icon` | string | false | | Deprecated: This field is unused and will be removed in a future version. Icon is an external URL to an icon that will be rendered in the UI. | +| `»»»» id` | string(uuid) | false | | | +| `»»»» message` | string | false | | | +| `»»»» needs_user_attention` | boolean | false | | Deprecated: This field is unused and will be removed in a future version. NeedsUserAttention specifies whether the status needs user attention. | +| `»»»» state` | [codersdk.WorkspaceAppStatusState](schemas.md#codersdkworkspaceappstatusstate) | false | | | +| `»»»» uri` | string | false | | Uri is the URI of the resource that the status is for. e.g. https://github.com/org/repo/pull/123 e.g. file:///path/to/file | +| `»»»» workspace_id` | string(uuid) | false | | | +| `»»» subdomain` | boolean | false | | Subdomain denotes whether the app should be accessed via a path on the `coder server` or via a hostname-based dev URL. If this is set to true and there is no app wildcard configured on the server, the app will not be accessible in the UI. | +| `»»» subdomain_name` | string | false | | Subdomain name is the application domain exposed on the `coder server`. | +| `»»» tooltip` | string | false | | Tooltip is an optional markdown supported field that is displayed when hovering over workspace apps in the UI. | +| `»»» url` | string | false | | URL is the address being proxied to inside the workspace. If external is specified, this will be opened on the client. | +| `»» architecture` | string | false | | | +| `»» connection_timeout_seconds` | integer | false | | | +| `»» created_at` | string(date-time) | false | | | +| `»» directory` | string | false | | | +| `»» disconnected_at` | string(date-time) | false | | | +| `»» display_apps` | array | false | | | +| `»» environment_variables` | object | false | | | +| `»»» [any property]` | string | false | | | +| `»» expanded_directory` | string | false | | | +| `»» first_connected_at` | string(date-time) | false | | | +| `»» health` | [codersdk.WorkspaceAgentHealth](schemas.md#codersdkworkspaceagenthealth) | false | | Health reports the health of the agent. | +| `»»» healthy` | boolean | false | | Healthy is true if the agent is healthy. | +| `»»» reason` | string | false | | Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true. | +| `»» id` | string(uuid) | false | | | +| `»» instance_id` | string | false | | | +| `»» last_connected_at` | string(date-time) | false | | | +| `»» latency` | object | false | | Latency is mapped by region name (e.g. "New York City", "Seattle"). | +| `»»» [any property]` | [codersdk.DERPRegion](schemas.md#codersdkderpregion) | false | | | +| `»»»» latency_ms` | number | false | | | +| `»»»» preferred` | boolean | false | | | +| `»» lifecycle_state` | [codersdk.WorkspaceAgentLifecycle](schemas.md#codersdkworkspaceagentlifecycle) | false | | | +| `»» log_sources` | array | false | | | +| `»»» created_at` | string(date-time) | false | | | +| `»»» display_name` | string | false | | | +| `»»» icon` | string | false | | | +| `»»» id` | string(uuid) | false | | | +| `»»» workspace_agent_id` | string(uuid) | false | | | +| `»» logs_length` | integer | false | | | +| `»» logs_overflowed` | boolean | false | | | +| `»» metadata` | array | false | | Metadata is only populated on the workspaces list endpoint when the request opts in with the include_agent_metadata search key, and it only carries the requested keys. The description's script is always empty here: it can be long, and list consumers want values. | +| `»»» description` | [codersdk.WorkspaceAgentMetadataDescription](schemas.md#codersdkworkspaceagentmetadatadescription) | false | | | +| `»»»» display_name` | string | false | | | +| `»»»» interval` | integer | false | | | +| `»»»» key` | string | false | | | +| `»»»» script` | string | false | | | +| `»»»» timeout` | integer | false | | | +| `»»» result` | [codersdk.WorkspaceAgentMetadataResult](schemas.md#codersdkworkspaceagentmetadataresult) | false | | | +| `»»»» age` | integer | false | | Age is the number of seconds since the metadata was collected. It is provided in addition to CollectedAt to protect against clock skew. | +| `»»»» collected_at` | string(date-time) | false | | | +| `»»»» error` | string | false | | | +| `»»»» value` | string | false | | | +| `»» name` | string | false | | | +| `»» operating_system` | string | false | | | +| `»» parent_id` | [uuid.NullUUID](schemas.md#uuidnulluuid) | false | | | +| `»»» uuid` | string | false | | | +| `»»» valid` | boolean | false | | Valid is true if UUID is not NULL | +| `»» ready_at` | string(date-time) | false | | | +| `»» resource_id` | string(uuid) | false | | | +| `»» scripts` | array | false | | | +| `»»» cron` | string | false | | | +| `»»» display_name` | string | false | | | +| `»»» exit_code` | integer | false | | | +| `»»» id` | string(uuid) | false | | | +| `»»» log_path` | string | false | | | +| `»»» log_source_id` | string(uuid) | false | | | +| `»»» run_on_start` | boolean | false | | | +| `»»» run_on_stop` | boolean | false | | | +| `»»» script` | string | false | | | +| `»»» start_blocks_login` | boolean | false | | | +| `»»» status` | [codersdk.WorkspaceAgentScriptStatus](schemas.md#codersdkworkspaceagentscriptstatus) | false | | | +| `»»» timeout` | integer | false | | | +| `»» started_at` | string(date-time) | false | | | +| `»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | false | | Startup script behavior is a legacy field that is deprecated in favor of the `coder_script` resource. It's only referenced by old clients. Deprecated: Remove in the future! | +| `»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | | +| `»» subsystems` | array | false | | | +| `»» troubleshooting_url` | string | false | | | +| `»» updated_at` | string(date-time) | false | | | +| `»» version` | string | false | | | +| `» created_at` | string(date-time) | false | | | +| `» daily_cost` | integer | false | | | +| `» hide` | boolean | false | | | +| `» icon` | string | false | | | +| `» id` | string(uuid) | false | | | +| `» job_id` | string(uuid) | false | | | +| `» metadata` | array | false | | | +| `»» key` | string | false | | | +| `»» sensitive` | boolean | false | | | +| `»» value` | string | false | | | +| `» name` | string | false | | | +| `» type` | string | false | | | +| `» workspace_transition` | [codersdk.WorkspaceTransition](schemas.md#codersdkworkspacetransition) | false | | | #### Enumerated Values @@ -1657,184 +1657,184 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ Status Code **200** -| Name | Type | Required | Restrictions | Description | -|----------------------------------|--------------------------------------------------------------------------------------------------------|----------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `[array item]` | array | false | | | -| `» build_number` | integer | false | | | -| `» created_at` | string(date-time) | false | | | -| `» daily_cost` | integer | false | | | -| `» deadline` | string(date-time) | false | | | -| `» has_ai_task` | boolean | false | | Deprecated: This field has been deprecated in favor of Task WorkspaceID. | -| `» has_external_agent` | boolean | false | | | -| `» id` | string(uuid) | false | | | -| `» initiator_id` | string(uuid) | false | | | -| `» initiator_name` | string | false | | | -| `» job` | [codersdk.ProvisionerJob](schemas.md#codersdkprovisionerjob) | false | | | -| `»» available_workers` | array | false | | | -| `»» canceled_at` | string(date-time) | false | | | -| `»» completed_at` | string(date-time) | false | | | -| `»» created_at` | string(date-time) | false | | | -| `»» error` | string | false | | | -| `»» error_code` | [codersdk.JobErrorCode](schemas.md#codersdkjoberrorcode) | false | | | -| `»» file_id` | string(uuid) | false | | | -| `»» id` | string(uuid) | false | | | -| `»» initiator_id` | string(uuid) | false | | | -| `»» input` | [codersdk.ProvisionerJobInput](schemas.md#codersdkprovisionerjobinput) | false | | | -| `»»» error` | string | false | | | -| `»»» template_version_id` | string(uuid) | false | | | -| `»»» workspace_build_id` | string(uuid) | false | | | -| `»» logs_overflowed` | boolean | false | | | -| `»» metadata` | [codersdk.ProvisionerJobMetadata](schemas.md#codersdkprovisionerjobmetadata) | false | | | -| `»»» template_display_name` | string | false | | | -| `»»» template_icon` | string | false | | | -| `»»» template_id` | string(uuid) | false | | | -| `»»» template_name` | string | false | | | -| `»»» template_version_name` | string | false | | | -| `»»» workspace_build_transition` | [codersdk.WorkspaceTransition](schemas.md#codersdkworkspacetransition) | false | | | -| `»»» workspace_id` | string(uuid) | false | | | -| `»»» workspace_name` | string | false | | | -| `»» organization_id` | string(uuid) | false | | | -| `»» queue_position` | integer | false | | | -| `»» queue_size` | integer | false | | | -| `»» started_at` | string(date-time) | false | | | -| `»» status` | [codersdk.ProvisionerJobStatus](schemas.md#codersdkprovisionerjobstatus) | false | | | -| `»» tags` | object | false | | | -| `»»» [any property]` | string | false | | | -| `»» type` | [codersdk.ProvisionerJobType](schemas.md#codersdkprovisionerjobtype) | false | | | -| `»» worker_id` | string(uuid) | false | | | -| `»» worker_name` | string | false | | | -| `» matched_provisioners` | [codersdk.MatchedProvisioners](schemas.md#codersdkmatchedprovisioners) | false | | | -| `»» available` | integer | false | | Available is the number of provisioner daemons that are available to take jobs. This may be less than the count if some provisioners are busy or have been stopped. | -| `»» count` | integer | false | | Count is the number of provisioner daemons that matched the given tags. If the count is 0, it means no provisioner daemons matched the requested tags. | -| `»» most_recently_seen` | string(date-time) | false | | Most recently seen is the most recently seen time of the set of matched provisioners. If no provisioners matched, this field will be null. | -| `» max_deadline` | string(date-time) | false | | | -| `» reason` | [codersdk.BuildReason](schemas.md#codersdkbuildreason) | false | | | -| `» resources` | array | false | | | -| `»» agents` | array | false | | | -| `»»» api_version` | string | false | | | -| `»»» apps` | array | false | | | -| `»»»» command` | string | false | | | -| `»»»» display_name` | string | false | | Display name is a friendly name for the app. | -| `»»»» external` | boolean | false | | External specifies whether the URL should be opened externally on the client or not. | -| `»»»» group` | string | false | | | -| `»»»» health` | [codersdk.WorkspaceAppHealth](schemas.md#codersdkworkspaceapphealth) | false | | | -| `»»»» healthcheck` | [codersdk.Healthcheck](schemas.md#codersdkhealthcheck) | false | | Healthcheck specifies the configuration for checking app health. | -| `»»»»» interval` | integer | false | | Interval specifies the seconds between each health check. | -| `»»»»» threshold` | integer | false | | Threshold specifies the number of consecutive failed health checks before returning "unhealthy". | -| `»»»»» url` | string | false | | URL specifies the endpoint to check for the app health. | -| `»»»» hidden` | boolean | false | | | -| `»»»» icon` | string | false | | Icon is a relative path or external URL that specifies an icon to be displayed in the dashboard. | -| `»»»» id` | string(uuid) | false | | | -| `»»»» open_in` | [codersdk.WorkspaceAppOpenIn](schemas.md#codersdkworkspaceappopenin) | false | | | -| `»»»» sharing_level` | [codersdk.WorkspaceAppSharingLevel](schemas.md#codersdkworkspaceappsharinglevel) | false | | | -| `»»»» slug` | string | false | | Slug is a unique identifier within the agent. | -| `»»»» statuses` | array | false | | Statuses is a list of statuses for the app. | -| `»»»»» agent_id` | string(uuid) | false | | | -| `»»»»» app_id` | string(uuid) | false | | | -| `»»»»» created_at` | string(date-time) | false | | | -| `»»»»» icon` | string | false | | Deprecated: This field is unused and will be removed in a future version. Icon is an external URL to an icon that will be rendered in the UI. | -| `»»»»» id` | string(uuid) | false | | | -| `»»»»» message` | string | false | | | -| `»»»»» needs_user_attention` | boolean | false | | Deprecated: This field is unused and will be removed in a future version. NeedsUserAttention specifies whether the status needs user attention. | -| `»»»»» state` | [codersdk.WorkspaceAppStatusState](schemas.md#codersdkworkspaceappstatusstate) | false | | | -| `»»»»» uri` | string | false | | Uri is the URI of the resource that the status is for. e.g. https://github.com/org/repo/pull/123 e.g. file:///path/to/file | -| `»»»»» workspace_id` | string(uuid) | false | | | -| `»»»» subdomain` | boolean | false | | Subdomain denotes whether the app should be accessed via a path on the `coder server` or via a hostname-based dev URL. If this is set to true and there is no app wildcard configured on the server, the app will not be accessible in the UI. | -| `»»»» subdomain_name` | string | false | | Subdomain name is the application domain exposed on the `coder server`. | -| `»»»» tooltip` | string | false | | Tooltip is an optional markdown supported field that is displayed when hovering over workspace apps in the UI. | -| `»»»» url` | string | false | | URL is the address being proxied to inside the workspace. If external is specified, this will be opened on the client. | -| `»»» architecture` | string | false | | | -| `»»» connection_timeout_seconds` | integer | false | | | -| `»»» created_at` | string(date-time) | false | | | -| `»»» directory` | string | false | | | -| `»»» disconnected_at` | string(date-time) | false | | | -| `»»» display_apps` | array | false | | | -| `»»» environment_variables` | object | false | | | -| `»»»» [any property]` | string | false | | | -| `»»» expanded_directory` | string | false | | | -| `»»» first_connected_at` | string(date-time) | false | | | -| `»»» health` | [codersdk.WorkspaceAgentHealth](schemas.md#codersdkworkspaceagenthealth) | false | | Health reports the health of the agent. | -| `»»»» healthy` | boolean | false | | Healthy is true if the agent is healthy. | -| `»»»» reason` | string | false | | Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true. | -| `»»» id` | string(uuid) | false | | | -| `»»» instance_id` | string | false | | | -| `»»» last_connected_at` | string(date-time) | false | | | -| `»»» latency` | object | false | | Latency is mapped by region name (e.g. "New York City", "Seattle"). | -| `»»»» [any property]` | [codersdk.DERPRegion](schemas.md#codersdkderpregion) | false | | | -| `»»»»» latency_ms` | number | false | | | -| `»»»»» preferred` | boolean | false | | | -| `»»» lifecycle_state` | [codersdk.WorkspaceAgentLifecycle](schemas.md#codersdkworkspaceagentlifecycle) | false | | | -| `»»» log_sources` | array | false | | | -| `»»»» created_at` | string(date-time) | false | | | -| `»»»» display_name` | string | false | | | -| `»»»» icon` | string | false | | | -| `»»»» id` | string(uuid) | false | | | -| `»»»» workspace_agent_id` | string(uuid) | false | | | -| `»»» logs_length` | integer | false | | | -| `»»» logs_overflowed` | boolean | false | | | -| `»»» metadata` | array | false | | Metadata is only populated on the workspaces list endpoint when the request opts in with the include_agent_metadata search key, and it only carries the requested keys. | -| `»»»» description` | [codersdk.WorkspaceAgentMetadataDescription](schemas.md#codersdkworkspaceagentmetadatadescription) | false | | | -| `»»»»» display_name` | string | false | | | -| `»»»»» interval` | integer | false | | | -| `»»»»» key` | string | false | | | -| `»»»»» script` | string | false | | | -| `»»»»» timeout` | integer | false | | | -| `»»»» result` | [codersdk.WorkspaceAgentMetadataResult](schemas.md#codersdkworkspaceagentmetadataresult) | false | | | -| `»»»»» age` | integer | false | | Age is the number of seconds since the metadata was collected. It is provided in addition to CollectedAt to protect against clock skew. | -| `»»»»» collected_at` | string(date-time) | false | | | -| `»»»»» error` | string | false | | | -| `»»»»» value` | string | false | | | -| `»»» name` | string | false | | | -| `»»» operating_system` | string | false | | | -| `»»» parent_id` | [uuid.NullUUID](schemas.md#uuidnulluuid) | false | | | -| `»»»» uuid` | string | false | | | -| `»»»» valid` | boolean | false | | Valid is true if UUID is not NULL | -| `»»» ready_at` | string(date-time) | false | | | -| `»»» resource_id` | string(uuid) | false | | | -| `»»» scripts` | array | false | | | -| `»»»» cron` | string | false | | | -| `»»»» display_name` | string | false | | | -| `»»»» exit_code` | integer | false | | | -| `»»»» id` | string(uuid) | false | | | -| `»»»» log_path` | string | false | | | -| `»»»» log_source_id` | string(uuid) | false | | | -| `»»»» run_on_start` | boolean | false | | | -| `»»»» run_on_stop` | boolean | false | | | -| `»»»» script` | string | false | | | -| `»»»» start_blocks_login` | boolean | false | | | -| `»»»» status` | [codersdk.WorkspaceAgentScriptStatus](schemas.md#codersdkworkspaceagentscriptstatus) | false | | | -| `»»»» timeout` | integer | false | | | -| `»»» started_at` | string(date-time) | false | | | -| `»»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | false | | Startup script behavior is a legacy field that is deprecated in favor of the `coder_script` resource. It's only referenced by old clients. Deprecated: Remove in the future! | -| `»»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | | -| `»»» subsystems` | array | false | | | -| `»»» troubleshooting_url` | string | false | | | -| `»»» updated_at` | string(date-time) | false | | | -| `»»» version` | string | false | | | -| `»» created_at` | string(date-time) | false | | | -| `»» daily_cost` | integer | false | | | -| `»» hide` | boolean | false | | | -| `»» icon` | string | false | | | -| `»» id` | string(uuid) | false | | | -| `»» job_id` | string(uuid) | false | | | -| `»» metadata` | array | false | | | -| `»»» key` | string | false | | | -| `»»» sensitive` | boolean | false | | | -| `»»» value` | string | false | | | -| `»» name` | string | false | | | -| `»» type` | string | false | | | -| `»» workspace_transition` | [codersdk.WorkspaceTransition](schemas.md#codersdkworkspacetransition) | false | | | -| `» status` | [codersdk.WorkspaceStatus](schemas.md#codersdkworkspacestatus) | false | | | -| `» template_version_id` | string(uuid) | false | | | -| `» template_version_name` | string | false | | | -| `» template_version_preset_id` | string(uuid) | false | | | -| `» transition` | [codersdk.WorkspaceTransition](schemas.md#codersdkworkspacetransition) | false | | | -| `» updated_at` | string(date-time) | false | | | -| `» workspace_id` | string(uuid) | false | | | -| `» workspace_name` | string | false | | | -| `» workspace_owner_avatar_url` | string | false | | | -| `» workspace_owner_id` | string(uuid) | false | | | -| `» workspace_owner_name` | string | false | | Workspace owner name is the username of the owner of the workspace. | +| Name | Type | Required | Restrictions | Description | +|----------------------------------|--------------------------------------------------------------------------------------------------------|----------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `[array item]` | array | false | | | +| `» build_number` | integer | false | | | +| `» created_at` | string(date-time) | false | | | +| `» daily_cost` | integer | false | | | +| `» deadline` | string(date-time) | false | | | +| `» has_ai_task` | boolean | false | | Deprecated: This field has been deprecated in favor of Task WorkspaceID. | +| `» has_external_agent` | boolean | false | | | +| `» id` | string(uuid) | false | | | +| `» initiator_id` | string(uuid) | false | | | +| `» initiator_name` | string | false | | | +| `» job` | [codersdk.ProvisionerJob](schemas.md#codersdkprovisionerjob) | false | | | +| `»» available_workers` | array | false | | | +| `»» canceled_at` | string(date-time) | false | | | +| `»» completed_at` | string(date-time) | false | | | +| `»» created_at` | string(date-time) | false | | | +| `»» error` | string | false | | | +| `»» error_code` | [codersdk.JobErrorCode](schemas.md#codersdkjoberrorcode) | false | | | +| `»» file_id` | string(uuid) | false | | | +| `»» id` | string(uuid) | false | | | +| `»» initiator_id` | string(uuid) | false | | | +| `»» input` | [codersdk.ProvisionerJobInput](schemas.md#codersdkprovisionerjobinput) | false | | | +| `»»» error` | string | false | | | +| `»»» template_version_id` | string(uuid) | false | | | +| `»»» workspace_build_id` | string(uuid) | false | | | +| `»» logs_overflowed` | boolean | false | | | +| `»» metadata` | [codersdk.ProvisionerJobMetadata](schemas.md#codersdkprovisionerjobmetadata) | false | | | +| `»»» template_display_name` | string | false | | | +| `»»» template_icon` | string | false | | | +| `»»» template_id` | string(uuid) | false | | | +| `»»» template_name` | string | false | | | +| `»»» template_version_name` | string | false | | | +| `»»» workspace_build_transition` | [codersdk.WorkspaceTransition](schemas.md#codersdkworkspacetransition) | false | | | +| `»»» workspace_id` | string(uuid) | false | | | +| `»»» workspace_name` | string | false | | | +| `»» organization_id` | string(uuid) | false | | | +| `»» queue_position` | integer | false | | | +| `»» queue_size` | integer | false | | | +| `»» started_at` | string(date-time) | false | | | +| `»» status` | [codersdk.ProvisionerJobStatus](schemas.md#codersdkprovisionerjobstatus) | false | | | +| `»» tags` | object | false | | | +| `»»» [any property]` | string | false | | | +| `»» type` | [codersdk.ProvisionerJobType](schemas.md#codersdkprovisionerjobtype) | false | | | +| `»» worker_id` | string(uuid) | false | | | +| `»» worker_name` | string | false | | | +| `» matched_provisioners` | [codersdk.MatchedProvisioners](schemas.md#codersdkmatchedprovisioners) | false | | | +| `»» available` | integer | false | | Available is the number of provisioner daemons that are available to take jobs. This may be less than the count if some provisioners are busy or have been stopped. | +| `»» count` | integer | false | | Count is the number of provisioner daemons that matched the given tags. If the count is 0, it means no provisioner daemons matched the requested tags. | +| `»» most_recently_seen` | string(date-time) | false | | Most recently seen is the most recently seen time of the set of matched provisioners. If no provisioners matched, this field will be null. | +| `» max_deadline` | string(date-time) | false | | | +| `» reason` | [codersdk.BuildReason](schemas.md#codersdkbuildreason) | false | | | +| `» resources` | array | false | | | +| `»» agents` | array | false | | | +| `»»» api_version` | string | false | | | +| `»»» apps` | array | false | | | +| `»»»» command` | string | false | | | +| `»»»» display_name` | string | false | | Display name is a friendly name for the app. | +| `»»»» external` | boolean | false | | External specifies whether the URL should be opened externally on the client or not. | +| `»»»» group` | string | false | | | +| `»»»» health` | [codersdk.WorkspaceAppHealth](schemas.md#codersdkworkspaceapphealth) | false | | | +| `»»»» healthcheck` | [codersdk.Healthcheck](schemas.md#codersdkhealthcheck) | false | | Healthcheck specifies the configuration for checking app health. | +| `»»»»» interval` | integer | false | | Interval specifies the seconds between each health check. | +| `»»»»» threshold` | integer | false | | Threshold specifies the number of consecutive failed health checks before returning "unhealthy". | +| `»»»»» url` | string | false | | URL specifies the endpoint to check for the app health. | +| `»»»» hidden` | boolean | false | | | +| `»»»» icon` | string | false | | Icon is a relative path or external URL that specifies an icon to be displayed in the dashboard. | +| `»»»» id` | string(uuid) | false | | | +| `»»»» open_in` | [codersdk.WorkspaceAppOpenIn](schemas.md#codersdkworkspaceappopenin) | false | | | +| `»»»» sharing_level` | [codersdk.WorkspaceAppSharingLevel](schemas.md#codersdkworkspaceappsharinglevel) | false | | | +| `»»»» slug` | string | false | | Slug is a unique identifier within the agent. | +| `»»»» statuses` | array | false | | Statuses is a list of statuses for the app. | +| `»»»»» agent_id` | string(uuid) | false | | | +| `»»»»» app_id` | string(uuid) | false | | | +| `»»»»» created_at` | string(date-time) | false | | | +| `»»»»» icon` | string | false | | Deprecated: This field is unused and will be removed in a future version. Icon is an external URL to an icon that will be rendered in the UI. | +| `»»»»» id` | string(uuid) | false | | | +| `»»»»» message` | string | false | | | +| `»»»»» needs_user_attention` | boolean | false | | Deprecated: This field is unused and will be removed in a future version. NeedsUserAttention specifies whether the status needs user attention. | +| `»»»»» state` | [codersdk.WorkspaceAppStatusState](schemas.md#codersdkworkspaceappstatusstate) | false | | | +| `»»»»» uri` | string | false | | Uri is the URI of the resource that the status is for. e.g. https://github.com/org/repo/pull/123 e.g. file:///path/to/file | +| `»»»»» workspace_id` | string(uuid) | false | | | +| `»»»» subdomain` | boolean | false | | Subdomain denotes whether the app should be accessed via a path on the `coder server` or via a hostname-based dev URL. If this is set to true and there is no app wildcard configured on the server, the app will not be accessible in the UI. | +| `»»»» subdomain_name` | string | false | | Subdomain name is the application domain exposed on the `coder server`. | +| `»»»» tooltip` | string | false | | Tooltip is an optional markdown supported field that is displayed when hovering over workspace apps in the UI. | +| `»»»» url` | string | false | | URL is the address being proxied to inside the workspace. If external is specified, this will be opened on the client. | +| `»»» architecture` | string | false | | | +| `»»» connection_timeout_seconds` | integer | false | | | +| `»»» created_at` | string(date-time) | false | | | +| `»»» directory` | string | false | | | +| `»»» disconnected_at` | string(date-time) | false | | | +| `»»» display_apps` | array | false | | | +| `»»» environment_variables` | object | false | | | +| `»»»» [any property]` | string | false | | | +| `»»» expanded_directory` | string | false | | | +| `»»» first_connected_at` | string(date-time) | false | | | +| `»»» health` | [codersdk.WorkspaceAgentHealth](schemas.md#codersdkworkspaceagenthealth) | false | | Health reports the health of the agent. | +| `»»»» healthy` | boolean | false | | Healthy is true if the agent is healthy. | +| `»»»» reason` | string | false | | Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true. | +| `»»» id` | string(uuid) | false | | | +| `»»» instance_id` | string | false | | | +| `»»» last_connected_at` | string(date-time) | false | | | +| `»»» latency` | object | false | | Latency is mapped by region name (e.g. "New York City", "Seattle"). | +| `»»»» [any property]` | [codersdk.DERPRegion](schemas.md#codersdkderpregion) | false | | | +| `»»»»» latency_ms` | number | false | | | +| `»»»»» preferred` | boolean | false | | | +| `»»» lifecycle_state` | [codersdk.WorkspaceAgentLifecycle](schemas.md#codersdkworkspaceagentlifecycle) | false | | | +| `»»» log_sources` | array | false | | | +| `»»»» created_at` | string(date-time) | false | | | +| `»»»» display_name` | string | false | | | +| `»»»» icon` | string | false | | | +| `»»»» id` | string(uuid) | false | | | +| `»»»» workspace_agent_id` | string(uuid) | false | | | +| `»»» logs_length` | integer | false | | | +| `»»» logs_overflowed` | boolean | false | | | +| `»»» metadata` | array | false | | Metadata is only populated on the workspaces list endpoint when the request opts in with the include_agent_metadata search key, and it only carries the requested keys. The description's script is always empty here: it can be long, and list consumers want values. | +| `»»»» description` | [codersdk.WorkspaceAgentMetadataDescription](schemas.md#codersdkworkspaceagentmetadatadescription) | false | | | +| `»»»»» display_name` | string | false | | | +| `»»»»» interval` | integer | false | | | +| `»»»»» key` | string | false | | | +| `»»»»» script` | string | false | | | +| `»»»»» timeout` | integer | false | | | +| `»»»» result` | [codersdk.WorkspaceAgentMetadataResult](schemas.md#codersdkworkspaceagentmetadataresult) | false | | | +| `»»»»» age` | integer | false | | Age is the number of seconds since the metadata was collected. It is provided in addition to CollectedAt to protect against clock skew. | +| `»»»»» collected_at` | string(date-time) | false | | | +| `»»»»» error` | string | false | | | +| `»»»»» value` | string | false | | | +| `»»» name` | string | false | | | +| `»»» operating_system` | string | false | | | +| `»»» parent_id` | [uuid.NullUUID](schemas.md#uuidnulluuid) | false | | | +| `»»»» uuid` | string | false | | | +| `»»»» valid` | boolean | false | | Valid is true if UUID is not NULL | +| `»»» ready_at` | string(date-time) | false | | | +| `»»» resource_id` | string(uuid) | false | | | +| `»»» scripts` | array | false | | | +| `»»»» cron` | string | false | | | +| `»»»» display_name` | string | false | | | +| `»»»» exit_code` | integer | false | | | +| `»»»» id` | string(uuid) | false | | | +| `»»»» log_path` | string | false | | | +| `»»»» log_source_id` | string(uuid) | false | | | +| `»»»» run_on_start` | boolean | false | | | +| `»»»» run_on_stop` | boolean | false | | | +| `»»»» script` | string | false | | | +| `»»»» start_blocks_login` | boolean | false | | | +| `»»»» status` | [codersdk.WorkspaceAgentScriptStatus](schemas.md#codersdkworkspaceagentscriptstatus) | false | | | +| `»»»» timeout` | integer | false | | | +| `»»» started_at` | string(date-time) | false | | | +| `»»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | false | | Startup script behavior is a legacy field that is deprecated in favor of the `coder_script` resource. It's only referenced by old clients. Deprecated: Remove in the future! | +| `»»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | | +| `»»» subsystems` | array | false | | | +| `»»» troubleshooting_url` | string | false | | | +| `»»» updated_at` | string(date-time) | false | | | +| `»»» version` | string | false | | | +| `»» created_at` | string(date-time) | false | | | +| `»» daily_cost` | integer | false | | | +| `»» hide` | boolean | false | | | +| `»» icon` | string | false | | | +| `»» id` | string(uuid) | false | | | +| `»» job_id` | string(uuid) | false | | | +| `»» metadata` | array | false | | | +| `»»» key` | string | false | | | +| `»»» sensitive` | boolean | false | | | +| `»»» value` | string | false | | | +| `»» name` | string | false | | | +| `»» type` | string | false | | | +| `»» workspace_transition` | [codersdk.WorkspaceTransition](schemas.md#codersdkworkspacetransition) | false | | | +| `» status` | [codersdk.WorkspaceStatus](schemas.md#codersdkworkspacestatus) | false | | | +| `» template_version_id` | string(uuid) | false | | | +| `» template_version_name` | string | false | | | +| `» template_version_preset_id` | string(uuid) | false | | | +| `» transition` | [codersdk.WorkspaceTransition](schemas.md#codersdkworkspacetransition) | false | | | +| `» updated_at` | string(date-time) | false | | | +| `» workspace_id` | string(uuid) | false | | | +| `» workspace_name` | string | false | | | +| `» workspace_owner_avatar_url` | string | false | | | +| `» workspace_owner_id` | string(uuid) | false | | | +| `» workspace_owner_name` | string | false | | Workspace owner name is the username of the owner of the workspace. | #### Enumerated Values diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 23b1cbdd18254..9235fe55c499b 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -15634,44 +15634,44 @@ If the schedule is empty, the user will be updated to use the default schedule.| ### Properties -| Name | Type | Required | Restrictions | Description | -|------------------------------|----------------------------------------------------------------------------------------------|----------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `api_version` | string | false | | | -| `apps` | array of [codersdk.WorkspaceApp](#codersdkworkspaceapp) | false | | | -| `architecture` | string | false | | | -| `connection_timeout_seconds` | integer | false | | | -| `created_at` | string | false | | | -| `directory` | string | false | | | -| `disconnected_at` | string | false | | | -| `display_apps` | array of [codersdk.DisplayApp](#codersdkdisplayapp) | false | | | -| `environment_variables` | object | false | | | -| » `[any property]` | string | false | | | -| `expanded_directory` | string | false | | | -| `first_connected_at` | string | false | | | -| `health` | [codersdk.WorkspaceAgentHealth](#codersdkworkspaceagenthealth) | false | | Health reports the health of the agent. | -| `id` | string | false | | | -| `instance_id` | string | false | | | -| `last_connected_at` | string | false | | | -| `latency` | object | false | | Latency is mapped by region name (e.g. "New York City", "Seattle"). | -| » `[any property]` | [codersdk.DERPRegion](#codersdkderpregion) | false | | | -| `lifecycle_state` | [codersdk.WorkspaceAgentLifecycle](#codersdkworkspaceagentlifecycle) | false | | | -| `log_sources` | array of [codersdk.WorkspaceAgentLogSource](#codersdkworkspaceagentlogsource) | false | | | -| `logs_length` | integer | false | | | -| `logs_overflowed` | boolean | false | | | -| `metadata` | array of [codersdk.WorkspaceAgentMetadata](#codersdkworkspaceagentmetadata) | false | | Metadata is only populated on the workspaces list endpoint when the request opts in with the include_agent_metadata search key, and it only carries the requested keys. | -| `name` | string | false | | | -| `operating_system` | string | false | | | -| `parent_id` | [uuid.NullUUID](#uuidnulluuid) | false | | | -| `ready_at` | string | false | | | -| `resource_id` | string | false | | | -| `scripts` | array of [codersdk.WorkspaceAgentScript](#codersdkworkspaceagentscript) | false | | | -| `started_at` | string | false | | | -| `startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](#codersdkworkspaceagentstartupscriptbehavior) | false | | Startup script behavior is a legacy field that is deprecated in favor of the `coder_script` resource. It's only referenced by old clients. Deprecated: Remove in the future! | -| `status` | [codersdk.WorkspaceAgentStatus](#codersdkworkspaceagentstatus) | false | | | -| `subsystems` | array of [codersdk.AgentSubsystem](#codersdkagentsubsystem) | false | | | -| `troubleshooting_url` | string | false | | | -| `updated_at` | string | false | | | -| `version` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|------------------------------|----------------------------------------------------------------------------------------------|----------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `api_version` | string | false | | | +| `apps` | array of [codersdk.WorkspaceApp](#codersdkworkspaceapp) | false | | | +| `architecture` | string | false | | | +| `connection_timeout_seconds` | integer | false | | | +| `created_at` | string | false | | | +| `directory` | string | false | | | +| `disconnected_at` | string | false | | | +| `display_apps` | array of [codersdk.DisplayApp](#codersdkdisplayapp) | false | | | +| `environment_variables` | object | false | | | +| » `[any property]` | string | false | | | +| `expanded_directory` | string | false | | | +| `first_connected_at` | string | false | | | +| `health` | [codersdk.WorkspaceAgentHealth](#codersdkworkspaceagenthealth) | false | | Health reports the health of the agent. | +| `id` | string | false | | | +| `instance_id` | string | false | | | +| `last_connected_at` | string | false | | | +| `latency` | object | false | | Latency is mapped by region name (e.g. "New York City", "Seattle"). | +| » `[any property]` | [codersdk.DERPRegion](#codersdkderpregion) | false | | | +| `lifecycle_state` | [codersdk.WorkspaceAgentLifecycle](#codersdkworkspaceagentlifecycle) | false | | | +| `log_sources` | array of [codersdk.WorkspaceAgentLogSource](#codersdkworkspaceagentlogsource) | false | | | +| `logs_length` | integer | false | | | +| `logs_overflowed` | boolean | false | | | +| `metadata` | array of [codersdk.WorkspaceAgentMetadata](#codersdkworkspaceagentmetadata) | false | | Metadata is only populated on the workspaces list endpoint when the request opts in with the include_agent_metadata search key, and it only carries the requested keys. The description's script is always empty here: it can be long, and list consumers want values. | +| `name` | string | false | | | +| `operating_system` | string | false | | | +| `parent_id` | [uuid.NullUUID](#uuidnulluuid) | false | | | +| `ready_at` | string | false | | | +| `resource_id` | string | false | | | +| `scripts` | array of [codersdk.WorkspaceAgentScript](#codersdkworkspaceagentscript) | false | | | +| `started_at` | string | false | | | +| `startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](#codersdkworkspaceagentstartupscriptbehavior) | false | | Startup script behavior is a legacy field that is deprecated in favor of the `coder_script` resource. It's only referenced by old clients. Deprecated: Remove in the future! | +| `status` | [codersdk.WorkspaceAgentStatus](#codersdkworkspaceagentstatus) | false | | | +| `subsystems` | array of [codersdk.AgentSubsystem](#codersdkagentsubsystem) | false | | | +| `troubleshooting_url` | string | false | | | +| `updated_at` | string | false | | | +| `version` | string | false | | | ## codersdk.WorkspaceAgentContainer diff --git a/docs/reference/api/templates.md b/docs/reference/api/templates.md index 754870c491a25..cdc73eff2756a 100644 --- a/docs/reference/api/templates.md +++ b/docs/reference/api/templates.md @@ -2584,123 +2584,123 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/d Status Code **200** -| Name | Type | Required | Restrictions | Description | -|---------------------------------|--------------------------------------------------------------------------------------------------------|----------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `[array item]` | array | false | | | -| `» agents` | array | false | | | -| `»» api_version` | string | false | | | -| `»» apps` | array | false | | | -| `»»» command` | string | false | | | -| `»»» display_name` | string | false | | Display name is a friendly name for the app. | -| `»»» external` | boolean | false | | External specifies whether the URL should be opened externally on the client or not. | -| `»»» group` | string | false | | | -| `»»» health` | [codersdk.WorkspaceAppHealth](schemas.md#codersdkworkspaceapphealth) | false | | | -| `»»» healthcheck` | [codersdk.Healthcheck](schemas.md#codersdkhealthcheck) | false | | Healthcheck specifies the configuration for checking app health. | -| `»»»» interval` | integer | false | | Interval specifies the seconds between each health check. | -| `»»»» threshold` | integer | false | | Threshold specifies the number of consecutive failed health checks before returning "unhealthy". | -| `»»»» url` | string | false | | URL specifies the endpoint to check for the app health. | -| `»»» hidden` | boolean | false | | | -| `»»» icon` | string | false | | Icon is a relative path or external URL that specifies an icon to be displayed in the dashboard. | -| `»»» id` | string(uuid) | false | | | -| `»»» open_in` | [codersdk.WorkspaceAppOpenIn](schemas.md#codersdkworkspaceappopenin) | false | | | -| `»»» sharing_level` | [codersdk.WorkspaceAppSharingLevel](schemas.md#codersdkworkspaceappsharinglevel) | false | | | -| `»»» slug` | string | false | | Slug is a unique identifier within the agent. | -| `»»» statuses` | array | false | | Statuses is a list of statuses for the app. | -| `»»»» agent_id` | string(uuid) | false | | | -| `»»»» app_id` | string(uuid) | false | | | -| `»»»» created_at` | string(date-time) | false | | | -| `»»»» icon` | string | false | | Deprecated: This field is unused and will be removed in a future version. Icon is an external URL to an icon that will be rendered in the UI. | -| `»»»» id` | string(uuid) | false | | | -| `»»»» message` | string | false | | | -| `»»»» needs_user_attention` | boolean | false | | Deprecated: This field is unused and will be removed in a future version. NeedsUserAttention specifies whether the status needs user attention. | -| `»»»» state` | [codersdk.WorkspaceAppStatusState](schemas.md#codersdkworkspaceappstatusstate) | false | | | -| `»»»» uri` | string | false | | Uri is the URI of the resource that the status is for. e.g. https://github.com/org/repo/pull/123 e.g. file:///path/to/file | -| `»»»» workspace_id` | string(uuid) | false | | | -| `»»» subdomain` | boolean | false | | Subdomain denotes whether the app should be accessed via a path on the `coder server` or via a hostname-based dev URL. If this is set to true and there is no app wildcard configured on the server, the app will not be accessible in the UI. | -| `»»» subdomain_name` | string | false | | Subdomain name is the application domain exposed on the `coder server`. | -| `»»» tooltip` | string | false | | Tooltip is an optional markdown supported field that is displayed when hovering over workspace apps in the UI. | -| `»»» url` | string | false | | URL is the address being proxied to inside the workspace. If external is specified, this will be opened on the client. | -| `»» architecture` | string | false | | | -| `»» connection_timeout_seconds` | integer | false | | | -| `»» created_at` | string(date-time) | false | | | -| `»» directory` | string | false | | | -| `»» disconnected_at` | string(date-time) | false | | | -| `»» display_apps` | array | false | | | -| `»» environment_variables` | object | false | | | -| `»»» [any property]` | string | false | | | -| `»» expanded_directory` | string | false | | | -| `»» first_connected_at` | string(date-time) | false | | | -| `»» health` | [codersdk.WorkspaceAgentHealth](schemas.md#codersdkworkspaceagenthealth) | false | | Health reports the health of the agent. | -| `»»» healthy` | boolean | false | | Healthy is true if the agent is healthy. | -| `»»» reason` | string | false | | Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true. | -| `»» id` | string(uuid) | false | | | -| `»» instance_id` | string | false | | | -| `»» last_connected_at` | string(date-time) | false | | | -| `»» latency` | object | false | | Latency is mapped by region name (e.g. "New York City", "Seattle"). | -| `»»» [any property]` | [codersdk.DERPRegion](schemas.md#codersdkderpregion) | false | | | -| `»»»» latency_ms` | number | false | | | -| `»»»» preferred` | boolean | false | | | -| `»» lifecycle_state` | [codersdk.WorkspaceAgentLifecycle](schemas.md#codersdkworkspaceagentlifecycle) | false | | | -| `»» log_sources` | array | false | | | -| `»»» created_at` | string(date-time) | false | | | -| `»»» display_name` | string | false | | | -| `»»» icon` | string | false | | | -| `»»» id` | string(uuid) | false | | | -| `»»» workspace_agent_id` | string(uuid) | false | | | -| `»» logs_length` | integer | false | | | -| `»» logs_overflowed` | boolean | false | | | -| `»» metadata` | array | false | | Metadata is only populated on the workspaces list endpoint when the request opts in with the include_agent_metadata search key, and it only carries the requested keys. | -| `»»» description` | [codersdk.WorkspaceAgentMetadataDescription](schemas.md#codersdkworkspaceagentmetadatadescription) | false | | | -| `»»»» display_name` | string | false | | | -| `»»»» interval` | integer | false | | | -| `»»»» key` | string | false | | | -| `»»»» script` | string | false | | | -| `»»»» timeout` | integer | false | | | -| `»»» result` | [codersdk.WorkspaceAgentMetadataResult](schemas.md#codersdkworkspaceagentmetadataresult) | false | | | -| `»»»» age` | integer | false | | Age is the number of seconds since the metadata was collected. It is provided in addition to CollectedAt to protect against clock skew. | -| `»»»» collected_at` | string(date-time) | false | | | -| `»»»» error` | string | false | | | -| `»»»» value` | string | false | | | -| `»» name` | string | false | | | -| `»» operating_system` | string | false | | | -| `»» parent_id` | [uuid.NullUUID](schemas.md#uuidnulluuid) | false | | | -| `»»» uuid` | string | false | | | -| `»»» valid` | boolean | false | | Valid is true if UUID is not NULL | -| `»» ready_at` | string(date-time) | false | | | -| `»» resource_id` | string(uuid) | false | | | -| `»» scripts` | array | false | | | -| `»»» cron` | string | false | | | -| `»»» display_name` | string | false | | | -| `»»» exit_code` | integer | false | | | -| `»»» id` | string(uuid) | false | | | -| `»»» log_path` | string | false | | | -| `»»» log_source_id` | string(uuid) | false | | | -| `»»» run_on_start` | boolean | false | | | -| `»»» run_on_stop` | boolean | false | | | -| `»»» script` | string | false | | | -| `»»» start_blocks_login` | boolean | false | | | -| `»»» status` | [codersdk.WorkspaceAgentScriptStatus](schemas.md#codersdkworkspaceagentscriptstatus) | false | | | -| `»»» timeout` | integer | false | | | -| `»» started_at` | string(date-time) | false | | | -| `»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | false | | Startup script behavior is a legacy field that is deprecated in favor of the `coder_script` resource. It's only referenced by old clients. Deprecated: Remove in the future! | -| `»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | | -| `»» subsystems` | array | false | | | -| `»» troubleshooting_url` | string | false | | | -| `»» updated_at` | string(date-time) | false | | | -| `»» version` | string | false | | | -| `» created_at` | string(date-time) | false | | | -| `» daily_cost` | integer | false | | | -| `» hide` | boolean | false | | | -| `» icon` | string | false | | | -| `» id` | string(uuid) | false | | | -| `» job_id` | string(uuid) | false | | | -| `» metadata` | array | false | | | -| `»» key` | string | false | | | -| `»» sensitive` | boolean | false | | | -| `»» value` | string | false | | | -| `» name` | string | false | | | -| `» type` | string | false | | | -| `» workspace_transition` | [codersdk.WorkspaceTransition](schemas.md#codersdkworkspacetransition) | false | | | +| Name | Type | Required | Restrictions | Description | +|---------------------------------|--------------------------------------------------------------------------------------------------------|----------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `[array item]` | array | false | | | +| `» agents` | array | false | | | +| `»» api_version` | string | false | | | +| `»» apps` | array | false | | | +| `»»» command` | string | false | | | +| `»»» display_name` | string | false | | Display name is a friendly name for the app. | +| `»»» external` | boolean | false | | External specifies whether the URL should be opened externally on the client or not. | +| `»»» group` | string | false | | | +| `»»» health` | [codersdk.WorkspaceAppHealth](schemas.md#codersdkworkspaceapphealth) | false | | | +| `»»» healthcheck` | [codersdk.Healthcheck](schemas.md#codersdkhealthcheck) | false | | Healthcheck specifies the configuration for checking app health. | +| `»»»» interval` | integer | false | | Interval specifies the seconds between each health check. | +| `»»»» threshold` | integer | false | | Threshold specifies the number of consecutive failed health checks before returning "unhealthy". | +| `»»»» url` | string | false | | URL specifies the endpoint to check for the app health. | +| `»»» hidden` | boolean | false | | | +| `»»» icon` | string | false | | Icon is a relative path or external URL that specifies an icon to be displayed in the dashboard. | +| `»»» id` | string(uuid) | false | | | +| `»»» open_in` | [codersdk.WorkspaceAppOpenIn](schemas.md#codersdkworkspaceappopenin) | false | | | +| `»»» sharing_level` | [codersdk.WorkspaceAppSharingLevel](schemas.md#codersdkworkspaceappsharinglevel) | false | | | +| `»»» slug` | string | false | | Slug is a unique identifier within the agent. | +| `»»» statuses` | array | false | | Statuses is a list of statuses for the app. | +| `»»»» agent_id` | string(uuid) | false | | | +| `»»»» app_id` | string(uuid) | false | | | +| `»»»» created_at` | string(date-time) | false | | | +| `»»»» icon` | string | false | | Deprecated: This field is unused and will be removed in a future version. Icon is an external URL to an icon that will be rendered in the UI. | +| `»»»» id` | string(uuid) | false | | | +| `»»»» message` | string | false | | | +| `»»»» needs_user_attention` | boolean | false | | Deprecated: This field is unused and will be removed in a future version. NeedsUserAttention specifies whether the status needs user attention. | +| `»»»» state` | [codersdk.WorkspaceAppStatusState](schemas.md#codersdkworkspaceappstatusstate) | false | | | +| `»»»» uri` | string | false | | Uri is the URI of the resource that the status is for. e.g. https://github.com/org/repo/pull/123 e.g. file:///path/to/file | +| `»»»» workspace_id` | string(uuid) | false | | | +| `»»» subdomain` | boolean | false | | Subdomain denotes whether the app should be accessed via a path on the `coder server` or via a hostname-based dev URL. If this is set to true and there is no app wildcard configured on the server, the app will not be accessible in the UI. | +| `»»» subdomain_name` | string | false | | Subdomain name is the application domain exposed on the `coder server`. | +| `»»» tooltip` | string | false | | Tooltip is an optional markdown supported field that is displayed when hovering over workspace apps in the UI. | +| `»»» url` | string | false | | URL is the address being proxied to inside the workspace. If external is specified, this will be opened on the client. | +| `»» architecture` | string | false | | | +| `»» connection_timeout_seconds` | integer | false | | | +| `»» created_at` | string(date-time) | false | | | +| `»» directory` | string | false | | | +| `»» disconnected_at` | string(date-time) | false | | | +| `»» display_apps` | array | false | | | +| `»» environment_variables` | object | false | | | +| `»»» [any property]` | string | false | | | +| `»» expanded_directory` | string | false | | | +| `»» first_connected_at` | string(date-time) | false | | | +| `»» health` | [codersdk.WorkspaceAgentHealth](schemas.md#codersdkworkspaceagenthealth) | false | | Health reports the health of the agent. | +| `»»» healthy` | boolean | false | | Healthy is true if the agent is healthy. | +| `»»» reason` | string | false | | Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true. | +| `»» id` | string(uuid) | false | | | +| `»» instance_id` | string | false | | | +| `»» last_connected_at` | string(date-time) | false | | | +| `»» latency` | object | false | | Latency is mapped by region name (e.g. "New York City", "Seattle"). | +| `»»» [any property]` | [codersdk.DERPRegion](schemas.md#codersdkderpregion) | false | | | +| `»»»» latency_ms` | number | false | | | +| `»»»» preferred` | boolean | false | | | +| `»» lifecycle_state` | [codersdk.WorkspaceAgentLifecycle](schemas.md#codersdkworkspaceagentlifecycle) | false | | | +| `»» log_sources` | array | false | | | +| `»»» created_at` | string(date-time) | false | | | +| `»»» display_name` | string | false | | | +| `»»» icon` | string | false | | | +| `»»» id` | string(uuid) | false | | | +| `»»» workspace_agent_id` | string(uuid) | false | | | +| `»» logs_length` | integer | false | | | +| `»» logs_overflowed` | boolean | false | | | +| `»» metadata` | array | false | | Metadata is only populated on the workspaces list endpoint when the request opts in with the include_agent_metadata search key, and it only carries the requested keys. The description's script is always empty here: it can be long, and list consumers want values. | +| `»»» description` | [codersdk.WorkspaceAgentMetadataDescription](schemas.md#codersdkworkspaceagentmetadatadescription) | false | | | +| `»»»» display_name` | string | false | | | +| `»»»» interval` | integer | false | | | +| `»»»» key` | string | false | | | +| `»»»» script` | string | false | | | +| `»»»» timeout` | integer | false | | | +| `»»» result` | [codersdk.WorkspaceAgentMetadataResult](schemas.md#codersdkworkspaceagentmetadataresult) | false | | | +| `»»»» age` | integer | false | | Age is the number of seconds since the metadata was collected. It is provided in addition to CollectedAt to protect against clock skew. | +| `»»»» collected_at` | string(date-time) | false | | | +| `»»»» error` | string | false | | | +| `»»»» value` | string | false | | | +| `»» name` | string | false | | | +| `»» operating_system` | string | false | | | +| `»» parent_id` | [uuid.NullUUID](schemas.md#uuidnulluuid) | false | | | +| `»»» uuid` | string | false | | | +| `»»» valid` | boolean | false | | Valid is true if UUID is not NULL | +| `»» ready_at` | string(date-time) | false | | | +| `»» resource_id` | string(uuid) | false | | | +| `»» scripts` | array | false | | | +| `»»» cron` | string | false | | | +| `»»» display_name` | string | false | | | +| `»»» exit_code` | integer | false | | | +| `»»» id` | string(uuid) | false | | | +| `»»» log_path` | string | false | | | +| `»»» log_source_id` | string(uuid) | false | | | +| `»»» run_on_start` | boolean | false | | | +| `»»» run_on_stop` | boolean | false | | | +| `»»» script` | string | false | | | +| `»»» start_blocks_login` | boolean | false | | | +| `»»» status` | [codersdk.WorkspaceAgentScriptStatus](schemas.md#codersdkworkspaceagentscriptstatus) | false | | | +| `»»» timeout` | integer | false | | | +| `»» started_at` | string(date-time) | false | | | +| `»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | false | | Startup script behavior is a legacy field that is deprecated in favor of the `coder_script` resource. It's only referenced by old clients. Deprecated: Remove in the future! | +| `»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | | +| `»» subsystems` | array | false | | | +| `»» troubleshooting_url` | string | false | | | +| `»» updated_at` | string(date-time) | false | | | +| `»» version` | string | false | | | +| `» created_at` | string(date-time) | false | | | +| `» daily_cost` | integer | false | | | +| `» hide` | boolean | false | | | +| `» icon` | string | false | | | +| `» id` | string(uuid) | false | | | +| `» job_id` | string(uuid) | false | | | +| `» metadata` | array | false | | | +| `»» key` | string | false | | | +| `»» sensitive` | boolean | false | | | +| `»» value` | string | false | | | +| `» name` | string | false | | | +| `» type` | string | false | | | +| `» workspace_transition` | [codersdk.WorkspaceTransition](schemas.md#codersdkworkspacetransition) | false | | | #### Enumerated Values @@ -3284,123 +3284,123 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/r Status Code **200** -| Name | Type | Required | Restrictions | Description | -|---------------------------------|--------------------------------------------------------------------------------------------------------|----------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `[array item]` | array | false | | | -| `» agents` | array | false | | | -| `»» api_version` | string | false | | | -| `»» apps` | array | false | | | -| `»»» command` | string | false | | | -| `»»» display_name` | string | false | | Display name is a friendly name for the app. | -| `»»» external` | boolean | false | | External specifies whether the URL should be opened externally on the client or not. | -| `»»» group` | string | false | | | -| `»»» health` | [codersdk.WorkspaceAppHealth](schemas.md#codersdkworkspaceapphealth) | false | | | -| `»»» healthcheck` | [codersdk.Healthcheck](schemas.md#codersdkhealthcheck) | false | | Healthcheck specifies the configuration for checking app health. | -| `»»»» interval` | integer | false | | Interval specifies the seconds between each health check. | -| `»»»» threshold` | integer | false | | Threshold specifies the number of consecutive failed health checks before returning "unhealthy". | -| `»»»» url` | string | false | | URL specifies the endpoint to check for the app health. | -| `»»» hidden` | boolean | false | | | -| `»»» icon` | string | false | | Icon is a relative path or external URL that specifies an icon to be displayed in the dashboard. | -| `»»» id` | string(uuid) | false | | | -| `»»» open_in` | [codersdk.WorkspaceAppOpenIn](schemas.md#codersdkworkspaceappopenin) | false | | | -| `»»» sharing_level` | [codersdk.WorkspaceAppSharingLevel](schemas.md#codersdkworkspaceappsharinglevel) | false | | | -| `»»» slug` | string | false | | Slug is a unique identifier within the agent. | -| `»»» statuses` | array | false | | Statuses is a list of statuses for the app. | -| `»»»» agent_id` | string(uuid) | false | | | -| `»»»» app_id` | string(uuid) | false | | | -| `»»»» created_at` | string(date-time) | false | | | -| `»»»» icon` | string | false | | Deprecated: This field is unused and will be removed in a future version. Icon is an external URL to an icon that will be rendered in the UI. | -| `»»»» id` | string(uuid) | false | | | -| `»»»» message` | string | false | | | -| `»»»» needs_user_attention` | boolean | false | | Deprecated: This field is unused and will be removed in a future version. NeedsUserAttention specifies whether the status needs user attention. | -| `»»»» state` | [codersdk.WorkspaceAppStatusState](schemas.md#codersdkworkspaceappstatusstate) | false | | | -| `»»»» uri` | string | false | | Uri is the URI of the resource that the status is for. e.g. https://github.com/org/repo/pull/123 e.g. file:///path/to/file | -| `»»»» workspace_id` | string(uuid) | false | | | -| `»»» subdomain` | boolean | false | | Subdomain denotes whether the app should be accessed via a path on the `coder server` or via a hostname-based dev URL. If this is set to true and there is no app wildcard configured on the server, the app will not be accessible in the UI. | -| `»»» subdomain_name` | string | false | | Subdomain name is the application domain exposed on the `coder server`. | -| `»»» tooltip` | string | false | | Tooltip is an optional markdown supported field that is displayed when hovering over workspace apps in the UI. | -| `»»» url` | string | false | | URL is the address being proxied to inside the workspace. If external is specified, this will be opened on the client. | -| `»» architecture` | string | false | | | -| `»» connection_timeout_seconds` | integer | false | | | -| `»» created_at` | string(date-time) | false | | | -| `»» directory` | string | false | | | -| `»» disconnected_at` | string(date-time) | false | | | -| `»» display_apps` | array | false | | | -| `»» environment_variables` | object | false | | | -| `»»» [any property]` | string | false | | | -| `»» expanded_directory` | string | false | | | -| `»» first_connected_at` | string(date-time) | false | | | -| `»» health` | [codersdk.WorkspaceAgentHealth](schemas.md#codersdkworkspaceagenthealth) | false | | Health reports the health of the agent. | -| `»»» healthy` | boolean | false | | Healthy is true if the agent is healthy. | -| `»»» reason` | string | false | | Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true. | -| `»» id` | string(uuid) | false | | | -| `»» instance_id` | string | false | | | -| `»» last_connected_at` | string(date-time) | false | | | -| `»» latency` | object | false | | Latency is mapped by region name (e.g. "New York City", "Seattle"). | -| `»»» [any property]` | [codersdk.DERPRegion](schemas.md#codersdkderpregion) | false | | | -| `»»»» latency_ms` | number | false | | | -| `»»»» preferred` | boolean | false | | | -| `»» lifecycle_state` | [codersdk.WorkspaceAgentLifecycle](schemas.md#codersdkworkspaceagentlifecycle) | false | | | -| `»» log_sources` | array | false | | | -| `»»» created_at` | string(date-time) | false | | | -| `»»» display_name` | string | false | | | -| `»»» icon` | string | false | | | -| `»»» id` | string(uuid) | false | | | -| `»»» workspace_agent_id` | string(uuid) | false | | | -| `»» logs_length` | integer | false | | | -| `»» logs_overflowed` | boolean | false | | | -| `»» metadata` | array | false | | Metadata is only populated on the workspaces list endpoint when the request opts in with the include_agent_metadata search key, and it only carries the requested keys. | -| `»»» description` | [codersdk.WorkspaceAgentMetadataDescription](schemas.md#codersdkworkspaceagentmetadatadescription) | false | | | -| `»»»» display_name` | string | false | | | -| `»»»» interval` | integer | false | | | -| `»»»» key` | string | false | | | -| `»»»» script` | string | false | | | -| `»»»» timeout` | integer | false | | | -| `»»» result` | [codersdk.WorkspaceAgentMetadataResult](schemas.md#codersdkworkspaceagentmetadataresult) | false | | | -| `»»»» age` | integer | false | | Age is the number of seconds since the metadata was collected. It is provided in addition to CollectedAt to protect against clock skew. | -| `»»»» collected_at` | string(date-time) | false | | | -| `»»»» error` | string | false | | | -| `»»»» value` | string | false | | | -| `»» name` | string | false | | | -| `»» operating_system` | string | false | | | -| `»» parent_id` | [uuid.NullUUID](schemas.md#uuidnulluuid) | false | | | -| `»»» uuid` | string | false | | | -| `»»» valid` | boolean | false | | Valid is true if UUID is not NULL | -| `»» ready_at` | string(date-time) | false | | | -| `»» resource_id` | string(uuid) | false | | | -| `»» scripts` | array | false | | | -| `»»» cron` | string | false | | | -| `»»» display_name` | string | false | | | -| `»»» exit_code` | integer | false | | | -| `»»» id` | string(uuid) | false | | | -| `»»» log_path` | string | false | | | -| `»»» log_source_id` | string(uuid) | false | | | -| `»»» run_on_start` | boolean | false | | | -| `»»» run_on_stop` | boolean | false | | | -| `»»» script` | string | false | | | -| `»»» start_blocks_login` | boolean | false | | | -| `»»» status` | [codersdk.WorkspaceAgentScriptStatus](schemas.md#codersdkworkspaceagentscriptstatus) | false | | | -| `»»» timeout` | integer | false | | | -| `»» started_at` | string(date-time) | false | | | -| `»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | false | | Startup script behavior is a legacy field that is deprecated in favor of the `coder_script` resource. It's only referenced by old clients. Deprecated: Remove in the future! | -| `»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | | -| `»» subsystems` | array | false | | | -| `»» troubleshooting_url` | string | false | | | -| `»» updated_at` | string(date-time) | false | | | -| `»» version` | string | false | | | -| `» created_at` | string(date-time) | false | | | -| `» daily_cost` | integer | false | | | -| `» hide` | boolean | false | | | -| `» icon` | string | false | | | -| `» id` | string(uuid) | false | | | -| `» job_id` | string(uuid) | false | | | -| `» metadata` | array | false | | | -| `»» key` | string | false | | | -| `»» sensitive` | boolean | false | | | -| `»» value` | string | false | | | -| `» name` | string | false | | | -| `» type` | string | false | | | -| `» workspace_transition` | [codersdk.WorkspaceTransition](schemas.md#codersdkworkspacetransition) | false | | | +| Name | Type | Required | Restrictions | Description | +|---------------------------------|--------------------------------------------------------------------------------------------------------|----------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `[array item]` | array | false | | | +| `» agents` | array | false | | | +| `»» api_version` | string | false | | | +| `»» apps` | array | false | | | +| `»»» command` | string | false | | | +| `»»» display_name` | string | false | | Display name is a friendly name for the app. | +| `»»» external` | boolean | false | | External specifies whether the URL should be opened externally on the client or not. | +| `»»» group` | string | false | | | +| `»»» health` | [codersdk.WorkspaceAppHealth](schemas.md#codersdkworkspaceapphealth) | false | | | +| `»»» healthcheck` | [codersdk.Healthcheck](schemas.md#codersdkhealthcheck) | false | | Healthcheck specifies the configuration for checking app health. | +| `»»»» interval` | integer | false | | Interval specifies the seconds between each health check. | +| `»»»» threshold` | integer | false | | Threshold specifies the number of consecutive failed health checks before returning "unhealthy". | +| `»»»» url` | string | false | | URL specifies the endpoint to check for the app health. | +| `»»» hidden` | boolean | false | | | +| `»»» icon` | string | false | | Icon is a relative path or external URL that specifies an icon to be displayed in the dashboard. | +| `»»» id` | string(uuid) | false | | | +| `»»» open_in` | [codersdk.WorkspaceAppOpenIn](schemas.md#codersdkworkspaceappopenin) | false | | | +| `»»» sharing_level` | [codersdk.WorkspaceAppSharingLevel](schemas.md#codersdkworkspaceappsharinglevel) | false | | | +| `»»» slug` | string | false | | Slug is a unique identifier within the agent. | +| `»»» statuses` | array | false | | Statuses is a list of statuses for the app. | +| `»»»» agent_id` | string(uuid) | false | | | +| `»»»» app_id` | string(uuid) | false | | | +| `»»»» created_at` | string(date-time) | false | | | +| `»»»» icon` | string | false | | Deprecated: This field is unused and will be removed in a future version. Icon is an external URL to an icon that will be rendered in the UI. | +| `»»»» id` | string(uuid) | false | | | +| `»»»» message` | string | false | | | +| `»»»» needs_user_attention` | boolean | false | | Deprecated: This field is unused and will be removed in a future version. NeedsUserAttention specifies whether the status needs user attention. | +| `»»»» state` | [codersdk.WorkspaceAppStatusState](schemas.md#codersdkworkspaceappstatusstate) | false | | | +| `»»»» uri` | string | false | | Uri is the URI of the resource that the status is for. e.g. https://github.com/org/repo/pull/123 e.g. file:///path/to/file | +| `»»»» workspace_id` | string(uuid) | false | | | +| `»»» subdomain` | boolean | false | | Subdomain denotes whether the app should be accessed via a path on the `coder server` or via a hostname-based dev URL. If this is set to true and there is no app wildcard configured on the server, the app will not be accessible in the UI. | +| `»»» subdomain_name` | string | false | | Subdomain name is the application domain exposed on the `coder server`. | +| `»»» tooltip` | string | false | | Tooltip is an optional markdown supported field that is displayed when hovering over workspace apps in the UI. | +| `»»» url` | string | false | | URL is the address being proxied to inside the workspace. If external is specified, this will be opened on the client. | +| `»» architecture` | string | false | | | +| `»» connection_timeout_seconds` | integer | false | | | +| `»» created_at` | string(date-time) | false | | | +| `»» directory` | string | false | | | +| `»» disconnected_at` | string(date-time) | false | | | +| `»» display_apps` | array | false | | | +| `»» environment_variables` | object | false | | | +| `»»» [any property]` | string | false | | | +| `»» expanded_directory` | string | false | | | +| `»» first_connected_at` | string(date-time) | false | | | +| `»» health` | [codersdk.WorkspaceAgentHealth](schemas.md#codersdkworkspaceagenthealth) | false | | Health reports the health of the agent. | +| `»»» healthy` | boolean | false | | Healthy is true if the agent is healthy. | +| `»»» reason` | string | false | | Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true. | +| `»» id` | string(uuid) | false | | | +| `»» instance_id` | string | false | | | +| `»» last_connected_at` | string(date-time) | false | | | +| `»» latency` | object | false | | Latency is mapped by region name (e.g. "New York City", "Seattle"). | +| `»»» [any property]` | [codersdk.DERPRegion](schemas.md#codersdkderpregion) | false | | | +| `»»»» latency_ms` | number | false | | | +| `»»»» preferred` | boolean | false | | | +| `»» lifecycle_state` | [codersdk.WorkspaceAgentLifecycle](schemas.md#codersdkworkspaceagentlifecycle) | false | | | +| `»» log_sources` | array | false | | | +| `»»» created_at` | string(date-time) | false | | | +| `»»» display_name` | string | false | | | +| `»»» icon` | string | false | | | +| `»»» id` | string(uuid) | false | | | +| `»»» workspace_agent_id` | string(uuid) | false | | | +| `»» logs_length` | integer | false | | | +| `»» logs_overflowed` | boolean | false | | | +| `»» metadata` | array | false | | Metadata is only populated on the workspaces list endpoint when the request opts in with the include_agent_metadata search key, and it only carries the requested keys. The description's script is always empty here: it can be long, and list consumers want values. | +| `»»» description` | [codersdk.WorkspaceAgentMetadataDescription](schemas.md#codersdkworkspaceagentmetadatadescription) | false | | | +| `»»»» display_name` | string | false | | | +| `»»»» interval` | integer | false | | | +| `»»»» key` | string | false | | | +| `»»»» script` | string | false | | | +| `»»»» timeout` | integer | false | | | +| `»»» result` | [codersdk.WorkspaceAgentMetadataResult](schemas.md#codersdkworkspaceagentmetadataresult) | false | | | +| `»»»» age` | integer | false | | Age is the number of seconds since the metadata was collected. It is provided in addition to CollectedAt to protect against clock skew. | +| `»»»» collected_at` | string(date-time) | false | | | +| `»»»» error` | string | false | | | +| `»»»» value` | string | false | | | +| `»» name` | string | false | | | +| `»» operating_system` | string | false | | | +| `»» parent_id` | [uuid.NullUUID](schemas.md#uuidnulluuid) | false | | | +| `»»» uuid` | string | false | | | +| `»»» valid` | boolean | false | | Valid is true if UUID is not NULL | +| `»» ready_at` | string(date-time) | false | | | +| `»» resource_id` | string(uuid) | false | | | +| `»» scripts` | array | false | | | +| `»»» cron` | string | false | | | +| `»»» display_name` | string | false | | | +| `»»» exit_code` | integer | false | | | +| `»»» id` | string(uuid) | false | | | +| `»»» log_path` | string | false | | | +| `»»» log_source_id` | string(uuid) | false | | | +| `»»» run_on_start` | boolean | false | | | +| `»»» run_on_stop` | boolean | false | | | +| `»»» script` | string | false | | | +| `»»» start_blocks_login` | boolean | false | | | +| `»»» status` | [codersdk.WorkspaceAgentScriptStatus](schemas.md#codersdkworkspaceagentscriptstatus) | false | | | +| `»»» timeout` | integer | false | | | +| `»» started_at` | string(date-time) | false | | | +| `»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | false | | Startup script behavior is a legacy field that is deprecated in favor of the `coder_script` resource. It's only referenced by old clients. Deprecated: Remove in the future! | +| `»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | | +| `»» subsystems` | array | false | | | +| `»» troubleshooting_url` | string | false | | | +| `»» updated_at` | string(date-time) | false | | | +| `»» version` | string | false | | | +| `» created_at` | string(date-time) | false | | | +| `» daily_cost` | integer | false | | | +| `» hide` | boolean | false | | | +| `» icon` | string | false | | | +| `» id` | string(uuid) | false | | | +| `» job_id` | string(uuid) | false | | | +| `» metadata` | array | false | | | +| `»» key` | string | false | | | +| `»» sensitive` | boolean | false | | | +| `»» value` | string | false | | | +| `» name` | string | false | | | +| `» type` | string | false | | | +| `» workspace_transition` | [codersdk.WorkspaceTransition](schemas.md#codersdkworkspacetransition) | false | | | #### Enumerated Values diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 083d1628b9857..60c148f87af35 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -10664,7 +10664,8 @@ export interface WorkspaceAgent { /** * Metadata is only populated on the workspaces list endpoint when the * request opts in with the include_agent_metadata search key, and it - * only carries the requested keys. + * only carries the requested keys. The description's script is always + * empty here: it can be long, and list consumers want values. */ readonly metadata?: readonly WorkspaceAgentMetadata[]; /** From 01dc8cd28b0a04683d60d940a71ac3a250df2379 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 6 Aug 2026 22:05:19 +0000 Subject: [PATCH 3/7] refactor(coderd/database): reuse the latest build job for the metadata aggregate The latest_build lateral already resolves the provisioner job; propagate it through the CTE chain as latest_build_provisioner_job_id so the agent_metadata aggregate joins resources by job ID instead of re-deriving the latest build with a max(build_number) lookup. --- coderd/database/modelqueries.go | 1 + coderd/database/queries.sql.go | 30 +++++++++++--------------- coderd/database/queries/workspaces.sql | 22 ++++++------------- 3 files changed, 20 insertions(+), 33 deletions(-) diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 3a73660a6020c..df1b61ffbb595 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -337,6 +337,7 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa &i.LatestBuildTransition, &i.LatestBuildStatus, &i.LatestBuildHasExternalAgent, + &i.LatestBuildProvisionerJobID, &i.AgentMetadata, &i.Count, ); err != nil { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 3672469fcd410..b3f68df429d82 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -38523,7 +38523,8 @@ SELECT latest_build.error as latest_build_error, latest_build.transition as latest_build_transition, latest_build.job_status as latest_build_status, - latest_build.has_external_agent as latest_build_has_external_agent + latest_build.has_external_agent as latest_build_has_external_agent, + latest_build.provisioner_job_id as latest_build_provisioner_job_id FROM workspaces_expanded as workspaces JOIN @@ -38800,7 +38801,7 @@ WHERE -- @authorize_filter ), filtered_workspaces_order AS ( SELECT - fw.id, fw.created_at, fw.updated_at, fw.owner_id, fw.organization_id, fw.template_id, fw.deleted, fw.name, fw.autostart_schedule, fw.ttl, fw.last_used_at, fw.dormant_at, fw.deleting_at, fw.automatic_updates, fw.favorite, fw.next_start_at, fw.group_acl, fw.user_acl, fw.owner_avatar_url, fw.owner_username, fw.owner_name, fw.organization_name, fw.organization_display_name, fw.organization_icon, fw.organization_description, fw.template_name, fw.template_display_name, fw.template_icon, fw.template_description, fw.task_id, fw.group_acl_display_info, fw.user_acl_display_info, fw.template_version_id, fw.template_version_name, fw.latest_build_completed_at, fw.latest_build_canceled_at, fw.latest_build_error, fw.latest_build_transition, fw.latest_build_status, fw.latest_build_has_external_agent + fw.id, fw.created_at, fw.updated_at, fw.owner_id, fw.organization_id, fw.template_id, fw.deleted, fw.name, fw.autostart_schedule, fw.ttl, fw.last_used_at, fw.dormant_at, fw.deleting_at, fw.automatic_updates, fw.favorite, fw.next_start_at, fw.group_acl, fw.user_acl, fw.owner_avatar_url, fw.owner_username, fw.owner_name, fw.organization_name, fw.organization_display_name, fw.organization_icon, fw.organization_description, fw.template_name, fw.template_display_name, fw.template_icon, fw.template_description, fw.task_id, fw.group_acl_display_info, fw.user_acl_display_info, fw.template_version_id, fw.template_version_name, fw.latest_build_completed_at, fw.latest_build_canceled_at, fw.latest_build_error, fw.latest_build_transition, fw.latest_build_status, fw.latest_build_has_external_agent, fw.latest_build_provisioner_job_id FROM filtered_workspaces fw ORDER BY @@ -38821,7 +38822,7 @@ WHERE $26 ), filtered_workspaces_order_with_summary AS ( SELECT - fwo.id, fwo.created_at, fwo.updated_at, fwo.owner_id, fwo.organization_id, fwo.template_id, fwo.deleted, fwo.name, fwo.autostart_schedule, fwo.ttl, fwo.last_used_at, fwo.dormant_at, fwo.deleting_at, fwo.automatic_updates, fwo.favorite, fwo.next_start_at, fwo.group_acl, fwo.user_acl, fwo.owner_avatar_url, fwo.owner_username, fwo.owner_name, fwo.organization_name, fwo.organization_display_name, fwo.organization_icon, fwo.organization_description, fwo.template_name, fwo.template_display_name, fwo.template_icon, fwo.template_description, fwo.task_id, fwo.group_acl_display_info, fwo.user_acl_display_info, fwo.template_version_id, fwo.template_version_name, fwo.latest_build_completed_at, fwo.latest_build_canceled_at, fwo.latest_build_error, fwo.latest_build_transition, fwo.latest_build_status, fwo.latest_build_has_external_agent + fwo.id, fwo.created_at, fwo.updated_at, fwo.owner_id, fwo.organization_id, fwo.template_id, fwo.deleted, fwo.name, fwo.autostart_schedule, fwo.ttl, fwo.last_used_at, fwo.dormant_at, fwo.deleting_at, fwo.automatic_updates, fwo.favorite, fwo.next_start_at, fwo.group_acl, fwo.user_acl, fwo.owner_avatar_url, fwo.owner_username, fwo.owner_name, fwo.organization_name, fwo.organization_display_name, fwo.organization_icon, fwo.organization_description, fwo.template_name, fwo.template_display_name, fwo.template_icon, fwo.template_description, fwo.task_id, fwo.group_acl_display_info, fwo.user_acl_display_info, fwo.template_version_id, fwo.template_version_name, fwo.latest_build_completed_at, fwo.latest_build_canceled_at, fwo.latest_build_error, fwo.latest_build_transition, fwo.latest_build_status, fwo.latest_build_has_external_agent, fwo.latest_build_provisioner_job_id FROM filtered_workspaces_order fwo -- Return a technical summary row with total count of workspaces. @@ -38868,7 +38869,8 @@ WHERE '', -- latest_build_error 'start'::workspace_transition, -- latest_build_transition 'unknown'::provisioner_job_status, -- latest_build_status - false -- latest_build_has_external_agent + false, -- latest_build_has_external_agent + '00000000-0000-0000-0000-000000000000'::uuid -- latest_build_provisioner_job_id WHERE $28 :: boolean = true ), total_count AS ( @@ -38878,7 +38880,7 @@ WHERE filtered_workspaces ) SELECT - fwos.id, fwos.created_at, fwos.updated_at, fwos.owner_id, fwos.organization_id, fwos.template_id, fwos.deleted, fwos.name, fwos.autostart_schedule, fwos.ttl, fwos.last_used_at, fwos.dormant_at, fwos.deleting_at, fwos.automatic_updates, fwos.favorite, fwos.next_start_at, fwos.group_acl, fwos.user_acl, fwos.owner_avatar_url, fwos.owner_username, fwos.owner_name, fwos.organization_name, fwos.organization_display_name, fwos.organization_icon, fwos.organization_description, fwos.template_name, fwos.template_display_name, fwos.template_icon, fwos.template_description, fwos.task_id, fwos.group_acl_display_info, fwos.user_acl_display_info, fwos.template_version_id, fwos.template_version_name, fwos.latest_build_completed_at, fwos.latest_build_canceled_at, fwos.latest_build_error, fwos.latest_build_transition, fwos.latest_build_status, fwos.latest_build_has_external_agent, + fwos.id, fwos.created_at, fwos.updated_at, fwos.owner_id, fwos.organization_id, fwos.template_id, fwos.deleted, fwos.name, fwos.autostart_schedule, fwos.ttl, fwos.last_used_at, fwos.dormant_at, fwos.deleting_at, fwos.automatic_updates, fwos.favorite, fwos.next_start_at, fwos.group_acl, fwos.user_acl, fwos.owner_avatar_url, fwos.owner_username, fwos.owner_name, fwos.organization_name, fwos.organization_display_name, fwos.organization_icon, fwos.organization_description, fwos.template_name, fwos.template_display_name, fwos.template_icon, fwos.template_description, fwos.task_id, fwos.group_acl_display_info, fwos.user_acl_display_info, fwos.template_version_id, fwos.template_version_name, fwos.latest_build_completed_at, fwos.latest_build_canceled_at, fwos.latest_build_error, fwos.latest_build_transition, fwos.latest_build_status, fwos.latest_build_has_external_agent, fwos.latest_build_provisioner_job_id, -- agent_metadata expands the response with the requested agent -- metadata keys for the latest build's agents. The CASE keeps the -- subquery unevaluated for every caller that does not opt in, and @@ -38908,24 +38910,14 @@ SELECT workspace_resources ON workspace_resources.id = workspace_agents.resource_id - JOIN - workspace_builds - ON - workspace_builds.job_id = workspace_resources.job_id JOIN workspace_agent_metadata ON workspace_agent_metadata.workspace_agent_id = workspace_agents.id WHERE - workspace_builds.workspace_id = fwos.id - AND workspace_builds.build_number = ( - SELECT - max(build_number) - FROM - workspace_builds - WHERE - workspace_builds.workspace_id = fwos.id - ) + -- The latest build's job was already resolved by the + -- latest_build lateral; resources hang off its job. + workspace_resources.job_id = fwos.latest_build_provisioner_job_id -- Filter out deleted sub agents. AND workspace_agents.deleted = FALSE AND LOWER(workspace_agent_metadata.key) = ANY($1 :: text[]) @@ -39013,6 +39005,7 @@ type GetWorkspacesRow struct { LatestBuildTransition WorkspaceTransition `db:"latest_build_transition" json:"latest_build_transition"` LatestBuildStatus ProvisionerJobStatus `db:"latest_build_status" json:"latest_build_status"` LatestBuildHasExternalAgent sql.NullBool `db:"latest_build_has_external_agent" json:"latest_build_has_external_agent"` + LatestBuildProvisionerJobID uuid.UUID `db:"latest_build_provisioner_job_id" json:"latest_build_provisioner_job_id"` AgentMetadata json.RawMessage `db:"agent_metadata" json:"agent_metadata"` Count int64 `db:"count" json:"count"` } @@ -39099,6 +39092,7 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) &i.LatestBuildTransition, &i.LatestBuildStatus, &i.LatestBuildHasExternalAgent, + &i.LatestBuildProvisionerJobID, &i.AgentMetadata, &i.Count, ); err != nil { diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index b8705277c87e8..db66e28af02ba 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -117,7 +117,8 @@ SELECT latest_build.error as latest_build_error, latest_build.transition as latest_build_transition, latest_build.job_status as latest_build_status, - latest_build.has_external_agent as latest_build_has_external_agent + latest_build.has_external_agent as latest_build_has_external_agent, + latest_build.provisioner_job_id as latest_build_provisioner_job_id FROM workspaces_expanded as workspaces JOIN @@ -462,7 +463,8 @@ WHERE '', -- latest_build_error 'start'::workspace_transition, -- latest_build_transition 'unknown'::provisioner_job_status, -- latest_build_status - false -- latest_build_has_external_agent + false, -- latest_build_has_external_agent + '00000000-0000-0000-0000-000000000000'::uuid -- latest_build_provisioner_job_id WHERE @with_summary :: boolean = true ), total_count AS ( @@ -502,24 +504,14 @@ SELECT workspace_resources ON workspace_resources.id = workspace_agents.resource_id - JOIN - workspace_builds - ON - workspace_builds.job_id = workspace_resources.job_id JOIN workspace_agent_metadata ON workspace_agent_metadata.workspace_agent_id = workspace_agents.id WHERE - workspace_builds.workspace_id = fwos.id - AND workspace_builds.build_number = ( - SELECT - max(build_number) - FROM - workspace_builds - WHERE - workspace_builds.workspace_id = fwos.id - ) + -- The latest build's job was already resolved by the + -- latest_build lateral; resources hang off its job. + workspace_resources.job_id = fwos.latest_build_provisioner_job_id -- Filter out deleted sub agents. AND workspace_agents.deleted = FALSE AND LOWER(workspace_agent_metadata.key) = ANY(@include_agent_metadata :: text[]) From ec1e4d26838a66872432aa8272d4463a0b42b38b Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 6 Aug 2026 22:07:08 +0000 Subject: [PATCH 4/7] chore(coderd): rename the test metadata key to task_status --- coderd/searchquery/search_test.go | 4 ++-- coderd/workspaces_test.go | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/coderd/searchquery/search_test.go b/coderd/searchquery/search_test.go index 752a966f566cf..ea82f343345ea 100644 --- a/coderd/searchquery/search_test.go +++ b/coderd/searchquery/search_test.go @@ -343,9 +343,9 @@ func TestSearchWorkspace(t *testing.T) { }, { Name: "IncludeAgentMetadata", - Query: `include_agent_metadata:"byoc_status" include_agent_metadata:"cpu"`, + Query: `include_agent_metadata:"task_status" include_agent_metadata:"cpu"`, Expected: database.GetWorkspacesParams{ - IncludeAgentMetadata: []string{"byoc_status", "cpu"}, + IncludeAgentMetadata: []string{"task_status", "cpu"}, }, }, { diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 37b6bc8bcafaf..815c1b7e8404d 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -2885,7 +2885,7 @@ func TestWorkspaceFilterManual(t *testing.T) { //nolint:gocritic // This is a test; only the agent API writes metadata. ctx := dbauthz.AsSystemRestricted(context.Background()) collectedAt := dbtime.Now() - for i, key := range []string{"byoc_status", "cpu", "unrequested"} { + for i, key := range []string{"task_status", "cpu", "unrequested"} { err := db.InsertWorkspaceAgentMetadata(ctx, database.InsertWorkspaceAgentMetadataParams{ WorkspaceAgentID: agentID, DisplayName: key, @@ -2926,7 +2926,7 @@ func TestWorkspaceFilterManual(t *testing.T) { // Opting in returns exactly the requested keys, ordered by // display_order, with their collected values. res, err = client.Workspaces(reqCtx, codersdk.WorkspaceFilter{ - IncludeAgentMetadata: []string{"byoc_status", "cpu"}, + IncludeAgentMetadata: []string{"task_status", "cpu"}, }) require.NoError(t, err) metadata := findAgent(res).Metadata @@ -2936,8 +2936,8 @@ func TestWorkspaceFilterManual(t *testing.T) { // The collection script is deliberately not exposed on the list // endpoint; it can be long. require.Empty(t, metadata[0].Description.Script) - require.Equal(t, "byoc_status", metadata[1].Description.Key) - require.Equal(t, "value-byoc_status", metadata[1].Result.Value) + require.Equal(t, "task_status", metadata[1].Description.Key) + require.Equal(t, "value-task_status", metadata[1].Result.Value) require.WithinDuration(t, collectedAt, metadata[1].Result.CollectedAt, time.Second) // Unknown keys are not an error; the metadata is just absent. From 92524db60e4953e1f4ea43458fbecaf72fa64f5f Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 6 Aug 2026 22:13:53 +0000 Subject: [PATCH 5/7] refactor(coderd): move the agent metadata JSON decoding into the database package sqlc column overrides require tablename.colname against a real relation, and agent_metadata is a query expression, so a types.go Scanner cannot be wired to the generated row. Instead the row gains ParseAgentMetadata, keeping the JSON handling in the database package and the handler free of unmarshaling. --- coderd/database/modelmethods.go | 27 +++++++++++++++++++++++++++ coderd/workspaces.go | 15 +++++---------- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index fae247adbf81a..b131a311e89c3 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -2,6 +2,7 @@ package database import ( "database/sql" + "encoding/json" "fmt" "slices" "sort" @@ -769,6 +770,32 @@ func ConvertUserRows(rows []GetUsersRow) []User { return users } +// AgentMetadataAggregate is the agent_metadata jsonb array the +// GetWorkspaces query aggregates for the include_agent_metadata +// expansion. Elements have WorkspaceAgentMetadatum's JSON shape; each +// carries its workspace_agent_id so multi-agent workspaces can map +// values onto the right agent. +// +// A sqlc column override would be cleaner, but overrides require a +// '[catalog.][schema.]tablename.colname' against a real relation and +// agent_metadata is a query expression, so the row keeps +// json.RawMessage and ParseAgentMetadata does the decoding here. +type AgentMetadataAggregate []WorkspaceAgentMetadatum + +// ParseAgentMetadata decodes the row's agent_metadata aggregate. It is +// empty unless the query opted in with include_agent_metadata. +func (r GetWorkspacesRow) ParseAgentMetadata() (AgentMetadataAggregate, error) { + if len(r.AgentMetadata) == 0 { + return nil, nil + } + var metadata AgentMetadataAggregate + err := json.Unmarshal(r.AgentMetadata, &metadata) + if err != nil { + return nil, xerrors.Errorf("unmarshal agent metadata for workspace %q: %w", r.ID, err) + } + return metadata, nil +} + func ConvertWorkspaceRows(rows []GetWorkspacesRow) ([]Workspace, error) { workspaces := make([]Workspace, len(rows)) for i, r := range rows { diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 9740dbdb9e940..1d9f5a90d395c 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -2769,22 +2769,17 @@ func (api *API) workspaceData(ctx context.Context, workspaces []database.Workspa }, nil } -// attachAgentMetadata maps the agent_metadata JSON the workspaces query +// attachAgentMetadata maps the agent metadata the workspaces query // aggregated per workspace onto the agents in the converted response. -// The aggregate elements have the database.WorkspaceAgentMetadatum JSON -// shape, each carrying its workspace_agent_id. +// Each aggregated datum carries its workspace_agent_id. func attachAgentMetadata(workspaces []codersdk.Workspace, rows []database.GetWorkspacesRow) error { byAgent := map[uuid.UUID][]database.WorkspaceAgentMetadatum{} for _, row := range rows { - if len(row.AgentMetadata) == 0 { - continue - } - var data []database.WorkspaceAgentMetadatum - err := json.Unmarshal(row.AgentMetadata, &data) + metadata, err := row.ParseAgentMetadata() if err != nil { - return xerrors.Errorf("unmarshal agent metadata for workspace %q: %w", row.ID, err) + return err } - for _, datum := range data { + for _, datum := range metadata { byAgent[datum.WorkspaceAgentID] = append(byAgent[datum.WorkspaceAgentID], datum) } } From cd0813606aa4883fa7e7f77f4b908eeb9eb279fd Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 6 Aug 2026 22:21:10 +0000 Subject: [PATCH 6/7] refactor(coderd/database): scan the agent metadata aggregate like the ACL columns AgentMetadataAggregate moves to types.go with Scan/Value, matching how ConvertWorkspaceRows handles user_acl and group_acl; the handler scans the row's raw JSON into the typed aggregate. --- coderd/database/modelmethods.go | 27 --------------------------- coderd/database/types.go | 28 ++++++++++++++++++++++++++++ coderd/workspaces.go | 8 ++++++-- 3 files changed, 34 insertions(+), 29 deletions(-) diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index b131a311e89c3..fae247adbf81a 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -2,7 +2,6 @@ package database import ( "database/sql" - "encoding/json" "fmt" "slices" "sort" @@ -770,32 +769,6 @@ func ConvertUserRows(rows []GetUsersRow) []User { return users } -// AgentMetadataAggregate is the agent_metadata jsonb array the -// GetWorkspaces query aggregates for the include_agent_metadata -// expansion. Elements have WorkspaceAgentMetadatum's JSON shape; each -// carries its workspace_agent_id so multi-agent workspaces can map -// values onto the right agent. -// -// A sqlc column override would be cleaner, but overrides require a -// '[catalog.][schema.]tablename.colname' against a real relation and -// agent_metadata is a query expression, so the row keeps -// json.RawMessage and ParseAgentMetadata does the decoding here. -type AgentMetadataAggregate []WorkspaceAgentMetadatum - -// ParseAgentMetadata decodes the row's agent_metadata aggregate. It is -// empty unless the query opted in with include_agent_metadata. -func (r GetWorkspacesRow) ParseAgentMetadata() (AgentMetadataAggregate, error) { - if len(r.AgentMetadata) == 0 { - return nil, nil - } - var metadata AgentMetadataAggregate - err := json.Unmarshal(r.AgentMetadata, &metadata) - if err != nil { - return nil, xerrors.Errorf("unmarshal agent metadata for workspace %q: %w", r.ID, err) - } - return metadata, nil -} - func ConvertWorkspaceRows(rows []GetWorkspacesRow) ([]Workspace, error) { workspaces := make([]Workspace, len(rows)) for i, r := range rows { diff --git a/coderd/database/types.go b/coderd/database/types.go index 22c18cc7c008d..68ad4b5a40f97 100644 --- a/coderd/database/types.go +++ b/coderd/database/types.go @@ -120,6 +120,34 @@ type ChatACLEntry struct { Permissions []policy.Action `json:"permissions"` } +// AgentMetadataAggregate is the agent_metadata jsonb array the +// GetWorkspaces query aggregates for the include_agent_metadata +// expansion. Elements have WorkspaceAgentMetadatum's JSON shape; each +// carries its workspace_agent_id so multi-agent workspaces can map +// values onto the right agent. The generated row keeps +// json.RawMessage because sqlc overrides cannot target expression +// columns; callers Scan the raw value into this type. +type AgentMetadataAggregate []WorkspaceAgentMetadatum + +func (a *AgentMetadataAggregate) Scan(src interface{}) error { + switch v := src.(type) { + case nil: + return nil + case string: + return json.Unmarshal([]byte(v), &a) + case []byte: + return json.Unmarshal(v, &a) + case json.RawMessage: + return json.Unmarshal(v, &a) + } + + return xerrors.Errorf("unexpected type %T", src) +} + +func (a AgentMetadataAggregate) Value() (driver.Value, error) { + return json.Marshal(a) +} + type WorkspaceACL map[string]WorkspaceACLEntry func (t *WorkspaceACL) Scan(src interface{}) error { diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 1d9f5a90d395c..f9b3e5267e7d5 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -2775,9 +2775,13 @@ func (api *API) workspaceData(ctx context.Context, workspaces []database.Workspa func attachAgentMetadata(workspaces []codersdk.Workspace, rows []database.GetWorkspacesRow) error { byAgent := map[uuid.UUID][]database.WorkspaceAgentMetadatum{} for _, row := range rows { - metadata, err := row.ParseAgentMetadata() + if len(row.AgentMetadata) == 0 { + continue + } + var metadata database.AgentMetadataAggregate + err := metadata.Scan(row.AgentMetadata) if err != nil { - return err + return xerrors.Errorf("scan agent metadata for workspace %q: %w", row.ID, err) } for _, datum := range metadata { byAgent[datum.WorkspaceAgentID] = append(byAgent[datum.WorkspaceAgentID], datum) From 5ddb4df90445150ba31abcd94112c715dae5956d Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 10 Aug 2026 12:50:49 +0000 Subject: [PATCH 7/7] fix(coderd): lowercase both sides of the metadata key match and document the search key Review feedback: the stored key was lowercased but the requested array was only lowercase by virtue of the search parser, which is surprising for any other caller of the query; normalize both sides in SQL. The list endpoint's q parameter doc now names include_agent_metadata. --- coderd/apidoc/docs.go | 2 +- coderd/apidoc/swagger.json | 2 +- coderd/database/queries.sql.go | 10 +++++++--- coderd/database/queries/workspaces.sql | 10 +++++++--- coderd/workspaces.go | 2 +- coderd/workspaces_test.go | 9 ++++++--- docs/reference/api/workspaces.md | 10 +++++----- 7 files changed, 28 insertions(+), 17 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 25d6b51be261b..afc1f9e7f31df 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -13551,7 +13551,7 @@ const docTemplate = `{ "parameters": [ { "type": "string", - "description": "Search query in the format ` + "`" + `key:value` + "`" + `. Available keys are: owner, template, name, status, has-agent, dormant, last_used_after, last_used_before, has-ai-task, has_external_agent, healthy.", + "description": "Search query in the format ` + "`" + `key:value` + "`" + `. Available keys are: owner, template, name, status, has-agent, dormant, last_used_after, last_used_before, has-ai-task, has_external_agent, healthy, include_agent_metadata (expands each agent with the named metadata keys rather than filtering; repeat the key for multiple items).", "name": "q", "in": "query" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 0a8aee377ef70..c14b8c2c82811 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -12021,7 +12021,7 @@ "parameters": [ { "type": "string", - "description": "Search query in the format `key:value`. Available keys are: owner, template, name, status, has-agent, dormant, last_used_after, last_used_before, has-ai-task, has_external_agent, healthy.", + "description": "Search query in the format `key:value`. Available keys are: owner, template, name, status, has-agent, dormant, last_used_after, last_used_before, has-ai-task, has_external_agent, healthy, include_agent_metadata (expands each agent with the named metadata keys rather than filtering; repeat the key for multiple items).", "name": "q", "in": "query" }, diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index b3f68df429d82..d79af1da687fc 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -38886,8 +38886,7 @@ SELECT -- subquery unevaluated for every caller that does not opt in, and -- it only runs for the returned page. Each element carries the -- workspace_agent_id so multi-agent workspaces can map values onto - -- the right agent. Keys match case-insensitively because search - -- queries are lowercased. + -- the right agent. CASE WHEN cardinality($1 :: text[]) > 0 THEN COALESCE(( SELECT @@ -38920,7 +38919,12 @@ SELECT workspace_resources.job_id = fwos.latest_build_provisioner_job_id -- Filter out deleted sub agents. AND workspace_agents.deleted = FALSE - AND LOWER(workspace_agent_metadata.key) = ANY($1 :: text[]) + -- Both sides are lowercased so matching is + -- case-insensitive regardless of how the caller cased + -- the requested keys. + AND LOWER(workspace_agent_metadata.key) = ANY(ARRAY( + SELECT LOWER(key) FROM unnest($1 :: text[]) AS k(key) + )) ), '[]'::jsonb) ELSE -- Never NULL: lib/pq cannot scan NULL into json.RawMessage. diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index db66e28af02ba..42cdec235a3ae 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -480,8 +480,7 @@ SELECT -- subquery unevaluated for every caller that does not opt in, and -- it only runs for the returned page. Each element carries the -- workspace_agent_id so multi-agent workspaces can map values onto - -- the right agent. Keys match case-insensitively because search - -- queries are lowercased. + -- the right agent. CASE WHEN cardinality(@include_agent_metadata :: text[]) > 0 THEN COALESCE(( SELECT @@ -514,7 +513,12 @@ SELECT workspace_resources.job_id = fwos.latest_build_provisioner_job_id -- Filter out deleted sub agents. AND workspace_agents.deleted = FALSE - AND LOWER(workspace_agent_metadata.key) = ANY(@include_agent_metadata :: text[]) + -- Both sides are lowercased so matching is + -- case-insensitive regardless of how the caller cased + -- the requested keys. + AND LOWER(workspace_agent_metadata.key) = ANY(ARRAY( + SELECT LOWER(key) FROM unnest(@include_agent_metadata :: text[]) AS k(key) + )) ), '[]'::jsonb) ELSE -- Never NULL: lib/pq cannot scan NULL into json.RawMessage. diff --git a/coderd/workspaces.go b/coderd/workspaces.go index f9b3e5267e7d5..8d9f1c4c1aae3 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -143,7 +143,7 @@ func (api *API) workspace(rw http.ResponseWriter, r *http.Request) { // @Security CoderSessionToken // @Produce json // @Tags Workspaces -// @Param q query string false "Search query in the format `key:value`. Available keys are: owner, template, name, status, has-agent, dormant, last_used_after, last_used_before, has-ai-task, has_external_agent, healthy." +// @Param q query string false "Search query in the format `key:value`. Available keys are: owner, template, name, status, has-agent, dormant, last_used_after, last_used_before, has-ai-task, has_external_agent, healthy, include_agent_metadata (expands each agent with the named metadata keys rather than filtering; repeat the key for multiple items)." // @Param limit query int false "Page limit" // @Param offset query int false "Page offset" // @Success 200 {object} codersdk.WorkspacesResponse diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 815c1b7e8404d..1c75bd7de5a40 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -2885,7 +2885,10 @@ func TestWorkspaceFilterManual(t *testing.T) { //nolint:gocritic // This is a test; only the agent API writes metadata. ctx := dbauthz.AsSystemRestricted(context.Background()) collectedAt := dbtime.Now() - for i, key := range []string{"task_status", "cpu", "unrequested"} { + // Task_Status is mixed-case on purpose: requested keys are + // lowercased by the search parser, and the query matches stored + // keys case-insensitively. + for i, key := range []string{"Task_Status", "cpu", "unrequested"} { err := db.InsertWorkspaceAgentMetadata(ctx, database.InsertWorkspaceAgentMetadataParams{ WorkspaceAgentID: agentID, DisplayName: key, @@ -2936,8 +2939,8 @@ func TestWorkspaceFilterManual(t *testing.T) { // The collection script is deliberately not exposed on the list // endpoint; it can be long. require.Empty(t, metadata[0].Description.Script) - require.Equal(t, "task_status", metadata[1].Description.Key) - require.Equal(t, "value-task_status", metadata[1].Result.Value) + require.Equal(t, "Task_Status", metadata[1].Description.Key) + require.Equal(t, "value-Task_Status", metadata[1].Result.Value) require.WithinDuration(t, collectedAt, metadata[1].Result.CollectedAt, time.Second) // Unknown keys are not an error; the metadata is just absent. diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md index 5f163082873c2..eb2787b06b5c5 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -1099,11 +1099,11 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \ ### Parameters -| Name | In | Type | Required | Description | -|----------|-------|---------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `q` | query | string | false | Search query in the format `key:value`. Available keys are: owner, template, name, status, has-agent, dormant, last_used_after, last_used_before, has-ai-task, has_external_agent, healthy. | -| `limit` | query | integer | false | Page limit | -| `offset` | query | integer | false | Page offset | +| Name | In | Type | Required | Description | +|----------|-------|---------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `q` | query | string | false | Search query in the format `key:value`. Available keys are: owner, template, name, status, has-agent, dormant, last_used_after, last_used_before, has-ai-task, has_external_agent, healthy, include_agent_metadata (expands each agent with the named metadata keys rather than filtering; repeat the key for multiple items). | +| `limit` | query | integer | false | Page limit | +| `offset` | query | integer | false | Page offset | ### Example responses