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" "template_version_id"
], ],
"properties": { "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": { "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.", "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" "type": "integer"
@@ -23315,6 +23320,10 @@ const docTemplate = `{
"codersdk.Template": { "codersdk.Template": {
"type": "object", "type": "object",
"properties": { "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": { "active_user_count": {
"description": "ActiveUserCount is set to -1 when loading.", "description": "ActiveUserCount is set to -1 when loading.",
"type": "integer" "type": "integer"
@@ -24427,6 +24436,11 @@ const docTemplate = `{
"codersdk.UpdateTemplateMeta": { "codersdk.UpdateTemplateMeta": {
"type": "object", "type": "object",
"properties": { "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": { "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.", "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" "type": "integer"
+14
View File
@@ -16174,6 +16174,11 @@
"type": "object", "type": "object",
"required": ["name", "template_version_id"], "required": ["name", "template_version_id"],
"properties": { "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": { "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.", "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" "type": "integer"
@@ -21411,6 +21416,10 @@
"codersdk.Template": { "codersdk.Template": {
"type": "object", "type": "object",
"properties": { "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": { "active_user_count": {
"description": "ActiveUserCount is set to -1 when loading.", "description": "ActiveUserCount is set to -1 when loading.",
"type": "integer" "type": "integer"
@@ -22464,6 +22473,11 @@
"codersdk.UpdateTemplateMeta": { "codersdk.UpdateTemplateMeta": {
"type": "object", "type": "object",
"properties": { "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": { "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.", "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" "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), Provisioner: takeFirst(seed.Provisioner, database.ProvisionerTypeEcho),
ActiveVersionID: takeFirst(seed.ActiveVersionID, uuid.New()), ActiveVersionID: takeFirst(seed.ActiveVersionID, uuid.New()),
Description: takeFirst(seed.Description, testutil.GetRandomName(t)), Description: takeFirst(seed.Description, testutil.GetRandomName(t)),
Abstract: seed.Abstract,
CreatedBy: takeFirst(seed.CreatedBy, uuid.New()), CreatedBy: takeFirst(seed.CreatedBy, uuid.New()),
Icon: takeFirst(seed.Icon, testutil.GetRandomName(t)), Icon: takeFirst(seed.Icon, testutil.GetRandomName(t)),
UserACL: seed.UserACL, 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, max_port_sharing_level app_sharing_level DEFAULT 'owner'::app_sharing_level NOT NULL,
use_classic_parameter_flow boolean DEFAULT false NOT NULL, use_classic_parameter_flow boolean DEFAULT false NOT NULL,
cors_behavior cors_behavior DEFAULT 'simple'::cors_behavior 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.'; 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.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 CREATE VIEW template_with_names AS
SELECT templates.id, SELECT templates.id,
templates.created_at, templates.created_at,
@@ -3150,6 +3153,7 @@ CREATE VIEW template_with_names AS
templates.use_classic_parameter_flow, templates.use_classic_parameter_flow,
templates.cors_behavior, templates.cors_behavior,
templates.disable_module_cache, templates.disable_module_cache,
templates.abstract,
COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url, COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url,
COALESCE(visible_users.username, ''::text) AS created_by_username, COALESCE(visible_users.username, ''::text) AS created_by_username,
COALESCE(visible_users.name, ''::text) AS created_by_name, 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.UseClassicParameterFlow,
&i.CorsBehavior, &i.CorsBehavior,
&i.DisableModuleCache, &i.DisableModuleCache,
&i.Abstract,
&i.CreatedByAvatarURL, &i.CreatedByAvatarURL,
&i.CreatedByUsername, &i.CreatedByUsername,
&i.CreatedByName, &i.CreatedByName,
+3
View File
@@ -5479,6 +5479,7 @@ type Template struct {
UseClassicParameterFlow bool `db:"use_classic_parameter_flow" json:"use_classic_parameter_flow"` UseClassicParameterFlow bool `db:"use_classic_parameter_flow" json:"use_classic_parameter_flow"`
CorsBehavior CorsBehavior `db:"cors_behavior" json:"cors_behavior"` CorsBehavior CorsBehavior `db:"cors_behavior" json:"cors_behavior"`
DisableModuleCache bool `db:"disable_module_cache" json:"disable_module_cache"` 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"` CreatedByAvatarURL string `db:"created_by_avatar_url" json:"created_by_avatar_url"`
CreatedByUsername string `db:"created_by_username" json:"created_by_username"` CreatedByUsername string `db:"created_by_username" json:"created_by_username"`
CreatedByName string `db:"created_by_name" json:"created_by_name"` 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"` UseClassicParameterFlow bool `db:"use_classic_parameter_flow" json:"use_classic_parameter_flow"`
CorsBehavior CorsBehavior `db:"cors_behavior" json:"cors_behavior"` CorsBehavior CorsBehavior `db:"cors_behavior" json:"cors_behavior"`
DisableModuleCache bool `db:"disable_module_cache" json:"disable_module_cache"` 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. // 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 const getTemplateByID = `-- name: GetTemplateByID :one
SELECT 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 FROM
template_with_names template_with_names
WHERE WHERE
@@ -24811,6 +24811,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
&i.UseClassicParameterFlow, &i.UseClassicParameterFlow,
&i.CorsBehavior, &i.CorsBehavior,
&i.DisableModuleCache, &i.DisableModuleCache,
&i.Abstract,
&i.CreatedByAvatarURL, &i.CreatedByAvatarURL,
&i.CreatedByUsername, &i.CreatedByUsername,
&i.CreatedByName, &i.CreatedByName,
@@ -24823,7 +24824,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one
SELECT 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 FROM
template_with_names AS templates template_with_names AS templates
WHERE WHERE
@@ -24875,6 +24876,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
&i.UseClassicParameterFlow, &i.UseClassicParameterFlow,
&i.CorsBehavior, &i.CorsBehavior,
&i.DisableModuleCache, &i.DisableModuleCache,
&i.Abstract,
&i.CreatedByAvatarURL, &i.CreatedByAvatarURL,
&i.CreatedByUsername, &i.CreatedByUsername,
&i.CreatedByName, &i.CreatedByName,
@@ -24886,7 +24888,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
} }
const getTemplates = `-- name: GetTemplates :many 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 ORDER BY (name, id) ASC
` `
@@ -24931,6 +24933,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
&i.UseClassicParameterFlow, &i.UseClassicParameterFlow,
&i.CorsBehavior, &i.CorsBehavior,
&i.DisableModuleCache, &i.DisableModuleCache,
&i.Abstract,
&i.CreatedByAvatarURL, &i.CreatedByAvatarURL,
&i.CreatedByUsername, &i.CreatedByUsername,
&i.CreatedByName, &i.CreatedByName,
@@ -24953,7 +24956,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
const getTemplatesWithFilter = `-- name: GetTemplatesWithFilter :many const getTemplatesWithFilter = `-- name: GetTemplatesWithFilter :many
SELECT 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 FROM
template_with_names AS t template_with_names AS t
LEFT JOIN LEFT JOIN
@@ -25113,6 +25116,7 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate
&i.UseClassicParameterFlow, &i.UseClassicParameterFlow,
&i.CorsBehavior, &i.CorsBehavior,
&i.DisableModuleCache, &i.DisableModuleCache,
&i.Abstract,
&i.CreatedByAvatarURL, &i.CreatedByAvatarURL,
&i.CreatedByUsername, &i.CreatedByUsername,
&i.CreatedByName, &i.CreatedByName,
@@ -25144,6 +25148,7 @@ INSERT INTO
provisioner, provisioner,
active_version_id, active_version_id,
description, description,
abstract,
created_by, created_by,
icon, icon,
user_acl, user_acl,
@@ -25155,7 +25160,7 @@ INSERT INTO
cors_behavior cors_behavior
) )
VALUES 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 { type InsertTemplateParams struct {
@@ -25167,6 +25172,7 @@ type InsertTemplateParams struct {
Provisioner ProvisionerType `db:"provisioner" json:"provisioner"` Provisioner ProvisionerType `db:"provisioner" json:"provisioner"`
ActiveVersionID uuid.UUID `db:"active_version_id" json:"active_version_id"` ActiveVersionID uuid.UUID `db:"active_version_id" json:"active_version_id"`
Description string `db:"description" json:"description"` Description string `db:"description" json:"description"`
Abstract string `db:"abstract" json:"abstract"`
CreatedBy uuid.UUID `db:"created_by" json:"created_by"` CreatedBy uuid.UUID `db:"created_by" json:"created_by"`
Icon string `db:"icon" json:"icon"` Icon string `db:"icon" json:"icon"`
UserACL TemplateACL `db:"user_acl" json:"user_acl"` UserACL TemplateACL `db:"user_acl" json:"user_acl"`
@@ -25188,6 +25194,7 @@ func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParam
arg.Provisioner, arg.Provisioner,
arg.ActiveVersionID, arg.ActiveVersionID,
arg.Description, arg.Description,
arg.Abstract,
arg.CreatedBy, arg.CreatedBy,
arg.Icon, arg.Icon,
arg.UserACL, arg.UserACL,
@@ -25291,15 +25298,16 @@ UPDATE
SET SET
updated_at = $2, updated_at = $2,
description = $3, description = $3,
name = $4, abstract = $4,
icon = $5, name = $5,
display_name = $6, icon = $6,
allow_user_cancel_workspace_jobs = $7, display_name = $7,
group_acl = $8, allow_user_cancel_workspace_jobs = $8,
max_port_sharing_level = $9, group_acl = $9,
use_classic_parameter_flow = $10, max_port_sharing_level = $10,
cors_behavior = $11, use_classic_parameter_flow = $11,
disable_module_cache = $12 cors_behavior = $12,
disable_module_cache = $13
WHERE WHERE
id = $1 id = $1
` `
@@ -25308,6 +25316,7 @@ type UpdateTemplateMetaByIDParams struct {
ID uuid.UUID `db:"id" json:"id"` ID uuid.UUID `db:"id" json:"id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Description string `db:"description" json:"description"` Description string `db:"description" json:"description"`
Abstract string `db:"abstract" json:"abstract"`
Name string `db:"name" json:"name"` Name string `db:"name" json:"name"`
Icon string `db:"icon" json:"icon"` Icon string `db:"icon" json:"icon"`
DisplayName string `db:"display_name" json:"display_name"` DisplayName string `db:"display_name" json:"display_name"`
@@ -25324,6 +25333,7 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl
arg.ID, arg.ID,
arg.UpdatedAt, arg.UpdatedAt,
arg.Description, arg.Description,
arg.Abstract,
arg.Name, arg.Name,
arg.Icon, arg.Icon,
arg.DisplayName, arg.DisplayName,
@@ -35319,7 +35329,7 @@ LEFT JOIN LATERAL (
) latest_build ON TRUE ) latest_build ON TRUE
LEFT JOIN LATERAL ( LEFT JOIN LATERAL (
SELECT 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 FROM
templates templates
WHERE WHERE
+12 -10
View File
@@ -129,6 +129,7 @@ INSERT INTO
provisioner, provisioner,
active_version_id, active_version_id,
description, description,
abstract,
created_by, created_by,
icon, icon,
user_acl, user_acl,
@@ -140,7 +141,7 @@ INSERT INTO
cors_behavior cors_behavior
) )
VALUES 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 -- name: UpdateTemplateActiveVersionByID :exec
UPDATE UPDATE
@@ -166,15 +167,16 @@ UPDATE
SET SET
updated_at = $2, updated_at = $2,
description = $3, description = $3,
name = $4, abstract = $4,
icon = $5, name = $5,
display_name = $6, icon = $6,
allow_user_cancel_workspace_jobs = $7, display_name = $7,
group_acl = $8, allow_user_cancel_workspace_jobs = $8,
max_port_sharing_level = $9, group_acl = $9,
use_classic_parameter_flow = $10, max_port_sharing_level = $10,
cors_behavior = $11, use_classic_parameter_flow = $11,
disable_module_cache = $12 cors_behavior = $12,
disable_module_cache = $13
WHERE WHERE
id = $1 id = $1
; ;
+4
View File
@@ -220,6 +220,7 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
OrganizationID: organization.ID, OrganizationID: organization.ID,
Name: createTemplate.Name, Name: createTemplate.Name,
Description: createTemplate.Description, Description: createTemplate.Description,
Abstract: createTemplate.Abstract,
CreatedBy: apiKey.UserID, CreatedBy: apiKey.UserID,
Icon: createTemplate.Icon, Icon: createTemplate.Icon,
DisplayName: createTemplate.DisplayName, DisplayName: createTemplate.DisplayName,
@@ -429,6 +430,7 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
Provisioner: importJob.Provisioner, Provisioner: importJob.Provisioner,
ActiveVersionID: templateVersion.ID, ActiveVersionID: templateVersion.ID,
Description: createTemplate.Description, Description: createTemplate.Description,
Abstract: createTemplate.Abstract,
CreatedBy: apiKey.UserID, CreatedBy: apiKey.UserID,
UserACL: database.TemplateACL{}, UserACL: database.TemplateACL{},
GroupACL: defaultsGroups, GroupACL: defaultsGroups,
@@ -764,6 +766,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
Name: resolved.name, Name: resolved.name,
DisplayName: resolved.displayName, DisplayName: resolved.displayName,
Description: resolved.description, Description: resolved.description,
Abstract: resolved.abstract,
Icon: resolved.icon, Icon: resolved.icon,
AllowUserCancelWorkspaceJobs: resolved.allowUserCancelWorkspaceJobs, AllowUserCancelWorkspaceJobs: resolved.allowUserCancelWorkspaceJobs,
GroupACL: resolved.groupACL, GroupACL: resolved.groupACL,
@@ -1017,6 +1020,7 @@ func (api *API) convertTemplate(
ActiveUserCount: owners, ActiveUserCount: owners,
BuildTimeStats: buildTimeStats, BuildTimeStats: buildTimeStats,
Description: template.Description, Description: template.Description,
Abstract: template.Abstract,
Icon: template.Icon, Icon: template.Icon,
DefaultTTLMillis: time.Duration(template.DefaultTTL).Milliseconds(), DefaultTTLMillis: time.Duration(template.DefaultTTL).Milliseconds(),
ActivityBumpMillis: time.Duration(template.ActivityBump).Milliseconds(), ActivityBumpMillis: time.Duration(template.ActivityBump).Milliseconds(),
+2
View File
@@ -19,6 +19,7 @@ type templateMetaUpdate struct {
name string name string
displayName string displayName string
description string description string
abstract string
icon string icon string
defaultTTLMillis int64 defaultTTLMillis int64
activityBumpMillis int64 activityBumpMillis int64
@@ -70,6 +71,7 @@ func resolveTemplateMetaUpdate(
name: ptr.NilToDefault(req.Name, template.Name), name: ptr.NilToDefault(req.Name, template.Name),
displayName: ptr.NilToDefault(req.DisplayName, template.DisplayName), displayName: ptr.NilToDefault(req.DisplayName, template.DisplayName),
description: ptr.NilToDefault(req.Description, template.Description), description: ptr.NilToDefault(req.Description, template.Description),
abstract: ptr.NilToDefault(req.Abstract, template.Abstract),
icon: ptr.NilToDefault(req.Icon, template.Icon), icon: ptr.NilToDefault(req.Icon, template.Icon),
defaultTTLMillis: ptr.NilToDefault(req.DefaultTTLMillis, time.Duration(template.DefaultTTL).Milliseconds()), defaultTTLMillis: ptr.NilToDefault(req.DefaultTTLMillis, time.Duration(template.DefaultTTL).Milliseconds()),
activityBumpMillis: ptr.NilToDefault(req.ActivityBumpMillis, time.Duration(template.ActivityBump).Milliseconds()), activityBumpMillis: ptr.NilToDefault(req.ActivityBumpMillis, time.Duration(template.ActivityBump).Milliseconds()),
@@ -42,6 +42,7 @@ func baselineTemplate() database.Template {
UseClassicParameterFlow: true, UseClassicParameterFlow: true,
CorsBehavior: database.CorsBehaviorPassthru, CorsBehavior: database.CorsBehaviorPassthru,
DisableModuleCache: true, DisableModuleCache: true,
Abstract: "Existing abstract.",
GroupACL: database.TemplateACL{ GroupACL: database.TemplateACL{
orgID.String(): {"read"}, orgID.String(): {"read"},
}, },
@@ -70,6 +71,7 @@ func baselineResolved() templateMetaUpdate {
name: tpl.Name, name: tpl.Name,
displayName: tpl.DisplayName, displayName: tpl.DisplayName,
description: tpl.Description, description: tpl.Description,
abstract: tpl.Abstract,
icon: tpl.Icon, icon: tpl.Icon,
defaultTTLMillis: tpl.DefaultTTL / 1e6, defaultTTLMillis: tpl.DefaultTTL / 1e6,
activityBumpMillis: tpl.ActivityBump / 1e6, activityBumpMillis: tpl.ActivityBump / 1e6,
@@ -148,6 +150,20 @@ func TestResolveTemplateMetaUpdate(t *testing.T) {
r.description = "New description" 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", name: "Icon",
req: codersdk.UpdateTemplateMeta{Icon: ptr.Ref("/new.svg")}, req: codersdk.UpdateTemplateMeta{Icon: ptr.Ref("/new.svg")},
+116 -7
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"net/http" "net/http"
"strings"
"sync/atomic" "sync/atomic"
"testing" "testing"
"time" "time"
@@ -86,6 +87,50 @@ func TestPostTemplateByOrganization(t *testing.T) {
assert.Equal(t, database.AuditActionCreate, auditor.AuditLogs()[2].Action) 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.Run("AlreadyExists", func(t *testing.T) {
t.Parallel() t.Parallel()
ownerClient := coderdtest.New(t, nil) ownerClient := coderdtest.New(t, nil)
@@ -942,6 +987,50 @@ func TestPatchTemplateMeta(t *testing.T) {
assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs()[4].Action) 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.Run("AlreadyExists", func(t *testing.T) {
t.Parallel() t.Parallel()
@@ -1336,6 +1425,7 @@ func TestPatchTemplateMeta(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, template.Name, updated.Name) assert.Equal(t, template.Name, updated.Name)
assert.Equal(t, template.Description, updated.Description) 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.Icon, updated.Icon)
assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis) assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
assert.Equal(t, template.ActivityBumpMillis, updated.ActivityBumpMillis) 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.WithinDuration(t, template.UpdatedAt, updated.UpdatedAt, time.Minute)
assert.Equal(t, template.Name, updated.Name) assert.Equal(t, template.Name, updated.Name)
assert.Equal(t, template.Description, updated.Description) 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.Icon, updated.Icon)
assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis) assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
}) })
@@ -1665,22 +1756,26 @@ func TestPatchTemplateMeta(t *testing.T) {
displayName := "Test Display Name" displayName := "Test Display Name"
description := "test-description" description := "test-description"
abstract := "test abstract for agents"
icon := "/icon/icon.png" icon := "/icon/icon.png"
defaultTTLMillis := 10 * time.Hour.Milliseconds() defaultTTLMillis := 10 * time.Hour.Milliseconds()
reference := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { reference := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.DisplayName = displayName ctr.DisplayName = displayName
ctr.Description = description ctr.Description = description
ctr.Abstract = abstract
ctr.Icon = icon ctr.Icon = icon
ctr.DefaultTTLMillis = ptr.Ref(defaultTTLMillis) ctr.DefaultTTLMillis = ptr.Ref(defaultTTLMillis)
}) })
require.Equal(t, displayName, reference.DisplayName) require.Equal(t, displayName, reference.DisplayName)
require.Equal(t, description, reference.Description) require.Equal(t, description, reference.Description)
require.Equal(t, abstract, reference.Abstract)
require.Equal(t, icon, reference.Icon) require.Equal(t, icon, reference.Icon)
restoreReq := codersdk.UpdateTemplateMeta{ restoreReq := codersdk.UpdateTemplateMeta{
DisplayName: &displayName, DisplayName: &displayName,
Description: &description, Description: &description,
Abstract: &abstract,
Icon: &icon, Icon: &icon,
DefaultTTLMillis: ptr.Ref(defaultTTLMillis), DefaultTTLMillis: ptr.Ref(defaultTTLMillis),
} }
@@ -1688,6 +1783,7 @@ func TestPatchTemplateMeta(t *testing.T) {
type expected struct { type expected struct {
displayName string displayName string
description string description string
abstract string
icon string icon string
defaultTTLMillis int64 defaultTTLMillis int64
} }
@@ -1702,22 +1798,27 @@ func TestPatchTemplateMeta(t *testing.T) {
{ {
name: "Only update default_ttl_ms", name: "Only update default_ttl_ms",
req: codersdk.UpdateTemplateMeta{DefaultTTLMillis: ptr.Ref(99 * time.Hour.Milliseconds())}, 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", name: "Clear display name",
req: codersdk.UpdateTemplateMeta{DisplayName: ptr.Ref("")}, 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", name: "Clear description",
req: codersdk.UpdateTemplateMeta{Description: ptr.Ref("")}, 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", name: "Clear icon",
req: codersdk.UpdateTemplateMeta{Icon: ptr.Ref("")}, 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 // 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 // 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", name: "Nil display name is a no-op",
req: codersdk.UpdateTemplateMeta{DisplayName: nil}, 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", name: "Nil description is a no-op",
req: codersdk.UpdateTemplateMeta{Description: nil}, 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", name: "Nil icon is a no-op",
req: codersdk.UpdateTemplateMeta{Icon: nil}, 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) require.NoError(t, err)
assert.Equal(t, tc.expected.displayName, updated.DisplayName) assert.Equal(t, tc.expected.displayName, updated.DisplayName)
assert.Equal(t, tc.expected.description, updated.Description) 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.icon, updated.Icon)
assert.Equal(t, tc.expected.defaultTTLMillis, updated.DefaultTTLMillis) 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) { template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.DisplayName = "Original Display" ctr.DisplayName = "Original Display"
ctr.Description = "Original description" ctr.Description = "Original description"
ctr.Abstract = "Original abstract"
ctr.Icon = "/icon/original.png" ctr.Icon = "/icon/original.png"
ctr.DefaultTTLMillis = ptr.Ref((24 * time.Hour).Milliseconds()) ctr.DefaultTTLMillis = ptr.Ref((24 * time.Hour).Milliseconds())
ctr.AllowUserCancelWorkspaceJobs = ptr.Ref(true) 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.Name, updated.Name)
assert.Equal(t, template.DisplayName, updated.DisplayName) assert.Equal(t, template.DisplayName, updated.DisplayName)
assert.Equal(t, template.Description, updated.Description) 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.Icon, updated.Icon)
assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis) assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
assert.Equal(t, template.AllowUserCancelWorkspaceJobs, updated.AllowUserCancelWorkspaceJobs) 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_templates",
"List available workspace templates. Optionally filter by a "+ "List available workspace templates. Optionally filter by a "+
"search query matching template name or description. "+ "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). "+ "Results are ordered by number of active developers (most popular first). "+
"Returns 10 per page. Use the page parameter to paginate through results.", "Returns 10 per page. Use the page parameter to paginate through results.",
func(ctx context.Context, args listTemplatesArgs, _ fantasy.ToolCall) (fantasy.ToolResponse, error) { 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 != "" { if desc := strings.TrimSpace(t.Description); desc != "" {
item["description"] = truncateRunes(desc, 200) item["description"] = truncateRunes(desc, 200)
} }
if abstract := strings.TrimSpace(t.Abstract); abstract != "" {
item["abstract"] = abstract
}
if count, ok := ownerCounts[t.ID]; ok && count > 0 { if count, ok := ownerCounts[t.ID]; ok && count > 0 {
item["active_developers"] = count item["active_developers"] = count
} }
@@ -3,6 +3,7 @@ package chattool_test
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"strings"
"testing" "testing"
"charm.land/fantasy" "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. //nolint:tparallel,paralleltest // Subtests share a single DB and run sequentially.
func TestTemplateAllowlistEnforcement(t *testing.T) { func TestTemplateAllowlistEnforcement(t *testing.T) {
t.Parallel() 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 != "" { if desc := strings.TrimSpace(template.Description); desc != "" {
templateInfo["description"] = desc templateInfo["description"] = desc
} }
if abstract := strings.TrimSpace(template.Abstract); abstract != "" {
templateInfo["abstract"] = abstract
}
paramList := make([]map[string]any, 0, len(params)) paramList := make([]map[string]any, 0, len(params))
for _, p := range params { for _, p := range params {
@@ -181,3 +181,74 @@ func TestReadTemplate_NoPresets(t *testing.T) {
_, hasPresets := result["presets"] _, hasPresets := result["presets"]
require.False(t, hasPresets, "presets key should be absent when there are none") 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 // Description is a description of what the template contains. It must be
// less than 128 bytes. // less than 128 bytes.
Description string `json:"description,omitempty" validate:"lt=128"` 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 // Icon is a relative path or external URL that specifies
// an icon to be displayed in the dashboard. // an icon to be displayed in the dashboard.
Icon string `json:"icon,omitempty"` Icon string `json:"icon,omitempty"`
+18 -12
View File
@@ -27,15 +27,18 @@ type Template struct {
Provisioner ProvisionerType `json:"provisioner" enums:"terraform"` Provisioner ProvisionerType `json:"provisioner" enums:"terraform"`
ActiveVersionID uuid.UUID `json:"active_version_id" format:"uuid"` ActiveVersionID uuid.UUID `json:"active_version_id" format:"uuid"`
// ActiveUserCount is set to -1 when loading. // ActiveUserCount is set to -1 when loading.
ActiveUserCount int `json:"active_user_count"` ActiveUserCount int `json:"active_user_count"`
BuildTimeStats TemplateBuildTimeStats `json:"build_time_stats"` BuildTimeStats TemplateBuildTimeStats `json:"build_time_stats"`
Description string `json:"description"` Description string `json:"description"`
Deprecated bool `json:"deprecated"` // Abstract is a longer-form summary surfaced to agents to help them pick
DeprecationMessage string `json:"deprecation_message"` // the right template. Up to 2048 characters.
Deleted bool `json:"deleted"` Abstract string `json:"abstract"`
Icon string `json:"icon"` Deprecated bool `json:"deprecated"`
DefaultTTLMillis int64 `json:"default_ttl_ms"` DeprecationMessage string `json:"deprecation_message"`
ActivityBumpMillis int64 `json:"activity_bump_ms"` 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 // AutostopRequirement and AutostartRequirement are enterprise features. Its
// value is only used if your license is entitled to use the advanced template // value is only used if your license is entitled to use the advanced template
// scheduling feature. // scheduling feature.
@@ -218,9 +221,12 @@ type ACLAvailable struct {
// UpdateTemplateMeta is the request body for the PATCH /templates/{template} // UpdateTemplateMeta is the request body for the PATCH /templates/{template}
// endpoint. All fields are optional. Fields that are nil are not modified. // endpoint. All fields are optional. Fields that are nil are not modified.
type UpdateTemplateMeta struct { type UpdateTemplateMeta struct {
Name *string `json:"name,omitempty" validate:"omitempty,template_name"` Name *string `json:"name,omitempty" validate:"omitempty,template_name"`
DisplayName *string `json:"display_name,omitempty" validate:"omitempty,template_display_name"` DisplayName *string `json:"display_name,omitempty" validate:"omitempty,template_display_name"`
Description *string `json:"description,omitempty"` 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"` Icon *string `json:"icon,omitempty"`
DefaultTTLMillis *int64 `json:"default_ttl_ms,omitempty"` DefaultTTLMillis *int64 `json:"default_ttl_ms,omitempty"`
// ActivityBumpMillis allows optionally specifying the activity bump // 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)) results := make([]SearchResultItem, len(templates))
for i, template := range 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{ results[i] = SearchResultItem{
ID: createObjectID(ObjectTypeTemplate, template.ID.String()).String(), ID: createObjectID(ObjectTypeTemplate, template.ID.String()).String(),
Title: template.DisplayName, Title: template.DisplayName,
Text: template.Description, Text: strings.Join(parts, "\n\n"),
URL: fmt.Sprintf("%s/templates/%s/%s", serverURL, template.OrganizationName, template.Name), 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") 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) { func TestChatGPTSearch_WorkspaceSearch(t *testing.T) {
t.Parallel() t.Parallel()
+5 -2
View File
@@ -621,7 +621,7 @@ var ListWorkspaces = Tool[ListWorkspacesArgs, []MinimalWorkspace]{
var ListTemplates = Tool[NoArgs, []MinimalTemplate]{ var ListTemplates = Tool[NoArgs, []MinimalTemplate]{
Tool: aisdk.Tool{ Tool: aisdk.Tool{
Name: ToolNameListTemplates, 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{ Schema: aisdk.Schema{
Properties: map[string]any{}, Properties: map[string]any{},
Required: []string{}, Required: []string{},
@@ -640,6 +640,7 @@ var ListTemplates = Tool[NoArgs, []MinimalTemplate]{
ID: template.ID.String(), ID: template.ID.String(),
Name: template.Name, Name: template.Name,
Description: template.Description, Description: template.Description,
Abstract: template.Abstract,
ActiveVersionID: template.ActiveVersionID, ActiveVersionID: template.ActiveVersionID,
ActiveUserCount: template.ActiveUserCount, ActiveUserCount: template.ActiveUserCount,
} }
@@ -732,7 +733,7 @@ func toPresetView(p codersdk.Preset) presetView {
var GetTemplate = Tool[GetTemplateArgs, TemplateDetail]{ var GetTemplate = Tool[GetTemplateArgs, TemplateDetail]{
Tool: aisdk.Tool{ Tool: aisdk.Tool{
Name: ToolNameGetTemplate, 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. 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(), ID: template.ID.String(),
Name: template.Name, Name: template.Name,
Description: template.Description, Description: template.Description,
Abstract: template.Abstract,
ActiveVersionID: template.ActiveVersionID, ActiveVersionID: template.ActiveVersionID,
ActiveUserCount: template.ActiveUserCount, ActiveUserCount: template.ActiveUserCount,
}, },
@@ -1719,6 +1721,7 @@ type MinimalTemplate struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Abstract string `json:"abstract,omitempty"`
ActiveVersionID uuid.UUID `json:"active_version_id"` ActiveVersionID uuid.UUID `json:"active_version_id"`
ActiveUserCount int `json:"active_user_count"` ActiveUserCount int `json:"active_user_count"`
} }
+23
View File
@@ -11,6 +11,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
"strings"
"sync" "sync"
"testing" "testing"
"time" "time"
@@ -309,6 +310,7 @@ func TestTools(t *testing.T) {
}) })
for i, template := range result { for i, template := range result {
require.Equal(t, expected[i].ID.String(), template.ID) 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") 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) { 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 --> <!-- Code generated by 'make docs/admin/security/audit-logs.md'. DO NOT EDIT -->
| <b>Resource<b> | | | | <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> | | 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> | | 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> | | 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> | | 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> | | 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> | | 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> | | 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> | | 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> | | 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> | | 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> | | 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> | | 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> | | 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> | | 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> | | 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> | | 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> | | 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> | | 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> | | 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> | | 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> | | 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> | | 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> | | 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> | | 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> | | 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> | | 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> | | 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> | | 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> | | 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> | | 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> | | 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> | | 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'. --> <!-- 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 ```json
{ {
"abstract": "string",
"activity_bump_ms": 0, "activity_bump_ms": 0,
"allow_user_autostart": true, "allow_user_autostart": true,
"allow_user_autostop": 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 | | 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. | | `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_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. | | `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 ```json
{ {
"abstract": "string",
"active_user_count": 0, "active_user_count": 0,
"active_version_id": "eae64611-bd53-4a80-bb77-df1e432c0fbc", "active_version_id": "eae64611-bd53-4a80-bb77-df1e432c0fbc",
"activity_bump_ms": 0, "activity_bump_ms": 0,
@@ -12029,6 +12032,7 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
| Name | Type | Required | Restrictions | Description | | 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_user_count` | integer | false | | Active user count is set to -1 when loading. |
| `active_version_id` | string | false | | | | `active_version_id` | string | false | | |
| `activity_bump_ms` | integer | 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 ```json
{ {
"abstract": "string",
"activity_bump_ms": 0, "activity_bump_ms": 0,
"allow_user_autostart": true, "allow_user_autostart": true,
"allow_user_autostop": 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 | | 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. | | `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` | boolean | false | | |
| `allow_user_autostop` | 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 ```json
[ [
{ {
"abstract": "string",
"active_user_count": 0, "active_user_count": 0,
"active_version_id": "eae64611-bd53-4a80-bb77-df1e432c0fbc", "active_version_id": "eae64611-bd53-4a80-bb77-df1e432c0fbc",
"activity_bump_ms": 0, "activity_bump_ms": 0,
@@ -100,6 +101,7 @@ Status Code **200**
| Name | Type | Required | Restrictions | Description | | Name | Type | Required | Restrictions | Description |
|--------------------------------------|------------------------------------------------------------------------------------------|----------|--------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |--------------------------------------|------------------------------------------------------------------------------------------|----------|--------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `[array item]` | array | false | | | | `[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_user_count` | integer | false | | Active user count is set to -1 when loading. |
| `» active_version_id` | string(uuid) | false | | | | `» active_version_id` | string(uuid) | false | | |
| `» activity_bump_ms` | integer | false | | | | `» activity_bump_ms` | integer | false | | |
@@ -171,6 +173,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa
```json ```json
{ {
"abstract": "string",
"activity_bump_ms": 0, "activity_bump_ms": 0,
"allow_user_autostart": true, "allow_user_autostart": true,
"allow_user_autostop": true, "allow_user_autostop": true,
@@ -216,6 +219,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa
```json ```json
{ {
"abstract": "string",
"active_user_count": 0, "active_user_count": 0,
"active_version_id": "eae64611-bd53-4a80-bb77-df1e432c0fbc", "active_version_id": "eae64611-bd53-4a80-bb77-df1e432c0fbc",
"activity_bump_ms": 0, "activity_bump_ms": 0,
@@ -368,6 +372,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat
```json ```json
{ {
"abstract": "string",
"active_user_count": 0, "active_user_count": 0,
"active_version_id": "eae64611-bd53-4a80-bb77-df1e432c0fbc", "active_version_id": "eae64611-bd53-4a80-bb77-df1e432c0fbc",
"activity_bump_ms": 0, "activity_bump_ms": 0,
@@ -790,6 +795,7 @@ To include deprecated templates, specify `deprecated:true` in the search query.
```json ```json
[ [
{ {
"abstract": "string",
"active_user_count": 0, "active_user_count": 0,
"active_version_id": "eae64611-bd53-4a80-bb77-df1e432c0fbc", "active_version_id": "eae64611-bd53-4a80-bb77-df1e432c0fbc",
"activity_bump_ms": 0, "activity_bump_ms": 0,
@@ -860,6 +866,7 @@ Status Code **200**
| Name | Type | Required | Restrictions | Description | | Name | Type | Required | Restrictions | Description |
|--------------------------------------|------------------------------------------------------------------------------------------|----------|--------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |--------------------------------------|------------------------------------------------------------------------------------------|----------|--------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `[array item]` | array | false | | | | `[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_user_count` | integer | false | | Active user count is set to -1 when loading. |
| `» active_version_id` | string(uuid) | false | | | | `» active_version_id` | string(uuid) | false | | |
| `» activity_bump_ms` | integer | false | | | | `» activity_bump_ms` | integer | false | | |
@@ -994,6 +1001,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template} \
```json ```json
{ {
"abstract": "string",
"active_user_count": 0, "active_user_count": 0,
"active_version_id": "eae64611-bd53-4a80-bb77-df1e432c0fbc", "active_version_id": "eae64611-bd53-4a80-bb77-df1e432c0fbc",
"activity_bump_ms": 0, "activity_bump_ms": 0,
@@ -1120,6 +1128,7 @@ curl -X PATCH http://coder-server:8080/api/v2/templates/{template} \
```json ```json
{ {
"abstract": "string",
"activity_bump_ms": 0, "activity_bump_ms": 0,
"allow_user_autostart": true, "allow_user_autostart": true,
"allow_user_autostop": true, "allow_user_autostop": true,
@@ -1168,6 +1177,7 @@ curl -X PATCH http://coder-server:8080/api/v2/templates/{template} \
```json ```json
{ {
"abstract": "string",
"active_user_count": 0, "active_user_count": 0,
"active_version_id": "eae64611-bd53-4a80-bb77-df1e432c0fbc", "active_version_id": "eae64611-bd53-4a80-bb77-df1e432c0fbc",
"activity_bump_ms": 0, "activity_bump_ms": 0,
+1
View File
@@ -105,6 +105,7 @@ var auditableResourcesTypes = map[any]map[string]Action{
"provisioner": ActionTrack, "provisioner": ActionTrack,
"active_version_id": ActionTrack, "active_version_id": ActionTrack,
"description": ActionTrack, "description": ActionTrack,
"abstract": ActionTrack,
"icon": ActionTrack, "icon": ActionTrack,
"default_ttl": ActionTrack, "default_ttl": ActionTrack,
"autostart_block_days_of_week": ActionTrack, "autostart_block_days_of_week": ActionTrack,
+15
View File
@@ -3476,6 +3476,11 @@ export interface CreateTemplateRequest {
* less than 128 bytes. * less than 128 bytes.
*/ */
readonly description?: 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;
/** /**
* Icon is a relative path or external URL that specifies * Icon is a relative path or external URL that specifies
* an icon to be displayed in the dashboard. * an icon to be displayed in the dashboard.
@@ -8025,6 +8030,11 @@ export interface Template {
readonly active_user_count: number; readonly active_user_count: number;
readonly build_time_stats: TemplateBuildTimeStats; readonly build_time_stats: TemplateBuildTimeStats;
readonly description: 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 deprecated: boolean; readonly deprecated: boolean;
readonly deprecation_message: string; readonly deprecation_message: string;
readonly deleted: boolean; readonly deleted: boolean;
@@ -8841,6 +8851,11 @@ export interface UpdateTemplateMeta {
readonly name?: string; readonly name?: string;
readonly display_name?: string; readonly display_name?: string;
readonly description?: 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 icon?: string;
readonly default_ttl_ms?: number; readonly default_ttl_ms?: number;
/** /**
@@ -38,6 +38,8 @@ import {
const MAX_DESCRIPTION_CHAR_LIMIT = 128; 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_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({ export const validationSchema = Yup.object({
name: nameValidator("Name"), name: nameValidator("Name"),
@@ -46,6 +48,7 @@ export const validationSchema = Yup.object({
MAX_DESCRIPTION_CHAR_LIMIT, MAX_DESCRIPTION_CHAR_LIMIT,
MAX_DESCRIPTION_MESSAGE, MAX_DESCRIPTION_MESSAGE,
), ),
abstract: Yup.string().max(MAX_ABSTRACT_CHAR_LIMIT, MAX_ABSTRACT_MESSAGE),
allow_user_cancel_workspace_jobs: Yup.boolean(), allow_user_cancel_workspace_jobs: Yup.boolean(),
icon: iconValidator, icon: iconValidator,
require_active_version: Yup.boolean(), require_active_version: Yup.boolean(),
@@ -85,6 +88,7 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
name: template.name, name: template.name,
display_name: template.display_name, display_name: template.display_name,
description: template.description, description: template.description,
abstract: template.abstract,
icon: template.icon, icon: template.icon,
allow_user_cancel_workspace_jobs: allow_user_cancel_workspace_jobs:
template.allow_user_cancel_workspace_jobs, template.allow_user_cancel_workspace_jobs,
@@ -148,6 +152,19 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
rows={2} 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 <IconField
{...getFieldHelpers("icon")} {...getFieldHelpers("icon")}
disabled={isSubmitting} disabled={isSubmitting}
@@ -27,6 +27,7 @@ const validFormValues: FormValues = {
name: "Name", name: "Name",
display_name: "A display name", display_name: "A display name",
description: "A description", description: "A description",
abstract: "An agent-facing summary",
icon: "vscode.png", icon: "vscode.png",
allow_user_cancel_workspace_jobs: false, allow_user_cancel_workspace_jobs: false,
allow_user_autostart: false, allow_user_autostart: false,
@@ -74,6 +75,7 @@ const fillAndSubmitForm = async ({
name, name,
display_name, display_name,
description, description,
abstract,
icon, icon,
allow_user_cancel_workspace_jobs, allow_user_cancel_workspace_jobs,
}: FormValues) => { }: FormValues) => {
@@ -89,6 +91,12 @@ const fillAndSubmitForm = async ({
await userEvent.clear(descriptionField); await userEvent.clear(descriptionField);
await userEvent.type(descriptionField, description); 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"); const iconField = await screen.findByLabelText("Icon");
await userEvent.clear(iconField); await userEvent.clear(iconField);
await userEvent.type(iconField, icon); await userEvent.type(iconField, icon);
@@ -118,6 +126,10 @@ describe("TemplateSettingsPage", { timeout: 20_000 }, () => {
}); });
await fillAndSubmitForm(validFormValues); await fillAndSubmitForm(validFormValues);
await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1)); 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 () => { it("displays an error if the name is taken", async () => {
@@ -165,6 +177,22 @@ describe("TemplateSettingsPage", { timeout: 20_000 }, () => {
expect(validate).toThrowError(); 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", () => { describe("Deprecate template", () => {
it("deprecates a template when has access control", async () => { it("deprecates a template when has access control", async () => {
server.use( server.use(
@@ -57,3 +57,13 @@ export const NoEntitlementsExpiredSettings: Story = {
advancedSchedulingEnabled: false, 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.", description: "This is a test description.",
abstract: "",
default_ttl_ms: 24 * 60 * 60 * 1000, default_ttl_ms: 24 * 60 * 60 * 1000,
activity_bump_ms: 1 * 60 * 60 * 1000, activity_bump_ms: 1 * 60 * 60 * 1000,
autostop_requirement: { autostop_requirement: {