Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .nextchanges/cli/aitools-install-add-claude-marketplace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* `aitools install` now registers the official Claude marketplace if it is missing before installing the Databricks Claude plugin. ([#6485](https://github.com/databricks/cli/pull/6485))
21 changes: 9 additions & 12 deletions libs/aitools/agents/agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ type PluginSpec struct {
// ID is the plugin identifier that is installed/enabled (e.g. "databricks").
ID string
// Source is the argument passed to `<agent> plugin marketplace add`
// (e.g. "databricks/databricks-agent-skills"). Empty marks a built-in
// marketplace that must not be added or de-registered.
// (e.g. "databricks/databricks-agent-skills").
Source string
// Shared marks a marketplace we do not own. It is still added when missing — an
// unregistered marketplace can't be refreshed or installed from — but is never
// de-registered on uninstall, since other plugins may rely on it.
Shared bool
}

// Agent defines a supported coding agent.
Expand Down Expand Up @@ -141,12 +144,8 @@ const (
databricksPluginID = "databricks"
databricksPluginSrc = "databricks/databricks-agent-skills"

// claudeOfficialMarketplace is Claude Code's built-in marketplace
// (anthropics/claude-plugins-official), registered by default. The databricks
// plugin is published there, so Claude installs from it and we never register
// our own marketplace for Claude. An empty PluginSpec.Source marks a built-in
// marketplace that must not be added.
claudeOfficialMarketplace = "claude-plugins-official"
claudeOfficialMarketplace = "claude-plugins-official"
claudeOfficialMarketplaceSrc = "anthropics/claude-plugins-official"
)

// databricksPlugin returns the shared plugin descriptor for an agent that
Expand All @@ -159,14 +158,12 @@ func databricksPlugin() *PluginSpec {
}
}

// claudePlugin returns Claude's plugin descriptor. Claude installs the databricks
// plugin from its built-in claude-plugins-official marketplace (Source empty), so
// the CLI doesn't register a separate databricks-agent-skills marketplace for it.
func claudePlugin() *PluginSpec {
return &PluginSpec{
Marketplace: claudeOfficialMarketplace,
ID: databricksPluginID,
Source: "",
Source: claudeOfficialMarketplaceSrc,
Comment thread
rclarey marked this conversation as resolved.
Shared: true,
}
}

Expand Down
9 changes: 2 additions & 7 deletions libs/aitools/installer/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,10 +246,6 @@ func InstallPluginForAgent(ctx context.Context, agent *agents.Agent, nativeScope
// our add succeeded, so we never remove a marketplace another plugin shares.
// On any uncertainty marketplaceRegistered returns true, keeping us off the
// de-register path.
//
// An empty Source marks a built-in marketplace (e.g. Claude's
// claude-plugins-official): it is already registered, so we never add or
// de-register it.
installedMarketplace := false
if agent.Plugin.Source != "" {
alreadyPresent := marketplaceRegistered(ctx, bin, agent.Plugin.Marketplace)
Expand Down Expand Up @@ -331,9 +327,8 @@ func UninstallPluginForAgent(ctx context.Context, agent *agents.Agent, rec Plugi
if _, err := runAgentCmd(ctx, pluginCmdTimeout, prepend(bin, pluginUninstallArgs(agent, rec))); err != nil {
return &BlockedError{Agent: agent.Name, Reason: ReasonInstallFailed, Detail: stderrOf(err)}
}
// Never de-register a built-in marketplace (empty Source, e.g. Claude's
// claude-plugins-official): it is shared infrastructure we did not add.
if rec.InstalledMarketplace && !keepMarketplace && agent.Plugin.Source != "" {
// Never de-register a Shared marketplace, it is shared infrastructure we don't own
if rec.InstalledMarketplace && !keepMarketplace && !agent.Plugin.Shared {
if _, err := runAgentCmd(ctx, pluginCmdTimeout, prepend(bin, marketplaceRemoveArgsForRecord(agent, rec))); err != nil {
log.Warnf(ctx, "Removed the %s plugin but could not de-register its marketplace (remove it manually if needed): %v", agent.DisplayName, stderrOf(err))
}
Expand Down
31 changes: 14 additions & 17 deletions libs/aitools/installer/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,29 +65,23 @@ func TestInstallPluginForAgentClaudeSuccess(t *testing.T) {
assert.Contains(t, cmds, "claude plugin install databricks@databricks-agent-skills --scope user")
}

func TestInstallPluginForAgentBuiltinMarketplace(t *testing.T) {
func TestInstallPluginForAgentSharedMarketplace(t *testing.T) {
stubAgentLookPath(t, true)
ctx, stub := process.WithStub(t.Context())
stub.WithCallback(func(*exec.Cmd) error { return nil })

// 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: ""},
}
// Claude installs from its official, Shared marketplace (claude-plugins-official).
// It is not reliably registered locally, so the CLI must add it from its source
// before refreshing and installing. Use the real registry spec.
agent := agents.ByName(agents.NameClaudeCode)

rec, err := InstallPluginForAgent(ctx, agent, "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")
assert.True(t, rec.InstalledMarketplace, "an absent shared marketplace is added by us")

cmds := stub.Commands()
for _, c := range cmds {
assert.NotContains(t, c, "marketplace add", "must not register a built-in marketplace")
}
assert.Contains(t, cmds, "claude plugin marketplace add anthropics/claude-plugins-official")
assert.Contains(t, cmds, "claude plugin marketplace update")
assert.Contains(t, cmds, "claude plugin install databricks@claude-plugins-official --scope user")
}
Expand Down Expand Up @@ -309,17 +303,17 @@ func TestUninstallSkillsOptsTargetsPluginAgents(t *testing.T) {
assert.Contains(t, state.Plugins, agents.NameCopilot)
}

func TestUninstallNeverDeregistersBuiltinMarketplace(t *testing.T) {
func TestUninstallNeverDeregistersSharedMarketplace(t *testing.T) {
setupTestHome(t)
stubAgentLookPath(t, true)
ctx, stub := process.WithStub(t.Context())
stub.WithCallback(func(*exec.Cmd) error { return nil })
ctx = cmdio.MockDiscard(ctx)
ctx, stderr := cmdio.NewTestContextWithStderr(ctx)

dir, err := GlobalSkillsDir(ctx)
require.NoError(t, err)
// Claude installs from its built-in claude-plugins-official marketplace; even a
// stale InstalledMarketplace=true must never trigger a de-register.
// Claude's claude-plugins-official is a Shared marketplace; even a stale
// InstalledMarketplace=true must never trigger a de-register.
require.NoError(t, SaveState(dir, &InstallState{
SchemaVersion: schemaVersionV2,
Plugins: map[string]PluginRecord{
Expand All @@ -334,6 +328,9 @@ func TestUninstallNeverDeregistersBuiltinMarketplace(t *testing.T) {
for _, c := range cmds {
assert.NotContains(t, c, "marketplace remove")
}
// The message must not claim to have removed the shared marketplace.
assert.Contains(t, stderr.String(), "removed databricks plugin")
assert.NotContains(t, stderr.String(), "+ marketplace")
}

func TestUninstallKeepMarketplace(t *testing.T) {
Expand Down
4 changes: 3 additions & 1 deletion libs/aitools/installer/uninstall.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,10 @@ func UninstallSkillsOpts(ctx context.Context, opts UninstallOptions) error {
}
delete(state.Plugins, name)
pluginCount++
// Mirror the de-register condition in UninstallPluginForAgent: a Shared
// marketplace is never removed, so don't claim we did.
note := " + marketplace"
if opts.KeepMarketplace || !rec.InstalledMarketplace {
if opts.KeepMarketplace || !rec.InstalledMarketplace || agent.Plugin.Shared {
note = ""
}
cmdio.LogString(ctx, fmt.Sprintf(" %s removed databricks plugin%s", agent.DisplayName, note))
Expand Down
Loading