feat: add template abstract metadata

This commit is contained in:
Michael Suchacz
2026-06-02 20:24:25 +00:00
parent b49344519b
commit eaf86a753b
33 changed files with 667 additions and 83 deletions
+14
View File
@@ -17883,6 +17883,11 @@ const docTemplate = `{
"template_version_id"
],
"properties": {
"abstract": {
"description": "Abstract is a longer-form summary surfaced to agents to help them pick\nthe right template. Up to 2048 characters.",
"type": "string",
"maxLength": 2048
},
"activity_bump_ms": {
"description": "ActivityBumpMillis allows optionally specifying the activity bump\nduration for all workspaces created from this template. Defaults to 1h\nbut can be set to 0 to disable activity bumping.",
"type": "integer"
@@ -23315,6 +23320,10 @@ const docTemplate = `{
"codersdk.Template": {
"type": "object",
"properties": {
"abstract": {
"description": "Abstract is a longer-form summary surfaced to agents to help them pick\nthe right template. Up to 2048 characters.",
"type": "string"
},
"active_user_count": {
"description": "ActiveUserCount is set to -1 when loading.",
"type": "integer"
@@ -24427,6 +24436,11 @@ const docTemplate = `{
"codersdk.UpdateTemplateMeta": {
"type": "object",
"properties": {
"abstract": {
"description": "Abstract is a longer-form summary surfaced to agents to help them pick\nthe right template. Up to 2048 characters.",
"type": "string",
"maxLength": 2048
},
"activity_bump_ms": {
"description": "ActivityBumpMillis allows optionally specifying the activity bump\nduration for all workspaces created from this template. Defaults to 1h\nbut can be set to 0 to disable activity bumping.",
"type": "integer"
+14
View File
@@ -16174,6 +16174,11 @@
"type": "object",
"required": ["name", "template_version_id"],
"properties": {
"abstract": {
"description": "Abstract is a longer-form summary surfaced to agents to help them pick\nthe right template. Up to 2048 characters.",
"type": "string",
"maxLength": 2048
},
"activity_bump_ms": {
"description": "ActivityBumpMillis allows optionally specifying the activity bump\nduration for all workspaces created from this template. Defaults to 1h\nbut can be set to 0 to disable activity bumping.",
"type": "integer"
@@ -21411,6 +21416,10 @@
"codersdk.Template": {
"type": "object",
"properties": {
"abstract": {
"description": "Abstract is a longer-form summary surfaced to agents to help them pick\nthe right template. Up to 2048 characters.",
"type": "string"
},
"active_user_count": {
"description": "ActiveUserCount is set to -1 when loading.",
"type": "integer"
@@ -22464,6 +22473,11 @@
"codersdk.UpdateTemplateMeta": {
"type": "object",
"properties": {
"abstract": {
"description": "Abstract is a longer-form summary surfaced to agents to help them pick\nthe right template. Up to 2048 characters.",
"type": "string",
"maxLength": 2048
},
"activity_bump_ms": {
"description": "ActivityBumpMillis allows optionally specifying the activity bump\nduration for all workspaces created from this template. Defaults to 1h\nbut can be set to 0 to disable activity bumping.",
"type": "integer"
+1
View File
@@ -535,6 +535,7 @@ func Template(t testing.TB, db database.Store, seed database.Template) database.
Provisioner: takeFirst(seed.Provisioner, database.ProvisionerTypeEcho),
ActiveVersionID: takeFirst(seed.ActiveVersionID, uuid.New()),
Description: takeFirst(seed.Description, testutil.GetRandomName(t)),
Abstract: seed.Abstract,
CreatedBy: takeFirst(seed.CreatedBy, uuid.New()),
Icon: takeFirst(seed.Icon, testutil.GetRandomName(t)),
UserACL: seed.UserACL,
+5 -1
View File
@@ -3095,7 +3095,8 @@ CREATE TABLE templates (
max_port_sharing_level app_sharing_level DEFAULT 'owner'::app_sharing_level NOT NULL,
use_classic_parameter_flow boolean DEFAULT false NOT NULL,
cors_behavior cors_behavior DEFAULT 'simple'::cors_behavior NOT NULL,
disable_module_cache boolean DEFAULT false NOT NULL
disable_module_cache boolean DEFAULT false NOT NULL,
abstract character varying(2048) DEFAULT ''::character varying NOT NULL
);
COMMENT ON COLUMN templates.default_ttl IS 'The default duration for autostop for workspaces created from this template.';
@@ -3118,6 +3119,8 @@ COMMENT ON COLUMN templates.deprecated IS 'If set to a non empty string, the tem
COMMENT ON COLUMN templates.use_classic_parameter_flow IS 'Determines whether to default to the dynamic parameter creation flow for this template or continue using the legacy classic parameter creation flow.This is a template wide setting, the template admin can revert to the classic flow if there are any issues. An escape hatch is required, as workspace creation is a core workflow and cannot break. This column will be removed when the dynamic parameter creation flow is stable.';
COMMENT ON COLUMN templates.abstract IS 'Longer-form summary used to help agents pick the right template.';
CREATE VIEW template_with_names AS
SELECT templates.id,
templates.created_at,
@@ -3150,6 +3153,7 @@ CREATE VIEW template_with_names AS
templates.use_classic_parameter_flow,
templates.cors_behavior,
templates.disable_module_cache,
templates.abstract,
COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url,
COALESCE(visible_users.username, ''::text) AS created_by_username,
COALESCE(visible_users.name, ''::text) AS created_by_name,
@@ -0,0 +1,17 @@
DROP VIEW template_with_names;
ALTER TABLE templates DROP COLUMN abstract;
CREATE VIEW template_with_names AS
SELECT templates.*,
COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url,
COALESCE(visible_users.username, ''::text) AS created_by_username,
COALESCE(visible_users.name, ''::text) AS created_by_name,
COALESCE(organizations.name, ''::text) AS organization_name,
COALESCE(organizations.display_name, ''::text) AS organization_display_name,
COALESCE(organizations.icon, ''::text) AS organization_icon
FROM ((templates
LEFT JOIN visible_users ON ((templates.created_by = visible_users.id)))
LEFT JOIN organizations ON ((templates.organization_id = organizations.id)));
COMMENT ON VIEW template_with_names IS 'Joins in the display name information such as username, avatar, and organization name.';
@@ -0,0 +1,20 @@
DROP VIEW template_with_names;
ALTER TABLE templates
ADD COLUMN abstract character varying(2048) DEFAULT ''::character varying NOT NULL;
COMMENT ON COLUMN templates.abstract IS 'Longer-form summary used to help agents pick the right template.';
CREATE VIEW template_with_names AS
SELECT templates.*,
COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url,
COALESCE(visible_users.username, ''::text) AS created_by_username,
COALESCE(visible_users.name, ''::text) AS created_by_name,
COALESCE(organizations.name, ''::text) AS organization_name,
COALESCE(organizations.display_name, ''::text) AS organization_display_name,
COALESCE(organizations.icon, ''::text) AS organization_icon
FROM ((templates
LEFT JOIN visible_users ON ((templates.created_by = visible_users.id)))
LEFT JOIN organizations ON ((templates.organization_id = organizations.id)));
COMMENT ON VIEW template_with_names IS 'Joins in the display name information such as username, avatar, and organization name.';
+1
View File
@@ -129,6 +129,7 @@ func (q *sqlQuerier) GetAuthorizedTemplates(ctx context.Context, arg GetTemplate
&i.UseClassicParameterFlow,
&i.CorsBehavior,
&i.DisableModuleCache,
&i.Abstract,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
&i.CreatedByName,
+3
View File
@@ -5479,6 +5479,7 @@ type Template struct {
UseClassicParameterFlow bool `db:"use_classic_parameter_flow" json:"use_classic_parameter_flow"`
CorsBehavior CorsBehavior `db:"cors_behavior" json:"cors_behavior"`
DisableModuleCache bool `db:"disable_module_cache" json:"disable_module_cache"`
Abstract string `db:"abstract" json:"abstract"`
CreatedByAvatarURL string `db:"created_by_avatar_url" json:"created_by_avatar_url"`
CreatedByUsername string `db:"created_by_username" json:"created_by_username"`
CreatedByName string `db:"created_by_name" json:"created_by_name"`
@@ -5529,6 +5530,8 @@ type TemplateTable struct {
UseClassicParameterFlow bool `db:"use_classic_parameter_flow" json:"use_classic_parameter_flow"`
CorsBehavior CorsBehavior `db:"cors_behavior" json:"cors_behavior"`
DisableModuleCache bool `db:"disable_module_cache" json:"disable_module_cache"`
// Longer-form summary used to help agents pick the right template.
Abstract string `db:"abstract" json:"abstract"`
}
// Records aggregated usage statistics for templates/users. All usage is rounded up to the nearest minute.
+25 -15
View File
@@ -24767,7 +24767,7 @@ func (q *sqlQuerier) GetTemplateAverageBuildTime(ctx context.Context, templateID
const getTemplateByID = `-- name: GetTemplateByID :one
SELECT
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, abstract, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon
FROM
template_with_names
WHERE
@@ -24811,6 +24811,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
&i.UseClassicParameterFlow,
&i.CorsBehavior,
&i.DisableModuleCache,
&i.Abstract,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
&i.CreatedByName,
@@ -24823,7 +24824,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one
SELECT
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, abstract, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon
FROM
template_with_names AS templates
WHERE
@@ -24875,6 +24876,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
&i.UseClassicParameterFlow,
&i.CorsBehavior,
&i.DisableModuleCache,
&i.Abstract,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
&i.CreatedByName,
@@ -24886,7 +24888,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
}
const getTemplates = `-- name: GetTemplates :many
SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon FROM template_with_names AS templates
SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, abstract, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon FROM template_with_names AS templates
ORDER BY (name, id) ASC
`
@@ -24931,6 +24933,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
&i.UseClassicParameterFlow,
&i.CorsBehavior,
&i.DisableModuleCache,
&i.Abstract,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
&i.CreatedByName,
@@ -24953,7 +24956,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
const getTemplatesWithFilter = `-- name: GetTemplatesWithFilter :many
SELECT
t.id, t.created_at, t.updated_at, t.organization_id, t.deleted, t.name, t.provisioner, t.active_version_id, t.description, t.default_ttl, t.created_by, t.icon, t.user_acl, t.group_acl, t.display_name, t.allow_user_cancel_workspace_jobs, t.allow_user_autostart, t.allow_user_autostop, t.failure_ttl, t.time_til_dormant, t.time_til_dormant_autodelete, t.autostop_requirement_days_of_week, t.autostop_requirement_weeks, t.autostart_block_days_of_week, t.require_active_version, t.deprecated, t.activity_bump, t.max_port_sharing_level, t.use_classic_parameter_flow, t.cors_behavior, t.disable_module_cache, t.created_by_avatar_url, t.created_by_username, t.created_by_name, t.organization_name, t.organization_display_name, t.organization_icon
t.id, t.created_at, t.updated_at, t.organization_id, t.deleted, t.name, t.provisioner, t.active_version_id, t.description, t.default_ttl, t.created_by, t.icon, t.user_acl, t.group_acl, t.display_name, t.allow_user_cancel_workspace_jobs, t.allow_user_autostart, t.allow_user_autostop, t.failure_ttl, t.time_til_dormant, t.time_til_dormant_autodelete, t.autostop_requirement_days_of_week, t.autostop_requirement_weeks, t.autostart_block_days_of_week, t.require_active_version, t.deprecated, t.activity_bump, t.max_port_sharing_level, t.use_classic_parameter_flow, t.cors_behavior, t.disable_module_cache, t.abstract, t.created_by_avatar_url, t.created_by_username, t.created_by_name, t.organization_name, t.organization_display_name, t.organization_icon
FROM
template_with_names AS t
LEFT JOIN
@@ -25113,6 +25116,7 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate
&i.UseClassicParameterFlow,
&i.CorsBehavior,
&i.DisableModuleCache,
&i.Abstract,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
&i.CreatedByName,
@@ -25144,6 +25148,7 @@ INSERT INTO
provisioner,
active_version_id,
description,
abstract,
created_by,
icon,
user_acl,
@@ -25155,7 +25160,7 @@ INSERT INTO
cors_behavior
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
`
type InsertTemplateParams struct {
@@ -25167,6 +25172,7 @@ type InsertTemplateParams struct {
Provisioner ProvisionerType `db:"provisioner" json:"provisioner"`
ActiveVersionID uuid.UUID `db:"active_version_id" json:"active_version_id"`
Description string `db:"description" json:"description"`
Abstract string `db:"abstract" json:"abstract"`
CreatedBy uuid.UUID `db:"created_by" json:"created_by"`
Icon string `db:"icon" json:"icon"`
UserACL TemplateACL `db:"user_acl" json:"user_acl"`
@@ -25188,6 +25194,7 @@ func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParam
arg.Provisioner,
arg.ActiveVersionID,
arg.Description,
arg.Abstract,
arg.CreatedBy,
arg.Icon,
arg.UserACL,
@@ -25291,15 +25298,16 @@ UPDATE
SET
updated_at = $2,
description = $3,
name = $4,
icon = $5,
display_name = $6,
allow_user_cancel_workspace_jobs = $7,
group_acl = $8,
max_port_sharing_level = $9,
use_classic_parameter_flow = $10,
cors_behavior = $11,
disable_module_cache = $12
abstract = $4,
name = $5,
icon = $6,
display_name = $7,
allow_user_cancel_workspace_jobs = $8,
group_acl = $9,
max_port_sharing_level = $10,
use_classic_parameter_flow = $11,
cors_behavior = $12,
disable_module_cache = $13
WHERE
id = $1
`
@@ -25308,6 +25316,7 @@ type UpdateTemplateMetaByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Description string `db:"description" json:"description"`
Abstract string `db:"abstract" json:"abstract"`
Name string `db:"name" json:"name"`
Icon string `db:"icon" json:"icon"`
DisplayName string `db:"display_name" json:"display_name"`
@@ -25324,6 +25333,7 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl
arg.ID,
arg.UpdatedAt,
arg.Description,
arg.Abstract,
arg.Name,
arg.Icon,
arg.DisplayName,
@@ -35319,7 +35329,7 @@ LEFT JOIN LATERAL (
) latest_build ON TRUE
LEFT JOIN LATERAL (
SELECT
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, abstract
FROM
templates
WHERE
+12 -10
View File
@@ -129,6 +129,7 @@ INSERT INTO
provisioner,
active_version_id,
description,
abstract,
created_by,
icon,
user_acl,
@@ -140,7 +141,7 @@ INSERT INTO
cors_behavior
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17);
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18);
-- name: UpdateTemplateActiveVersionByID :exec
UPDATE
@@ -166,15 +167,16 @@ UPDATE
SET
updated_at = $2,
description = $3,
name = $4,
icon = $5,
display_name = $6,
allow_user_cancel_workspace_jobs = $7,
group_acl = $8,
max_port_sharing_level = $9,
use_classic_parameter_flow = $10,
cors_behavior = $11,
disable_module_cache = $12
abstract = $4,
name = $5,
icon = $6,
display_name = $7,
allow_user_cancel_workspace_jobs = $8,
group_acl = $9,
max_port_sharing_level = $10,
use_classic_parameter_flow = $11,
cors_behavior = $12,
disable_module_cache = $13
WHERE
id = $1
;
+4
View File
@@ -220,6 +220,7 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
OrganizationID: organization.ID,
Name: createTemplate.Name,
Description: createTemplate.Description,
Abstract: createTemplate.Abstract,
CreatedBy: apiKey.UserID,
Icon: createTemplate.Icon,
DisplayName: createTemplate.DisplayName,
@@ -429,6 +430,7 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
Provisioner: importJob.Provisioner,
ActiveVersionID: templateVersion.ID,
Description: createTemplate.Description,
Abstract: createTemplate.Abstract,
CreatedBy: apiKey.UserID,
UserACL: database.TemplateACL{},
GroupACL: defaultsGroups,
@@ -764,6 +766,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
Name: resolved.name,
DisplayName: resolved.displayName,
Description: resolved.description,
Abstract: resolved.abstract,
Icon: resolved.icon,
AllowUserCancelWorkspaceJobs: resolved.allowUserCancelWorkspaceJobs,
GroupACL: resolved.groupACL,
@@ -1017,6 +1020,7 @@ func (api *API) convertTemplate(
ActiveUserCount: owners,
BuildTimeStats: buildTimeStats,
Description: template.Description,
Abstract: template.Abstract,
Icon: template.Icon,
DefaultTTLMillis: time.Duration(template.DefaultTTL).Milliseconds(),
ActivityBumpMillis: time.Duration(template.ActivityBump).Milliseconds(),
+2
View File
@@ -19,6 +19,7 @@ type templateMetaUpdate struct {
name string
displayName string
description string
abstract string
icon string
defaultTTLMillis int64
activityBumpMillis int64
@@ -70,6 +71,7 @@ func resolveTemplateMetaUpdate(
name: ptr.NilToDefault(req.Name, template.Name),
displayName: ptr.NilToDefault(req.DisplayName, template.DisplayName),
description: ptr.NilToDefault(req.Description, template.Description),
abstract: ptr.NilToDefault(req.Abstract, template.Abstract),
icon: ptr.NilToDefault(req.Icon, template.Icon),
defaultTTLMillis: ptr.NilToDefault(req.DefaultTTLMillis, time.Duration(template.DefaultTTL).Milliseconds()),
activityBumpMillis: ptr.NilToDefault(req.ActivityBumpMillis, time.Duration(template.ActivityBump).Milliseconds()),
@@ -42,6 +42,7 @@ func baselineTemplate() database.Template {
UseClassicParameterFlow: true,
CorsBehavior: database.CorsBehaviorPassthru,
DisableModuleCache: true,
Abstract: "Existing abstract.",
GroupACL: database.TemplateACL{
orgID.String(): {"read"},
},
@@ -70,6 +71,7 @@ func baselineResolved() templateMetaUpdate {
name: tpl.Name,
displayName: tpl.DisplayName,
description: tpl.Description,
abstract: tpl.Abstract,
icon: tpl.Icon,
defaultTTLMillis: tpl.DefaultTTL / 1e6,
activityBumpMillis: tpl.ActivityBump / 1e6,
@@ -148,6 +150,20 @@ func TestResolveTemplateMetaUpdate(t *testing.T) {
r.description = "New description"
}},
},
{
name: "Abstract",
req: codersdk.UpdateTemplateMeta{Abstract: ptr.Ref("New abstract.")},
expected: expected{override: func(r *templateMetaUpdate) {
r.abstract = "New abstract."
}},
},
{
name: "AbstractEmptyStringClears",
req: codersdk.UpdateTemplateMeta{Abstract: ptr.Ref("")},
expected: expected{override: func(r *templateMetaUpdate) {
r.abstract = ""
}},
},
{
name: "Icon",
req: codersdk.UpdateTemplateMeta{Icon: ptr.Ref("/new.svg")},
+116 -7
View File
@@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"net/http"
"strings"
"sync/atomic"
"testing"
"time"
@@ -86,6 +87,50 @@ func TestPostTemplateByOrganization(t *testing.T) {
assert.Equal(t, database.AuditActionCreate, auditor.AuditLogs()[2].Action)
})
t.Run("Abstract", func(t *testing.T) {
t.Parallel()
tests := []struct {
name string
abstract string
wantError bool
}{
{name: "PersistsValid", abstract: strings.Repeat("a", 2048)},
{name: "RejectsOverLimit", abstract: strings.Repeat("a", 2049), wantError: true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
ctx := testutil.Context(t, testutil.WaitLong)
created, err := client.CreateTemplate(ctx, user.OrganizationID, codersdk.CreateTemplateRequest{
Name: "abstract-test",
VersionID: version.ID,
Abstract: tc.abstract,
})
if tc.wantError {
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
require.Len(t, apiErr.Validations, 1)
require.Equal(t, "abstract", apiErr.Validations[0].Field)
return
}
require.NoError(t, err)
assert.Equal(t, tc.abstract, created.Abstract)
got, err := client.Template(ctx, created.ID)
require.NoError(t, err)
assert.Equal(t, tc.abstract, got.Abstract)
})
}
})
t.Run("AlreadyExists", func(t *testing.T) {
t.Parallel()
ownerClient := coderdtest.New(t, nil)
@@ -942,6 +987,50 @@ func TestPatchTemplateMeta(t *testing.T) {
assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs()[4].Action)
})
t.Run("Abstract", func(t *testing.T) {
t.Parallel()
tests := []struct {
name string
abstract string
wantError bool
}{
{name: "PersistsValid", abstract: strings.Repeat("a", 2048)},
{name: "RejectsOverLimit", abstract: strings.Repeat("a", 2049), wantError: true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
abstract := tc.abstract
ctx := testutil.Context(t, testutil.WaitLong)
updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Abstract: &abstract,
})
if tc.wantError {
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
require.Len(t, apiErr.Validations, 1)
require.Equal(t, "abstract", apiErr.Validations[0].Field)
return
}
require.NoError(t, err)
assert.Equal(t, tc.abstract, updated.Abstract)
got, err := client.Template(ctx, template.ID)
require.NoError(t, err)
assert.Equal(t, tc.abstract, got.Abstract)
})
}
})
t.Run("AlreadyExists", func(t *testing.T) {
t.Parallel()
@@ -1336,6 +1425,7 @@ func TestPatchTemplateMeta(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, template.Name, updated.Name)
assert.Equal(t, template.Description, updated.Description)
assert.Equal(t, template.Abstract, updated.Abstract)
assert.Equal(t, template.Icon, updated.Icon)
assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
assert.Equal(t, template.ActivityBumpMillis, updated.ActivityBumpMillis)
@@ -1371,6 +1461,7 @@ func TestPatchTemplateMeta(t *testing.T) {
assert.WithinDuration(t, template.UpdatedAt, updated.UpdatedAt, time.Minute)
assert.Equal(t, template.Name, updated.Name)
assert.Equal(t, template.Description, updated.Description)
assert.Equal(t, template.Abstract, updated.Abstract)
assert.Equal(t, template.Icon, updated.Icon)
assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
})
@@ -1665,22 +1756,26 @@ func TestPatchTemplateMeta(t *testing.T) {
displayName := "Test Display Name"
description := "test-description"
abstract := "test abstract for agents"
icon := "/icon/icon.png"
defaultTTLMillis := 10 * time.Hour.Milliseconds()
reference := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.DisplayName = displayName
ctr.Description = description
ctr.Abstract = abstract
ctr.Icon = icon
ctr.DefaultTTLMillis = ptr.Ref(defaultTTLMillis)
})
require.Equal(t, displayName, reference.DisplayName)
require.Equal(t, description, reference.Description)
require.Equal(t, abstract, reference.Abstract)
require.Equal(t, icon, reference.Icon)
restoreReq := codersdk.UpdateTemplateMeta{
DisplayName: &displayName,
Description: &description,
Abstract: &abstract,
Icon: &icon,
DefaultTTLMillis: ptr.Ref(defaultTTLMillis),
}
@@ -1688,6 +1783,7 @@ func TestPatchTemplateMeta(t *testing.T) {
type expected struct {
displayName string
description string
abstract string
icon string
defaultTTLMillis int64
}
@@ -1702,22 +1798,27 @@ func TestPatchTemplateMeta(t *testing.T) {
{
name: "Only update default_ttl_ms",
req: codersdk.UpdateTemplateMeta{DefaultTTLMillis: ptr.Ref(99 * time.Hour.Milliseconds())},
expected: expected{displayName: reference.DisplayName, description: reference.Description, icon: reference.Icon, defaultTTLMillis: 99 * time.Hour.Milliseconds()},
expected: expected{displayName: reference.DisplayName, description: reference.Description, abstract: reference.Abstract, icon: reference.Icon, defaultTTLMillis: 99 * time.Hour.Milliseconds()},
},
{
name: "Clear display name",
req: codersdk.UpdateTemplateMeta{DisplayName: ptr.Ref("")},
expected: expected{displayName: "", description: reference.Description, icon: reference.Icon, defaultTTLMillis: defaultTTLMillis},
expected: expected{displayName: "", description: reference.Description, abstract: reference.Abstract, icon: reference.Icon, defaultTTLMillis: defaultTTLMillis},
},
{
name: "Clear description",
req: codersdk.UpdateTemplateMeta{Description: ptr.Ref("")},
expected: expected{displayName: reference.DisplayName, description: "", icon: reference.Icon, defaultTTLMillis: defaultTTLMillis},
expected: expected{displayName: reference.DisplayName, description: "", abstract: reference.Abstract, icon: reference.Icon, defaultTTLMillis: defaultTTLMillis},
},
{
name: "Clear abstract",
req: codersdk.UpdateTemplateMeta{Abstract: ptr.Ref("")},
expected: expected{displayName: reference.DisplayName, description: reference.Description, abstract: "", icon: reference.Icon, defaultTTLMillis: defaultTTLMillis},
},
{
name: "Clear icon",
req: codersdk.UpdateTemplateMeta{Icon: ptr.Ref("")},
expected: expected{displayName: reference.DisplayName, description: reference.Description, icon: "", defaultTTLMillis: defaultTTLMillis},
expected: expected{displayName: reference.DisplayName, description: reference.Description, abstract: reference.Abstract, icon: "", defaultTTLMillis: defaultTTLMillis},
},
// A request whose only field is nil is a true no-op under the new
// PATCH semantics; the handler returns 304 Not Modified and the
@@ -1725,17 +1826,22 @@ func TestPatchTemplateMeta(t *testing.T) {
{
name: "Nil display name is a no-op",
req: codersdk.UpdateTemplateMeta{DisplayName: nil},
expected: expected{displayName: reference.DisplayName, description: reference.Description, icon: reference.Icon, defaultTTLMillis: defaultTTLMillis},
expected: expected{displayName: reference.DisplayName, description: reference.Description, abstract: reference.Abstract, icon: reference.Icon, defaultTTLMillis: defaultTTLMillis},
},
{
name: "Nil description is a no-op",
req: codersdk.UpdateTemplateMeta{Description: nil},
expected: expected{displayName: reference.DisplayName, description: reference.Description, icon: reference.Icon, defaultTTLMillis: defaultTTLMillis},
expected: expected{displayName: reference.DisplayName, description: reference.Description, abstract: reference.Abstract, icon: reference.Icon, defaultTTLMillis: defaultTTLMillis},
},
{
name: "Nil abstract is a no-op",
req: codersdk.UpdateTemplateMeta{Abstract: nil},
expected: expected{displayName: reference.DisplayName, description: reference.Description, abstract: reference.Abstract, icon: reference.Icon, defaultTTLMillis: defaultTTLMillis},
},
{
name: "Nil icon is a no-op",
req: codersdk.UpdateTemplateMeta{Icon: nil},
expected: expected{displayName: reference.DisplayName, description: reference.Description, icon: reference.Icon, defaultTTLMillis: defaultTTLMillis},
expected: expected{displayName: reference.DisplayName, description: reference.Description, abstract: reference.Abstract, icon: reference.Icon, defaultTTLMillis: defaultTTLMillis},
},
}
@@ -1757,6 +1863,7 @@ func TestPatchTemplateMeta(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, tc.expected.displayName, updated.DisplayName)
assert.Equal(t, tc.expected.description, updated.Description)
assert.Equal(t, tc.expected.abstract, updated.Abstract)
assert.Equal(t, tc.expected.icon, updated.Icon)
assert.Equal(t, tc.expected.defaultTTLMillis, updated.DefaultTTLMillis)
})
@@ -1774,6 +1881,7 @@ func TestPatchTemplateMeta(t *testing.T) {
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.DisplayName = "Original Display"
ctr.Description = "Original description"
ctr.Abstract = "Original abstract"
ctr.Icon = "/icon/original.png"
ctr.DefaultTTLMillis = ptr.Ref((24 * time.Hour).Milliseconds())
ctr.AllowUserCancelWorkspaceJobs = ptr.Ref(true)
@@ -1789,6 +1897,7 @@ func TestPatchTemplateMeta(t *testing.T) {
assert.Equal(t, template.Name, updated.Name)
assert.Equal(t, template.DisplayName, updated.DisplayName)
assert.Equal(t, template.Description, updated.Description)
assert.Equal(t, template.Abstract, updated.Abstract)
assert.Equal(t, template.Icon, updated.Icon)
assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
assert.Equal(t, template.AllowUserCancelWorkspaceJobs, updated.AllowUserCancelWorkspaceJobs)
+4 -1
View File
@@ -41,7 +41,7 @@ func ListTemplates(db database.Store, organizationID uuid.UUID, options ListTemp
"list_templates",
"List available workspace templates. Optionally filter by a "+
"search query matching template name or description. "+
"Use this to find a template before creating a workspace. "+
"Results include short UI descriptions and agent-facing abstracts when present. "+
"Results are ordered by number of active developers (most popular first). "+
"Returns 10 per page. Use the page parameter to paginate through results.",
func(ctx context.Context, args listTemplatesArgs, _ fantasy.ToolCall) (fantasy.ToolResponse, error) {
@@ -128,6 +128,9 @@ func ListTemplates(db database.Store, organizationID uuid.UUID, options ListTemp
if desc := strings.TrimSpace(t.Description); desc != "" {
item["description"] = truncateRunes(desc, 200)
}
if abstract := strings.TrimSpace(t.Abstract); abstract != "" {
item["abstract"] = abstract
}
if count, ok := ownerCounts[t.ID]; ok && count > 0 {
item["active_developers"] = count
}
@@ -3,6 +3,7 @@ package chattool_test
import (
"context"
"encoding/json"
"strings"
"testing"
"charm.land/fantasy"
@@ -121,6 +122,120 @@ func TestListTemplates_OrganizationFilter(t *testing.T) {
})
}
func TestListTemplates_Abstract(t *testing.T) {
t.Parallel()
tests := []struct {
name string
template database.Template
want string
wantPresent bool
}{
{
name: "LongAbstract",
template: database.Template{
Name: "with-abstract",
Description: "short description",
Abstract: strings.Repeat("a", 1000),
},
want: strings.Repeat("a", 1000),
wantPresent: true,
},
{
name: "ShortAbstractUntouched",
template: database.Template{
Name: "short-abstract",
Description: "short description",
Abstract: "a concise abstract",
},
want: "a concise abstract",
wantPresent: true,
},
{
name: "EmptyAbstractOmitted",
template: database.Template{
Name: "no-abstract",
Description: "short description",
Abstract: "",
},
},
{
name: "WhitespaceOnlyAbstractOmitted",
template: database.Template{
Name: "whitespace-abstract",
Description: "short description",
Abstract: " \t \n ",
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
fixture := newListTemplatesFixture(t)
template := tc.template
template.OrganizationID = fixture.org.ID
template.CreatedBy = fixture.user.ID
tpl := dbgen.Template(t, fixture.db, template)
items := fixture.listTemplates(t, "{}")
require.Len(t, items, 1)
require.Equal(t, tpl.ID.String(), items[0]["id"].(string))
require.Equal(t, "short description", items[0]["description"].(string))
got, ok := items[0]["abstract"]
require.Equal(t, tc.wantPresent, ok)
if tc.wantPresent {
require.Equal(t, tc.want, got.(string))
}
})
}
}
type listTemplatesFixture struct {
db database.Store
user database.User
org database.Organization
}
func newListTemplatesFixture(t *testing.T) listTemplatesFixture {
t.Helper()
db, _ := dbtestutil.NewDB(t)
user := dbgen.User(t, db, database.User{})
org := dbgen.Organization(t, db, database.Organization{})
_ = dbgen.OrganizationMember(t, db, database.OrganizationMember{
UserID: user.ID,
OrganizationID: org.ID,
})
return listTemplatesFixture{db: db, user: user, org: org}
}
func (f listTemplatesFixture) listTemplates(t *testing.T, input string) []map[string]any {
t.Helper()
tool := chattool.ListTemplates(f.db, f.org.ID, chattool.ListTemplatesOptions{
OwnerID: f.user.ID,
})
resp, err := tool.Run(testutil.Context(t, testutil.WaitShort), fantasy.ToolCall{
ID: "list-templates",
Name: "list_templates",
Input: input,
})
require.NoError(t, err)
require.False(t, resp.IsError)
var result map[string]any
require.NoError(t, json.Unmarshal([]byte(resp.Content), &result))
templates := result["templates"].([]any)
items := make([]map[string]any, 0, len(templates))
for _, template := range templates {
items = append(items, template.(map[string]any))
}
return items
}
//nolint:tparallel,paralleltest // Subtests share a single DB and run sequentially.
func TestTemplateAllowlistEnforcement(t *testing.T) {
t.Parallel()
+3
View File
@@ -88,6 +88,9 @@ func ReadTemplate(db database.Store, organizationID uuid.UUID, options ReadTempl
if desc := strings.TrimSpace(template.Description); desc != "" {
templateInfo["description"] = desc
}
if abstract := strings.TrimSpace(template.Abstract); abstract != "" {
templateInfo["abstract"] = abstract
}
paramList := make([]map[string]any, 0, len(params))
for _, p := range params {
@@ -181,3 +181,74 @@ func TestReadTemplate_NoPresets(t *testing.T) {
_, hasPresets := result["presets"]
require.False(t, hasPresets, "presets key should be absent when there are none")
}
func TestReadTemplate_Abstract(t *testing.T) {
t.Parallel()
tests := []struct {
name string
abstract string
want string
wantPresent bool
}{
{
name: "Present",
abstract: "A long-form summary used by agents to pick the right template.",
want: "A long-form summary used by agents to pick the right template.",
wantPresent: true,
},
{name: "Empty"},
{name: "WhitespaceOnly", abstract: " \t \n "},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
tmplInfo := readTemplateInfo(t, tc.abstract)
got, ok := tmplInfo["abstract"]
require.Equal(t, tc.wantPresent, ok)
if tc.wantPresent {
require.Equal(t, tc.want, got.(string))
}
})
}
}
func readTemplateInfo(t *testing.T, abstract string) map[string]any {
t.Helper()
db, _ := dbtestutil.NewDB(t)
user := dbgen.User(t, db, database.User{})
org := dbgen.Organization(t, db, database.Organization{})
_ = dbgen.OrganizationMember(t, db, database.OrganizationMember{
UserID: user.ID,
OrganizationID: org.ID,
})
tv := dbgen.TemplateVersion(t, db, database.TemplateVersion{
OrganizationID: org.ID,
CreatedBy: user.ID,
})
tmpl := dbgen.Template(t, db, database.Template{
OrganizationID: org.ID,
CreatedBy: user.ID,
ActiveVersionID: tv.ID,
Abstract: abstract,
})
tool := chattool.ReadTemplate(db, org.ID, chattool.ReadTemplateOptions{
OwnerID: user.ID,
})
resp, err := tool.Run(testutil.Context(t, testutil.WaitShort), fantasy.ToolCall{
ID: "abstract",
Name: "read_template",
Input: `{"template_id":"` + tmpl.ID.String() + `"}`,
})
require.NoError(t, err)
require.False(t, resp.IsError)
var result map[string]any
require.NoError(t, json.Unmarshal([]byte(resp.Content), &result))
return result["template"].(map[string]any)
}
+3
View File
@@ -144,6 +144,9 @@ type CreateTemplateRequest struct {
// Description is a description of what the template contains. It must be
// less than 128 bytes.
Description string `json:"description,omitempty" validate:"lt=128"`
// Abstract is a longer-form summary surfaced to agents to help them pick
// the right template. Up to 2048 characters.
Abstract string `json:"abstract,omitempty" validate:"max=2048"`
// Icon is a relative path or external URL that specifies
// an icon to be displayed in the dashboard.
Icon string `json:"icon,omitempty"`
+18 -12
View File
@@ -27,15 +27,18 @@ type Template struct {
Provisioner ProvisionerType `json:"provisioner" enums:"terraform"`
ActiveVersionID uuid.UUID `json:"active_version_id" format:"uuid"`
// ActiveUserCount is set to -1 when loading.
ActiveUserCount int `json:"active_user_count"`
BuildTimeStats TemplateBuildTimeStats `json:"build_time_stats"`
Description string `json:"description"`
Deprecated bool `json:"deprecated"`
DeprecationMessage string `json:"deprecation_message"`
Deleted bool `json:"deleted"`
Icon string `json:"icon"`
DefaultTTLMillis int64 `json:"default_ttl_ms"`
ActivityBumpMillis int64 `json:"activity_bump_ms"`
ActiveUserCount int `json:"active_user_count"`
BuildTimeStats TemplateBuildTimeStats `json:"build_time_stats"`
Description string `json:"description"`
// Abstract is a longer-form summary surfaced to agents to help them pick
// the right template. Up to 2048 characters.
Abstract string `json:"abstract"`
Deprecated bool `json:"deprecated"`
DeprecationMessage string `json:"deprecation_message"`
Deleted bool `json:"deleted"`
Icon string `json:"icon"`
DefaultTTLMillis int64 `json:"default_ttl_ms"`
ActivityBumpMillis int64 `json:"activity_bump_ms"`
// AutostopRequirement and AutostartRequirement are enterprise features. Its
// value is only used if your license is entitled to use the advanced template
// scheduling feature.
@@ -218,9 +221,12 @@ type ACLAvailable struct {
// UpdateTemplateMeta is the request body for the PATCH /templates/{template}
// endpoint. All fields are optional. Fields that are nil are not modified.
type UpdateTemplateMeta struct {
Name *string `json:"name,omitempty" validate:"omitempty,template_name"`
DisplayName *string `json:"display_name,omitempty" validate:"omitempty,template_display_name"`
Description *string `json:"description,omitempty"`
Name *string `json:"name,omitempty" validate:"omitempty,template_name"`
DisplayName *string `json:"display_name,omitempty" validate:"omitempty,template_display_name"`
Description *string `json:"description,omitempty"`
// Abstract is a longer-form summary surfaced to agents to help them pick
// the right template. Up to 2048 characters.
Abstract *string `json:"abstract,omitempty" validate:"omitempty,max=2048"`
Icon *string `json:"icon,omitempty"`
DefaultTTLMillis *int64 `json:"default_ttl_ms,omitempty"`
// ActivityBumpMillis allows optionally specifying the activity bump
+8 -1
View File
@@ -57,10 +57,17 @@ func searchTemplates(ctx context.Context, deps Deps, query string) ([]SearchResu
}
results := make([]SearchResultItem, len(templates))
for i, template := range templates {
parts := make([]string, 0, 2)
if d := strings.TrimSpace(template.Description); d != "" {
parts = append(parts, d)
}
if a := strings.TrimSpace(template.Abstract); a != "" {
parts = append(parts, a)
}
results[i] = SearchResultItem{
ID: createObjectID(ObjectTypeTemplate, template.ID.String()).String(),
Title: template.DisplayName,
Text: template.Description,
Text: strings.Join(parts, "\n\n"),
URL: fmt.Sprintf("%s/templates/%s/%s", serverURL, template.OrganizationName, template.Name),
}
}
+45
View File
@@ -151,6 +151,51 @@ func TestChatGPTSearch_TemplateMultipleFilters(t *testing.T) {
require.Equal(t, expectedID, result.Results[0].ID, "Should match the docker template in org2")
}
func TestChatGPTSearch_TemplateIncludesAbstract(t *testing.T) {
t.Parallel()
client, store := coderdtest.NewWithDatabase(t, nil)
owner := coderdtest.CreateFirstUser(t, client)
const (
desc = "Short description."
abstract = "Detailed summary that distinguishes this template."
)
dbgen.Template(t, store, database.Template{
OrganizationID: owner.OrganizationID,
CreatedBy: owner.UserID,
Name: "with-abstract",
DisplayName: "With Abstract",
Description: desc,
Abstract: abstract,
})
dbgen.Template(t, store, database.Template{
OrganizationID: owner.OrganizationID,
CreatedBy: owner.UserID,
Name: "no-abstract",
DisplayName: "No Abstract",
Description: desc,
})
deps, err := toolsdk.NewDeps(client)
require.NoError(t, err)
result, err := testTool(t, toolsdk.ChatGPTSearch, deps, toolsdk.SearchArgs{Query: "templates"})
require.NoError(t, err)
require.Len(t, result.Results, 2)
byTitle := make(map[string]toolsdk.SearchResultItem, len(result.Results))
for _, item := range result.Results {
byTitle[item.Title] = item
}
require.Contains(t, byTitle, "With Abstract")
require.Contains(t, byTitle, "No Abstract")
require.Contains(t, byTitle["With Abstract"].Text, desc)
require.Contains(t, byTitle["With Abstract"].Text, abstract)
require.Equal(t, desc, byTitle["No Abstract"].Text)
}
func TestChatGPTSearch_WorkspaceSearch(t *testing.T) {
t.Parallel()
+5 -2
View File
@@ -621,7 +621,7 @@ var ListWorkspaces = Tool[ListWorkspacesArgs, []MinimalWorkspace]{
var ListTemplates = Tool[NoArgs, []MinimalTemplate]{
Tool: aisdk.Tool{
Name: ToolNameListTemplates,
Description: "Lists templates for the authenticated user.",
Description: "Lists templates for the authenticated user. Description is short UI text; abstract is richer selection context.",
Schema: aisdk.Schema{
Properties: map[string]any{},
Required: []string{},
@@ -640,6 +640,7 @@ var ListTemplates = Tool[NoArgs, []MinimalTemplate]{
ID: template.ID.String(),
Name: template.Name,
Description: template.Description,
Abstract: template.Abstract,
ActiveVersionID: template.ActiveVersionID,
ActiveUserCount: template.ActiveUserCount,
}
@@ -732,7 +733,7 @@ func toPresetView(p codersdk.Preset) presetView {
var GetTemplate = Tool[GetTemplateArgs, TemplateDetail]{
Tool: aisdk.Tool{
Name: ToolNameGetTemplate,
Description: `Get details about a workspace template, including its configurable parameters and available presets for the active version.
Description: `Get details about a workspace template, including its abstract, configurable parameters, and available presets for the active version.
Use this after finding a template with coder_list_templates and before creating a workspace with coder_create_workspace. Presets, when present, can be passed to coder_create_workspace as template_version_preset_id.
@@ -777,6 +778,7 @@ When selecting a preset: if a preset is marked default and the user has not spec
ID: template.ID.String(),
Name: template.Name,
Description: template.Description,
Abstract: template.Abstract,
ActiveVersionID: template.ActiveVersionID,
ActiveUserCount: template.ActiveUserCount,
},
@@ -1719,6 +1721,7 @@ type MinimalTemplate struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Abstract string `json:"abstract,omitempty"`
ActiveVersionID uuid.UUID `json:"active_version_id"`
ActiveUserCount int `json:"active_user_count"`
}
+23
View File
@@ -11,6 +11,7 @@ import (
"os"
"path/filepath"
"sort"
"strings"
"sync"
"testing"
"time"
@@ -309,6 +310,7 @@ func TestTools(t *testing.T) {
})
for i, template := range result {
require.Equal(t, expected[i].ID.String(), template.ID)
require.Equal(t, expected[i].Abstract, template.Abstract)
}
})
@@ -738,6 +740,27 @@ func TestTools(t *testing.T) {
})
require.ErrorContains(t, err, "get template")
})
t.Run("Abstract", func(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitShort)
absBuild := dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{
OrganizationID: owner.OrganizationID,
OwnerID: member.ID,
}).Do()
abstract := strings.Repeat("b", 600)
_, err := client.UpdateTemplateMeta(ctx, absBuild.Template.ID, codersdk.UpdateTemplateMeta{
Abstract: &abstract,
})
require.NoError(t, err)
tb, err := toolsdk.NewDeps(memberClient)
require.NoError(t, err)
result, err := testTool(t, toolsdk.GetTemplate, tb, toolsdk.GetTemplateArgs{
TemplateID: absBuild.Template.ID.String(),
})
require.NoError(t, err)
require.Equal(t, abstract, result.Abstract)
})
})
t.Run("GetWorkspaceAgentLogs", func(t *testing.T) {
+34 -34
View File
@@ -13,40 +13,40 @@ We track the following resources:
<!-- Code generated by 'make docs/admin/security/audit-logs.md'. DO NOT EDIT -->
| <b>Resource<b> | | |
|-----------------------------------------------------------------|----------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| AIGatewayKey<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>hashed_secret</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>secret_prefix</td><td>true</td></tr></tbody></table> |
| AIProvider<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>base_url</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>enabled</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>settings</td><td>true</td></tr><tr><td>settings_key_id</td><td>false</td></tr><tr><td>type</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| AIProviderKey<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>api_key</td><td>true</td></tr><tr><td>api_key_key_id</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>provider_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| APIKey<br><i>login, logout, register, create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>allow_list</td><td>false</td></tr><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>ip_address</td><td>false</td></tr><tr><td>last_used</td><td>true</td></tr><tr><td>lifetime_seconds</td><td>false</td></tr><tr><td>login_type</td><td>false</td></tr><tr><td>scopes</td><td>false</td></tr><tr><td>token_name</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| AiSeatState<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>first_used_at</td><td>true</td></tr><tr><td>last_event_description</td><td>true</td></tr><tr><td>last_event_type</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| AuditOAuthConvertState<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>from_login_type</td><td>true</td></tr><tr><td>to_login_type</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| Group<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>avatar_url</td><td>true</td></tr><tr><td>chat_spend_limit_micros</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>members</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>quota_allowance</td><td>true</td></tr><tr><td>source</td><td>false</td></tr></tbody></table> |
| AuditableGroupAiBudget<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>group_id</td><td>false</td></tr><tr><td>group_name</td><td>false</td></tr><tr><td>spend_limit</td><td>true</td></tr><tr><td>spend_limit_micros</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| AuditableOrganizationMember<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>roles</td><td>true</td></tr><tr><td>updated_at</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
| Chat<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>agent_id</td><td>false</td></tr><tr><td>archived</td><td>true</td></tr><tr><td>build_id</td><td>false</td></tr><tr><td>client_type</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>dynamic_tools</td><td>false</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>heartbeat_at</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>labels</td><td>true</td></tr><tr><td>last_error</td><td>false</td></tr><tr><td>last_injected_context</td><td>false</td></tr><tr><td>last_model_config_id</td><td>false</td></tr><tr><td>last_read_message_id</td><td>false</td></tr><tr><td>last_turn_summary</td><td>false</td></tr><tr><td>mcp_server_ids</td><td>true</td></tr><tr><td>mode</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>owner_name</td><td>false</td></tr><tr><td>owner_username</td><td>false</td></tr><tr><td>parent_chat_id</td><td>false</td></tr><tr><td>pin_order</td><td>true</td></tr><tr><td>plan_mode</td><td>false</td></tr><tr><td>root_chat_id</td><td>false</td></tr><tr><td>started_at</td><td>false</td></tr><tr><td>status</td><td>false</td></tr><tr><td>title</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_acl</td><td>true</td></tr><tr><td>worker_id</td><td>false</td></tr><tr><td>workspace_id</td><td>true</td></tr></tbody></table> |
| CustomRole<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>is_system</td><td>false</td></tr><tr><td>member_permissions</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>org_permissions</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>site_permissions</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_permissions</td><td>true</td></tr></tbody></table> |
| GitSSHKey<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>private_key</td><td>true</td></tr><tr><td>private_key_key_id</td><td>false</td></tr><tr><td>public_key</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| GroupSyncSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>auto_create_missing_groups</td><td>true</td></tr><tr><td>field</td><td>true</td></tr><tr><td>legacy_group_name_mapping</td><td>false</td></tr><tr><td>mapping</td><td>true</td></tr><tr><td>regex_filter</td><td>true</td></tr></tbody></table> |
| HealthSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>dismissed_healthchecks</td><td>true</td></tr><tr><td>id</td><td>false</td></tr></tbody></table> |
| License<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>exp</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwt</td><td>false</td></tr><tr><td>uploaded_at</td><td>true</td></tr><tr><td>uuid</td><td>true</td></tr></tbody></table> |
| NotificationTemplate<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>actions</td><td>true</td></tr><tr><td>body_template</td><td>true</td></tr><tr><td>enabled_by_default</td><td>true</td></tr><tr><td>group</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>kind</td><td>true</td></tr><tr><td>method</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>title_template</td><td>true</td></tr></tbody></table> |
| NotificationsSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>id</td><td>false</td></tr><tr><td>notifier_paused</td><td>true</td></tr></tbody></table> |
| OAuth2ProviderApp<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>callback_url</td><td>true</td></tr><tr><td>client_id_issued_at</td><td>false</td></tr><tr><td>client_secret_expires_at</td><td>true</td></tr><tr><td>client_type</td><td>true</td></tr><tr><td>client_uri</td><td>true</td></tr><tr><td>contacts</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>dynamically_registered</td><td>true</td></tr><tr><td>grant_types</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwks</td><td>true</td></tr><tr><td>jwks_uri</td><td>true</td></tr><tr><td>logo_uri</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>policy_uri</td><td>true</td></tr><tr><td>redirect_uris</td><td>true</td></tr><tr><td>registration_access_token</td><td>true</td></tr><tr><td>registration_client_uri</td><td>true</td></tr><tr><td>response_types</td><td>true</td></tr><tr><td>scope</td><td>true</td></tr><tr><td>software_id</td><td>true</td></tr><tr><td>software_version</td><td>true</td></tr><tr><td>token_endpoint_auth_method</td><td>true</td></tr><tr><td>tos_uri</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| OAuth2ProviderAppSecret<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>app_id</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>display_secret</td><td>false</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>secret_prefix</td><td>false</td></tr></tbody></table> |
| Organization<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>is_default</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>shareable_workspace_owners</td><td>true</td></tr><tr><td>updated_at</td><td>true</td></tr></tbody></table> |
| OrganizationSyncSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>assign_default</td><td>true</td></tr><tr><td>field</td><td>true</td></tr><tr><td>mapping</td><td>true</td></tr></tbody></table> |
| PrebuildsSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>id</td><td>false</td></tr><tr><td>reconciliation_paused</td><td>true</td></tr></tbody></table> |
| RoleSyncSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>field</td><td>true</td></tr><tr><td>mapping</td><td>true</td></tr></tbody></table> |
| TaskTable<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>deleted_at</td><td>false</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>prompt</td><td>true</td></tr><tr><td>template_parameters</td><td>true</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>workspace_id</td><td>true</td></tr></tbody></table> |
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>active_version_id</td><td>true</td></tr><tr><td>activity_bump</td><td>true</td></tr><tr><td>allow_user_autostart</td><td>true</td></tr><tr><td>allow_user_autostop</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>autostart_block_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_weeks</td><td>true</td></tr><tr><td>cors_behavior</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_name</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deprecated</td><td>true</td></tr><tr><td>description</td><td>true</td></tr><tr><td>disable_module_cache</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>failure_ttl</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>max_port_sharing_level</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_display_name</td><td>false</td></tr><tr><td>organization_icon</td><td>false</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>organization_name</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>require_active_version</td><td>true</td></tr><tr><td>time_til_dormant</td><td>true</td></tr><tr><td>time_til_dormant_autodelete</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>use_classic_parameter_flow</td><td>true</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>archived</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_name</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>external_auth_providers</td><td>false</td></tr><tr><td>has_ai_task</td><td>false</td></tr><tr><td>has_external_agent</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>message</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>source_example_id</td><td>false</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>avatar_url</td><td>false</td></tr><tr><td>chat_spend_limit_micros</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>github_com_user_id</td><td>false</td></tr><tr><td>hashed_one_time_passcode</td><td>false</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>is_service_account</td><td>true</td></tr><tr><td>is_system</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>one_time_passcode_expires_at</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
| UserSecret<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>description</td><td>true</td></tr><tr><td>env_name</td><td>true</td></tr><tr><td>file_path</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr><tr><td>value</td><td>true</td></tr><tr><td>value_key_id</td><td>false</td></tr></tbody></table> |
| UserSkill<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>content</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>description</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>has_ai_task</td><td>false</td></tr><tr><td>has_external_agent</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_by_avatar_url</td><td>false</td></tr><tr><td>initiator_by_name</td><td>false</td></tr><tr><td>initiator_by_username</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>max_deadline</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>template_version_preset_id</td><td>false</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |
| WorkspaceProxy<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>derp_enabled</td><td>true</td></tr><tr><td>derp_only</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>region_id</td><td>true</td></tr><tr><td>token_hashed_secret</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>url</td><td>true</td></tr><tr><td>version</td><td>true</td></tr><tr><td>wildcard_hostname</td><td>true</td></tr></tbody></table> |
| WorkspaceTable<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>automatic_updates</td><td>true</td></tr><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deleting_at</td><td>true</td></tr><tr><td>dormant_at</td><td>true</td></tr><tr><td>favorite</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>next_start_at</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
| <b>Resource<b> | | |
|-----------------------------------------------------------------|----------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| AIGatewayKey<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>hashed_secret</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>secret_prefix</td><td>true</td></tr></tbody></table> |
| AIProvider<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>base_url</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>enabled</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>settings</td><td>true</td></tr><tr><td>settings_key_id</td><td>false</td></tr><tr><td>type</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| AIProviderKey<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>api_key</td><td>true</td></tr><tr><td>api_key_key_id</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>provider_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| APIKey<br><i>login, logout, register, create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>allow_list</td><td>false</td></tr><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>ip_address</td><td>false</td></tr><tr><td>last_used</td><td>true</td></tr><tr><td>lifetime_seconds</td><td>false</td></tr><tr><td>login_type</td><td>false</td></tr><tr><td>scopes</td><td>false</td></tr><tr><td>token_name</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| AiSeatState<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>first_used_at</td><td>true</td></tr><tr><td>last_event_description</td><td>true</td></tr><tr><td>last_event_type</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| AuditOAuthConvertState<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>from_login_type</td><td>true</td></tr><tr><td>to_login_type</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| Group<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>avatar_url</td><td>true</td></tr><tr><td>chat_spend_limit_micros</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>members</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>quota_allowance</td><td>true</td></tr><tr><td>source</td><td>false</td></tr></tbody></table> |
| AuditableGroupAiBudget<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>group_id</td><td>false</td></tr><tr><td>group_name</td><td>false</td></tr><tr><td>spend_limit</td><td>true</td></tr><tr><td>spend_limit_micros</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| AuditableOrganizationMember<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>roles</td><td>true</td></tr><tr><td>updated_at</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
| Chat<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>agent_id</td><td>false</td></tr><tr><td>archived</td><td>true</td></tr><tr><td>build_id</td><td>false</td></tr><tr><td>client_type</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>dynamic_tools</td><td>false</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>heartbeat_at</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>labels</td><td>true</td></tr><tr><td>last_error</td><td>false</td></tr><tr><td>last_injected_context</td><td>false</td></tr><tr><td>last_model_config_id</td><td>false</td></tr><tr><td>last_read_message_id</td><td>false</td></tr><tr><td>last_turn_summary</td><td>false</td></tr><tr><td>mcp_server_ids</td><td>true</td></tr><tr><td>mode</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>owner_name</td><td>false</td></tr><tr><td>owner_username</td><td>false</td></tr><tr><td>parent_chat_id</td><td>false</td></tr><tr><td>pin_order</td><td>true</td></tr><tr><td>plan_mode</td><td>false</td></tr><tr><td>root_chat_id</td><td>false</td></tr><tr><td>started_at</td><td>false</td></tr><tr><td>status</td><td>false</td></tr><tr><td>title</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_acl</td><td>true</td></tr><tr><td>worker_id</td><td>false</td></tr><tr><td>workspace_id</td><td>true</td></tr></tbody></table> |
| CustomRole<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>is_system</td><td>false</td></tr><tr><td>member_permissions</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>org_permissions</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>site_permissions</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_permissions</td><td>true</td></tr></tbody></table> |
| GitSSHKey<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>private_key</td><td>true</td></tr><tr><td>private_key_key_id</td><td>false</td></tr><tr><td>public_key</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| GroupSyncSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>auto_create_missing_groups</td><td>true</td></tr><tr><td>field</td><td>true</td></tr><tr><td>legacy_group_name_mapping</td><td>false</td></tr><tr><td>mapping</td><td>true</td></tr><tr><td>regex_filter</td><td>true</td></tr></tbody></table> |
| HealthSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>dismissed_healthchecks</td><td>true</td></tr><tr><td>id</td><td>false</td></tr></tbody></table> |
| License<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>exp</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwt</td><td>false</td></tr><tr><td>uploaded_at</td><td>true</td></tr><tr><td>uuid</td><td>true</td></tr></tbody></table> |
| NotificationTemplate<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>actions</td><td>true</td></tr><tr><td>body_template</td><td>true</td></tr><tr><td>enabled_by_default</td><td>true</td></tr><tr><td>group</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>kind</td><td>true</td></tr><tr><td>method</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>title_template</td><td>true</td></tr></tbody></table> |
| NotificationsSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>id</td><td>false</td></tr><tr><td>notifier_paused</td><td>true</td></tr></tbody></table> |
| OAuth2ProviderApp<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>callback_url</td><td>true</td></tr><tr><td>client_id_issued_at</td><td>false</td></tr><tr><td>client_secret_expires_at</td><td>true</td></tr><tr><td>client_type</td><td>true</td></tr><tr><td>client_uri</td><td>true</td></tr><tr><td>contacts</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>dynamically_registered</td><td>true</td></tr><tr><td>grant_types</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwks</td><td>true</td></tr><tr><td>jwks_uri</td><td>true</td></tr><tr><td>logo_uri</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>policy_uri</td><td>true</td></tr><tr><td>redirect_uris</td><td>true</td></tr><tr><td>registration_access_token</td><td>true</td></tr><tr><td>registration_client_uri</td><td>true</td></tr><tr><td>response_types</td><td>true</td></tr><tr><td>scope</td><td>true</td></tr><tr><td>software_id</td><td>true</td></tr><tr><td>software_version</td><td>true</td></tr><tr><td>token_endpoint_auth_method</td><td>true</td></tr><tr><td>tos_uri</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| OAuth2ProviderAppSecret<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>app_id</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>display_secret</td><td>false</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>secret_prefix</td><td>false</td></tr></tbody></table> |
| Organization<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>is_default</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>shareable_workspace_owners</td><td>true</td></tr><tr><td>updated_at</td><td>true</td></tr></tbody></table> |
| OrganizationSyncSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>assign_default</td><td>true</td></tr><tr><td>field</td><td>true</td></tr><tr><td>mapping</td><td>true</td></tr></tbody></table> |
| PrebuildsSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>id</td><td>false</td></tr><tr><td>reconciliation_paused</td><td>true</td></tr></tbody></table> |
| RoleSyncSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>field</td><td>true</td></tr><tr><td>mapping</td><td>true</td></tr></tbody></table> |
| TaskTable<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>deleted_at</td><td>false</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>prompt</td><td>true</td></tr><tr><td>template_parameters</td><td>true</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>workspace_id</td><td>true</td></tr></tbody></table> |
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>abstract</td><td>true</td></tr><tr><td>active_version_id</td><td>true</td></tr><tr><td>activity_bump</td><td>true</td></tr><tr><td>allow_user_autostart</td><td>true</td></tr><tr><td>allow_user_autostop</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>autostart_block_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_weeks</td><td>true</td></tr><tr><td>cors_behavior</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_name</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deprecated</td><td>true</td></tr><tr><td>description</td><td>true</td></tr><tr><td>disable_module_cache</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>failure_ttl</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>max_port_sharing_level</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_display_name</td><td>false</td></tr><tr><td>organization_icon</td><td>false</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>organization_name</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>require_active_version</td><td>true</td></tr><tr><td>time_til_dormant</td><td>true</td></tr><tr><td>time_til_dormant_autodelete</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>use_classic_parameter_flow</td><td>true</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>archived</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_name</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>external_auth_providers</td><td>false</td></tr><tr><td>has_ai_task</td><td>false</td></tr><tr><td>has_external_agent</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>message</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>source_example_id</td><td>false</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>avatar_url</td><td>false</td></tr><tr><td>chat_spend_limit_micros</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>github_com_user_id</td><td>false</td></tr><tr><td>hashed_one_time_passcode</td><td>false</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>is_service_account</td><td>true</td></tr><tr><td>is_system</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>one_time_passcode_expires_at</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
| UserSecret<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>description</td><td>true</td></tr><tr><td>env_name</td><td>true</td></tr><tr><td>file_path</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr><tr><td>value</td><td>true</td></tr><tr><td>value_key_id</td><td>false</td></tr></tbody></table> |
| UserSkill<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>content</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>description</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>has_ai_task</td><td>false</td></tr><tr><td>has_external_agent</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_by_avatar_url</td><td>false</td></tr><tr><td>initiator_by_name</td><td>false</td></tr><tr><td>initiator_by_username</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>max_deadline</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>template_version_preset_id</td><td>false</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |
| WorkspaceProxy<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>derp_enabled</td><td>true</td></tr><tr><td>derp_only</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>region_id</td><td>true</td></tr><tr><td>token_hashed_secret</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>url</td><td>true</td></tr><tr><td>version</td><td>true</td></tr><tr><td>wildcard_hostname</td><td>true</td></tr></tbody></table> |
| WorkspaceTable<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>automatic_updates</td><td>true</td></tr><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deleting_at</td><td>true</td></tr><tr><td>dormant_at</td><td>true</td></tr><tr><td>favorite</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>next_start_at</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
<!-- End generated by 'make docs/admin/security/audit-logs.md'. -->
+6
View File
@@ -4876,6 +4876,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
```json
{
"abstract": "string",
"activity_bump_ms": 0,
"allow_user_autostart": true,
"allow_user_autostop": true,
@@ -4912,6 +4913,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| Name | Type | Required | Restrictions | Description |
|---------------------------------------|--------------------------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `abstract` | string | false | | Abstract is a longer-form summary surfaced to agents to help them pick the right template. Up to 2048 characters. |
| `activity_bump_ms` | integer | false | | Activity bump ms allows optionally specifying the activity bump duration for all workspaces created from this template. Defaults to 1h but can be set to 0 to disable activity bumping. |
| `allow_user_autostart` | boolean | false | | Allow user autostart allows users to set a schedule for autostarting their workspace. By default this is true. This can only be disabled when using an enterprise license. |
| `allow_user_autostop` | boolean | false | | Allow user autostop allows users to set a custom workspace TTL to use in place of the template's DefaultTTL field. By default this is true. If false, the DefaultTTL will always be used. This can only be disabled when using an enterprise license. |
@@ -11969,6 +11971,7 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
```json
{
"abstract": "string",
"active_user_count": 0,
"active_version_id": "eae64611-bd53-4a80-bb77-df1e432c0fbc",
"activity_bump_ms": 0,
@@ -12029,6 +12032,7 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
| Name | Type | Required | Restrictions | Description |
|------------------------------------|--------------------------------------------------------------------------------|----------|--------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `abstract` | string | false | | Abstract is a longer-form summary surfaced to agents to help them pick the right template. Up to 2048 characters. |
| `active_user_count` | integer | false | | Active user count is set to -1 when loading. |
| `active_version_id` | string | false | | |
| `activity_bump_ms` | integer | false | | |
@@ -13212,6 +13216,7 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
```json
{
"abstract": "string",
"activity_bump_ms": 0,
"allow_user_autostart": true,
"allow_user_autostop": true,
@@ -13251,6 +13256,7 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
| Name | Type | Required | Restrictions | Description |
|------------------------------------|--------------------------------------------------------------------------------|----------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `abstract` | string | false | | Abstract is a longer-form summary surfaced to agents to help them pick the right template. Up to 2048 characters. |
| `activity_bump_ms` | integer | false | | Activity bump ms allows optionally specifying the activity bump duration for all workspaces created from this template. Defaults to 1h but can be set to 0 to disable activity bumping. |
| `allow_user_autostart` | boolean | false | | |
| `allow_user_autostop` | boolean | false | | |
+10
View File
@@ -30,6 +30,7 @@ To include deprecated templates, specify `deprecated:true` in the search query.
```json
[
{
"abstract": "string",
"active_user_count": 0,
"active_version_id": "eae64611-bd53-4a80-bb77-df1e432c0fbc",
"activity_bump_ms": 0,
@@ -100,6 +101,7 @@ Status Code **200**
| Name | Type | Required | Restrictions | Description |
|--------------------------------------|------------------------------------------------------------------------------------------|----------|--------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `[array item]` | array | false | | |
| `» abstract` | string | false | | Abstract is a longer-form summary surfaced to agents to help them pick the right template. Up to 2048 characters. |
| `» active_user_count` | integer | false | | Active user count is set to -1 when loading. |
| `» active_version_id` | string(uuid) | false | | |
| `» activity_bump_ms` | integer | false | | |
@@ -171,6 +173,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa
```json
{
"abstract": "string",
"activity_bump_ms": 0,
"allow_user_autostart": true,
"allow_user_autostop": true,
@@ -216,6 +219,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa
```json
{
"abstract": "string",
"active_user_count": 0,
"active_version_id": "eae64611-bd53-4a80-bb77-df1e432c0fbc",
"activity_bump_ms": 0,
@@ -368,6 +372,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat
```json
{
"abstract": "string",
"active_user_count": 0,
"active_version_id": "eae64611-bd53-4a80-bb77-df1e432c0fbc",
"activity_bump_ms": 0,
@@ -790,6 +795,7 @@ To include deprecated templates, specify `deprecated:true` in the search query.
```json
[
{
"abstract": "string",
"active_user_count": 0,
"active_version_id": "eae64611-bd53-4a80-bb77-df1e432c0fbc",
"activity_bump_ms": 0,
@@ -860,6 +866,7 @@ Status Code **200**
| Name | Type | Required | Restrictions | Description |
|--------------------------------------|------------------------------------------------------------------------------------------|----------|--------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `[array item]` | array | false | | |
| `» abstract` | string | false | | Abstract is a longer-form summary surfaced to agents to help them pick the right template. Up to 2048 characters. |
| `» active_user_count` | integer | false | | Active user count is set to -1 when loading. |
| `» active_version_id` | string(uuid) | false | | |
| `» activity_bump_ms` | integer | false | | |
@@ -994,6 +1001,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template} \
```json
{
"abstract": "string",
"active_user_count": 0,
"active_version_id": "eae64611-bd53-4a80-bb77-df1e432c0fbc",
"activity_bump_ms": 0,
@@ -1120,6 +1128,7 @@ curl -X PATCH http://coder-server:8080/api/v2/templates/{template} \
```json
{
"abstract": "string",
"activity_bump_ms": 0,
"allow_user_autostart": true,
"allow_user_autostop": true,
@@ -1168,6 +1177,7 @@ curl -X PATCH http://coder-server:8080/api/v2/templates/{template} \
```json
{
"abstract": "string",
"active_user_count": 0,
"active_version_id": "eae64611-bd53-4a80-bb77-df1e432c0fbc",
"activity_bump_ms": 0,
+1
View File
@@ -105,6 +105,7 @@ var auditableResourcesTypes = map[any]map[string]Action{
"provisioner": ActionTrack,
"active_version_id": ActionTrack,
"description": ActionTrack,
"abstract": ActionTrack,
"icon": ActionTrack,
"default_ttl": ActionTrack,
"autostart_block_days_of_week": ActionTrack,
+15
View File
@@ -3476,6 +3476,11 @@ export interface CreateTemplateRequest {
* less than 128 bytes.
*/
readonly description?: string;
/**
* Abstract is a longer-form summary surfaced to agents to help them pick
* the right template. Up to 2048 characters.
*/
readonly abstract?: string;
/**
* Icon is a relative path or external URL that specifies
* an icon to be displayed in the dashboard.
@@ -8025,6 +8030,11 @@ export interface Template {
readonly active_user_count: number;
readonly build_time_stats: TemplateBuildTimeStats;
readonly description: string;
/**
* Abstract is a longer-form summary surfaced to agents to help them pick
* the right template. Up to 2048 characters.
*/
readonly abstract: string;
readonly deprecated: boolean;
readonly deprecation_message: string;
readonly deleted: boolean;
@@ -8841,6 +8851,11 @@ export interface UpdateTemplateMeta {
readonly name?: string;
readonly display_name?: string;
readonly description?: string;
/**
* Abstract is a longer-form summary surfaced to agents to help them pick
* the right template. Up to 2048 characters.
*/
readonly abstract?: string;
readonly icon?: string;
readonly default_ttl_ms?: number;
/**
@@ -38,6 +38,8 @@ import {
const MAX_DESCRIPTION_CHAR_LIMIT = 128;
const MAX_DESCRIPTION_MESSAGE = `Please enter a description that is no longer than ${MAX_DESCRIPTION_CHAR_LIMIT} characters.`;
const MAX_ABSTRACT_CHAR_LIMIT = 2048;
const MAX_ABSTRACT_MESSAGE = `Please enter an abstract that is no longer than ${MAX_ABSTRACT_CHAR_LIMIT} characters.`;
export const validationSchema = Yup.object({
name: nameValidator("Name"),
@@ -46,6 +48,7 @@ export const validationSchema = Yup.object({
MAX_DESCRIPTION_CHAR_LIMIT,
MAX_DESCRIPTION_MESSAGE,
),
abstract: Yup.string().max(MAX_ABSTRACT_CHAR_LIMIT, MAX_ABSTRACT_MESSAGE),
allow_user_cancel_workspace_jobs: Yup.boolean(),
icon: iconValidator,
require_active_version: Yup.boolean(),
@@ -85,6 +88,7 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
name: template.name,
display_name: template.display_name,
description: template.description,
abstract: template.abstract,
icon: template.icon,
allow_user_cancel_workspace_jobs:
template.allow_user_cancel_workspace_jobs,
@@ -148,6 +152,19 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
rows={2}
/>
<TextField
{...getFieldHelpers("abstract", {
helperText:
"Detailed summary for agents to help choose the right template.",
maxLength: MAX_ABSTRACT_CHAR_LIMIT,
})}
multiline
disabled={isSubmitting}
fullWidth
label="Abstract"
rows={5}
/>
<IconField
{...getFieldHelpers("icon")}
disabled={isSubmitting}
@@ -27,6 +27,7 @@ const validFormValues: FormValues = {
name: "Name",
display_name: "A display name",
description: "A description",
abstract: "An agent-facing summary",
icon: "vscode.png",
allow_user_cancel_workspace_jobs: false,
allow_user_autostart: false,
@@ -74,6 +75,7 @@ const fillAndSubmitForm = async ({
name,
display_name,
description,
abstract,
icon,
allow_user_cancel_workspace_jobs,
}: FormValues) => {
@@ -89,6 +91,12 @@ const fillAndSubmitForm = async ({
await userEvent.clear(descriptionField);
await userEvent.type(descriptionField, description);
const abstractField = await screen.findByLabelText("Abstract");
await userEvent.clear(abstractField);
if (abstract !== "") {
await userEvent.type(abstractField, abstract);
}
const iconField = await screen.findByLabelText("Icon");
await userEvent.clear(iconField);
await userEvent.type(iconField, icon);
@@ -118,6 +126,10 @@ describe("TemplateSettingsPage", { timeout: 20_000 }, () => {
});
await fillAndSubmitForm(validFormValues);
await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1));
expect(API.updateTemplateMeta).toHaveBeenCalledWith(
MockTemplate.id,
expect.objectContaining({ abstract: validFormValues.abstract }),
);
});
it("displays an error if the name is taken", async () => {
@@ -165,6 +177,22 @@ describe("TemplateSettingsPage", { timeout: 20_000 }, () => {
expect(validate).toThrowError();
});
it.each<[string, string, boolean]>([
["allows the maximum abstract length", "a".repeat(2048), false],
["rejects an over-limit abstract", "a".repeat(2049), true],
])("%s", (_, abstract, wantError) => {
const values: UpdateTemplateMeta = {
...validFormValues,
abstract,
};
const validate = () => validationSchema.validateSync(values);
if (wantError) {
expect(validate).toThrowError();
} else {
expect(validate).not.toThrowError();
}
});
describe("Deprecate template", () => {
it("deprecates a template when has access control", async () => {
server.use(
@@ -57,3 +57,13 @@ export const NoEntitlementsExpiredSettings: Story = {
advancedSchedulingEnabled: false,
},
};
export const WithAbstract: Story = {
args: {
template: {
...MockTemplate,
abstract:
"This template provisions a remote VS Code environment backed by a Kubernetes pod. Agents should prefer it when the user mentions Kubernetes, k8s, or remote development.",
},
},
};
+1
View File
@@ -894,6 +894,7 @@ export const MockTemplate: TypesGen.Template = {
},
},
description: "This is a test description.",
abstract: "",
default_ttl_ms: 24 * 60 * 60 * 1000,
activity_bump_ms: 1 * 60 * 60 * 1000,
autostop_requirement: {