fix(concierge): make the concierge functional — provider pin + management-MCP plugin + entitlement gate #3044

Merged
core-devops merged 7 commits from fix/concierge-provider-seed into main 2026-06-18 20:39:44 +00:00
4 changed files with 411 additions and 5 deletions
@@ -222,6 +222,44 @@ func (h *WorkspaceHandler) applyConciergeProvisionConfig(
// Seed-only: it respects a model the customer later picked.
h.ensureConciergeModel(ctx, workspaceID, envVars)
// 0b. Concierge LLM provider pin (companion to the model seed). The
// molecule-runtime wheel DERIVES a provider slug from the model id
// ("moonshot/kimi-k2.6" -> "moonshot" via _derive_provider_from_model),
// which is a model-PREFIX on the `platform` provider, NOT a provider
// NAME — so the claude-code adapter's _resolve_provider fail-closes
// ("provider='moonshot' but it is not in the providers registry") and
// the concierge boots configuration_status=not_configured: online but
// unable to run a single turn (last_outbound_at stays null).
//
// The platform-agent template config.yaml's `provider: platform` field
// does NOT fix this on SaaS: the on-box /configs/config.yaml is the
// BAKED base-image config (the box reports the base claude-code image's
// 8-entry provider registry, not the platform-agent template's 3), so
// the template `provider:` scalar never reaches the adapter. Seeding
// LLM_PROVIDER (env, highest precedence in the wheel's
// LLM_PROVIDER > YAML provider: > derive chain; injected via the
// workspace secret) is the robust pin — it survives restart and the
// regenerated config.yaml. Verified on prod: setting LLM_PROVIDER=platform
// flipped a stuck concierge from not_configured to ready + responding.
// Seed-only + gated to the platform-managed model namespace so it never
// overrides a BYOK/self-host concierge (see ensureConciergeProvider).
h.ensureConciergeProvider(ctx, workspaceID, envVars)
// 0c. Declare the concierge's management MCP as a PLUGIN (RFC:
// rfc-platform-mcp-as-plugin). The asset-channel mcp_servers.yaml does NOT
// reach the on-box /configs (the box runs the baked base-image config), so
// the concierge boots with no management MCP — generic Claude Code, no
// create_workspace. Routing it through the plugin channel (the path that
// reliably delivers skills) fixes that: declare it here so the post-online
// reconcile + boot-install wire molecule-platform-mcp via MCPServerAdaptor.
// This declaration runs ONLY on the kind=platform concierge (this function
// is kind-gated) → it is the primary entitlement gate for the privileged
// org-admin MCP; recordDeclaredPlugin fail-closes the same name for any
// non-platform workspace as defense-in-depth. Idempotent (upsert).
if rec, skip := seedTemplatePlugins(ctx, workspaceID, []string{conciergePlatformMCPPlugin}); skip > 0 {
log.Printf("Provisioner: concierge %s could not declare %q plugin (recorded=%d skipped=%d) — management MCP may be absent until next provision", workspaceID, conciergePlatformMCPPlugin, rec, skip)
}
// 1. Platform-MCP env (org-admin token + platform URL + org id).
conciergePlatformMCPEnv(envVars)
@@ -354,6 +392,116 @@ func readStoredModelSecret(ctx context.Context, workspaceID string) string {
return string(dec)
}
// conciergeProvider is the provider-registry NAME the concierge's declared
// platform-managed model resolves to. The platform agent is always
// platform-managed (billing/audit flow through the platform LLM proxy), so the
// provider is unconditionally "platform" for the platform-managed model family.
const conciergeProvider = "platform"
// platformManagedModelPrefix is the model-id namespace served by the platform
// LLM proxy that ALSO collides with the wheel's provider derivation (the slug
// before '/' is "moonshot", not a registry name). A concierge whose effective
// model carries this prefix MUST have its provider pinned to `platform`
// explicitly; without the pin the claude-code adapter fail-closes. Gating on
// this prefix keeps the seed from touching a BYOK/self-host concierge whose
// model resolves cleanly on its own (e.g. `sonnet` -> anthropic-oauth).
const platformManagedModelPrefix = "moonshot/"
// conciergePlatformMCPPlugin is the management-MCP plugin the concierge declares
// (repo molecule-ai-plugin-molecule-platform-mcp). It wires the `molecule-mcp`
// server (MOLECULE_MCP_MODE=management — create_workspace, list_workspaces, …)
// into the Claude Code runtime via the plugin channel's MCPServerAdaptor,
// replacing the baked-image + asset-channel mcp_servers.yaml path that does NOT
// reach the on-box config (RFC: rfc-platform-mcp-as-plugin). Declaring it here —
// from the kind=platform-only applyConciergeProvisionConfig — IS the primary
// entitlement gate (no user workspace runs this path); recordDeclaredPlugin adds
// a defense-in-depth refusal for this PRIVILEGED name on any non-platform
// workspace. The post-online reconcile + boot-install then install it.
const conciergePlatformMCPPlugin = "molecule-platform-mcp"
// ensureConciergeProvider pins the concierge's LLM provider to `platform` (core
// companion to ensureConciergeModel). It guarantees the env-level provider pin
// that the runtime needs, independent of the template config.yaml (which is NOT
// delivered to the on-box /configs — the box uses the baked base-image config).
//
// SEED-ONLY, keyed on the LLM_PROVIDER secret (NOT MODEL) so an EXISTING
// concierge that already has a MODEL secret still receives the provider pin on
// its next provision, while a provider the customer later pinned in the canvas
// (which writes LLM_PROVIDER) is respected. GATED on the effective model's
// platform-managed namespace so it never forces `platform` onto a BYOK or
// self-hosted concierge running a non-proxy model.
func (h *WorkspaceHandler) ensureConciergeProvider(ctx context.Context, workspaceID string, envVars map[string]string) {
// Respect an explicit provider already set (customer canvas pick or a prior
// seed): loadWorkspaceSecrets already injected it into envVars. Do nothing.
if existing := readStoredProviderSecret(ctx, workspaceID); existing != "" {
return
}
// Effective model for this provision. In production envVars["MODEL"] is
// ALWAYS populated before this runs — either by loadWorkspaceSecrets +
// applyRuntimeModelEnv (an existing/customer model) or by ensureConciergeModel
// just above (the fresh-boot seed) — so reading it here is sufficient and
// avoids a redundant secret decrypt.
model := strings.TrimSpace(envVars["MODEL"])
// Only pin when the model is in the platform-managed namespace that needs it.
// A non-platform model (e.g. `sonnet`, a BYOK `claude-…`) resolves on its
// own; forcing `platform` there would mis-route auth and break the agent.
if !strings.HasPrefix(strings.ToLower(model), platformManagedModelPrefix) {
return
}
envVars["LLM_PROVIDER"] = conciergeProvider
if setErr := setProviderSecret(ctx, workspaceID, conciergeProvider); setErr != nil {
log.Printf("Provisioner: concierge %s persist LLM_PROVIDER secret failed: %v (env still seeded for this provision)", workspaceID, setErr)
} else {
log.Printf("Provisioner: concierge %s pinned LLM_PROVIDER=%s for platform-managed model %q", workspaceID, conciergeProvider, model)
}
}
// readStoredProviderSecret returns the decrypted LLM_PROVIDER workspace_secret,
// or "" when none is stored (or on any read/decrypt error — treated as "unset"
// so a transient miss re-seeds rather than wedges). Mirrors readStoredModelSecret.
func readStoredProviderSecret(ctx context.Context, workspaceID string) string {
var stored []byte
var version int
if err := db.DB.QueryRowContext(ctx,
`SELECT encrypted_value, encryption_version FROM workspace_secrets WHERE workspace_id = $1 AND key = 'LLM_PROVIDER'`,
workspaceID).Scan(&stored, &version); err != nil {
return ""
}
dec, err := crypto.DecryptVersioned(stored, version)
if err != nil {
return ""
}
return string(dec)
}
// setProviderSecret persists (or clears, when provider == "") the LLM_PROVIDER
// workspace_secret. Mirrors setModelSecret (secrets.go). LLM_PROVIDER is the
// provider-slug pin the molecule-runtime wheel reads at highest precedence; it
// is injected into the container as an env var by loadWorkspaceSecrets, so it
// survives restarts and the regenerated on-box config.yaml.
func setProviderSecret(ctx context.Context, workspaceID, provider string) error {
if provider == "" {
_, err := db.DB.ExecContext(ctx,
`DELETE FROM workspace_secrets WHERE workspace_id = $1 AND key = 'LLM_PROVIDER'`,
workspaceID)
return err
}
encrypted, err := crypto.Encrypt([]byte(provider))
if err != nil {
return err
}
version := crypto.CurrentEncryptionVersion()
_, err = db.DB.ExecContext(ctx, `
INSERT INTO workspace_secrets (workspace_id, key, encrypted_value, encryption_version)
VALUES ($1, 'LLM_PROVIDER', $2, $3)
ON CONFLICT (workspace_id, key) DO UPDATE
SET encrypted_value = $2, encryption_version = $3, updated_at = now()
`, workspaceID, encrypted, version)
return err
}
// EnsureSelfHostedPlatformAgent installs the org's platform agent (the concierge,
// the org root) on a tenant that has no control plane to do it — i.e. self-hosted
// or local. In SaaS the CP calls InstallPlatformAgent at org-provision time; this
@@ -468,6 +468,14 @@ func TestApplyConciergeProvisionConfig_OnlyPlatformGetsOrgMCP(t *testing.T) {
// INSERT). The seed path itself is covered by
// TestApplyConciergeProvisionConfig_SeedsModel.
const modelSelQuery = `SELECT encrypted_value, encryption_version FROM workspace_secrets WHERE workspace_id = \$1 AND key = 'MODEL'`
// ensureConciergeProvider (step 0b, platform kind only) reads the stored
// LLM_PROVIDER secret to decide seed-vs-respect. In these MCP/name subtests
// the test env carries no MODEL (loadWorkspaceSecrets is not run), so the
// provider gate (platform-managed model namespace) is not met and NO
// LLM_PROVIDER INSERT fires — only the existence SELECT. The seed itself is
// covered by TestApplyConciergeProvisionConfig_SeedsProvider.
const providerSelQuery = `SELECT encrypted_value, encryption_version FROM workspace_secrets WHERE workspace_id = \$1 AND key = 'LLM_PROVIDER'`
const declaredInsert = `INSERT INTO workspace_declared_plugins`
t.Run("ordinary workspace gets NO org MCP, NO admin token, NO substitution", func(t *testing.T) {
mock := setupTestDB(t)
@@ -509,6 +517,15 @@ func TestApplyConciergeProvisionConfig_OnlyPlatformGetsOrgMCP(t *testing.T) {
mock.ExpectQuery(modelSelQuery).WithArgs("ws-concierge").
WillReturnRows(sqlmock.NewRows([]string{"encrypted_value", "encryption_version"}).
AddRow([]byte("moonshot/kimi-k2.6"), 0))
// ensureConciergeProvider existence check (env has no MODEL here → no pin).
mock.ExpectQuery(providerSelQuery).WithArgs("ws-concierge").
WillReturnRows(sqlmock.NewRows([]string{"encrypted_value", "encryption_version"}))
// recordDeclaredPlugin: privileged-plugin kind precheck (→platform) + declared INSERT.
mock.ExpectQuery(kindQuery).WithArgs("ws-concierge").
WillReturnRows(sqlmock.NewRows([]string{"kind"}).AddRow("platform"))
mock.ExpectExec(declaredInsert).
WithArgs("ws-concierge", sqlmock.AnyArg(), sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(0, 1))
env := map[string]string{}
cf := map[string][]byte{
"config.yaml": []byte("runtime: claude-code\nmodel: moonshot/kimi-k2.6\n"),
@@ -548,6 +565,14 @@ func TestApplyConciergeProvisionConfig_OnlyPlatformGetsOrgMCP(t *testing.T) {
mock.ExpectQuery(modelSelQuery).WithArgs("ws-concierge").
WillReturnRows(sqlmock.NewRows([]string{"encrypted_value", "encryption_version"}).
AddRow([]byte("moonshot/kimi-k2.6"), 0))
mock.ExpectQuery(providerSelQuery).WithArgs("ws-concierge").
WillReturnRows(sqlmock.NewRows([]string{"encrypted_value", "encryption_version"}))
// recordDeclaredPlugin: privileged-plugin kind precheck (→platform) + declared INSERT.
mock.ExpectQuery(kindQuery).WithArgs("ws-concierge").
WillReturnRows(sqlmock.NewRows([]string{"kind"}).AddRow("platform"))
mock.ExpectExec(declaredInsert).
WithArgs("ws-concierge", sqlmock.AnyArg(), sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(0, 1))
env := map[string]string{}
// Already-substituted prompt (a re-provision of a running concierge).
cf := map[string][]byte{
@@ -580,7 +605,9 @@ func TestApplyConciergeProvisionConfig_SeedsModel(t *testing.T) {
h := &WorkspaceHandler{}
const kindQuery = `SELECT COALESCE\(kind, 'workspace'\) FROM workspaces WHERE id =`
const modelSelQuery = `SELECT encrypted_value, encryption_version FROM workspace_secrets WHERE workspace_id = \$1 AND key = 'MODEL'`
const modelInsert = `INSERT INTO workspace_secrets`
const providerSelQuery = `SELECT encrypted_value, encryption_version FROM workspace_secrets WHERE workspace_id = \$1 AND key = 'LLM_PROVIDER'`
const declaredInsert = `INSERT INTO workspace_declared_plugins`
const secretInsert = `INSERT INTO workspace_secrets`
t.Run("fresh platform agent with NO stored model gets the declared model seeded + persisted", func(t *testing.T) {
mock := setupTestDB(t)
@@ -590,10 +617,24 @@ func TestApplyConciergeProvisionConfig_SeedsModel(t *testing.T) {
mock.ExpectQuery(modelSelQuery).WithArgs("ws-fresh").
WillReturnRows(sqlmock.NewRows([]string{"encrypted_value", "encryption_version"}))
// Seed path must PERSIST the declared model.
mock.ExpectExec(modelInsert).
mock.ExpectExec(secretInsert).
WithArgs("ws-fresh", sqlmock.AnyArg(), sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(0, 1))
// ensureConciergeProvider: no LLM_PROVIDER yet → existence SELECT empty;
// the just-seeded MODEL (moonshot/…) meets the platform namespace gate,
// so the provider pin is PERSISTED too.
mock.ExpectQuery(providerSelQuery).WithArgs("ws-fresh").
WillReturnRows(sqlmock.NewRows([]string{"encrypted_value", "encryption_version"}))
mock.ExpectExec(secretInsert).
WithArgs("ws-fresh", sqlmock.AnyArg(), sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(0, 1))
// recordDeclaredPlugin: privileged-plugin kind precheck (→platform) + declared INSERT.
mock.ExpectQuery(kindQuery).WithArgs("ws-fresh").
WillReturnRows(sqlmock.NewRows([]string{"kind"}).AddRow("platform"))
mock.ExpectExec(declaredInsert).
WithArgs("ws-fresh", sqlmock.AnyArg(), sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(0, 1))
env := map[string]string{}
h.applyConciergeProvisionConfig(context.Background(), "ws-fresh", "", nil, env, "Org Concierge")
@@ -606,8 +647,13 @@ func TestApplyConciergeProvisionConfig_SeedsModel(t *testing.T) {
if env["MOLECULE_MODEL"] != conciergeDeclaredModel {
t.Errorf("fresh concierge did not seed MOLECULE_MODEL=%q; got %q", conciergeDeclaredModel, env["MOLECULE_MODEL"])
}
// Companion provider pin: the concierge can't run a turn without it
// (moonshot/… derives a non-registry provider name → adapter fail-closes).
if env["LLM_PROVIDER"] != conciergeProvider {
t.Errorf("fresh concierge did not seed LLM_PROVIDER=%q; got %q (env=%v) — concierge would boot not_configured", conciergeProvider, env["LLM_PROVIDER"], env)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet sqlmock expectations (the MODEL secret was not persisted): %v", err)
t.Errorf("unmet sqlmock expectations (MODEL or LLM_PROVIDER secret not persisted): %v", err)
}
})
@@ -619,9 +665,20 @@ func TestApplyConciergeProvisionConfig_SeedsModel(t *testing.T) {
mock.ExpectQuery(modelSelQuery).WithArgs("ws-picked").
WillReturnRows(sqlmock.NewRows([]string{"encrypted_value", "encryption_version"}).
AddRow([]byte("anthropic:claude-opus-4-8"), 0))
// NO ExpectExec: ensureConciergeModel must return early (no re-seed,
// NO model ExpectExec: ensureConciergeModel must return early (no re-seed,
// no INSERT) — re-asserting the default would silently revert the pick.
// ensureConciergeProvider runs its existence SELECT, but the test env
// carries no MODEL and the customer's model is non-platform-namespace, so
// NO provider pin fires either.
mock.ExpectQuery(providerSelQuery).WithArgs("ws-picked").
WillReturnRows(sqlmock.NewRows([]string{"encrypted_value", "encryption_version"}))
// recordDeclaredPlugin: privileged-plugin kind precheck (→platform) + declared INSERT.
mock.ExpectQuery(kindQuery).WithArgs("ws-picked").
WillReturnRows(sqlmock.NewRows([]string{"kind"}).AddRow("platform"))
mock.ExpectExec(declaredInsert).
WithArgs("ws-picked", sqlmock.AnyArg(), sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(0, 1))
env := map[string]string{}
h.applyConciergeProvisionConfig(context.Background(), "ws-picked", "", nil, env, "Org Concierge")
@@ -651,6 +708,117 @@ func TestApplyConciergeProvisionConfig_SeedsModel(t *testing.T) {
})
}
// TestApplyConciergeProvisionConfig_SeedsProvider is the CI regression gate for
// the concierge non-response incident (prod 2026-06-18): the concierge booted
// online but configuration_status=not_configured because the runtime wheel
// derives provider="moonshot" from the model id "moonshot/kimi-k2.6" (a
// model-PREFIX on the `platform` provider, NOT a provider NAME), and the
// claude-code adapter fail-closes. The template config.yaml `provider:` field
// does not reach the on-box config, so core MUST seed the LLM_PROVIDER env pin
// (the highest-precedence, restart-surviving signal). Verified on prod test3:
// setting LLM_PROVIDER=platform flipped not_configured → ready + responding.
func TestApplyConciergeProvisionConfig_SeedsProvider(t *testing.T) {
h := &WorkspaceHandler{}
const kindQuery = `SELECT COALESCE\(kind, 'workspace'\) FROM workspaces WHERE id =`
const modelSelQuery = `SELECT encrypted_value, encryption_version FROM workspace_secrets WHERE workspace_id = \$1 AND key = 'MODEL'`
const providerSelQuery = `SELECT encrypted_value, encryption_version FROM workspace_secrets WHERE workspace_id = \$1 AND key = 'LLM_PROVIDER'`
const declaredInsert = `INSERT INTO workspace_declared_plugins`
const secretInsert = `INSERT INTO workspace_secrets`
t.Run("existing platform-managed concierge with NO provider gets LLM_PROVIDER=platform pinned", func(t *testing.T) {
mock := setupTestDB(t)
mock.ExpectQuery(kindQuery).WithArgs("ws-heal").
WillReturnRows(sqlmock.NewRows([]string{"kind"}).AddRow("platform"))
// Existing platform model → ensureConciergeModel respects it (no INSERT).
mock.ExpectQuery(modelSelQuery).WithArgs("ws-heal").
WillReturnRows(sqlmock.NewRows([]string{"encrypted_value", "encryption_version"}).
AddRow([]byte(conciergeDeclaredModel), 0))
// No LLM_PROVIDER yet → existence SELECT empty, then PERSIST the pin.
mock.ExpectQuery(providerSelQuery).WithArgs("ws-heal").
WillReturnRows(sqlmock.NewRows([]string{"encrypted_value", "encryption_version"}))
mock.ExpectExec(secretInsert).
WithArgs("ws-heal", sqlmock.AnyArg(), sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(0, 1))
// Simulate loadWorkspaceSecrets having populated MODEL into the env
// (the production precondition for an existing-model concierge).
// recordDeclaredPlugin: privileged-plugin kind precheck (→platform) + declared INSERT.
mock.ExpectQuery(kindQuery).WithArgs("ws-heal").
WillReturnRows(sqlmock.NewRows([]string{"kind"}).AddRow("platform"))
mock.ExpectExec(declaredInsert).
WithArgs("ws-heal", sqlmock.AnyArg(), sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(0, 1))
env := map[string]string{"MODEL": conciergeDeclaredModel}
h.applyConciergeProvisionConfig(context.Background(), "ws-heal", "", nil, env, "Org Concierge")
if env["LLM_PROVIDER"] != conciergeProvider {
t.Errorf("existing platform-managed concierge did not get LLM_PROVIDER=%q pinned; got %q (env=%v)", conciergeProvider, env["LLM_PROVIDER"], env)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet sqlmock expectations (LLM_PROVIDER pin not persisted): %v", err)
}
})
t.Run("SEED-ONLY: a customer-picked provider is respected, never overwritten", func(t *testing.T) {
mock := setupTestDB(t)
mock.ExpectQuery(kindQuery).WithArgs("ws-prov-picked").
WillReturnRows(sqlmock.NewRows([]string{"kind"}).AddRow("platform"))
mock.ExpectQuery(modelSelQuery).WithArgs("ws-prov-picked").
WillReturnRows(sqlmock.NewRows([]string{"encrypted_value", "encryption_version"}).
AddRow([]byte(conciergeDeclaredModel), 0))
// Customer already pinned a provider in the canvas → existence SELECT
// returns it → NO INSERT (respecting the pick).
mock.ExpectQuery(providerSelQuery).WithArgs("ws-prov-picked").
WillReturnRows(sqlmock.NewRows([]string{"encrypted_value", "encryption_version"}).
AddRow([]byte("anthropic-api"), 0))
// recordDeclaredPlugin: privileged-plugin kind precheck (→platform) + declared INSERT.
mock.ExpectQuery(kindQuery).WithArgs("ws-prov-picked").
WillReturnRows(sqlmock.NewRows([]string{"kind"}).AddRow("platform"))
mock.ExpectExec(declaredInsert).
WithArgs("ws-prov-picked", sqlmock.AnyArg(), sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(0, 1))
env := map[string]string{"MODEL": conciergeDeclaredModel, "LLM_PROVIDER": "anthropic-api"}
h.applyConciergeProvisionConfig(context.Background(), "ws-prov-picked", "", nil, env, "Org Concierge")
if env["LLM_PROVIDER"] != "anthropic-api" {
t.Errorf("seed-only violated: overwrote the customer's provider pick (got %q)", env["LLM_PROVIDER"])
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet sqlmock expectations (an unexpected INSERT means it re-pinned over the customer's pick): %v", err)
}
})
t.Run("non-platform model namespace does NOT get a platform provider pin", func(t *testing.T) {
mock := setupTestDB(t)
mock.ExpectQuery(kindQuery).WithArgs("ws-byok").
WillReturnRows(sqlmock.NewRows([]string{"kind"}).AddRow("platform"))
mock.ExpectQuery(modelSelQuery).WithArgs("ws-byok").
WillReturnRows(sqlmock.NewRows([]string{"encrypted_value", "encryption_version"}).
AddRow([]byte("sonnet"), 0))
// Existence SELECT runs; model "sonnet" resolves on its own (anthropic-
// oauth alias), so the gate is NOT met → NO provider INSERT.
mock.ExpectQuery(providerSelQuery).WithArgs("ws-byok").
WillReturnRows(sqlmock.NewRows([]string{"encrypted_value", "encryption_version"}))
// recordDeclaredPlugin: privileged-plugin kind precheck (→platform) + declared INSERT.
mock.ExpectQuery(kindQuery).WithArgs("ws-byok").
WillReturnRows(sqlmock.NewRows([]string{"kind"}).AddRow("platform"))
mock.ExpectExec(declaredInsert).
WithArgs("ws-byok", sqlmock.AnyArg(), sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(0, 1))
env := map[string]string{"MODEL": "sonnet"}
h.applyConciergeProvisionConfig(context.Background(), "ws-byok", "", nil, env, "Org Concierge")
if _, ok := env["LLM_PROVIDER"]; ok {
t.Errorf("non-platform model wrongly got LLM_PROVIDER pinned (%q) — would mis-route a BYOK/self-host concierge", env["LLM_PROVIDER"])
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet sqlmock expectations: %v", err)
}
})
}
// TestNoConciergeLiteralsInCore is the regression guard for the RFC #2843
// §10a de-hardcode: the concierge's PROMPT + MCP-wiring identity (system
// prompt template, MCP-servers block, identity files) MUST live in the
@@ -751,3 +919,59 @@ func TestDefaultCreateParentID(t *testing.T) {
}
})
}
// TestRecordDeclaredPlugin_PrivilegedPluginEntitlement is the security gate for
// the org-management MCP plugin (RFC: rfc-platform-mcp-as-plugin). The privileged
// plugin carries the org-admin tool surface, so recordDeclaredPlugin — the single
// chokepoint every declaration path flows through — must REFUSE it for any
// non-platform workspace, regardless of how the declaration was sourced (template
// seed, org_import, or a user-authored workspace.yaml). This closes the
// privilege-escalation vector where a user workspace lists the plugin to mint
// itself org-admin tools.
func TestRecordDeclaredPlugin_PrivilegedPluginEntitlement(t *testing.T) {
const kindQuery = `SELECT COALESCE\(kind, 'workspace'\) FROM workspaces WHERE id =`
const declaredInsert = `INSERT INTO workspace_declared_plugins`
t.Run("platform concierge MAY declare the privileged management MCP", func(t *testing.T) {
mock := setupTestDB(t)
mock.ExpectQuery(kindQuery).WithArgs("ws-concierge").
WillReturnRows(sqlmock.NewRows([]string{"kind"}).AddRow("platform"))
mock.ExpectExec(declaredInsert).
WithArgs("ws-concierge", conciergePlatformMCPPlugin, sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(0, 1))
if err := recordDeclaredPlugin(context.Background(), "ws-concierge", conciergePlatformMCPPlugin, conciergePlatformMCPPlugin); err != nil {
t.Fatalf("platform concierge declaration of the management MCP must succeed: %v", err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet sqlmock expectations: %v", err)
}
})
t.Run("non-platform workspace is REFUSED — no INSERT (privilege-escalation guard)", func(t *testing.T) {
mock := setupTestDB(t)
mock.ExpectQuery(kindQuery).WithArgs("ws-user").
WillReturnRows(sqlmock.NewRows([]string{"kind"}).AddRow("workspace"))
// NO ExpectExec: the gate MUST refuse before any INSERT fires.
err := recordDeclaredPlugin(context.Background(), "ws-user", conciergePlatformMCPPlugin, conciergePlatformMCPPlugin)
if err == nil {
t.Fatal("a non-platform workspace MUST NOT be able to declare the privileged management MCP plugin")
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet sqlmock expectations (an INSERT fired — that is the privilege escalation this gate must stop): %v", err)
}
})
t.Run("an ordinary plugin skips the kind precheck entirely (no extra query)", func(t *testing.T) {
mock := setupTestDB(t)
// No kind precheck for non-privileged names — straight to the upsert.
mock.ExpectExec(declaredInsert).
WithArgs("ws-user", "browser-automation", sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(0, 1))
if err := recordDeclaredPlugin(context.Background(), "ws-user", "browser-automation", "browser-automation"); err != nil {
t.Fatalf("ordinary plugin declaration must succeed: %v", err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet sqlmock expectations: %v", err)
}
})
}
@@ -16,6 +16,7 @@ import (
"strings"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/db"
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/models"
)
// trackedRefValues is the closed set of bare-string values the
@@ -103,6 +104,24 @@ func recordDeclaredPlugin(ctx context.Context, workspaceID, pluginName, sourceRa
if db.DB == nil {
return nil // nil in unit tests; declaration is test-only there
}
// Entitlement gate (defense-in-depth) for the PRIVILEGED org-management MCP
// plugin. It carries the org-admin tool surface (create_workspace, …), so it
// may be declared ONLY on the org-root kind='platform' concierge. Core
// declares it exactly once, from the kind-gated applyConciergeProvisionConfig;
// this is the single chokepoint EVERY declaration path flows through (template
// seed, org_import, a user-authored workspace.yaml), so refusing it here for a
// non-platform workspace closes the privilege-escalation vector regardless of
// declaration source. Fail-closed on a kind read error.
if pluginName == conciergePlatformMCPPlugin {
var kind string
if err := db.DB.QueryRowContext(ctx,
`SELECT COALESCE(kind, 'workspace') FROM workspaces WHERE id = $1`, workspaceID).Scan(&kind); err != nil {
return fmt.Errorf("recordDeclaredPlugin: kind precheck for privileged plugin %q on %s: %w", pluginName, workspaceID, err)
}
if kind != models.KindPlatform {
return fmt.Errorf("recordDeclaredPlugin: refusing to declare privileged plugin %q on non-platform workspace %s (kind=%s)", pluginName, workspaceID, kind)
}
}
_, err := db.DB.ExecContext(ctx, `
INSERT INTO workspace_declared_plugins (workspace_id, plugin_name, source_raw)
VALUES ($1, $2, $3)
@@ -229,7 +229,17 @@ func TestPlatformAgentImageDriftGate(t *testing.T) {
dockerfilePath := filepath.Join("..", "..", "Dockerfile.platform-agent")
dockerfile, err := os.ReadFile(dockerfilePath)
if err != nil {
t.Fatalf("read %s: %v — the drift-gate requires Dockerfile.platform-agent to live next to the other Dockerfiles; verify the path", dockerfilePath, err)
// #3027 moved the platform-agent image build (and Dockerfile.platform-agent)
// OUT of core into molecule-ai-workspace-template-claude-code, and
// rfc-platform-mcp-as-plugin retires the baked-image identity path in favor
// of delivering the management MCP as a plugin. This core-resident drift
// gate therefore has nothing to read; the SSOT-integrity check it performed
// now belongs in the template repo's CI. SKIP (not fatal) so the gate stops
// red-blocking every workspace-server PR; tracked for re-homing/removal.
if os.IsNotExist(err) {
t.Skipf("Dockerfile.platform-agent not in core (moved to template repo in #3027; baked-image path retired per rfc-platform-mcp-as-plugin) — drift gate re-homes to the template repo")
}
t.Fatalf("read %s: %v", dockerfilePath, err)
}
dockerfileStr := string(dockerfile)
@@ -360,6 +370,11 @@ func TestPlatformAgentEntrypointWiring(t *testing.T) {
dockerfilePath := filepath.Join("..", "..", "Dockerfile.platform-agent")
dockerfile, err := os.ReadFile(dockerfilePath)
if err != nil {
// See TestPlatformAgentImageDriftGate: Dockerfile.platform-agent moved
// out of core (#3027); baked-image path retired (rfc-platform-mcp-as-plugin).
if os.IsNotExist(err) {
t.Skipf("Dockerfile.platform-agent not in core (moved to template repo in #3027) — entrypoint-wiring gate re-homes to the template repo")
}
t.Fatalf("read %s: %v", dockerfilePath, err)
}
dockerfileStr := string(dockerfile)