diff --git a/workspace-server/internal/handlers/concierge_template_test.go b/workspace-server/internal/handlers/concierge_template_test.go new file mode 100644 index 000000000..9d4f58352 --- /dev/null +++ b/workspace-server/internal/handlers/concierge_template_test.go @@ -0,0 +1,27 @@ +package handlers + +import ( + "testing" + + "git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/models" +) + +// RFC §5.7 / #30: a kind='platform' concierge with no explicit template must +// resolve to the platform-agent template (its identity), not the generic +// claude-code-default config. +func TestConciergeTemplateOrDefault(t *testing.T) { + cases := []struct { + name, kind, template, want string + }{ + {"platform empty -> platform-agent", models.KindPlatform, "", "platform-agent"}, + {"platform blank -> platform-agent", models.KindPlatform, " ", "platform-agent"}, + {"platform explicit kept", models.KindPlatform, "custom", "custom"}, + {"workspace empty stays empty", "workspace", "", ""}, + {"workspace seo-agent kept", "workspace", "seo-agent", "seo-agent"}, + } + for _, c := range cases { + if got := conciergeTemplateOrDefault(c.kind, c.template); got != c.want { + t.Errorf("%s: conciergeTemplateOrDefault(%q,%q)=%q want %q", c.name, c.kind, c.template, got, c.want) + } + } +} diff --git a/workspace-server/internal/handlers/platform_agent.go b/workspace-server/internal/handlers/platform_agent.go index 9cbf868f9..84836b323 100644 --- a/workspace-server/internal/handlers/platform_agent.go +++ b/workspace-server/internal/handlers/platform_agent.go @@ -587,9 +587,9 @@ func installPlatformAgent(ctx context.Context, database *sql.DB, platformID, nam // provisioning state. The integration tests use unique names per fixture // to avoid cross-test collision (CR-A RC 10610). if _, err := tx.ExecContext(ctx, ` - INSERT INTO workspaces (id, name, kind, tier, status, runtime, parent_id) - VALUES ($1, $2, 'platform', 0, 'offline', 'claude-code', NULL) - ON CONFLICT (id) DO UPDATE SET kind = 'platform', runtime = 'claude-code', parent_id = NULL + INSERT INTO workspaces (id, name, kind, tier, status, runtime, parent_id, template) + VALUES ($1, $2, 'platform', 0, 'offline', 'claude-code', NULL, 'platform-agent') + ON CONFLICT (id) DO UPDATE SET kind = 'platform', runtime = 'claude-code', parent_id = NULL, template = 'platform-agent' `, platformID, name); err != nil { return fmt.Errorf("upsert platform agent: %w", err) } diff --git a/workspace-server/internal/handlers/workspace_provision.go b/workspace-server/internal/handlers/workspace_provision.go index a8ba35ccb..b7dd50ea9 100644 --- a/workspace-server/internal/handlers/workspace_provision.go +++ b/workspace-server/internal/handlers/workspace_provision.go @@ -286,6 +286,25 @@ func workspaceMemoryNamespace(workspaceID string) string { return fmt.Sprintf("workspace:%s", workspaceID) } +// conciergeTemplateOrDefault forces the platform-agent template for a +// kind='platform' concierge when no explicit template is set. RFC §5.7: the +// concierge identity (config.yaml + prompts/concierge.md + mcp_servers.yaml) is +// delivered "like any other runtime template" via the platform-agent template +// entry. But the platform-agent workspace row was upserted with no `template` +// (platform_agent.go installPlatformAgent), so payload.Template was empty and +// the identity resolved to the GENERIC claude-code-default config — the +// concierge booted online but with no persona ("doesn't know it's the platform +// agent", #30/#2970). Forcing "platform-agent" here makes the asset fetcher pull +// the concierge identity for every concierge provision/restart, new or existing, +// without depending on the row's template column being backfilled. An explicit +// template (set by a future caller) still wins. +func conciergeTemplateOrDefault(kind, template string) string { + if kind == models.KindPlatform && strings.TrimSpace(template) == "" { + return "platform-agent" + } + return template +} + func (h *WorkspaceHandler) buildProvisionerConfig( ctx context.Context, workspaceID, templatePath string, @@ -416,7 +435,7 @@ func (h *WorkspaceHandler) buildProvisionerConfig( // not duplicated across first-provision + restart paths. // nil fetcher = "no fetcher wired" (self-host default; // falls through to the local TemplatePath path). - TemplateIdentity: templateIdentityForTemplateOrRuntime(payload.Template, payload.Runtime), + TemplateIdentity: templateIdentityForTemplateOrRuntime(conciergeTemplateOrDefault(kind, payload.Template), payload.Runtime), TemplateAssetFetcher: h.giteaTemplateFetcher, } }