From 8180cadd94360730cba22ffddfb634dae5ed5b7a Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Fri, 7 Aug 2026 11:03:18 +0200 Subject: [PATCH] Allow installing agent skills into .agents/skills --- .nextchanges/cli/aitools-agents-skills.md | 1 + .../skills/install-agents-dir/out.test.toml | 3 ++ .../skills/install-agents-dir/output.txt | 39 +++++++++++++++++++ .../aitools/skills/install-agents-dir/script | 19 +++++++++ .../skills/install-agents-dir/test.toml | 30 ++++++++++++++ cmd/aitools/telemetry.go | 2 + libs/aitools/agents/agents.go | 14 +++++++ libs/aitools/agents/agents_test.go | 32 +++++++++++++++ libs/aitools/installer/installer_test.go | 1 + libs/telemetry/protos/aitools_install.go | 1 + 10 files changed, 142 insertions(+) create mode 100644 .nextchanges/cli/aitools-agents-skills.md create mode 100644 acceptance/experimental/aitools/skills/install-agents-dir/out.test.toml create mode 100644 acceptance/experimental/aitools/skills/install-agents-dir/output.txt create mode 100644 acceptance/experimental/aitools/skills/install-agents-dir/script create mode 100644 acceptance/experimental/aitools/skills/install-agents-dir/test.toml create mode 100644 libs/aitools/agents/agents_test.go diff --git a/.nextchanges/cli/aitools-agents-skills.md b/.nextchanges/cli/aitools-agents-skills.md new file mode 100644 index 00000000000..86a81196290 --- /dev/null +++ b/.nextchanges/cli/aitools-agents-skills.md @@ -0,0 +1 @@ +`databricks aitools install` can now install skills into the vendor-neutral `.agents/skills/` location (`~/.agents/skills` globally, `/.agents/skills` for project scope) via `--agents agents` or the interactive picker. diff --git a/acceptance/experimental/aitools/skills/install-agents-dir/out.test.toml b/acceptance/experimental/aitools/skills/install-agents-dir/out.test.toml new file mode 100644 index 00000000000..e90b6d5d1ba --- /dev/null +++ b/acceptance/experimental/aitools/skills/install-agents-dir/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/experimental/aitools/skills/install-agents-dir/output.txt b/acceptance/experimental/aitools/skills/install-agents-dir/output.txt new file mode 100644 index 00000000000..be20fbb4711 --- /dev/null +++ b/acceptance/experimental/aitools/skills/install-agents-dir/output.txt @@ -0,0 +1,39 @@ + +=== install to the global .agents/skills destination +>>> [CLI] experimental aitools install --agents agents --scope=global +Command "install" is deprecated, use "databricks aitools install" instead. +Installing Databricks skills for Portable (.agents/skills)... +Using skills version test-ref +Fetching skills manifest... +Installed 1 skill. + +=== a single global agent gets a real copied file under ~/.agents/skills +>>> find.py .agents/skills/test-stable-a/SKILL.md --expect 1 +.agents/skills/test-stable-a/SKILL.md + +>>> cat .agents/skills/test-stable-a/SKILL.md +--- +name: test-stable-a +--- + +# A + +=== install to the project .agents/skills destination (cwd) +>>> [CLI] experimental aitools install --agents agents --scope=project +Command "install" is deprecated, use "databricks aitools install" instead. +Installing Databricks skills for Portable (.agents/skills)... +Using skills version test-ref +Fetching skills manifest... +Installed 1 skill. + +=== project scope writes the real file to the canonical dir +>>> find.py .databricks/aitools/skills/test-stable-a/SKILL.md --expect 1 +.databricks/aitools/skills/test-stable-a/SKILL.md + +=== and .agents/skills exposes the same content (symlinked into canonical) +>>> cat .agents/skills/test-stable-a/SKILL.md +--- +name: test-stable-a +--- + +# A diff --git a/acceptance/experimental/aitools/skills/install-agents-dir/script b/acceptance/experimental/aitools/skills/install-agents-dir/script new file mode 100644 index 00000000000..3ab536dfe4b --- /dev/null +++ b/acceptance/experimental/aitools/skills/install-agents-dir/script @@ -0,0 +1,19 @@ +# Isolate HOME so parallel aitools tests don't race on a shared ~/.databricks. +sethome home + +title "install to the global .agents/skills destination" +trace $CLI experimental aitools install --agents agents --scope=global + +title "a single global agent gets a real copied file under ~/.agents/skills" +withdir "${USERPROFILE:-$HOME}" trace find.py '.agents/skills/test-stable-a/SKILL.md' --expect 1 +withdir "${USERPROFILE:-$HOME}" trace cat .agents/skills/test-stable-a/SKILL.md + +title "install to the project .agents/skills destination (cwd)" +mkdir -p proj +withdir proj trace $CLI experimental aitools install --agents agents --scope=project + +title "project scope writes the real file to the canonical dir" +withdir proj trace find.py '.databricks/aitools/skills/test-stable-a/SKILL.md' --expect 1 + +title "and .agents/skills exposes the same content (symlinked into canonical)" +withdir proj trace cat .agents/skills/test-stable-a/SKILL.md diff --git a/acceptance/experimental/aitools/skills/install-agents-dir/test.toml b/acceptance/experimental/aitools/skills/install-agents-dir/test.toml new file mode 100644 index 00000000000..fde5e4f9e80 --- /dev/null +++ b/acceptance/experimental/aitools/skills/install-agents-dir/test.toml @@ -0,0 +1,30 @@ +Env.DATABRICKS_SKILLS_BASE_URL = "$DATABRICKS_HOST" +Env.DATABRICKS_SKILLS_REF = "test-ref" + +Ignore = [ + "home", + "proj", +] + +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] + +[[Server]] +Pattern = "GET /test-ref/manifest.json" +Response.Body = ''' +{ + "version": "2", + "updated_at": "2026-01-01T00:00:00Z", + "skills": { + "test-stable-a": {"version": "1.0.0", "files": ["SKILL.md"], "repo_dir": "skills"} + } +} +''' + +[[Server]] +Pattern = "GET /test-ref/skills/test-stable-a/SKILL.md" +Response.Body = '''--- +name: test-stable-a +--- + +# A +''' diff --git a/cmd/aitools/telemetry.go b/cmd/aitools/telemetry.go index 5d4293f5818..544ea4a7a28 100644 --- a/cmd/aitools/telemetry.go +++ b/cmd/aitools/telemetry.go @@ -81,6 +81,8 @@ func agentType(name string) protos.AitoolsAgentType { return protos.AitoolsAgentTypeCopilot case agents.NameAntigravity: return protos.AitoolsAgentTypeAntigravity + case agents.NameAgents: + return protos.AitoolsAgentTypeAgents default: return protos.AitoolsAgentTypeUnspecified } diff --git a/libs/aitools/agents/agents.go b/libs/aitools/agents/agents.go index 572282da03b..56c34c84552 100644 --- a/libs/aitools/agents/agents.go +++ b/libs/aitools/agents/agents.go @@ -109,6 +109,11 @@ const ( NameOpenCode = "opencode" NameCopilot = "copilot" NameAntigravity = "antigravity" + // NameAgents is the vendor-neutral ".agents/skills" destination rather than a + // coding agent. It has no CLI binary and no plugin: skill files placed in + // ~/.agents/skills (global) or /.agents/skills (project) are read + // directly by any tool that follows the AGENTS.md/.agents convention. + NameAgents = "agents" ) // Databricks plugin identity, shared across the agents that ship a plugin. @@ -204,6 +209,15 @@ var Registry = []*Agent{ SkillsSubdir: "global_skills", // Antigravity is IDE-only with no CLI binary, so it has no plugin path. }, + { + Name: NameAgents, + DisplayName: "Portable (.agents/skills)", + ConfigDir: homeSubdir(".agents"), + SupportsProjectScope: true, + ProjectConfigDir: ".agents", + // A destination convention, not a coding agent: no CLI binary and no + // plugin, so raw skill files are its only delivery. + }, } // openCodeConfigDir returns OpenCode's config directory. OpenCode stores its diff --git a/libs/aitools/agents/agents_test.go b/libs/aitools/agents/agents_test.go new file mode 100644 index 00000000000..0212ad4edda --- /dev/null +++ b/libs/aitools/agents/agents_test.go @@ -0,0 +1,32 @@ +package agents + +import ( + "path/filepath" + "testing" + + "github.com/databricks/cli/libs/env" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAgentsPseudoAgentResolvesDirs(t *testing.T) { + a := ByName(NameAgents) + require.NotNil(t, a) + + // Files-only destination: no CLI binary and no plugin. + assert.Empty(t, a.Binary) + assert.Nil(t, a.Plugin) + assert.True(t, a.SupportsProjectScope) + + home := t.TempDir() + ctx := env.Set(t.Context(), "HOME", home) + // USERPROFILE drives env.UserHomeDir on Windows. + ctx = env.Set(ctx, "USERPROFILE", home) + + globalDir, err := a.SkillsDir(ctx) + require.NoError(t, err) + assert.Equal(t, filepath.Join(home, ".agents", "skills"), globalDir) + + projectDir := a.ProjectSkillsDir("/repo") + assert.Equal(t, filepath.Join("/repo", ".agents", "skills"), projectDir) +} diff --git a/libs/aitools/installer/installer_test.go b/libs/aitools/installer/installer_test.go index f9256d2d809..ca11142bb35 100644 --- a/libs/aitools/installer/installer_test.go +++ b/libs/aitools/installer/installer_test.go @@ -1029,6 +1029,7 @@ func TestSupportsProjectScopeSetCorrectly(t *testing.T) { "opencode": false, "copilot": false, "antigravity": false, + "agents": true, } for _, agent := range agents.Registry { diff --git a/libs/telemetry/protos/aitools_install.go b/libs/telemetry/protos/aitools_install.go index ea78dc532f8..1e68fc9e6b7 100644 --- a/libs/telemetry/protos/aitools_install.go +++ b/libs/telemetry/protos/aitools_install.go @@ -13,6 +13,7 @@ const ( AitoolsAgentTypeOpenCode AitoolsAgentType = "OPENCODE" AitoolsAgentTypeCopilot AitoolsAgentType = "COPILOT" AitoolsAgentTypeAntigravity AitoolsAgentType = "ANTIGRAVITY" + AitoolsAgentTypeAgents AitoolsAgentType = "AGENTS" ) // AitoolsInstallScope mirrors AitoolsInstallScope.Type in the databricks_cli