skills: fix the download path (Files API place, securable vs bundle name) and prompt before fetching - #253
Open
xsh310 wants to merge 8 commits into
Open
Conversation
xsh310
commented
Jul 31, 2026
xsh310
marked this pull request as ready for review
July 31, 2026 01:49
xsh310
force-pushed
the
skills-dedup-before-fetch-main
branch
from
July 31, 2026 22:23
03c30d7 to
829d83a
Compare
/Skills place, and prompt before fetching
Download mode fetched every skill's bytes up front, then prompted to overwrite existing dirs at write time — so declining a skill threw away an already-completed download. Move the overwrite prompt and invalid-name check ahead of the fetch: split write_skill into should_download_skill (the disk-only decision, extracting existing_skill_on_disk) and a pure write_skill, and filter each schema's leaves through the decision before _fetch_bundles runs. The per-schema parallel fetch and the sequential location loop are unchanged, so cross-location same-leaf overwrite prompting still works.
Address review nit — restructure the download_skills docstring into explicit list/decide/fetch stages. Run ruff format to wrap an over-length line in tests (fixes the test_ruff_format CI check).
The download client read bundle bytes from `/Volumes/<cat>/<sch>/<leaf>`, but skills have no backing UC Volume, so every fetch failed against a real workspace: listing succeeded via the 2.1 skills API, then each file 404'd with "Volume ... does not exist". Point both the directory walk and the file fetch at the `Skills` place instead. The old path survived because the tests mocked the HTTP layer and asserted the `/Volumes/...` URLs, so they encoded the bug. Updated those and pinned the listing URL with a test, which was previously unasserted. Verified end to end against xsh.bb-0806 on eng-ml-inference.staging (the workspace where both Skills flags are on): download_skills writes each bundle, nested files included, into .claude/skills and .agents/skills. Co-authored-by: Isaac
xsh310
force-pushed
the
skills-dedup-before-fetch-main
branch
from
August 6, 2026 21:14
6e9e2f4 to
0768e1a
Compare
A skill has two names that are not interchangeable. The securable leaf of
`skills/<cat>.<sch>.<leaf>` is the only one the Files API resolves, while
`bundle_name` is set at finalize from the bundle's SKILL.md frontmatter and is
what an agent looks for on disk. They coincide only when a skill was created
under a securable matching its frontmatter.
`_skill_bundle_name` preferred `bundle_name` and used it for both jobs, so a
skill whose two names differ 404'd:
Path '/Skills/xsh/bb-0806/task-triage' did not resolve to a Unity Catalog
skill.
where the securable is `task-prioritizer` and the frontmatter says
`name: task-triage`.
Replace it with a frozen `SkillRef` carrying both, so the type makes the
distinction explicit rather than leaving it to a bare string: fetches take
`ref.securable`, directories and dedup use `ref.bundle`. `should_download_skill`
now validates both names, since each reaches a URL or the filesystem. `--skill`
matches either name, so whichever a user knows works.
Verified against xsh.bb-0806 on eng-ml-inference.staging: 5/5 skills download
(was 4/5), and the divergent one lands in `task-triage/` with a SKILL.md whose
frontmatter matches the directory.
Co-authored-by: Isaac
Rename `SkillRef.securable`/`.bundle` to `securable_name`/`bundle_name` so both read as the API fields they come from, rather than leaving `bundle` to be mistaken for the bundle itself. Narrow `--skill` to match the securable name only. It selects which skills to download, and the securable is what identifies a skill in UC, so accepting the bundle name too gave one skill two selectors with no gain. Requesting a bundle name now reports it as not found, alongside the existing unknown-name warning. Verified against xsh.bb-0806 on eng-ml-inference.staging: the whole schema still downloads 5/5, `--skill task-prioritizer` downloads it into `task-triage/`, and `--skill task-triage` is reported as not found. Co-authored-by: Isaac
xsh310
commented
Aug 6, 2026
xsh310
commented
Aug 6, 2026
Addresses review on _skill_ref: drop the bundle_name fallback and warn instead. A finalized skill is expected to carry both names -- `name` is immutable from CreateSkill, and FinalizeSkill is the sole writer of `bundle_name` -- so either one missing is an anomaly, not a case to paper over. Substituting the securable name for a missing bundle_name guessed a directory name that may not match the bundle's SKILL.md `name:`, which would silently hide the skill from the agent meant to load it. Now both names are required and a skill missing either is skipped with a warning naming the missing field(s). An unfinalized skill is still skipped quietly, since having no bundle yet is a normal in-progress state rather than an anomaly. Also correct the SkillRef docstring: finalize validates the frontmatter name for emptiness, length, and control characters, but never compares it to the securable, which is why the two can legitimately differ. Extract `_non_empty_str` so both names narrow from the untyped API payload without repeating the isinstance dance (also keeps `ty` happy). Co-authored-by: Isaac
/Skills place, and prompt before fetchingOnly the securable name is unique within a schema. `bundle_name` is parsed from
each bundle's SKILL.md frontmatter and never compared against its siblings, so
one schema can hold two finalized skills claiming the same directory.
The decide stage runs before any write, so neither sibling saw the other on disk
and both passed. Both then wrote to the same directory, whichever finished last
won, and `written` counted both:
✔ Downloaded 2/2 skill(s) # one surviving directory, no prompt
Reduce each location's skills to the first claimant of a bundle name and warn
about the rest, naming the winner and how to resolve it. Runs before the decide
stage, so a dropped sibling is never fetched and the summary's denominator counts
only skills that can reach disk. Now:
! Skipping `main.default.skill-b`: its bundle name `foo` is already claimed
by `main.default.skill-a`. Rename one skill's SKILL.md `name:` to download
both.
✔ Downloaded 1/1 skill(s)
Keeping the first rather than prompting matches the existing treatment of
unusable skills, which warn and skip; a prompt here would ask the user to choose
between two skills they cannot tell apart from the directory name alone.
The guard is per location, so a same-named skill from a *later* location still
reaches the overwrite prompt as before -- covered by a test, since that is the
behavior most at risk of regressing here.
Co-authored-by: Isaac
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Three problems in the
ucode configure skillsdownload path.1. Bundle reads went to the wrong Files API place, so downloads never worked.
The client read bundle bytes from
/Volumes/<cat>/<sch>/<leaf>, but a skill has no backing UC Volume. Listing succeeded (that goes through the 2.1 skills API), then every file fetch failed:The unit tests mocked the HTTP layer and asserted the
/Volumes/...URLs, so they encoded the bug instead of catching it, and no test pinned the listing URL at all.2. A skill's two names were treated as one, so any skill whose names differ 404'd.
A skill has a securable name (the leaf of
skills/<cat>.<sch>.<leaf>) and abundle_namethat FinalizeSkill parses from the bundle's SKILL.md frontmatter. Only the securable name resolves through the Files API, whilebundle_nameis what an agent looks for on disk._skill_bundle_namepreferredbundle_nameand used it for both jobs, so fixing problem 1 exposed this:Nothing keeps the two in sync.
FinalizeSkillValidator.verifyFrontmatterchecks the frontmatter name for emptiness, length, and control characters, but never compares it to the securable, so the divergence is permitted by design rather than being a data anomaly.3. The overwrite dedup prompt ran after the bytes were already downloaded.
download_skillsfetched every skill's full bundle up front, then prompted per skill at write time. Declining a skill threw away a download that had already completed, wasted work that scales with how much the user already has on disk, which is the common re-download case.Change
Files API place (
0768e1a): point the directory walk and the file fetch at theSkillsplace through a singleSKILL_FILES_API_PREFIXconstant.Two names (
fe1295b,410f21f,94fd4af): replace_skill_bundle_namewith a frozenSkillRef(securable_name, bundle_name), so the type carries the distinction instead of leaving it to a bare string two callers read differently.ref.securable_name, the only name the Files API resolves.ref.bundle_name, so the directory matches thename:in the SKILL.md written inside it.should_download_skillvalidates both names, since one reaches a URL and the other the filesystem.--skillmatches the securable name only, since that is what identifies a skill in UC.Prompt before fetch (
415e7c6,e056755): move the disk-only checks ahead of the fetch, so a declined or invalid skill is never downloaded.write_skillsplits intoshould_download_skill(the decision, needs no bytes) and a pure writer. The per schema parallel fetch and the sequentialfor location in locationsloop are unchanged, so cross location overwrite prompts still fire.Tests
uv run pytest tests/test_skills_download.pygives 48 passed. Full suite excluding the live gateway e2e tests: 1274 passed, 6 skipped.uv run ruff checkand thetytype check are clean.Unit coverage added for each change:
test_lists_under_the_skills_placepins the listing URL, previously unasserted, and the mocks that asserted/Volumes/...are updated.test_fetches_by_securable_and_writes_under_bundle_nameasserts the fetch uses the securable name while the directory is the bundle name, and that no securable named directory is created.test_skill_filter_matches_securable_name_onlyasserts both directions: the securable name selects, the bundle name does not.test_either_unsafe_name_is_skippedcovers an unsafe value in either position.test_skips_and_warns_when_a_name_is_missingis parametrized over all four missing name combinations, plustest_unfinalized_skill_is_skipped_without_a_warningfor the quiet case.test_declined_skill_is_not_fetchedassertsfetch_skill_bundleis never called for a declined skill and the existing copy is untouched.End to end against a real workspace
Verified against
xsh.bb-0806on eng-ml-inference.staging, the workspace where both Skills flags are on. That schema contains a skill whose two names differ, which is what surfaced problem 2:The two paths, confirmed directly against the Files API:
Full schema download.
download_skills(..., ["xsh.bb-0806"], path)reportsDownloaded 5/5, up from 4/5 before the name fix, wheretask-triagewas the one failure. The directories written are:The skill with differing names lands in
task-triage/(the bundle name), its bytes came from thetask-prioritizerpath (the securable name), and its SKILL.md frontmatter readsname: task-triage, so the directory and the frontmatter agree. Notask-prioritizer/directory is created, and both roots (.claude/skillsand.agents/skills) receive it.--skillon the mismatched skill. Requesting the securable name downloads it into the bundle named directory; requesting the bundle name is reported as not found:Bundle integrity. For a skill whose names match, the fetched bytes equal the sizes the Files API reports, and nested files are preserved:
Note that the server side
/Skillsbyte path is still gated bydatabricks.unitycatalog.aigov.enableSkillsFilesApi, which is default off and enabled only for that test workspace. This PR does not change that gating, it just stops ucode from asking the wrong place with the wrong name.This pull request and its description were written by Isaac.