Skip to content

RFC-0008: MVP Skill Registry - #26

Merged
mprahl merged 67 commits into
mlflow:mainfrom
jwm4:mvp-skill-registry
Aug 19, 2026
Merged

RFC-0008: MVP Skill Registry#26
mprahl merged 67 commits into
mlflow:mainfrom
jwm4:mvp-skill-registry

Conversation

@jwm4

@jwm4 jwm4 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Add a Skill Registry to MLflow: a governed, metadata-first registry for AI agent skills.

This is a restructured Phase 1 of the work originally proposed in PR #10, narrowed per Slack discussion with Databricks maintainers to focus on skills and skill bundles only.

  • Skills: SKILL.md directories with versioning, lifecycle, aliases, tags
  • Skill bundles: versioned collections grouping related skills
  • Pull semantics: harness-agnostic content fetch from Git, OCI, ZIP, MLflow artifact storage
  • Package manager integration: delegates harness-specific installation to APM, Lola, or other plugins
  • Trace integration: mlflow.skill_context() creates SKILL spans with registry coordinates

Phase 2 (RFC-0009, separate PR) will extend bundles with subagents, hooks, and MCP server references.

Changes from PR #10

  • Removed subagent and hook entity types (deferred to Phase 2)
  • Skill bundles contain only skills (no MCP server cross-references)
  • Replaced custom harness adapters with package manager plugin interface
  • Removed import command and lock file format
  • Aligned with RFC-0004 (MCP Registry) patterns throughout
  • All design decisions from the PR RFC-0008 and RFC-0009: Skill Registry and Harness Integration #10 review are preserved

Files

  • rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md: Main RFC
  • rfcs/0008-mvp-skill-registry/implementation-details.md: DB schema, store interface, REST API, SDK, CLI, package manager plugin interface

Test plan

Posted by Bill Murdock with assistance from Claude Code.

jwm4 and others added 7 commits July 14, 2026 12:37
Metadata-first registry for AI agent skills with versioning,
lifecycle management, trace integration, and package manager
plugin interface.

Phase 1 covers skills and skill bundles (skills-only). Phase 2
(RFC-0009, separate PR) will extend bundles with subagents, hooks,
and MCP server references.

Replaces the closed PR mlflow#10 with a phased approach per Slack
discussion with Databricks maintainers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Accept changes from Codex review: SDK namespace to mlflow.genai.*,
plugin import section, automatic harness instrumentation, install
via package manager. Trim auto instrumentation to essentials. Remove
register_skill idempotency and align with MCP Server Registry
behavior (fail if version exists), citing register_mcp_server() in
mlflow/mlflow#23696 as rationale.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fill in rfc_pr link, fix "The two entity types:" grammar, replace
all RFC-0009 references with "follow-up RFC", and change non-skill
member lists to examples (e.g., subagents, MCP server references)
since the set may change before that RFC is written.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ntics

Resolve both open investigation items: harness argument is now required
on install commands, reproducibility depends on the package manager
plugin (APM has full lockfile, Lola has version constraints). Drop
Phase 3 from adoption strategy. Add agentskills.io citation. Clarify
that bundles can contain non-skill content which is pulled and installed
but does not receive individual registry entries in Phase 1.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add missing --harness claude-code to both install-bundle examples.
Fix Phase 2 phrasing to say "add registry entries for" instead of
"extend bundles to include" non-skill members.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add mlflow-skills.lock resolution lock for reproducible cross-machine
installation. Package manager plugin interface now returns
PackageManagerInstallResult with harness-local skill names so trace
manifests are accurate even when plugins rename skills. Monolithic
bundle install passes bundle_path to the plugin so non-skill content
is installed as a unit. Fix span annotation from "registry" to
"workspace" and "skipped" to "unregistered" wording.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add Open questions section per RFC template. Note that structured
security scan metadata is valuable but should be addressed as a
cross-registry capability shared across all registries, not as a
skill-specific feature.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md Outdated
Comment on lines +127 to +131
MLflow discovers and registers the plugin's skills as members of a
monolithic bundle. It preserves the Git source on the bundle and warns
about subagents, hooks, and MCP configurations that are not registered
in Phase 1.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and warns about subagents, hooks, and MCP configurations that are not registered
in Phase 1.

I see these warnings are provided in various cases (like import). It feels unnecessary because it is expected that a skill api / option would register only skills.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could argue it either way, but Yuki specifically asked for the warnings, and they seem somewhat useful to me.

Comment thread rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md Outdated
Comment thread rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md Outdated
Comment thread rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md Outdated
Comment thread rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md Outdated
Comment thread rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md Outdated
Comment thread rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md

#### Automatic harness instrumentation

Phase 1 extends the Claude Code autologger to recognize skill

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if the RFC also points out the plan for the others in a future enhancement section, which, if any, should be prime candidates for auto log support.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Broadly speaking, I think the process would be something like this:

  1. Start with the list of harness agents that have auto-loggers at https://mlflow.org/docs/latest/genai/tracing/integrations/#coding-agents--long-running-agents
  2. Prioritize based on popularity, openness, importance to whatever contributor wants to do the work, etc.

In addition to supporting the harness agents, you might want to support the SDKs at https://mlflow.org/docs/latest/genai/tracing/integrations/#agent-frameworks-python and https://mlflow.org/docs/latest/genai/tracing/integrations/#agent-frameworks-typescript . Not all of those have a "skill" concept, and you can't autolog skill use if the SDK that you're using doesn't have a concept of a skill. (You could still log it yourself if you layer a skill concept on top of the SDK you're using of course).

This seems like a long, hard conversation to have to get any sort of meaningful conclusions. I'd prefer to keep it out of the RFC because I'm a little concerned that resolving this debate would take a bunch of time that would slow down work on this RFC in order to pre-decide things for future enhancements. Furthermore, I think a lot of things will change between now and the time that someone starts working on future enhancements, so whatever we decide now might turn out to be irrelevant by that time anyway.


### Manifest writing and discovery

Installation commands write or update the manifest after all requested

@HumairAK HumairAK Jul 23, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing that caught me by surprise was the decision to have MLflow locally materialize the skill or skull-bundle. I think this is a good way to ensure MLflow retains domain ownership over resolution semantics, instead of trying to squeeze registry behavior into another tool's package-resolution abstraction.

I think it would be good to explicitly mention the justification somewhere.

I'd imagine there are some tradeoffs here as well, for example if a package manager has features / capabilities around remote source semantics. If you agree, consider adding a small note about it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree this needs some more thought. We'll sort it out in the upcoming installation RFC now that we've decided to defer installation.

Comment thread rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md Outdated
Comment thread rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md Outdated
Comment thread rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md Outdated
Comment thread rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md Outdated
Comment thread rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md Outdated
Comment thread rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md Outdated
Comment thread rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md Outdated
Comment thread rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md Outdated
Comment thread rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md Outdated
Comment thread rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md Outdated
Comment thread rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md Outdated
Comment thread rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md Outdated
Comment thread rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md Outdated
Comment thread rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md
Comment thread rfcs/0008-mvp-skill-registry/implementation-details.md

@mprahl mprahl left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! I left a few comments.

jwm4 and others added 5 commits July 27, 2026 13:28
Replace all "Phase 1" qualifiers with "this RFC" or remove where
unnecessary. Replace "Phase 2" and "follow-up RFC" references with
explicit links to RFC-0009: Extended Skill Bundles (PR mlflow#27).
Restructure Adoption strategy from phased rollout into a description
of what this RFC delivers plus a Future improvements subsection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Avoids confusion with the existing `mlflow skills` CLI group that
inspects bundled Assistant skills. Updated all CLI command references
and narrative text in both the main RFC and implementation details.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bundle, import, and introspect commands now live under
`mlflow skills-registry bundles` instead of using flat
prefixed names (e.g., `bundles create-version` instead of
`create-bundle-version`).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All members are skills in this RFC. RFC-0009 can add the column
with a default of 'skill' via a standard schema migration when
it introduces non-skill member types.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add `list-package-managers` CLI command and SDK function
- Use fixed `.mlflow-skills/` directory for cached content instead
  of temporary directories
- Note that plugins may write native lockfiles alongside cached
  content for MLflow-free collaboration
- Add `check_requirements()` preflight method to plugin interface
- Include tracking server URL in `mlflow-skills.lock` so
  `--from-lock` can connect without separate configuration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
A version's kind is derived from its `source_type`: `assembled` is an
assembled plugin, and any external or MLflow-stored package
(`git`, `oci`, `zip`, `mlflow`) is monolithic. An agent plugin's kind is
fixed across its versions: every version of a given agent plugin must be

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we reconsider the "every version of a given agent plugin must be the same kind" requirement? I think each version should be consistent but I don't see why we'd restrict a user from initially registering a monolithic bundle and then later to use an assembled approach without having to tell everyone to update their lock files to use a new agent plugin name.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, relaxing this. Kind is now resolved per version and no longer required to be consistent across an agent plugin's versions: a plugin can be packaged in one version and assembled in another under a stable name, so an author can migrate authoring styles without forcing consumers onto a new name and new pinned references. This is safe because nothing aggregates kind across versions. Pull, latest-resolution, and member sourcing each read the kind of the specific version they operate on. One caveat I spelled out: a name-only pull resolves to the latest version, so it can change behavior across a kind boundary (packaged fetches the whole package as a unit; assembled fetches each member individually), while a version-pinned reference is unaffected.

One safeguard I made explicit in the process: because an assembled version can reference a standalone skill the caller does not own, and a later packaged import could then discover the same name, re-import now states that appending a version to a matched existing skill requires EDIT on that skill, evaluated separately from plugin permissions (the same rule as any version creation). So a reference-level relationship can never be laundered into a write on someone else's skill. Updated both the main RFC and implementation-details.

Comment from Bill Murdock with assistance from Claude Code.

skill version is removed from standalone discovery, and it also withdraws
every agent plugin version that contains it, whether the skill is a pinned
member of an assembled plugin or an embedded member of a monolithic
package. A plugin version that contains a `deleted` member is treated as

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we raise an error instead when someone tries to delete a skill referenced by a plugin version? I think this would be less surprising as a user may not be aware that their skill is being used by a plugin version.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, erroring is less surprising. This now falls out of a referential-integrity check that runs before anything is removed: a skill version referenced by a live (non-deleted) agent plugin version cannot be hard-deleted while that reference exists, and this applies to delete_skill on a member skill directly, not just to the plugin-side cascade. The delete fails atomically with nothing removed, and the operator clears the other reference and retries. References held only by soft-deleted plugin versions do not block the delete. This replaces the earlier behavior of deleting most members and silently keeping the referenced ones. See 0008-mvp-skill-registry.md around the top-level delete section.

Comment from Bill Murdock with assistance from Claude Code.

(`source_type="assembled"`), pull each member individually from its own
`source` to `skills/<member-name>/` under the destination, matching the
Agent Plugins `skills/*/SKILL.md` discovery layout. If a skill member in
an assembled agent plugin has no `source`, the pull fails rather than

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is source not required? When would it not be set.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

source is optional. It holds the typed pointer for external content, so it is set for git, oci, and zip, and for mlflow it is the MLflow artifact tree. It is unset for an assembled agent plugin version, which has no plugin-level package at all (its members carry their own sources), and transiently while an mlflow upload is still in draft before the artifact tree exists. The field is already marked optional in the SkillVersion description, and the null case is stated where assembled plugins are defined ("an assembled plugin has no agent plugin-level source"). I can add a one-line cross-reference if you would like it spelled out in one place.

Comment from Bill Murdock with assistance from Claude Code.

backend without deletion support can retain unreferenced uploaded
files until garbage collection.
version creation are separate operations. The upload flow keeps the
version in `draft` until the upload completes, so a failure leaves a

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Why not avoid creating the DB record in the first place until upload completes? If this is using the artifact repository APIs, then it should be able to delete artifacts.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The draft-first ordering is deliberate: the upload destination is derived from the created version's identity, so the version record has to exist before the client knows where to upload. Keeping the version in draft until the upload completes means a failure leaves a discarded draft rather than a content-less active version. You are right that a backend with deletion support can clean the orphaned files up immediately; the drawback note only calls out that a backend without deletion support retains them until GC. I can add a sentence noting the client attempts to delete the orphaned draft artifacts on failure where the backend supports it, if that reads better, but I would keep the create-then-upload flow itself.

Comment from Bill Murdock with assistance from Claude Code.

"""version is required when plugin_json is None or when
plugin_json does not contain a version field."""

def import_agent_plugin(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m wondering whether import_agent_plugin needs to be a separate API. create_agent_plugin_version already feels like the natural place for plugin members, but today it only accepts skills for assembled plugins. Would it be simpler to expand version creation to also support discovered embedded members for monolithic imports, rather than introducing a separate import-specific write path?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to keep import_agent_plugin as a separate write path. The two operations look similar but take fundamentally different member inputs: create_agent_plugin_version takes skills as URI references to skills that already exist in the registry (the assembled case), while import takes definitions for the skills embedded in the package source, discovered during local inspection, and creates them. That difference drives the rest: import creates N new skill versions plus the plugin version atomically as one unit of work with all-or-nothing rollback, and returns both the plugin version and the newly created skill versions, whereas version creation writes a single entity referencing pre-existing skills and returns just the version. Folding them together would mean one method with mutually exclusive skills vs. member_skills parameters, a mode switch, and two different return shapes and failure models behind one signature.

The shared intent you are pointing at is already honored where it is cheap: both go through the store layer, and the high-level mlflow.genai surface presents import as a single call (it fetches the source, detects the format, translates it, then posts the prepared payload). So the surface is not as duplicative as the two primitives suggest. Keeping the two write paths distinct keeps each REST contract (POST .../versions vs. POST /import) precise about what it creates.

Comment from Bill Murdock with assistance from Claude Code.

content lives inside the agent plugin artifact rather than in
standalone storage. `source_type` is not a user-facing
parameter. This keeps SDKs as thin REST wrappers, avoids
The server sets `source_type`; it is not a user-facing parameter. For

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason not to set source to the resolved MLflow artifacts URI and drop source_type? The MLflow client can infer the source type from the source value.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to keep source_type as an explicit server-set field rather than inferring it from source. The value cannot disambiguate every case: an assembled plugin has no source at all, and an mlflow artifact-tree URI versus other null-source flows cannot reliably be told apart by parsing alone. We worked through exactly this earlier in the review and landed on keying the type on the creation flow / endpoint rather than on the source value, which removed that ambiguity. Inferring from source would reintroduce it for the null and MLflow cases, and it would mean the server has to parse a user-supplied URL to classify content. I would like to leave this as-is.

Comment from Bill Murdock with assistance from Claude Code.

#### SkillVersion

A versioned record containing a server-set `source_type` (`git`, `oci`,
`zip`, `mlflow`, or `embedded`), an optional typed source pointer for

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's implied but not spelled out as far as I could see, but could you make it clear that source_type of mlflow is only supported when MLflow serves artifacts?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, making it explicit. source_type="mlflow" stores content in MLflow artifact storage, so it is only usable where the deployment serves artifacts; a tracking-only server can still use the external source types (git/oci/zip) but not mlflow. Added a sentence to the SkillVersion source description.

Comment from Bill Murdock with assistance from Claude Code.

@mprahl mprahl left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed the latest changes. My biggest concern is this comment:
#26 (comment)

The rest are mostly optional.

Rework the imported-plugin model per reviewer discussion (the "hybrid"):

- Rename plugin kind "monolithic" to "packaged"; keep "assembled".
- Drop source_type "embedded" and the owner_plugin_name field entirely.
- On import, each contained skill becomes an ordinary, independently
  addressable skill whose source is derived from the package (the
  package's source_type and source plus a subpath). For MLflow-stored
  packages the member persists an explicit pointer to the package
  artifact tree, so it resolves without reference to its membership.
- Skill pull uses the member's derived source; plugin pull still fetches
  the whole package (including mcp.json).
- delete_agent_plugin gains a cascade option: default leaves members as
  standalone skills; cascade removes them subject to referential
  integrity. MLflow package trees are refcounted and retained until the
  last referencing skill version is gone.
- Ownership/collision is derived from membership rows; skill names are
  unique per (workspace, organization) and import fails on collision.
- Permissions are uniform: imported skills are ordinary skills with
  their own ACLs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
different monolithic plugin. Members whose
names are no longer in the source are omitted from the new
agent plugin version.
those skills. A new name (not in this plugin's current member list) adds

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may be implied but it'd be nice for the MLflow client to detect skills that stayed the same and reuse those skills in the member references rather than creating a new version. This has benefits such as being able to see the diff clearly between agent plugin versions and old traces and new traces using those skills link to the same skill.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Matt, this pushed me to bring the content digest back (I'd dropped it earlier as non-essential to the MVP). On import, each discovered skill still gets a new version whose source is derived from the newly imported package, and I now record the content digest on that version. It's computed client-side during local inspection, stored, returned on get, and indexed so callers can filter by it (digest = '<hex>'). When a skill's content is unchanged the new version carries the same digest as the prior one, so a client can group versions by content after the fact, giving you the clean diff between plugin versions and the "old and new traces point at the same content" linkage, on the read side.

I didn't make import literally reuse the prior version because of the one-immutable-source-per-version commitment: a newly imported packaged-plugin version containing a member that points into an older package is exactly the incoherence I'm avoiding. Minting a new version per discovered skill keeps each version with one immutable, package-derived source, and the digest carries the identity signal instead.

One caveat: the digest is client-asserted and not server-verified, so equal digests mean the registering clients asserted identical content, not that the registry guarantees byte-for-byte identity. The RFC exposes and indexes the field; it doesn't build a diff or trace-linking feature on top of it.

Comment from Bill Murdock with assistance from Claude Code.

the plugin, subject to the same referential-integrity checks that guard
any skill hard delete. A member that is still referenced by a different
plugin version, or otherwise fails the integrity check, is retained
rather than deleted, so a cascade never breaks a different plugin that

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it may be better to error on this case rather than silently keeping some skills around. It's a less surprising behavior.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, erroring is less surprising. Cascade delete now checks before removing anything: if it would hit a member still referenced by a live (non-deleted) plugin version other than the one being deleted, the whole delete fails atomically and nothing is removed, and the operator clears the other reference and retries. Membership rows held only by soft-deleted plugin versions do not block the delete and are purged with the member. This replaces the earlier behavior of deleting most members and silently keeping the referenced ones.

Comment from Bill Murdock with assistance from Claude Code.

Comment on lines +1513 to +1519
**Field inference.** The server infers optional fields from source
content when possible, so the simplest call requires only what cannot
be derived. When `name` is omitted from a registration request, the
server fetches the source and extracts the name from the skill's
SKILL.md entry point. If the server cannot access the source (e.g.,
private repositories requiring client-side credentials), registration
fails with an error indicating that `name` must be provided explicitly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't we also want this name inference to happen client-side, similarly to the plugin import flow? How are we guarding against nefarious URL submission?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, agreed on both counts, and that's how it now works. Name inference for skill registration happens client-side during local inspection, exactly like the plugin import flow: the client reads the skill's SKILL.md and computes the content-derived fields (name, description, keywords) locally, then submits them. The registry server never fetches a user-supplied source URL, for skill registration and plugin import alike. That is the guard against nefarious URL submission: there is no server-side fetch of an untrusted URL to exploit. I've now stated this explicitly on the skill path rather than leaving it implied from the import path.

Comment from Bill Murdock with assistance from Claude Code.

Comment on lines +752 to +758
`member_version` at create time and that integer is frozen into the member row:
a pinned `skills:/name/version` reference stores the given version, a name-only
`skills:/name` reference resolves to the skill's current latest version, and a
`skills:/name@alias` reference resolves to the version the alias points to at
that moment. Name-only resolution uses the standard latest-resolution rule, so
it may select a `draft` when the skill has no `active` version and the draft is
its highest-numbered non-`deleted` version; if no version resolves, the create

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should name-only member resolution for plugins ever really select a draft? I agree with the ergonomics of get/pull for individual skills, especially being that it is reevaluated at each call. Having a draft skill be frozen as a permanently pinned plugin member (if the plugin is registered prematurely) seems counterintuitive.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, that would be counterintuitive. Name-only member resolution now resolves to the skill's latest active version and never pins a draft or deprecated one. Because a plugin member is frozen at registration rather than re-evaluated, if the skill has no active version the plugin create fails, directing the author to publish the skill or pin an explicit version rather than freezing a prematurely-registered draft. Individual get/pull keep the broader latest-resolution rule you mentioned, since they re-evaluate on each call.

Comment from Bill Murdock with assistance from Claude Code.

jwm4 and others added 3 commits August 18, 2026 16:37
Bring back the skill-version content digest as a client-asserted,
server-unverified, indexed field used only for read-side grouping
(clean diffs between agent plugin versions, trace continuity). Import
never reuses an existing version on a digest match: every discovered
member gets a new version, so each version keeps exactly one immutable,
package-derived source.

- Weaken the digest identity claim to reflect no server verification
  (equal digests are equal client assertions, not a guarantee).
- Make finalized (in-flight upload) handling internally consistent
  across latest, alias, and content resolution; confined to
  implementation-details, no main-RFC changes.
- Carry earlier review fixes: NFC digest determinism, REST name
  required, finalized-draft gating.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses mprahl review comment #3797616019: make explicit that
source_type="mlflow" is supported only where the deployment serves
artifacts, while git/oci/zip have no such requirement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses mprahl review comment #3797504085: an agent plugin no longer
must keep a single kind across all versions. Kind is resolved per version
(pull, latest-resolution, member sourcing each read the specific version's
kind), so an author can migrate authoring styles under a stable name
without forcing consumers to re-pin a new name.

Also makes explicit that re-import appending a version to a matched
existing skill requires EDIT on that skill, evaluated separately from
plugin permissions, so an assembled reference to a skill the caller does
not own cannot be laundered into a write via a later packaged import.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread rfcs/0008-mvp-skill-registry/implementation-details.md
Comment thread rfcs/0008-mvp-skill-registry/implementation-details.md
Drop the commitment that trace linking is MVP scope delivered as a
separate PR on this RFC. Matt noted overlapping edits and raised the
possibility of a standalone trace RFC, so the bullet now states only
that trace linking is planned follow-up work without fixing its form.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

class UpdateSkillVersionRequest(BaseModel):
status: str | None = None
finalize: bool = False # on a client upload, signals the upload is complete

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we drop finalize and just accept upload first then register approach?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with a single-call registration instead of a finalize step. Because a skill is small, a local-path registration now carries the packaged content inline in one atomic operation, so there is no create-then-upload-then-finalize sequence and no in-flight version state. I dropped the finalized column, the finalize PATCH parameter, and the skip-non-finalized resolution rules that only existed to guard the gap between creating a version and its content arriving.

Two supporting details: version numbers now come from a durable per-identity counter that outlives a hard delete, so a number and its content path are never reused; and the registration wire format is specified as multipart/form-data (a JSON metadata part plus a gzip-tar content part) with archive validation and a size limit.

Comment from Bill Murdock with assistance from Claude Code.

@mprahl mprahl left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple small comments but looks good otherwise!

jwm4 and others added 2 commits August 19, 2026 12:43
Address mprahl review comment #3814518122: pull now recomputes the
content digest over the fetched tree and fails on mismatch when the
version has a digest set, with the same per-member check on agent-plugin
pull. The check is client-side and does not change the server's role
(the server still stores the client-asserted digest without verifying
it). Add a "Digest verification" note to Pull semantics and tighten the
content-digest section in implementation-details.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the two-phase upload (create version, then upload content, then
finalize) with a single atomic local-path registration that carries the
packaged content inline, since skills are small. Remove the finalized
column, the finalize PATCH parameter, and the skip-non-finalized
resolution rules.

Assign version numbers from a durable next_version counter held in a new
skill_version_counters ledger that outlives hard deletion of the parent
skill, so numbers and their identity-derived artifact paths are never
reused (gaps allowed) and a failed or retired attempt cannot contaminate
later content.

Specify the registration wire format: multipart/form-data with a JSON
metadata part and a gzip-tar content part. Validate the archive
(reject absolute and .. paths; accept only regular files and
directories, rejecting symlinks, hard links, devices, and FIFOs;
enforce a configurable decompressed size limit, default 25 MiB) and
require the body to match source (null source needs multipart content;
a remote source needs JSON).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
behavior). Import and standalone registration both rely on the primary-key
uniqueness constraint to detect a name collision.

### `skill_version_counters`

@mprahl mprahl Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we drop skill_version_counters and allocate like the model registry: MAX(version)+1, restart at 1 after the parent is gone, retry on uniqueness races? If leftover blobs are a concern, I would follow the pattern for experiments rather than an immortal counte where we keep the skill in a deleted lifecycle until mlflow gc hard-deletes it and removes skills/.../<version>/, the same way it removes experiment artifacts. Skipping a version if that prefix is non-empty could be extra caution for source_type=mlflow only.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I dropped the durable counter entirely and switched version allocation to MAX(version) + 1 with a retry on the (workspace, organization, name, version) primary-key uniqueness violation, matching the model registry.

Pulling that thread further simplified two more things:

  • Registration is now a single multipart request that carries the content, so the two-phase upload and the finalize step are gone.
  • Each upload is stored at a unique server-chosen artifact path recorded on the version's source, never derived from the version number. So a version that restarts at 1 after a hard delete can never collide with a prior incarnation's content, which was the original reason the counter needed to be immortal.

Content is reclaimed when its owning version is hard-deleted: the server commits the row deletion first, then does best-effort blob cleanup (the artifact store isn't transactional with the DB, so committing first avoids stranding a restored row on missing bytes). The rare orphaned bytes from a crashed upload are intentionally accepted as a small leak rather than swept.

Pushed in e8a3aa1.

Comment from Bill Murdock with assistance from Claude Code.

…eclamation

Adopt mprahl's simplification: drop finalize/two-phase upload for a single
multipart registration. Allocate versions as MAX(version)+1 with PK-collision
retry; store each upload at a unique server-chosen token path recorded in
`source`. Hard delete reclaims owned content by committing the row deletion
first, then best-effort blob cleanup (artifact store is not transactional
with the DB). Remove the version-counter and lease tables entirely.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants