From 9125fb0028a546caca1ea43cd5c77ecf35e91fe5 Mon Sep 17 00:00:00 2001 From: dustinvannoy-db Date: Thu, 23 Jul 2026 19:05:24 -0700 Subject: [PATCH 1/4] Recover from missing/stale claude-plugins-official on aitools install Installing the databricks plugin for Claude Code installs from Claude's built-in claude-plugins-official marketplace. When a user has removed that marketplace or their local copy is out of date, the install fails with "Plugin not found in marketplace" and no guidance (issue #212). Add a recovery path for built-in marketplaces: on that failure, in an interactive session prompt to re-add and refresh the marketplace and retry the install once; in a non-interactive session (or when the user declines or the retry still fails) return a BlockedError whose detail includes the agent's own stderr plus the exact repair commands. A built-in marketplace stays shared infrastructure we never record as ours, so uninstall never de-registers it. Co-authored-by: Isaac --- libs/aitools/agents/agents.go | 19 +++- libs/aitools/installer/plugin.go | 109 +++++++++++++++++++ libs/aitools/installer/plugin_test.go | 146 ++++++++++++++++++++++++++ 3 files changed, 271 insertions(+), 3 deletions(-) diff --git a/libs/aitools/agents/agents.go b/libs/aitools/agents/agents.go index 572282da03b..d51da28a869 100644 --- a/libs/aitools/agents/agents.go +++ b/libs/aitools/agents/agents.go @@ -23,6 +23,13 @@ type PluginSpec struct { // (e.g. "databricks/databricks-agent-skills"). Empty marks a built-in // marketplace that must not be added or de-registered. Source string + // BuiltinAddSource is the ` plugin marketplace add` argument used only + // to repair a built-in marketplace (Source empty) that the install found + // missing or out of date (e.g. "anthropics/claude-plugins-official"). It is a + // recovery-only hint: unlike Source it never triggers a routine add or a + // de-register on uninstall, so the built-in marketplace stays shared infra we + // don't claim to own. Empty when the built-in marketplace can't be re-added. + BuiltinAddSource string } // Agent defines a supported coding agent. @@ -127,6 +134,11 @@ const ( // our own marketplace for Claude. An empty PluginSpec.Source marks a built-in // marketplace that must not be added. claudeOfficialMarketplace = "claude-plugins-official" + // claudeOfficialSource re-adds the built-in marketplace when a user has removed + // it or their local copy is out of date (the only case where the CLI touches a + // built-in marketplace). Per Claude Code's docs this is the `owner/repo` the + // `plugin marketplace add` command expects for the official marketplace. + claudeOfficialSource = "anthropics/claude-plugins-official" ) // databricksPlugin returns the shared plugin descriptor for an agent that @@ -144,9 +156,10 @@ func databricksPlugin() *PluginSpec { // the CLI doesn't register a separate databricks-agent-skills marketplace for it. func claudePlugin() *PluginSpec { return &PluginSpec{ - Marketplace: claudeOfficialMarketplace, - ID: databricksPluginID, - Source: "", + Marketplace: claudeOfficialMarketplace, + ID: databricksPluginID, + Source: "", + BuiltinAddSource: claudeOfficialSource, } } diff --git a/libs/aitools/installer/plugin.go b/libs/aitools/installer/plugin.go index 13fc9475e31..c9067c2cfd8 100644 --- a/libs/aitools/installer/plugin.go +++ b/libs/aitools/installer/plugin.go @@ -10,6 +10,7 @@ import ( "time" "github.com/databricks/cli/libs/aitools/agents" + "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/log" "github.com/databricks/cli/libs/process" ) @@ -111,6 +112,13 @@ func marketplaceAddArgs(spec *agents.PluginSpec) []string { return []string{"plugin", "marketplace", "add", spec.Source} } +// marketplaceAddSourceArgs builds `plugin marketplace add ` for an +// explicit source (used to re-add a built-in marketplace during recovery, where +// the source is BuiltinAddSource rather than the empty PluginSpec.Source). +func marketplaceAddSourceArgs(source string) []string { + return []string{"plugin", "marketplace", "add", source} +} + // marketplaceRegistered reports whether the named marketplace is already listed // by the agent's plugin CLI. On any uncertainty (command unsupported, error) it // returns true, so we never claim to have added a marketplace we didn't, which @@ -268,6 +276,22 @@ func InstallPluginForAgent(ctx context.Context, agent *agents.Agent, nativeScope } if _, err := runAgentCmd(ctx, pluginCmdTimeout, prepend(bin, pluginInstallArgs(agent, nativeScope))); err != nil { + // The proactive refresh above updates every registered marketplace, so a + // stale built-in copy is already handled. A user who removed the built-in + // marketplace entirely surfaces here instead, as "not found in marketplace": + // the refresh can't fail on a marketplace that is no longer registered, so + // this install step is where that case appears. Only that failure is + // repairable by re-adding the marketplace; auth or network errors are not, so + // builtinMarketplaceRepairable gates the recovery to avoid prompting to re-add + // a marketplace that is present. The repair re-adds and refreshes shared + // infrastructure; it never records ownership, so uninstall still leaves the + // built-in marketplace in place. + if builtinMarketplaceRepairable(agent, err) { + if rec, ok := recoverBuiltinMarketplace(ctx, bin, agent, nativeScope, ref, err); ok { + return rec, nil + } + return PluginRecord{}, builtinMarketplaceError(agent, err) + } // Roll back a marketplace we just added so a failed install doesn't // leave an orphaned, untracked marketplace registration behind. if installedMarketplace { @@ -287,6 +311,72 @@ func InstallPluginForAgent(ctx context.Context, agent *agents.Agent, nativeScope }, nil } +// recoverBuiltinMarketplace handles a built-in-marketplace install failure whose +// error says the marketplace is missing (a user removed it); the proactive +// refresh already covers a merely-stale copy. In an interactive session it asks +// permission, re-adds and refreshes the marketplace, and retries the install +// once. It returns (record, true) only when that retry succeeds. When the +// session is non-interactive, the user declines, or the retry still fails, it +// returns (_, false) so the caller surfaces an actionable error. +// +// The built-in marketplace is shared infrastructure: even after re-adding it we +// never set InstalledMarketplace, so uninstall never de-registers it. +func recoverBuiltinMarketplace(ctx context.Context, bin string, agent *agents.Agent, nativeScope, ref string, installErr error) (PluginRecord, bool) { + // Only prompt when there is a terminal that can answer. Code that calls + // InstallPluginForAgent without a cmdio (e.g. some tests) must not panic. + if !cmdio.HasIO(ctx) || !cmdio.IsPromptSupported(ctx) { + return PluginRecord{}, false + } + + cmdio.LogString(ctx, fmt.Sprintf( + "%s could not install the databricks plugin from the %q marketplace:\n %s", + agent.DisplayName, agent.Plugin.Marketplace, stderrOf(installErr))) + proceed, err := cmdio.AskYesOrNo(ctx, fmt.Sprintf( + "The marketplace may be missing or out of date. Add and refresh it (%s plugin marketplace add %s) and retry?", + agent.Binary, agent.Plugin.BuiltinAddSource)) + if err != nil || !proceed { + return PluginRecord{}, false + } + + // Re-add (fixes a removed marketplace) then refresh (fixes a stale copy). Each + // step may harmlessly fail (e.g. add when it is already present), so we only + // log at debug level and let the retried install be the real verdict. + if _, err := runAgentCmd(ctx, pluginCmdTimeout, prepend(bin, marketplaceAddSourceArgs(agent.Plugin.BuiltinAddSource))); err != nil { + log.Debugf(ctx, "re-adding the %s marketplace failed (it may already be present): %v", agent.Plugin.Marketplace, stderrOf(err)) + } + if args := marketplaceUpdateArgs(agent); args != nil { + if _, err := runAgentCmd(ctx, pluginCmdTimeout, prepend(bin, args)); err != nil { + log.Debugf(ctx, "refreshing the %s marketplace failed: %v", agent.Plugin.Marketplace, stderrOf(err)) + } + } + if _, err := runAgentCmd(ctx, pluginCmdTimeout, prepend(bin, pluginInstallArgs(agent, nativeScope))); err != nil { + return PluginRecord{}, false + } + + return PluginRecord{ + Marketplace: agent.Plugin.Marketplace, + Plugin: agent.Plugin.ID, + Scope: nativeScope, + Version: DisplaySkillsVersion(ref), + }, true +} + +// builtinMarketplaceError wraps a built-in-marketplace install failure with the +// exact commands that repair it, so a user who wasn't prompted (non-interactive) +// or whose automatic retry failed knows the issue and the fix. +func builtinMarketplaceError(agent *agents.Agent, installErr error) error { + return &BlockedError{ + Agent: agent.Name, + Reason: ReasonInstallFailed, + Detail: fmt.Sprintf( + "%s\n\nThe %q marketplace is missing or out of date. Fix it with:\n %s plugin marketplace add %s\n %s plugin marketplace update %s\nthen re-run the install.", + stderrOf(installErr), + agent.Plugin.Marketplace, + agent.Binary, agent.Plugin.BuiltinAddSource, + agent.Binary, agent.Plugin.Marketplace), + } +} + // UpdatePluginForAgent updates the plugin through the agent's own CLI. The // plugin's own update handles content the release dropped, so there is no // per-skill prune for plugin agents. @@ -345,6 +435,25 @@ func claudeAlreadyDisabled(err error) bool { return strings.Contains(strings.ToLower(stderrOf(err)), "already disabled") } +// marketplaceMissing reports whether a step failed because the marketplace is +// unknown to the agent CLI (the user removed it), as opposed to an auth or +// network failure. The agent CLIs expose no error code for this, so we match the +// stderr the way claudeAlreadyDisabled does; callers use it only to decide +// whether re-adding the marketplace is worth attempting, never as the final +// verdict (the retried install is). +func marketplaceMissing(err error) bool { + s := strings.ToLower(stderrOf(err)) + return strings.Contains(s, "not found in marketplace") || strings.Contains(s, "marketplace not found") +} + +// builtinMarketplaceRepairable reports whether a failed refresh or install is the +// removed-built-in-marketplace case recoverBuiltinMarketplace can repair: the +// marketplace has no routine add (empty Source) but a known re-add source, and +// the error says the marketplace is missing rather than an unrelated failure. +func builtinMarketplaceRepairable(agent *agents.Agent, err error) bool { + return agent.Plugin.Source == "" && agent.Plugin.BuiltinAddSource != "" && marketplaceMissing(err) +} + // RecordPluginInstalls persists plugin install records into the state file for // the given CLI scope (global or project), creating state if none exists. ref // is the resolved skills release the install corresponds to. diff --git a/libs/aitools/installer/plugin_test.go b/libs/aitools/installer/plugin_test.go index 2652918c441..a991e277f17 100644 --- a/libs/aitools/installer/plugin_test.go +++ b/libs/aitools/installer/plugin_test.go @@ -1,9 +1,12 @@ package installer import ( + "context" "errors" + "io" "os/exec" "path/filepath" + "strings" "testing" "github.com/databricks/cli/libs/aitools/agents" @@ -41,6 +44,29 @@ func pluginAgent(name, display, binary string) *agents.Agent { func claudeAgent() *agents.Agent { return pluginAgent(agents.NameClaudeCode, "Claude Code", "claude") } func codexAgent() *agents.Agent { return pluginAgent(agents.NameCodex, "Codex CLI", "codex") } +// builtinMarketplaceAgent models Claude Code: the plugin lives in a built-in +// marketplace (empty Source) that the CLI never adds routinely, but can re-add +// via BuiltinAddSource when an install fails because it's missing or stale. +func builtinMarketplaceAgent() *agents.Agent { + return &agents.Agent{ + Name: agents.NameClaudeCode, + DisplayName: "Claude Code", + Binary: "claude", + Plugin: &agents.PluginSpec{ + Marketplace: "claude-plugins-official", + ID: "databricks", + Source: "", + BuiltinAddSource: "anthropics/claude-plugins-official", + }, + } +} + +// promptCtx returns a context whose cmdio supports prompting and answers the +// next AskYesOrNo with the given stdin line (e.g. "y\n" or "n\n"). +func promptCtx(ctx context.Context, stdin string) context.Context { + return cmdio.InContext(ctx, cmdio.NewTestIO(strings.NewReader(stdin), io.Discard, io.Discard)) +} + func noPluginAgent() *agents.Agent { return &agents.Agent{Name: agents.NameOpenCode, DisplayName: "OpenCode", Binary: "opencode"} } @@ -92,6 +118,126 @@ func TestInstallPluginForAgentBuiltinMarketplace(t *testing.T) { assert.Contains(t, cmds, "claude plugin install databricks@claude-plugins-official --scope user") } +func TestInstallBuiltinMarketplaceRecoversAfterPrompt(t *testing.T) { + stubAgentLookPath(t, true) + ctx, stub := process.WithStub(t.Context()) + // The install fails while the marketplace is missing/stale, then succeeds after + // it's re-added and refreshed. Track the retried install via a call counter. + installs := 0 + stub.WithCallback(func(cmd *exec.Cmd) error { + if strings.Contains(strings.Join(cmd.Args, " "), "plugin install") { + installs++ + if installs == 1 { + // First install fails as it would when the marketplace is stale. + _, _ = cmd.Stderr.Write([]byte(`Plugin "databricks" not found in marketplace "claude-plugins-official"`)) + return errors.New("exit status 1") + } + } + return nil + }) + // User agrees to the repair prompt. + ctx = promptCtx(ctx, "y\n") + + rec, err := InstallPluginForAgent(ctx, builtinMarketplaceAgent(), "user", "main") + require.NoError(t, err) + assert.Equal(t, "claude-plugins-official", rec.Marketplace) + assert.False(t, rec.InstalledMarketplace, "a built-in marketplace is never recorded as ours, even after a repair") + + cmds := stub.Commands() + assert.Contains(t, cmds, "claude plugin marketplace add anthropics/claude-plugins-official") + assert.Contains(t, cmds, "claude plugin marketplace update") + assert.Equal(t, 2, installs, "the install is retried once after the repair") +} + +func TestInstallBuiltinMarketplaceUserDeclinesPrompt(t *testing.T) { + stubAgentLookPath(t, true) + ctx, stub := process.WithStub(t.Context()) + stub.WithFailureFor("plugin install", errors.New("exit status 1")) + stub.WithStderrFor("plugin install", `Plugin "databricks" not found in marketplace "claude-plugins-official"`) + stub.WithCallback(func(*exec.Cmd) error { return nil }) + // User declines the repair prompt. + ctx = promptCtx(ctx, "n\n") + + _, err := InstallPluginForAgent(ctx, builtinMarketplaceAgent(), "user", "main") + var be *BlockedError + require.ErrorAs(t, err, &be) + assert.Equal(t, ReasonInstallFailed, be.Reason) + // The actionable error names the exact repair commands. + assert.Contains(t, be.Detail, "claude plugin marketplace add anthropics/claude-plugins-official") + assert.Contains(t, be.Detail, "claude plugin marketplace update claude-plugins-official") + + // The proactive refresh (marketplace update, no name) runs before every Claude + // install; declining recovery must only skip the re-add, not that refresh. + for _, c := range stub.Commands() { + assert.NotContains(t, c, "marketplace add", "declining must not re-add the marketplace") + } +} + +func TestInstallBuiltinMarketplaceNonInteractiveActionableError(t *testing.T) { + stubAgentLookPath(t, true) + ctx, stub := process.WithStub(t.Context()) + stub.WithFailureFor("plugin install", errors.New("exit status 1")) + stub.WithStderrFor("plugin install", `Plugin "databricks" not found in marketplace "claude-plugins-official"`) + stub.WithCallback(func(*exec.Cmd) error { return nil }) + // No cmdio on the context: a non-interactive run must not prompt, must not + // touch the marketplace, and must return the actionable error. + _, err := InstallPluginForAgent(ctx, builtinMarketplaceAgent(), "user", "main") + var be *BlockedError + require.ErrorAs(t, err, &be) + assert.Equal(t, ReasonInstallFailed, be.Reason) + assert.Contains(t, be.Detail, `Plugin "databricks" not found`) + assert.Contains(t, be.Detail, "claude plugin marketplace add anthropics/claude-plugins-official") + + // The proactive refresh runs before install; a non-interactive run must only + // skip the recovery re-add, not that refresh. + for _, c := range stub.Commands() { + assert.NotContains(t, c, "marketplace add") + } +} + +func TestInstallBuiltinMarketplaceRetryStillFails(t *testing.T) { + stubAgentLookPath(t, true) + ctx, stub := process.WithStub(t.Context()) + // Every install fails, even after the repair, so the recovery gives up with the + // actionable error rather than reporting a false success. + stub.WithFailureFor("plugin install", errors.New("exit status 1")) + stub.WithStderrFor("plugin install", `Plugin "databricks" not found in marketplace "claude-plugins-official"`) + stub.WithCallback(func(*exec.Cmd) error { return nil }) + ctx = promptCtx(ctx, "y\n") + + _, err := InstallPluginForAgent(ctx, builtinMarketplaceAgent(), "user", "main") + var be *BlockedError + require.ErrorAs(t, err, &be) + assert.Equal(t, ReasonInstallFailed, be.Reason) + assert.Contains(t, be.Detail, "claude plugin marketplace add anthropics/claude-plugins-official") + + // The repair was still attempted before giving up. + cmds := stub.Commands() + assert.Contains(t, cmds, "claude plugin marketplace add anthropics/claude-plugins-official") +} + +func TestInstallBuiltinMarketplaceNonMissingErrorSkipsRecovery(t *testing.T) { + stubAgentLookPath(t, true) + ctx, stub := process.WithStub(t.Context()) + stub.WithCallback(func(*exec.Cmd) error { return nil }) + // The install fails for a reason unrelated to a missing marketplace (auth), so + // recovery must not prompt or re-add the marketplace even in an interactive + // session; the original error is surfaced verbatim. + stub.WithStderrFor("plugin install", "you must run `claude login`"). + WithFailureFor("plugin install", errors.New("exit status 1")) + ctx = promptCtx(ctx, "y\n") + + _, err := InstallPluginForAgent(ctx, builtinMarketplaceAgent(), "user", "main") + var be *BlockedError + require.ErrorAs(t, err, &be) + assert.Equal(t, ReasonInstallFailed, be.Reason) + assert.Equal(t, "you must run `claude login`", be.Detail) + + for _, c := range stub.Commands() { + assert.NotContains(t, c, "marketplace add", "a non-missing failure must not re-add the marketplace") + } +} + func TestInstallPluginForAgentCodexUsesAddNoScope(t *testing.T) { stubAgentLookPath(t, true) ctx, stub := process.WithStub(t.Context()) From 5850d7100b66390636f8df9c6f8bb7e4c55661db Mon Sep 17 00:00:00 2001 From: dustinvannoy-db Date: Thu, 23 Jul 2026 22:30:13 -0700 Subject: [PATCH 2/4] aitools: format claude marketplace recovery prompt for readability Split the built-in-marketplace recovery output onto separate lines: the error, the repair commands (add + update, each indented), and a short prompt. Mirrors the non-interactive builtinMarketplaceError formatting. Co-authored-by: Isaac --- libs/aitools/installer/plugin.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/libs/aitools/installer/plugin.go b/libs/aitools/installer/plugin.go index c9067c2cfd8..1a3100b00a5 100644 --- a/libs/aitools/installer/plugin.go +++ b/libs/aitools/installer/plugin.go @@ -328,12 +328,17 @@ func recoverBuiltinMarketplace(ctx context.Context, bin string, agent *agents.Ag return PluginRecord{}, false } + cmdio.LogString(ctx, "") cmdio.LogString(ctx, fmt.Sprintf( - "%s could not install the databricks plugin from the %q marketplace:\n %s", - agent.DisplayName, agent.Plugin.Marketplace, stderrOf(installErr))) - proceed, err := cmdio.AskYesOrNo(ctx, fmt.Sprintf( - "The marketplace may be missing or out of date. Add and refresh it (%s plugin marketplace add %s) and retry?", - agent.Binary, agent.Plugin.BuiltinAddSource)) + "%s could not install the databricks plugin from the %q marketplace:", + agent.DisplayName, agent.Plugin.Marketplace)) + cmdio.LogString(ctx, " "+stderrOf(installErr)) + cmdio.LogString(ctx, "") + cmdio.LogString(ctx, fmt.Sprintf("The %q marketplace may be missing or out of date. Add and refresh it with:", agent.Plugin.Marketplace)) + cmdio.LogString(ctx, fmt.Sprintf(" %s plugin marketplace add %s", agent.Binary, agent.Plugin.BuiltinAddSource)) + cmdio.LogString(ctx, fmt.Sprintf(" %s plugin marketplace update %s", agent.Binary, agent.Plugin.Marketplace)) + cmdio.LogString(ctx, "") + proceed, err := cmdio.AskYesOrNo(ctx, "Run these and retry the install?") if err != nil || !proceed { return PluginRecord{}, false } From 8f452cb2260ab3014f6aa8ced072e2a8dc894a8d Mon Sep 17 00:00:00 2001 From: dustinvannoy-db Date: Fri, 24 Jul 2026 09:23:08 -0700 Subject: [PATCH 3/4] aitools: dedupe built-in marketplace repair commands Extract repairCommands() so the interactive recovery prompt and the non-interactive BlockedError render the two re-add/refresh commands from one place, and fold marketplaceAddSourceArgs into marketplaceAddArgs. No behavior or output change. Co-authored-by: Isaac --- libs/aitools/installer/plugin.go | 36 +++++++++++++-------------- libs/aitools/installer/plugin_test.go | 9 +------ 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/libs/aitools/installer/plugin.go b/libs/aitools/installer/plugin.go index 1a3100b00a5..0d4d60a0ae6 100644 --- a/libs/aitools/installer/plugin.go +++ b/libs/aitools/installer/plugin.go @@ -107,15 +107,9 @@ func recordedInstallTarget(agent *agents.Agent, rec PluginRecord) string { return plugin + "@" + marketplace } -// marketplaceAddArgs builds the `plugin marketplace add ` argv (sans binary). -func marketplaceAddArgs(spec *agents.PluginSpec) []string { - return []string{"plugin", "marketplace", "add", spec.Source} -} - -// marketplaceAddSourceArgs builds `plugin marketplace add ` for an -// explicit source (used to re-add a built-in marketplace during recovery, where -// the source is BuiltinAddSource rather than the empty PluginSpec.Source). -func marketplaceAddSourceArgs(source string) []string { +// marketplaceAddArgs builds the `plugin marketplace add ` argv (sans +// binary). Routine registration passes spec.Source; recovery passes BuiltinAddSource. +func marketplaceAddArgs(source string) []string { return []string{"plugin", "marketplace", "add", source} } @@ -261,7 +255,7 @@ func InstallPluginForAgent(ctx context.Context, agent *agents.Agent, nativeScope installedMarketplace := false if agent.Plugin.Source != "" { alreadyPresent := marketplaceRegistered(ctx, bin, agent.Plugin.Marketplace) - _, addErr := runAgentCmd(ctx, pluginCmdTimeout, prepend(bin, marketplaceAddArgs(agent.Plugin))) + _, addErr := runAgentCmd(ctx, pluginCmdTimeout, prepend(bin, marketplaceAddArgs(agent.Plugin.Source))) installedMarketplace = addErr == nil && !alreadyPresent } if args := marketplaceUpdateArgs(agent); args != nil { @@ -329,14 +323,11 @@ func recoverBuiltinMarketplace(ctx context.Context, bin string, agent *agents.Ag } cmdio.LogString(ctx, "") - cmdio.LogString(ctx, fmt.Sprintf( - "%s could not install the databricks plugin from the %q marketplace:", - agent.DisplayName, agent.Plugin.Marketplace)) + cmdio.LogString(ctx, fmt.Sprintf("%s could not install the databricks plugin from the %q marketplace:", agent.DisplayName, agent.Plugin.Marketplace)) cmdio.LogString(ctx, " "+stderrOf(installErr)) cmdio.LogString(ctx, "") cmdio.LogString(ctx, fmt.Sprintf("The %q marketplace may be missing or out of date. Add and refresh it with:", agent.Plugin.Marketplace)) - cmdio.LogString(ctx, fmt.Sprintf(" %s plugin marketplace add %s", agent.Binary, agent.Plugin.BuiltinAddSource)) - cmdio.LogString(ctx, fmt.Sprintf(" %s plugin marketplace update %s", agent.Binary, agent.Plugin.Marketplace)) + cmdio.LogString(ctx, " "+strings.Join(repairCommands(agent), "\n ")) cmdio.LogString(ctx, "") proceed, err := cmdio.AskYesOrNo(ctx, "Run these and retry the install?") if err != nil || !proceed { @@ -346,7 +337,7 @@ func recoverBuiltinMarketplace(ctx context.Context, bin string, agent *agents.Ag // Re-add (fixes a removed marketplace) then refresh (fixes a stale copy). Each // step may harmlessly fail (e.g. add when it is already present), so we only // log at debug level and let the retried install be the real verdict. - if _, err := runAgentCmd(ctx, pluginCmdTimeout, prepend(bin, marketplaceAddSourceArgs(agent.Plugin.BuiltinAddSource))); err != nil { + if _, err := runAgentCmd(ctx, pluginCmdTimeout, prepend(bin, marketplaceAddArgs(agent.Plugin.BuiltinAddSource))); err != nil { log.Debugf(ctx, "re-adding the %s marketplace failed (it may already be present): %v", agent.Plugin.Marketplace, stderrOf(err)) } if args := marketplaceUpdateArgs(agent); args != nil { @@ -366,6 +357,14 @@ func recoverBuiltinMarketplace(ctx context.Context, bin string, agent *agents.Ag }, true } +// repairCommands returns the re-add and refresh commands shared by the recovery prompt and the BlockedError, so the two can't drift apart. +func repairCommands(agent *agents.Agent) []string { + return []string{ + fmt.Sprintf("%s plugin marketplace add %s", agent.Binary, agent.Plugin.BuiltinAddSource), + fmt.Sprintf("%s plugin marketplace update %s", agent.Binary, agent.Plugin.Marketplace), + } +} + // builtinMarketplaceError wraps a built-in-marketplace install failure with the // exact commands that repair it, so a user who wasn't prompted (non-interactive) // or whose automatic retry failed knows the issue and the fix. @@ -374,11 +373,10 @@ func builtinMarketplaceError(agent *agents.Agent, installErr error) error { Agent: agent.Name, Reason: ReasonInstallFailed, Detail: fmt.Sprintf( - "%s\n\nThe %q marketplace is missing or out of date. Fix it with:\n %s plugin marketplace add %s\n %s plugin marketplace update %s\nthen re-run the install.", + "%s\n\nThe %q marketplace is missing or out of date. Fix it with:\n %s\nthen re-run the install.", stderrOf(installErr), agent.Plugin.Marketplace, - agent.Binary, agent.Plugin.BuiltinAddSource, - agent.Binary, agent.Plugin.Marketplace), + strings.Join(repairCommands(agent), "\n ")), } } diff --git a/libs/aitools/installer/plugin_test.go b/libs/aitools/installer/plugin_test.go index a991e277f17..5884a28c7eb 100644 --- a/libs/aitools/installer/plugin_test.go +++ b/libs/aitools/installer/plugin_test.go @@ -98,14 +98,7 @@ func TestInstallPluginForAgentBuiltinMarketplace(t *testing.T) { // An agent whose plugin lives in a built-in marketplace (empty Source) like // Claude's claude-plugins-official: install from it, never register it. - agent := &agents.Agent{ - Name: agents.NameClaudeCode, - DisplayName: "Claude Code", - Binary: "claude", - Plugin: &agents.PluginSpec{Marketplace: "claude-plugins-official", ID: "databricks", Source: ""}, - } - - rec, err := InstallPluginForAgent(ctx, agent, "user", "main") + rec, err := InstallPluginForAgent(ctx, builtinMarketplaceAgent(), "user", "main") require.NoError(t, err) assert.Equal(t, "claude-plugins-official", rec.Marketplace) assert.False(t, rec.InstalledMarketplace, "a built-in marketplace is never registered by us") From 32c5cfce3dade08c7bbf59a191ea4c863d1e2210 Mon Sep 17 00:00:00 2001 From: dustinvannoy-db Date: Fri, 24 Jul 2026 09:35:34 -0700 Subject: [PATCH 4/4] aitools: wrap repairCommands doc comment for readability Co-authored-by: Isaac --- libs/aitools/installer/plugin.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/aitools/installer/plugin.go b/libs/aitools/installer/plugin.go index 0d4d60a0ae6..abfd7c6b156 100644 --- a/libs/aitools/installer/plugin.go +++ b/libs/aitools/installer/plugin.go @@ -357,7 +357,8 @@ func recoverBuiltinMarketplace(ctx context.Context, bin string, agent *agents.Ag }, true } -// repairCommands returns the re-add and refresh commands shared by the recovery prompt and the BlockedError, so the two can't drift apart. +// repairCommands returns the re-add and refresh commands shared by the recovery +// prompt and the non-interactive BlockedError, so the two can't drift apart. func repairCommands(agent *agents.Agent) []string { return []string{ fmt.Sprintf("%s plugin marketplace add %s", agent.Binary, agent.Plugin.BuiltinAddSource),