From fc47d021be9e59acf9ef0604a21b0bb3b38a28df Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Tue, 14 Jul 2026 12:37:47 -0400 Subject: [PATCH 1/7] Add RFC-0008: MVP Skill Registry (Phase 1) 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 #10 with a phased approach per Slack discussion with Databricks maintainers. Co-Authored-By: Claude Opus 4.6 --- .../0008-mvp-skill-registry.md | 1068 ++++++++++++ .../implementation-details.md | 1466 +++++++++++++++++ 2 files changed, 2534 insertions(+) create mode 100644 rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md create mode 100644 rfcs/0008-mvp-skill-registry/implementation-details.md diff --git a/rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md b/rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md new file mode 100644 index 0000000..3adca18 --- /dev/null +++ b/rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md @@ -0,0 +1,1068 @@ +# RFC 0008: Skill Registry + +| start_date | 2026-04-22 | +| :----------- | :--------- | +| mlflow_issue | https://github.com/mlflow/mlflow/issues/22833 | +| rfc_pr | | + +| Author(s) | [Bill Murdock](https://github.com/jwm4) (Red Hat) | +| :--------------------- | :-- | +| **Date Last Modified** | 2026-07-13 | +| **AI Assistant(s)** | Claude Code | + +**Table of contents** + +- [Summary](#summary) +- [Basic example](#basic-example) +- [Motivation](#motivation) + - [The problem](#the-problem) + - [User journeys](#user-journeys) + - [Out of scope](#out-of-scope) +- [Detailed design](#detailed-design) + - [Entities and data model](#entities-and-data-model) + - [Status and lifecycle](#status-and-lifecycle) + - [Pull semantics](#pull-semantics) + - [Workspace scoping](#workspace-scoping) + - [Permissions](#permissions) + - [UI](#ui) + - [Trace integration](#trace-integration) + - [Package manager integration](#package-manager-integration) +- [Drawbacks](#drawbacks) +- [Alternatives](#alternatives) +- [Adoption strategy](#adoption-strategy) + +# Summary + +Add a Skill Registry to MLflow: a governed, metadata-first registry for +AI agent skills. The registry stores metadata and typed source +pointers (to Git repos, OCI registries, ZIP archives, etc.). It can +also store content directly via MLflow artifact storage, but the +primary design is metadata-first. It provides enterprise governance +on top of existing distribution mechanisms: lifecycle management, +usage analytics via traces, and federated discovery across sources. + +The registry manages two entity types under the `mlflow.genai.skills` +SDK namespace (CLI: `mlflow skills`), each with full lifecycle +(versioning, aliases, tags, status). Note: MLflow already has an +`mlflow skills` CLI group with `list` and `view` subcommands for +inspecting bundled Assistant skills. The registry subcommands +(`register`, `pull`, `install`, etc.) extend this existing group; +none of the new subcommand names conflict with the existing ones. +See [implementation-details.md: Python SDK and +CLI](implementation-details.md#python-sdk-and-cli) for details. + +The two entity types: + +- **Skills**: a directory containing a SKILL.md entry point plus + supporting files (scripts, templates, reference material) +- **Skill bundles**: versioned collections that group related skills + into a single governed, installable unit + +`mlflow skills pull` provides a harness-agnostic way to fetch +registered content from its source. Harness-specific installation +delegates to package managers (APM, Lola, or others via a plugin +interface) that already support cross-harness skill installation. + +`mlflow.skill_context()` closes the observability loop by creating +SKILL spans in MLflow traces, annotated with registry coordinates, +enabling adoption tracking, deprecation impact analysis, per-skill +cost attribution, and regression detection. + +A follow-up RFC (RFC-0009) will extend skill bundles to include +non-skill members (subagents, hooks, MCP server references). + +# Basic example + +## Register a skill + +```python +import mlflow + +mlflow.genai.skills.register_skill( + name="code-review", + version="1.0.0", + description="Reviews pull requests for correctness, style, and security", + source_type="git", + source="https://github.com/acme/agent-skills.git@v1.0.0", + subpath="code-review", +) +``` + +## Create a skill bundle + +```python +from mlflow.genai.skills import SkillMemberRef + +mlflow.genai.skills.create_skill_bundle_version( + name="pr-workflow", + version="1.0.0", + skills=[ + SkillMemberRef(name="code-review", version="1.0.0"), + SkillMemberRef(name="style-check", version="2.0.0"), + ], +) +``` + +## Install and use + +```bash +# Install a skill bundle for Claude Code via a package manager +mlflow skills install-bundle --name pr-workflow --alias production + +# Or install a single skill directly (file download) +mlflow skills install --name code-review --alias production \ + --harness claude-code +``` + +## Motivation + +### The problem + +AI agent skills are becoming a critical asset class in enterprise AI +platforms. A cross-harness portable format is emerging around SKILL.md: +a markdown file with structured instructions for the agent, supported +by Claude Code, Codex CLI, Cursor, GitHub Copilot, OpenClaw, Kilo +Code, Antigravity, and others. + +Today, skills are managed as ad-hoc files in Git repositories. This +works well for individual developers and small teams. GitHub provides +versioning, collaboration, and access control. + +However, enterprises face governance challenges that Git alone does not +address: + +1. **No status lifecycle.** Git has no concept of "this version is + approved for production use" vs. "this is deprecated." Teams resort + to branch naming conventions or external tracking to manage + promotion. + +2. **Fragmented discovery.** Skills may live in multiple Git repos, OCI + registries, or other distribution systems. There is no single + discovery layer across all of these. + +3. **No trace-to-skill linkage.** MLflow already traces agent + conversations (Claude Code via `mlflow autolog claude`, SDK + applications via framework autologgers such as + `mlflow.langchain.autolog()` and `mlflow.anthropic.autolog()`). + These traces capture LLM calls, tool use, and token consumption, + but there is no way to know which governed, versioned skill was + active during any part of a trace. This RFC introduces + `mlflow.skill_context()` for manual instrumentation and an + install-time trace manifest for automatic instrumentation via + harness autologgers (see [Trace integration](#trace-integration)). + Without a registry, organizations cannot answer questions like + "which skill versions are most used?" or "show me all traces where + the deprecated code-review v1.0 was loaded." + +4. **No cross-source pull mechanism.** Skills may be distributed via + Git, OCI registries, ZIP archives, or stored directly in MLflow. + There is no standard way to fetch content from all of these with a + single command. + +### User journeys + +These journeys illustrate the end-to-end workflows that the Skill +Registry enables. Each shows both CLI and UI paths. + +#### Register a skill bundle + +1. Register individual skill versions pointing to their sources: + ```bash + mlflow skills register --name code-review --version 1.0.0 \ + --source https://github.com/acme/agent-skills.git@v1.0.0 \ + --subpath code-review + mlflow skills register --name style-check --version 2.0.0 \ + --source https://github.com/acme/agent-skills.git@v2.0.0 \ + --subpath style-check + ``` + **SDK equivalent:** + ```python + import mlflow + + mlflow.genai.skills.register_skill( + name="code-review", + version="1.0.0", + description="Reviews pull requests for correctness, style, and security", + source_type="git", + source="https://github.com/acme/agent-skills.git@v1.0.0", + subpath="code-review", + ) + ``` + **UI path:** Navigate to the Skills page, click "Register Skill," + fill in name, version, source type, and source URL, then submit. +2. Create a skill bundle version that pins these members: + ```bash + mlflow skills create-bundle-version --name pr-workflow --version 1.0.0 \ + --skill code-review:1.0.0 \ + --skill style-check:2.0.0 + ``` + **UI path:** Navigate to the Bundles tab, click "Create Bundle," + add members by searching and selecting from registered skills. +3. Transition the bundle version from draft to active: + ```bash + mlflow skills update-bundle-version --name pr-workflow \ + --version 1.0.0 --status active + ``` + **UI path:** Open the bundle version detail page, use the status + dropdown to change from "draft" to "active." +4. Set an alias for stable downstream resolution: + ```bash + mlflow skills set-bundle-alias --name pr-workflow \ + --alias production --version 1.0.0 + ``` + **UI path:** In the bundle detail page, click "Add Alias" and map + `production` to version `1.0.0`. + +#### Discover a skill for a specific purpose + +1. Search the registry by keyword: + ```bash + mlflow skills search --filter "name LIKE '%review%'" --status active + ``` + **UI path:** Navigate to the Skills page, type "review" in the + search bar, and filter by status "active" using the dropdown. +2. Browse the returned list of matching skills with names, + descriptions, and latest versions. + **UI path:** Scan the card-based list view. Each card shows the + skill name, description, latest version badge, status badge, and + tags. +3. Get details on a promising result: + ```bash + mlflow skills get --name code-review + ``` + **UI path:** Click a card to open the detail view with metadata, + version history, aliases, tags, and bundle memberships. +4. Inspect a specific version's source and metadata: + ```bash + mlflow skills get-version --name code-review --version 1.0.0 + ``` +5. Pull the skill locally to read the content and decide whether + it fits: + ```bash + mlflow skills pull --name code-review --version 1.0.0 \ + --destination ./review-skill + ``` + +#### Install a skill bundle, run the agent, browse traces + +1. Install the bundle using a package manager plugin: + ```bash + mlflow skills install-bundle --name pr-workflow --alias production + ``` + This resolves the bundle from the registry, pulls the bundle + content, delegates to the configured package manager (e.g., APM + or Lola) for harness-specific installation, and writes a trace + manifest (`mlflow-skills-manifest.json`) with installed registry + coordinates. For direct installation without a package manager: + ```bash + mlflow skills install --name code-review --alias production \ + --harness claude-code + ``` +2. Run the agent. The harness loads the installed skills during a + conversation. +3. Open the MLflow UI and navigate to the Traces page. Click the + "Skills" tab to filter for traces with SKILL spans. +4. Find the trace for the agent run. Skill invocations appear as + SKILL spans in the trace tree, annotated with registry coordinates + (skill name, version, registry). +5. Click a SKILL span to see which registered skill version was used + and how long it took. Click the skill name link to navigate to the + skill's registry detail page. + +#### Evaluate two bundle versions with LLM judges + +MLflow's +[LLM judges](https://mlflow.org/docs/latest/genai/eval-monitor/scorers/) +can autonomously explore execution traces via MCP tools. Because +skill invocations produce traced SKILL spans, LLM judges can +analyze how skills were used during an agent run. + +1. Register a new version of the bundle with updated members: + ```bash + mlflow skills register --name code-review --version 2.0.0 \ + --source https://github.com/acme/agent-skills.git@v2.0.0 \ + --subpath code-review + mlflow skills create-bundle-version --name pr-workflow --version 2.0.0 \ + --skill code-review:2.0.0 \ + --skill style-check:2.0.0 + ``` +2. Install v1.0.0 and run it on a set of test inputs. Traces are + recorded in MLflow under experiment A. +3. Install v2.0.0 and run it on the same test inputs. Traces are + recorded under experiment B. +4. Use `mlflow.genai.evaluate()` with a `make_judge` scorer that + uses the `{{ trace }}` template variable to score both sets of + traces against quality criteria (correctness, helpfulness, safety). +5. Compare the evaluation results side by side in the MLflow UI to + determine whether v2.0.0 is an improvement. +6. If v2.0.0 is better, transition it to active and update the + production alias: + ```bash + mlflow skills update-bundle-version --name pr-workflow \ + --version 2.0.0 --status active + mlflow skills set-bundle-alias --name pr-workflow \ + --alias production --version 2.0.0 + ``` + +#### Compare agent performance with and without a skill + +A common evaluation scenario is measuring the impact of adding or +removing a skill from an agent's configuration. This uses the same +evaluation infrastructure as version comparison, but one experiment +runs the agent without the skill installed. + +1. Run the agent without the skill on a set of test inputs. Traces + are recorded in MLflow under experiment A (baseline). +2. Install the skill: + ```bash + mlflow skills install --name code-review --alias production \ + --harness claude-code + ``` +3. Run the same agent on the same test inputs. Traces are recorded + under experiment B. Skill invocations appear as SKILL spans in + the traces. +4. Use `mlflow.genai.evaluate()` with the same scorers on both + experiments: + ```python + baseline_results = mlflow.genai.evaluate( + data=baseline_traces, scorers=scorers, + ) + skill_results = mlflow.genai.evaluate( + data=skill_traces, scorers=scorers, + ) + ``` +5. Compare the evaluation results side by side in the MLflow UI. + The SKILL spans in experiment B's traces confirm which skill + version was active during each run, enabling attribution of any + quality differences. + +The same pattern works for comparing different skill sets: install +bundle A for one experiment, bundle B for another, and evaluate +both against the same inputs and scorers. + +#### Trace skill lineage to evaluation results + +After running evaluations, a user wants to know which registered +skill version was active during a traced agent run. The lineage +path flows through traces: evaluation results link to traces, and +traces contain SKILL spans annotated with registry coordinates. + +1. Run an agent with installed skills. Skill invocations produce + SKILL spans in the recorded traces (see + [Trace integration](#trace-integration)). +2. Run evaluation against the collected traces: + ```python + results = mlflow.genai.evaluate( + data=traces_df, + scorers=[correctness_scorer, helpfulness_scorer], + ) + ``` + Each row in `results.result_df` includes a `trace_id` linking + the evaluation result back to its source trace. +3. Find which skill versions were used in a specific evaluation + result: + ```python + trace = mlflow.get_trace(trace_id) + skill_spans = trace.search_spans(span_type="SKILL") + for span in skill_spans: + print(span.attributes["mlflow.skill.name"], + span.attributes["mlflow.skill.version"]) + ``` +4. Find all traces that used a specific skill version: + ```python + traces = mlflow.search_traces( + experiment_ids=[experiment_id], + filter_string=( + 'span.type = "SKILL"' + ' AND span.attributes.mlflow.skill.name = "code-review"' + ' AND span.attributes.mlflow.skill.version = "1.0.0"' + ), + ) + ``` + Evaluation results for these traces can then be retrieved via + their `trace_id` values. + +This two-step approach (query traces by skill attributes, then +retrieve associated evaluation results) works with the existing +MLflow tracing and evaluation APIs. Richer integration, such as +filtering evaluation results directly by skill version, is +follow-up work (see [Adoption strategy](#adoption-strategy)). + +#### CI pipeline for automated regression detection + +1. A CI job (e.g., GitHub Actions) triggers on pushes to the skill + source repo. +2. The job registers a new skill version from the updated source: + ```bash + mlflow skills register --name code-review --version 1.1.0 \ + --source https://github.com/acme/agent-skills.git@v1.1.0 \ + --subpath code-review + mlflow skills create-bundle-version --name pr-workflow --version 1.1.0 \ + --skill code-review:1.1.0 \ + --skill style-check:2.0.0 + ``` +3. The job installs the new bundle version and runs it against a + benchmark dataset, collecting traces in a dedicated MLflow + experiment. +4. The job runs + [LLM judge](https://mlflow.org/docs/latest/genai/eval-monitor/scorers/) + evaluation on the collected traces, producing scored results. +5. The job fetches the benchmark results from the previous production + version (stored as MLflow metrics or evaluation artifacts). +6. The job compares the new scores against the previous scores. If + any quality metric regresses beyond a configured threshold, it + sends an alert (Slack, email, or fails the CI check). +7. If no regression is detected, the job transitions the new version + to active and optionally updates the production alias. + +See [implementation-details.md: SDK and CLI code +examples](implementation-details.md#sdk-and-cli-code-examples) for +additional SDK examples including OCI subpath registration and +discovery/search operations. + +### Out of scope + +- **Non-skill entity types.** Subagent definitions, hooks, and MCP + server references are deferred to a follow-up RFC (RFC-0009) that + will extend skill bundles with non-skill members. The registry + backend is designed to be extensible to these types. +- **Artifact storage as the only path.** The registry supports both + external source pointers (Git, OCI, ZIP) and direct artifact storage + (`source_type="mlflow"`). However, it is not an artifact-only store; + the metadata-first, source-pointer model remains the primary design. +- **Authoring or development tools.** The registry manages published + skills, not the process of writing them. +- **Format specification.** The registry is format-agnostic. It does + not define what a skill must contain or how it must be structured. + The SKILL.md convention is an ecosystem convention, not a registry + requirement. +- **Agent routing or orchestration.** The registry is a metadata and + governance layer. It does not decide which skills to invoke at + runtime or how agents compose capabilities. +- **MCP server hosting.** MCP server deployment and runtime management + are covered by the MCP Server Registry (RFC-0004) and the MCP + Gateway. +- **Prompts.** MLflow already has a Prompt Registry for versioned + prompt template management. Skills and prompts serve different + purposes: a skill is a directory containing a SKILL.md entry point + plus supporting files, with metadata controlling invocation. A + prompt provides templated text for structured generation. Skills may + reference prompts, but they belong in separate registries because + they have different lifecycles and different audiences (harness-based + agents vs. custom agentic code). +- **Custom harness adapters.** This RFC does not build per-harness + installation adapters. Instead, it delegates harness-specific + installation to existing package managers (APM, Lola) via a plugin + interface. + +## Detailed design + +### Entities and data model + +```mermaid +erDiagram +Skill ||--o{ SkillVersion : "has versions" +Skill ||--o{ SkillTag : "has tags" +Skill ||--o{ SkillAlias : "has aliases" +SkillBundle ||--o{ SkillBundleVersion : "has versions" +SkillBundle ||--o{ SkillBundleTag : "has tags" +SkillBundle ||--o{ SkillBundleAlias : "has aliases" +SkillBundleVersion ||--o{ SkillBundleVersionMember : "has members" +SkillBundleVersionMember }o--|| SkillVersion : "skill member" + +SkillBundleVersionMember { + string member_type + string member_name + string member_version + string member_subpath +} +``` + +#### Skill + +A skill is a directory containing a SKILL.md entry point plus +supporting files (scripts, templates, reference material). The +`Skill` entity is the logical governed asset, scoped to a workspace. +Key fields include `name` (unique within workspace), `display_name`, +`status` (read-only, derived from the parent-resolved version), +`latest_version` (read-only, highest active semver), and `aliases`. + +#### SkillVersion + +A versioned record containing a typed source pointer (`git`, `oci`, +`zip`, or `mlflow`), status, and tags. The `(name, version)` pair is +unique within a workspace. Source pointers and version strings are +immutable after creation; to point to different content, register a +new version. The optional `subpath` field identifies content within a +shared artifact (used with Git, OCI, and ZIP). The optional +`content_digest` field enables integrity verification. + +#### SkillBundle + +A skill bundle groups related skills into a governed unit that maps +to the "plugin" concept in agent harnesses: a curated set of +capabilities that work together. Bundles are a first-class entity +rather than a tag-based grouping because they provide versioned +membership snapshots (reproducible point-in-time combinations), +bundle-level source pointers (a single OCI image or Git repo), +independent lifecycle (deprecate a bundle without deprecating its +members), and direct mapping to the harness plugin concept. Follows +the same top-level pattern as Skill: versions, tags, aliases, and +derived status. + +A follow-up RFC (RFC-0009) will extend skill bundles to include +non-skill members (subagent definitions, hooks, and MCP server +references from RFC-0004), enabling full "plugin"-style bundles. +The member table schema includes a `member_type` field for forward +compatibility with this extension. + +#### SkillBundleVersion + +A versioned snapshot of a bundle's membership. A bundle version is +one of two kinds: + +- **Assembled:** captures member references for individual skills. + Each skill version has its own source. `pull` fetches members + individually. +- **Monolithic:** has its own source pointer (e.g., a single OCI + image or Git repo containing multiple skills) and member + references. Skill member versions may omit their own sources when + their content lives inside the bundle artifact; the optional + `member_subpath` on each membership identifies where the member + lives inside the bundle artifact. `pull` fetches the bundle + artifact as a unit. + +A bundle version cannot have both a bundle-level source and skill +member versions with their own sources. This avoids confusion about +which source is authoritative for skill content. + +#### Aliases and tags + +All entity types use the same alias pattern: a frozen `(name, alias, +version)` tuple mapping a stable name (e.g., `production`) to a +specific version string. Tags are `(key, value)` pairs at both the +entity level and version level. + +Dataclass definitions, field tables, source type details, and +database schema for all entity types are in +[implementation-details.md](implementation-details.md). + +### Status and lifecycle + +This lifecycle aligns with the MCP Server Registry (RFC-0004). + +#### Per-version status + +Each `SkillVersion` and `SkillBundleVersion` has an independent +status: + +```mermaid +stateDiagram-v2 + [*] --> draft + draft --> active : publish + draft --> deleted : discard + active --> draft : unpublish + active --> deprecated + deprecated --> active : re-activate + deprecated --> deleted +``` + +| State | Meaning | Downstream surfacing | +|---|---|---| +| `draft` | Registered but not yet ready for downstream use | Not surfaced to consumers | +| `active` | Ready for downstream use | Surfaced to discovery, traces, consumers | +| `deprecated` | Still functional but no longer recommended | Surfaced with deprecation signal | +| `deleted` | Soft-deleted; preserved internally for history, no longer active | Not surfaced by normal get/search/list APIs | + +New versions default to `draft` upon creation. + +Allowed transitions: + +| From | To | +|---|---| +| `draft` | `active`, `deleted` | +| `active` | `draft`, `deprecated` | +| `deprecated` | `active`, `deleted` | + +`draft` allows a version to be registered and reviewed before being +made visible to consumers. `active` can return to `draft` (unpublish) +for cases where a version needs to be pulled back for further review. +`deprecated` can return to `active` (re-activate) for cases where a +deprecation was premature. `deleted` is terminal. + +Normal version delete operations (`delete_skill_version` and +`delete_skill_bundle_version`) transition the version to `deleted` +rather than physically removing the version row. Active versions must +first be unpublished or deprecated before they can be deleted. +Deleting a version also removes aliases that point to that version. + +Top-level entity delete operations (`delete_skill` and +`delete_skill_bundle`) are administrative hard deletes that remove the +parent and cascade to child rows, following the Model Registry +registered-model pattern. These operations are subject to +referential-integrity checks: a skill version referenced by a bundle +version cannot be physically removed until the referencing bundle +version is removed or otherwise no longer references it. Normal +retirement should use version deprecation or version soft delete +rather than top-level hard delete. + +#### Entity-level status + +`Skill.status` and `SkillBundle.status` are read-only. They are +derived from the parent-resolved version: the highest semantic version +among `active` versions if one exists, otherwise the highest semantic +version among non-`deleted` non-`active` versions. Deleted versions +never drive parent status. This follows the MCP Server Registry +pattern (RFC-0004). + +#### `latest_version` resolution + +Version strings must follow [semantic versioning](https://semver.org/) +(e.g., `1.0.0`, `2.1.0-beta.1`). `get_latest_skill_version(name)` +returns the highest semantic version among `active` versions if one +exists, otherwise the highest semantic version among non-`deleted` +non-`active` versions. Prerelease identifiers participate in +semantic-version ordering, while build metadata does not. +`latest_version` is a read-only computed field on the parent entity +(not manually pinnable); aliases cover the use case of pointing a +stable name (e.g., `production`) at a specific version. + +The alias name `latest` is reserved: `set_skill_alias(..., +alias="latest", ...)` is rejected, while +`get_skill_version_by_alias(..., alias="latest")` is treated as a +convenience alias for `get_latest_skill_version(...)`. + +This aligns with the MCP Server Registry (RFC-0004). + +### Pull semantics + +`pull` is a client-side operation. The SDK reads the source pointer +from the registry via the REST API, then fetches content directly +from the source system to the caller's local filesystem. The registry +server is not involved in content transfer. `pull` is +source-type-aware: + +| Source type | Pull behavior | +|---|---| +| `git` | `git clone` or `git archive` of the referenced path/ref | +| `oci` | `oci pull` of the referenced image/tag; if `subpath` is set, extract only that path from the image | +| `zip` | HTTP download and extract; if `subpath` is set, extract only that path from the archive | +| `mlflow` | Download the version's MLflow-managed artifact directory tree using the same artifact APIs and credentials as other MLflow artifact operations | + +**Single skill pull.** Fetches the content at the skill version's +`source` to the destination directory. If `subpath` is set, only the +content at that path within the artifact is extracted. Returns an +error if the skill version has no source; source-less embedded skill +versions are pullable only through their containing monolithic +bundle. + +**Skill bundle pull.** For monolithic bundles, fetch the bundle +artifact as a single unit to the destination directory. For assembled +bundles, pull each member individually from its own `source` to a +subdirectory of the destination, named by the member's name. If a +skill member in an assembled bundle has no `source`, the pull fails +rather than producing a partial local bundle. + +If `content_digest` is set, `pull` verifies the fetched content +matches the digest and returns an error on mismatch. This +verification is client-side. The server stores the digest as metadata +but does not re-verify artifact store contents on each request. + +`pull` is harness-agnostic. It downloads content but does not generate +harness-specific manifests or place files in harness-specific +directories. Harness-specific installation is handled by package +manager plugins (see [Package manager +integration](#package-manager-integration)). + +See [implementation-details.md: Pull semantics +details](implementation-details.md#pull-semantics-details) for source +authentication mechanisms, error handling, and credential management. + +### Workspace scoping + +All skill registry operations are workspace-scoped, following MLflow's +existing workspace-aware registry patterns (model registry, MCP +registry). Cross-workspace sharing is out of scope for this RFC and +should be solved at the platform level across all MLflow registries. + +### Permissions + +The skill registry integrates with MLflow's existing permission +framework (READ / EDIT / MANAGE), applied at the `Skill` and +`SkillBundle` level. Versions, tags, aliases, and memberships inherit +permissions from their parent entity. + +| Permission | Operations | +|---|---| +| `READ` | Search entities, get versions, resolve aliases, list tags and memberships | +| `EDIT` | Create entities, create versions, set tags, update description, status transitions (activate, deprecate), set aliases. Mapped to `can_update` in MLflow's permission framework. | +| `MANAGE` | Delete aliases, delete tags, soft-delete versions, hard-delete entities, manage permissions. Mapped to `can_delete` in MLflow's permission framework. | + +This follows the same pattern as the model registry and MCP Server +Registry (RFC-0004). +- **Creator gets MANAGE.** When a user creates an entity (skill or + bundle), they automatically receive MANAGE permission, following + the MLflow model registry pattern. + +### UI + +The Skills page lives under the GenAI workflow in the MLflow sidebar, +alongside Experiments, Prompts, MCP Servers, and AI Gateway. + +#### List view + +The list view shows skills and bundles together using a card-based +layout consistent with the MCP Server Registry (RFC-0004). Each card +displays: + +- Entity type badge (skill or bundle) +- Name and optional display name +- Description (truncated to 2-3 lines) +- Latest version badge (e.g., "v1.0.0") +- Status badge with color coding: draft (gray), active (green), + deprecated (amber) +- Source type indicator (Git, OCI, ZIP, MLflow) +- Tag chips + +The filter bar provides: + +- **Type dropdown**: skill, bundle +- **Status dropdown**: draft, active, deprecated +- **Source type dropdown**: git, oci, zip, mlflow +- **Search**: by name or description + +A "Register Skill" button (with a dropdown for bundle) initiates +registration. + +#### Detail view: skills + +The detail view for an individual skill shows: + +- **Metadata section**: name, display name, description, status, + workspace, source type, created by, created at, last updated +- **Version table**: Version, Registered at, Status, Source type, + Created by, Description. Clicking a version row navigates to the + version detail page showing source, subpath, content digest, and + tags. +- **Aliases**: alias name to version mapping (e.g., + `production -> 1.0.0`) +- **Tags**: key-value list with edit controls +- **Bundle memberships**: list of bundles that include this skill, + with links to each bundle's detail page +- **Related traces**: link to the GenAI Traces page filtered by this + skill's name, showing recent SKILL spans that reference this skill + +#### Detail view: bundles + +The bundle detail view shows: + +- **Metadata section** (as above) +- **Members table** for the selected bundle version: Name, Pinned + version, Source type, Status. Each row links to the member skill's + detail page. +- **Version history table**: Version, Registered at, Status, Created + by, Member count +- **Aliases and tags** (as above) + +#### Trace integration display + +The GenAI Traces page includes a "Skills" tab alongside the existing +"Prompts" tab, showing SKILL spans for each trace. The trace detail +view displays SKILL spans with registry coordinates (skill name, +version, workspace) and links to the skill's registry detail page. +Skill version detail pages surface related traces using the same +association data. + +### Trace integration + +MLflow already traces agent conversations across multiple frameworks: +Claude Code (via `mlflow autolog claude`), SDK applications (via +framework autologgers such as `mlflow.langchain.autolog()` and +`mlflow.anthropic.autolog()`), and others. These traces capture LLM +calls, tool use, and timing as a tree of spans. The skill registry +closes the observability loop by letting agent developers indicate +which registered skill is active during each part of a trace. + +#### `mlflow.skill_context()` context manager + +The primary instrumentation API is a context manager that creates a +span of type `SKILL` and attaches registry coordinates as span +attributes: + +```python +with mlflow.skill_context( + name="code-review", version="1.0.0" +) as span: + # All spans created inside this block (including those from + # autologgers) become children of this SKILL span. + result = llm.chat([{"role": "user", "content": "Review this code..."}]) +``` + +The context manager creates a span with `mlflow.skill.name`, +`mlflow.skill.version`, and `mlflow.skill.workspace` +attributes that link the span back to a specific skill version in +the registry. See [implementation-details.md: skill_context() span +attributes](implementation-details.md#skill_context-span-attributes) +for the full attribute table. + +#### Skill stacks via nesting + +Skills can invoke other skills. Nesting `skill_context()` calls +produces a skill stack in the trace tree: + +``` ++-- Span: "code-review" (type: SKILL, version: 1.0.0) +| +-- Span: ChatCompletion (type: LLM) +| +-- Span: "style-check" (type: SKILL, version: 2.0.0) +| | +-- Span: ChatCompletion (type: LLM) +| +-- Span: ChatCompletion (type: LLM) +``` + +Walking up the ancestor chain and collecting SKILL spans reconstructs +the skill stack for any span. + +#### Autologger compatibility + +Because `skill_context()` creates a standard MLflow span, it works +with existing autologgers without modification. When an autologger +(Claude, LangChain, OpenAI, etc.) creates a span inside a +`skill_context()` block, that span automatically becomes a child of +the SKILL span. No changes to the autologgers are needed. + +#### Registry validation + +`skill_context()` does not validate that the named skill exists in +the registry at call time. Validating on every invocation would add +latency and create a hard dependency on registry availability. The +trace records the `{workspace, name, version}` coordinates +regardless; the MLflow UI performs a best-effort lookup when +displaying traces and shows a "not found in registry" indicator if +the coordinates do not resolve. + +#### Workspace resolution + +When `skill_context()` is called, the workspace is resolved from +the `mlflow-skills-manifest.json` written by the install commands. +The manifest contains the workspace for each installed skill. +For SDK users calling `skill_context()` directly without a manifest, +the workspace defaults to the current MLflow tracking URI's workspace +context, consistent with other MLflow operations. + +#### Relationship to MCP trace linking + +The MCP Registry (RFC-0004) uses after-the-fact, trace-level +association (`link_mcp_server_versions_to_trace()`). Skills use +span-level, inline annotation because skills are ambient (active +during inference) and can nest. Both approaches produce trace +metadata that the MLflow UI can display together. + +### Package manager integration + +Rather than building custom harness adapters for each agent harness, +the skill registry delegates harness-specific installation to existing +package managers that already support cross-harness skill +installation. This avoids duplicating work that projects like +[APM](https://github.com/microsoft/apm) and +[Lola](https://github.com/LobsterTrap/lola) already handle well, and +lets the MLflow community benefit from their evolving harness support. + +#### Two installation paths + +The registry supports two installation paths: + +1. **Direct install** (`mlflow skills install`): a simple file + download that places skill content in a harness-specific directory. + This handles the common case of installing a single skill from any + supported source type. The `--harness` flag determines the target + directory (e.g., `.claude/skills/` for Claude Code, + `.cursor/skills/` for Cursor). + +2. **Package manager install** (`mlflow skills install-bundle`): for + bundles or when full harness-specific manifest generation is + needed. MLflow resolves registry metadata, pulls content from + MLflow-specific sources (artifact store, OCI) to local paths, and + delegates to a configured package manager plugin for + harness-specific installation. + +#### Package manager plugin interface + +Package manager plugins are registered via Python entrypoints (group +`mlflow.skill_package_managers`), so third-party plugins can be +installed via `pip install` without modifying MLflow core. + +```python +class PackageManagerPlugin: + def install_skill( + self, + name: str, + local_path: str, + harness: str | None = None, + scope: str = "project", + ) -> str: + """Install a single skill from a local path. + Returns the installed path.""" + ... + + def install_bundle( + self, + bundle_name: str, + member_paths: dict[str, str], + harness: str | None = None, + scope: str = "project", + ) -> str: + """Install a bundle of skills from local paths. + member_paths maps skill names to local paths. + Returns the installed path.""" + ... + + def supported_harnesses(self) -> list[str]: + """Return list of harness identifiers this plugin supports.""" + ... +``` + +#### Source resolution flow + +When `mlflow skills install-bundle` is invoked: + +1. MLflow resolves the bundle version from the registry (by name + + version or alias). +2. For each member skill, MLflow pulls content to a local temporary + directory using the same source-type-aware logic as + `mlflow skills pull`. +3. MLflow passes the local paths to the configured package manager + plugin, which handles harness-specific directory placement and + manifest generation. +4. MLflow writes the `mlflow-skills-manifest.json` trace manifest + with installed registry coordinates. + +For Git-backed skills, the package manager can also fetch directly +from Git (APM and Lola both support Git sources natively). In this +case, MLflow can provide the Git coordinates from the registry +metadata and let the package manager handle the fetch, avoiding a +redundant local pull. + +#### Trace manifest + +Both installation paths write an `mlflow-skills-manifest.json` file +that records installed registry coordinates. This manifest enables +automatic SKILL span creation by harness autologgers: + +```json +{ + "manifest_version": "1.0", + "skills": { + "code-review": { + "name": "code-review", + "version": "1.0.0", + "workspace": "default" + }, + "style-check": { + "name": "style-check", + "version": "2.0.0", + "workspace": "default" + } + } +} +``` + +### Implementation details + +Database schema (table definitions), store interface (method +signatures), entity dataclass definitions, REST API endpoints, +pagination/filtering, SDK convenience functions, and CLI mapping are +in [implementation-details.md](implementation-details.md). + +## Drawbacks + +- **Source pointer validity.** For external sources (git, oci, zip), + the registry cannot guarantee pointers remain valid. The optional + `content_digest` field mitigates content tampering but does not + prevent link rot. Users who need self-contained storage can use + `source_type="mlflow"` to store content directly in MLflow artifact + storage. + +- **Package manager dependency.** Full harness-specific installation + requires a package manager plugin (APM, Lola, or similar). Users + who do not install a package manager can still use direct install + (`mlflow skills install`) for single skills, and `mlflow skills + pull` for harness-agnostic content download. + +# Alternatives + +## Store skill artifacts only in MLflow (no source pointers) + +Make MLflow artifact storage the sole storage mechanism, with no +support for external source pointers. + +Rejected because most organizations already manage skills in Git or +OCI. Source pointers federate across existing distribution mechanisms +without requiring migration. The current design supports both: +`source_type="mlflow"` for direct artifact storage alongside +`source_type="git"`, `"oci"`, and `"zip"` for external sources. + +## Use Git alone (no registry) + +Continue using Git repositories as the sole mechanism for skill +management. + +This is sufficient for individual developers and small teams. This RFC +proposes a governance layer on top of Git for enterprises that need +status lifecycle, trace-to-skill linkage, and federated discovery. +The two approaches are complementary. + +## Build custom harness adapters in MLflow + +Build per-harness installation adapters within MLflow (as proposed in +the earlier version of this RFC). + +Rejected in favor of delegating to existing package managers. APM and +Lola already support 8+ and 6+ harnesses respectively, and their +harness support evolves independently of MLflow releases. Building +custom adapters would duplicate this work and create an ongoing +maintenance burden as harness plugin formats evolve. The plugin +interface allows MLflow to integrate with any package manager without +coupling to a specific one. + +## Use APM or Lola directly without a registry + +Use a client-side package manager (APM, Lola, or `gh skill install`) +as the sole mechanism for skill management. + +These tools solve the client-side "make it portable and reproducible" +problem well. However, they are not server-side registries and do not +provide the governance and observability features that enterprises +need: + +- **Lifecycle management.** No concept of draft, active, deprecated, + or deleted status. No way to signal consumers that a skill version + is deprecated or approved for production. +- **Rich discovery.** Limited search and metadata capabilities. No + centralized catalog with tags, descriptions, and compatibility + information. +- **Trace integration.** No connection between installed skills and + runtime execution traces. No way to answer "which skill version + was active during this agent run?" +- **RBAC and workspace scoping.** No per-user or per-team access + controls. No visibility boundaries between teams or projects. + +The skill registry and package managers are complementary: the +registry provides the server-side governance, discovery, and +observability layer, while package managers handle client-side +installation and harness-specific adaptation. + +# Adoption strategy + +New feature, not a breaking change. Phased rollout: + +- **Phase 1 (this RFC):** Skill and SkillBundle entities, store, + REST API, SDK, CLI, UI, `mlflow skills pull`, + `mlflow skills install` (direct), package manager plugin interface, + and `mlflow.skill_context()` for trace integration. +- **Phase 2 (RFC-0009):** Extend skill bundles with non-skill + members: subagent definitions, hooks, and MCP server references + (cross-registry with RFC-0004). +- **Phase 3 (follow-up):** Usage analytics dashboards, install count + tracking, cross-workspace export/import (following cross-registry + patterns), shared base extraction with the MCP registry, and richer + evaluation-to-skill query integration (e.g., filtering evaluation + results directly by skill version attributes). diff --git a/rfcs/0008-mvp-skill-registry/implementation-details.md b/rfcs/0008-mvp-skill-registry/implementation-details.md new file mode 100644 index 0000000..ae85e8c --- /dev/null +++ b/rfcs/0008-mvp-skill-registry/implementation-details.md @@ -0,0 +1,1466 @@ +# RFC-0008: Skill Registry Implementation Details + +This document contains implementation-level specifications for +RFC-0008 (Skill Registry). It covers database schema, entity +dataclasses, store interface method signatures, REST API endpoints, +pagination/filtering, SDK convenience functions, CLI mapping, package +manager plugin interface, and trace integration details. These details +support implementers; the main RFC covers the design rationale. + +## Database schema + +Tables are created via a single Alembic migration. All tables are +workspace-scoped. + +### `skills` + +| Column | Type | Notes | +|--------|------|-------| +| `workspace` | `String(63)` | PK, default `'default'` | +| `name` | `String(256)` | PK | +| `display_name` | `String(256)` | mutable human-readable label | +| `description` | `String(5000)` | | +| `created_by` | `String(256)` | | +| `last_updated_by` | `String(256)` | | +| `creation_timestamp` | `BigInteger` | millis since epoch | +| `last_updated_timestamp` | `BigInteger` | millis since epoch | + +### `skill_versions` + +| Column | Type | Notes | +|--------|------|-------| +| `workspace` | `String(63)` | PK, FK | +| `name` | `String(256)` | PK, FK | +| `version` | `String(256)` | PK, valid semantic version | +| `version_major` | `Integer` | extracted from validated semantic version | +| `version_minor` | `Integer` | extracted from validated semantic version | +| `version_patch` | `Integer` | extracted from validated semantic version | +| `version_prerelease_sort_key` | `String(512)` | lexicographically sortable encoding of prerelease identifiers | +| `display_name` | `String(256)` | mutable human-readable label | +| `source_type` | `String(20)` | nullable; `git`, `oci`, `zip`, `mlflow` | +| `source` | `String(2048)` | nullable pointer to skill content | +| `subpath` | `String(2048)` | nullable; path within the artifact | +| `content_digest` | `String(512)` | optional integrity digest | +| `status` | `String(20)` | default `'draft'` | +| `created_by` | `String(256)` | | +| `last_updated_by` | `String(256)` | | +| `creation_timestamp` | `BigInteger` | millis since epoch | +| `last_updated_timestamp` | `BigInteger` | millis since epoch | + +FK: `(workspace, name)` references `skills`, CASCADE delete. This +supports administrative hard deletion of the parent `Skill`; normal +version deletion is a status transition to `deleted` and does not +physically remove the version row. + +**Semantic version ordering**: `version_major`, `version_minor`, +`version_patch`, and `version_prerelease_sort_key` are materialized +from the validated semantic version string at write time. The +prerelease sort key is a lexicographically sortable encoding of the +prerelease identifiers, following the approach in the MCP Server +Registry implementation (mlflow/mlflow#23952). Release versions +encode to a sentinel that sorts above all prerelease encodings, so +full semver precedence is resolved in SQL without application-level +tie-breaking. Build metadata is ignored for precedence. + +**Index**: `ix_skill_versions_latest_lookup` on `(workspace, name, +status, version_major, version_minor, version_patch)` supports +latest-resolution lookups. The prerelease sort key is not indexed +because the major/minor/patch prefix provides sufficient pruning. + +### `skill_tags` + +| Column | Type | Notes | +|--------|------|-------| +| `workspace` | `String(63)` | PK, FK | +| `name` | `String(256)` | PK, FK | +| `key` | `String(256)` | PK | +| `value` | `Text` | | + +### `skill_version_tags` + +| Column | Type | Notes | +|--------|------|-------| +| `workspace` | `String(63)` | PK, FK | +| `name` | `String(256)` | PK, FK | +| `version` | `String(256)` | PK, FK | +| `key` | `String(256)` | PK | +| `value` | `Text` | | + +### `skill_aliases` + +| Column | Type | Notes | +|--------|------|-------| +| `workspace` | `String(63)` | PK, FK | +| `name` | `String(256)` | PK, FK | +| `alias` | `String(256)` | PK | +| `version` | `String(256)` | target version string | + +### `skill_bundles` + +| Column | Type | Notes | +|--------|------|-------| +| `workspace` | `String(63)` | PK, default `'default'` | +| `name` | `String(256)` | PK | +| `display_name` | `String(256)` | mutable human-readable label | +| `description` | `String(5000)` | | +| `created_by` | `String(256)` | | +| `last_updated_by` | `String(256)` | | +| `creation_timestamp` | `BigInteger` | millis since epoch | +| `last_updated_timestamp` | `BigInteger` | millis since epoch | + +### `skill_bundle_versions` + +| Column | Type | Notes | +|--------|------|-------| +| `workspace` | `String(63)` | PK, FK | +| `name` | `String(256)` | PK, FK | +| `version` | `String(256)` | PK, valid semantic version | +| `version_major` | `Integer` | extracted from validated semantic version | +| `version_minor` | `Integer` | extracted from validated semantic version | +| `version_patch` | `Integer` | extracted from validated semantic version | +| `version_prerelease_sort_key` | `String(512)` | lexicographically sortable encoding of prerelease identifiers | +| `display_name` | `String(256)` | mutable human-readable label | +| `source_type` | `String(20)` | optional; `git`, `oci`, `zip`, `mlflow` | +| `source` | `String(2048)` | optional pointer to bundle artifact | +| `subpath` | `String(2048)` | nullable; path within the artifact | +| `content_digest` | `String(512)` | optional integrity digest | +| `status` | `String(20)` | default `'draft'` | +| `created_by` | `String(256)` | | +| `last_updated_by` | `String(256)` | | +| `creation_timestamp` | `BigInteger` | millis since epoch | +| `last_updated_timestamp` | `BigInteger` | millis since epoch | + +FK: `(workspace, name)` references `skill_bundles`, CASCADE delete. +Semantic version ordering and index follow the same pattern as +`skill_versions`. + +### `skill_bundle_version_members` + +| Column | Type | Notes | +|--------|------|-------| +| `workspace` | `String(63)` | PK | +| `bundle_name` | `String(256)` | PK, FK to `skill_bundle_versions` | +| `bundle_version` | `String(256)` | PK, FK to `skill_bundle_versions` | +| `member_type` | `String(20)` | PK; `skill` in Phase 1 (reserved for future extension) | +| `member_name` | `String(256)` | PK | +| `member_version` | `String(256)` | PK | +| `member_subpath` | `String(2048)` | nullable; member path inside bundle artifact | + +FK: `(workspace, bundle_name, bundle_version)` references +`skill_bundle_versions`, CASCADE delete. When `member_type` is +`skill`, a FK to `skill_versions` enforces referential integrity +with RESTRICT delete. + +The `member_type` column is included for forward compatibility with +RFC-0009, which will extend bundles to include non-skill members +(subagent definitions, hooks, MCP server references). In this RFC, +all members have `member_type='skill'`. + +### `skill_bundle_tags` + +| Column | Type | Notes | +|--------|------|-------| +| `workspace` | `String(63)` | PK, FK | +| `name` | `String(256)` | PK, FK | +| `key` | `String(256)` | PK | +| `value` | `Text` | | + +### `skill_bundle_version_tags` + +| Column | Type | Notes | +|--------|------|-------| +| `workspace` | `String(63)` | PK, FK | +| `name` | `String(256)` | PK, FK | +| `version` | `String(256)` | PK, FK | +| `key` | `String(256)` | PK | +| `value` | `Text` | | + +### `skill_bundle_aliases` + +| Column | Type | Notes | +|--------|------|-------| +| `workspace` | `String(63)` | PK, FK | +| `name` | `String(256)` | PK, FK | +| `alias` | `String(256)` | PK | +| `version` | `String(256)` | target bundle version string | + +**Workspace handling.** All tables use `(workspace, ...)` as the leading +primary key components. Single-tenant deployments use `'default'`. + +**Timestamps.** Set at the application layer via +`get_current_time_millis()`, not via DDL defaults. + +**Deletion semantics.** The registry follows the mixed deletion pattern +used by the Model Registry and RFC-0004: + +- Top-level entity delete operations (`delete_skill` and + `delete_skill_bundle`) are administrative hard deletes. They + physically remove the parent row and cascade to child rows, subject + to referential-integrity checks. +- Version delete operations (`delete_skill_version` and + `delete_skill_bundle_version`) are soft deletes. They set + `status='deleted'` when allowed by the lifecycle transition rules, + update `last_updated_timestamp`, remove aliases that point to the + deleted version, and exclude the version from normal + get/search/list/latest resolution. Active versions must first be + unpublished or deprecated before they can be deleted. +- The `deleted` status is terminal. Internal audit or provenance paths + may retain enough metadata to explain historical traces and bundle + snapshots, but deleted versions are not surfaced to consumers. + +## Entity dataclasses + +### Skill entity + +```python +from dataclasses import dataclass, field +from enum import StrEnum + + +class SkillStatus(StrEnum): + DRAFT = "draft" + ACTIVE = "active" + DEPRECATED = "deprecated" + DELETED = "deleted" + + +@dataclass +class Skill: + name: str + display_name: str | None = None + description: str | None = None + workspace: str | None = None + status: SkillStatus | None = None # read-only, derived from parent-resolved version + tags: dict[str, str] = field(default_factory=dict) + aliases: dict[str, str] = field(default_factory=dict) # read-only; populated from skill_aliases table, e.g. {"production": "1.2.0"} + latest_version: str | None = None # read-only, highest active semver + created_by: str | None = None + last_updated_by: str | None = None + creation_timestamp: int | None = None + last_updated_timestamp: int | None = None +``` + +| Field | Type | Description | +|---|---|---| +| `name` | `str` | Stable logical asset name, unique within a workspace | +| `display_name` | `str` | Mutable human-readable label for UI display | +| `status` | `SkillStatus` | Read-only; derived from the parent-resolved version: highest active semantic version if present, otherwise highest non-deleted non-active semantic version | +| `aliases` | `dict[str, str]` | Stable version pointers (e.g., `{"production": "1.2.0"}`); read-only, populated from `skill_aliases` table | +| `latest_version` | `str` | Read-only; highest semantic version among `active` versions if one exists, otherwise highest non-`deleted` non-`active` version | +| `workspace` | `str` | Visibility boundary | + +### SkillVersion entity + +```python +class SkillSourceType(StrEnum): + GIT = "git" + OCI = "oci" + ZIP = "zip" + MLFLOW = "mlflow" + + +@dataclass +class SkillVersion: + name: str + version: str + display_name: str | None = None + source_type: SkillSourceType | None = None + source: str | None = None + subpath: str | None = None + status: SkillStatus = SkillStatus.DRAFT + content_digest: str | None = None + tags: dict[str, str] = field(default_factory=dict) + aliases: list[str] = field(default_factory=list) + workspace: str | None = None + created_by: str | None = None + last_updated_by: str | None = None + creation_timestamp: int | None = None + last_updated_timestamp: int | None = None +``` + +| Field | Type | Description | +|---|---|---| +| `version` | `str` | Publisher-supplied version string. Semantic versioning is required (e.g., `1.0.0`, `2.1.0-beta.1`) | +| `display_name` | `str` | Mutable human-readable label for UI display | +| `source_type` | `SkillSourceType` | Optional distribution mechanism: `git`, `oci`, `zip`, `mlflow` | +| `source` | `str` | Pointer to the content in the source system. Required for standalone pull. May be omitted only when the version's content lives within a bundle-level artifact, in which case the containing bundle membership identifies the embedded content path | +| `subpath` | `str` | Optional path within the artifact where this skill's content lives. See subpath usage table below | +| `content_digest` | `str` | Optional digest for integrity verification (e.g., `sha256:abc123...`). Aligns with OCI digest terminology | +| `status` | `SkillStatus` | Per-version lifecycle: `draft`, `active`, `deprecated`, `deleted` | +| `aliases` | `list[str]` | Alias names currently pointing at this version (read-only, projected from alias table) | + +### SkillVersion field details + +**Source type extensibility.** The `source_type` enum is intentionally +small for the initial implementation. New source types (e.g., `s3`, +`azure-blob`, `opensharing`) can be added without schema changes +since the column stores a string value. In particular, the +[OpenSharing](https://github.com/OpenSharing-IO/OpenSharing) protocol +(Linux Foundation) defines AgentSkill as a first-class asset type +using the same SKILL.md directory structure. An `opensharing` source +type would let the registry govern and track skills whose content is +shared via OpenSharing's credential-vending protocol. + +**Subpath usage by source type.** The `subpath` field separates "what +to download" from "where inside the downloaded content the relevant +asset lives." Its applicability varies by source type: + +| Source type | `subpath` usage | +|---|---| +| `oci` | Path within the OCI image (e.g., `plugins/code-review`). Used when multiple skills share a single image. | +| `zip` | Path within the archive (e.g., `plugins/code-review`). Used when multiple skills share a single archive. | +| `git` | Path within the repository (e.g., `code-review`). Used when the skill content is not at the repository root. The `source` field contains the clone URL with `@` suffix; `subpath` locates the content within the repo. | +| `mlflow` | Not used. The artifact path is scoped to the specific skill version at upload time. | + +**Git source format.** For `source_type="git"`, `source` is a Git +clone URL with an `@` suffix to identify the branch, tag, or +commit (e.g., `https://github.com/acme/agent-skills.git@v1.0.0`). +The `subpath` field identifies the path within the repository where +the skill content lives (e.g., `code-review`). This separates the +clone target, the ref, and the content path into distinct fields +rather than relying on hosting-provider-specific tree URL conventions. +Mutable refs (branches, tags) are allowed; `content_digest` can be +used to detect content drift when the ref changes. + +**MLflow artifact storage (`source_type="mlflow"`).** In addition to +external source pointers, the registry supports storing skill content +directly in MLflow's artifact storage. This serves users who do not +have external Git/OCI infrastructure, who want agent capabilities +stored alongside their models, or who operate in airgapped +environments where external sources are not reachable. + +Content is stored as a directory tree of individual files under an +artifact path, consistent with how MLflow stores model artifacts. For +example, a skill with a SKILL.md, scripts, and reference material is +stored as separate artifacts under a version-specific prefix: + +``` +skills/code-review/1.0.0/ + SKILL.md + scripts/analyze.sh + scripts/lint-config.json + reference/style-guide.md +``` + +The `source` field contains the artifact URI as resolved by MLflow's +artifact storage (e.g., `mlflow-artifacts:/skills/code-review/1.0.0/` +when using the artifact proxy, or a direct artifact-store URI +otherwise). `source_type="mlflow"` means "stored in MLflow-managed +artifact storage," not a specific URI scheme. Pull downloads the +directory tree from the artifact store. The MLflow UI can browse +individual files within a stored skill version when artifact proxying +is enabled. + +The upload API accepts a local directory path and stores each file as +a separate artifact. The `content_digest` is computed over the full +directory contents at upload time. + +**Version uniqueness.** The combination of `(name, version)` is unique +within a workspace. A skill version represents a single logical +version of a capability; `source_type` and `source` describe where to +find it but are not part of its identity. + +**Content integrity.** The optional `content_digest` field stores a +digest of the skill content at registration time (e.g., +`sha256:abc123...`). For `source_type="mlflow"`, the server computes +the digest at upload time and stores it on the version; on pull, the +client recomputes the digest over the downloaded content and rejects +the result if it does not match, detecting out-of-band modification +of the underlying artifact store. For external source types (git, oci, +zip), `content_digest` is client-supplied: for OCI sources, this is +the native image digest; for Git sources, a digest of the file +contents at the pinned commit; for ZIP sources, a digest of the +archive. The registry stores the digest but does not verify it on +read; verification is the consumer's responsibility. + +**Immutability contract.** `source_type`, `source`, `subpath`, +`content_digest`, and `version` are immutable after creation. To point +to different content, register a new version. Mutable fields +(`display_name`, `status`, `tags`) can be updated independently. + +### SkillBundle entity + +A skill bundle groups related skills into a governed unit that maps +to the "plugin" concept in agent harnesses. Follows the same +top-level pattern as Skill: versions, tags, and aliases. + +```python +@dataclass +class SkillBundle: + name: str + display_name: str | None = None + description: str | None = None + workspace: str | None = None + status: SkillStatus | None = None # read-only, derived from parent-resolved version + tags: dict[str, str] = field(default_factory=dict) + aliases: dict[str, str] = field(default_factory=dict) # read-only; populated from skill_bundle_aliases table + latest_version: str | None = None # read-only, highest active semver + created_by: str | None = None + last_updated_by: str | None = None + creation_timestamp: int | None = None + last_updated_timestamp: int | None = None +``` + +`SkillBundle.status` is read-only and uses the same parent-resolved +version rule as `Skill`: highest active semantic version if present, +otherwise highest non-deleted non-active semantic version. Latest +version resolution follows the same fallback. + +### SkillBundleVersion entity + +A versioned snapshot of a skill bundle's membership. In Phase 1, all +members are skills. + +```python +@dataclass +class SkillMemberRef: + name: str + version: str + member_subpath: str | None = None + + +@dataclass +class SkillBundleVersion: + name: str + version: str + display_name: str | None = None + source_type: SkillSourceType | None = None + source: str | None = None + subpath: str | None = None + content_digest: str | None = None + status: SkillStatus = SkillStatus.DRAFT + tags: dict[str, str] = field(default_factory=dict) + skills: list[SkillMemberRef] = field(default_factory=list) + aliases: list[str] = field(default_factory=list) + workspace: str | None = None + created_by: str | None = None + last_updated_by: str | None = None + creation_timestamp: int | None = None + last_updated_timestamp: int | None = None +``` + +### SkillBundleVersion field details + +**Version uniqueness.** The combination of `(name, version)` is unique +within a workspace. + +**Bundle-level source.** A bundle version is either monolithic or +assembled, never both: + +- **Monolithic:** has its own `source_type`, `source`, `subpath`, + and `content_digest`, pointing to a single artifact (e.g., an OCI + image or Git repo) that contains the complete bundle. `pull` + fetches the bundle artifact as a unit. Member skill versions may + omit their own `source` because the bundle artifact is the + authoritative source. The optional membership `member_subpath` + identifies where each member lives inside the bundle artifact. +- **Assembled:** has individual member references. Each skill member + has its own source. `pull` fetches members individually. If a skill + member has no source, `pull` fails rather than producing a partial + local bundle. For assembled bundles, `member_subpath` must be null + because the member's own `source` and `subpath` identify its + content. + +The API rejects attempts to set `member_subpath` on a membership whose +member version has its own source. + +**Immutability contract.** The member list and source fields of a +bundle version are immutable after creation. To change the set of +members or source pointer, register a new bundle version. Mutable +fields (`display_name`, `status`, `tags`) can be updated independently. + +Correctness of the artifact layout is the publisher's responsibility; +the registry does not validate artifact contents at registration time. + +A member can appear in multiple bundles and multiple bundle versions. +Membership is at the version level, so a bundle version is a +reproducible snapshot of "these specific skill versions work together." + +## Store interface + +The store interface follows the mixin pattern established by the MCP +Server Registry (RFC-0004). Methods raise `NotImplementedError` rather +than using `@abstractmethod`, allowing stores that do not support +skills (e.g., `FileStore`) to work without stubbing every method. + +In the store interface, `delete_*` methods on top-level entities are +hard deletes, while `delete_*_version` methods are soft deletes that +transition the version to `deleted`. + +```python +NOT_SET = object() + + +class SkillRegistryMixin: + # Methods raise NotImplementedError rather than using @abstractmethod, + # following the GatewayStoreMixin pattern. This allows stores that don't + # support skills (e.g., FileStore) to work without stubbing every method. + + # --- Skill operations --- + + def create_skill( + self, name: str, + display_name: str | None = None, + description: str | None = None, + ) -> Skill: + raise NotImplementedError(self.__class__.__name__) + + def get_skill(self, name: str) -> Skill: + raise NotImplementedError(self.__class__.__name__) + + def search_skills( + self, + filter_string: str | None = None, + max_results: int = 100, + order_by: list[str] | None = None, + page_token: str | None = None, + ) -> PagedList[Skill]: + raise NotImplementedError(self.__class__.__name__) + + def update_skill( + self, + name: str, + display_name: str | None = NOT_SET, + description: str | None = NOT_SET, + ) -> Skill: + raise NotImplementedError(self.__class__.__name__) + + def delete_skill(self, name: str) -> None: + raise NotImplementedError(self.__class__.__name__) + + # --- SkillVersion operations --- + + def create_skill_version( + self, + name: str, + version: str, + display_name: str | None = None, + source_type: str | None = None, + source: str | None = None, + subpath: str | None = None, + content_digest: str | None = None, + ) -> SkillVersion: + raise NotImplementedError(self.__class__.__name__) + + def get_skill_version( + self, name: str, version: str, + ) -> SkillVersion: + raise NotImplementedError(self.__class__.__name__) + + def get_skill_version_by_alias( + self, name: str, alias: str, + ) -> SkillVersion: + raise NotImplementedError(self.__class__.__name__) + + def get_latest_skill_version(self, name: str) -> SkillVersion: + raise NotImplementedError(self.__class__.__name__) + + def search_skill_versions( + self, + name: str, + filter_string: str | None = None, + max_results: int = 100, + order_by: list[str] | None = None, + page_token: str | None = None, + ) -> PagedList[SkillVersion]: + raise NotImplementedError(self.__class__.__name__) + + def update_skill_version( + self, + name: str, + version: str, + display_name: str | None = NOT_SET, + status: SkillStatus | None = NOT_SET, + ) -> SkillVersion: + raise NotImplementedError(self.__class__.__name__) + + def delete_skill_version( + self, name: str, version: str, + ) -> None: + raise NotImplementedError(self.__class__.__name__) + + # --- Skill tag operations --- + + def set_skill_tag( + self, name: str, key: str, value: str, + ) -> None: + raise NotImplementedError(self.__class__.__name__) + + def delete_skill_tag(self, name: str, key: str) -> None: + raise NotImplementedError(self.__class__.__name__) + + def set_skill_version_tag( + self, name: str, version: str, + key: str, value: str, + ) -> None: + raise NotImplementedError(self.__class__.__name__) + + def delete_skill_version_tag( + self, name: str, version: str, key: str, + ) -> None: + raise NotImplementedError(self.__class__.__name__) + + # --- Skill alias operations --- + + def set_skill_alias( + self, name: str, alias: str, version: str, + ) -> None: + raise NotImplementedError(self.__class__.__name__) + + def delete_skill_alias( + self, name: str, alias: str, + ) -> None: + raise NotImplementedError(self.__class__.__name__) + + # --- SkillBundle operations --- + + def create_skill_bundle( + self, name: str, + display_name: str | None = None, + description: str | None = None, + ) -> SkillBundle: + raise NotImplementedError(self.__class__.__name__) + + def get_skill_bundle(self, name: str) -> SkillBundle: + raise NotImplementedError(self.__class__.__name__) + + def search_skill_bundles( + self, + filter_string: str | None = None, + max_results: int = 100, + order_by: list[str] | None = None, + page_token: str | None = None, + ) -> PagedList[SkillBundle]: + raise NotImplementedError(self.__class__.__name__) + + def update_skill_bundle( + self, + name: str, + display_name: str | None = NOT_SET, + description: str | None = NOT_SET, + ) -> SkillBundle: + raise NotImplementedError(self.__class__.__name__) + + def delete_skill_bundle(self, name: str) -> None: + raise NotImplementedError(self.__class__.__name__) + + # --- SkillBundleVersion operations --- + + def create_skill_bundle_version( + self, + name: str, + version: str, + display_name: str | None = None, + skills: list[SkillMemberRef] | None = None, + source_type: str | None = None, + source: str | None = None, + subpath: str | None = None, + content_digest: str | None = None, + ) -> SkillBundleVersion: + raise NotImplementedError(self.__class__.__name__) + + def get_skill_bundle_version( + self, name: str, version: str, + ) -> SkillBundleVersion: + raise NotImplementedError(self.__class__.__name__) + + def get_skill_bundle_version_by_alias( + self, name: str, alias: str, + ) -> SkillBundleVersion: + raise NotImplementedError(self.__class__.__name__) + + def get_latest_skill_bundle_version( + self, name: str, + ) -> SkillBundleVersion: + raise NotImplementedError(self.__class__.__name__) + + def search_skill_bundle_versions( + self, + name: str, + filter_string: str | None = None, + max_results: int = 100, + order_by: list[str] | None = None, + page_token: str | None = None, + ) -> PagedList[SkillBundleVersion]: + raise NotImplementedError(self.__class__.__name__) + + def update_skill_bundle_version( + self, + name: str, + version: str, + display_name: str | None = NOT_SET, + status: SkillStatus | None = NOT_SET, + ) -> SkillBundleVersion: + raise NotImplementedError(self.__class__.__name__) + + def delete_skill_bundle_version( + self, name: str, version: str, + ) -> None: + raise NotImplementedError(self.__class__.__name__) + + # --- SkillBundle tag operations --- + + def set_skill_bundle_tag( + self, name: str, key: str, value: str, + ) -> None: + raise NotImplementedError(self.__class__.__name__) + + def delete_skill_bundle_tag( + self, name: str, key: str, + ) -> None: + raise NotImplementedError(self.__class__.__name__) + + def set_skill_bundle_version_tag( + self, name: str, version: str, + key: str, value: str, + ) -> None: + raise NotImplementedError(self.__class__.__name__) + + def delete_skill_bundle_version_tag( + self, name: str, version: str, key: str, + ) -> None: + raise NotImplementedError(self.__class__.__name__) + + # --- SkillBundle alias operations --- + + def set_skill_bundle_alias( + self, name: str, alias: str, version: str, + ) -> None: + raise NotImplementedError(self.__class__.__name__) + + def delete_skill_bundle_alias( + self, name: str, alias: str, + ) -> None: + raise NotImplementedError(self.__class__.__name__) +``` + +For update fields, omitting a parameter leaves the stored value unchanged, +while passing `None` to a nullable field explicitly sets the field to +`null`. + +## SDK convenience functions + +The `mlflow.genai.skills` namespace provides convenience functions that +combine store operations, matching the pattern established by +`mlflow.genai.register_mcp_server()` in RFC-0004. + +```python +import mlflow + + +def register_skill( + *, + name: str, + version: str, + display_name: str | None = None, + description: str | None = None, + source_type: str | None = None, + source: str | None = None, + subpath: str | None = None, + content_path: str | None = None, + content_digest: str | None = None, +) -> SkillVersion: + """Register a skill version. Auto-creates the parent Skill if + it does not exist. If content_path is provided, uploads the + local directory to MLflow artifact storage and sets source_type + and source automatically.""" + + +def create_skill( + *, + name: str, + display_name: str | None = None, + description: str | None = None, +) -> Skill: ... + + +def get_skill(*, name: str) -> Skill: ... + + +def search_skills( + *, + filter_string: str | None = None, + max_results: int = 100, + order_by: list[str] | None = None, + page_token: str | None = None, +) -> PagedList[Skill]: ... + + +def update_skill( + *, + name: str, + display_name: str | None = NOT_SET, + description: str | None = NOT_SET, +) -> Skill: ... + + +def delete_skill(*, name: str) -> None: ... + + +def create_skill_version( + *, + name: str, + version: str, + display_name: str | None = None, + source_type: str | None = None, + source: str | None = None, + subpath: str | None = None, + content_digest: str | None = None, +) -> SkillVersion: ... + + +def get_skill_version(*, name: str, version: str) -> SkillVersion: ... + + +def get_skill_version_by_alias(*, name: str, alias: str) -> SkillVersion: ... + + +def get_latest_skill_version(*, name: str) -> SkillVersion: ... + + +def search_skill_versions( + *, + name: str, + filter_string: str | None = None, + max_results: int = 100, + order_by: list[str] | None = None, + page_token: str | None = None, +) -> PagedList[SkillVersion]: ... + + +def update_skill_version( + *, + name: str, + version: str, + display_name: str | None = NOT_SET, + status: str | None = NOT_SET, +) -> SkillVersion: ... + + +def delete_skill_version(*, name: str, version: str) -> None: ... + + +def create_skill_bundle( + *, + name: str, + display_name: str | None = None, + description: str | None = None, +) -> SkillBundle: ... + + +def create_skill_bundle_version( + *, + name: str, + version: str, + display_name: str | None = None, + skills: list[SkillMemberRef] | None = None, + source_type: str | None = None, + source: str | None = None, + subpath: str | None = None, + content_digest: str | None = None, +) -> SkillBundleVersion: ... + + +def get_skill_bundle(*, name: str) -> SkillBundle: ... + + +def search_skill_bundles( + *, + filter_string: str | None = None, + max_results: int = 100, + order_by: list[str] | None = None, + page_token: str | None = None, +) -> PagedList[SkillBundle]: ... + + +def update_skill_bundle( + *, + name: str, + display_name: str | None = NOT_SET, + description: str | None = NOT_SET, +) -> SkillBundle: ... + + +def delete_skill_bundle(*, name: str) -> None: ... + + +def get_skill_bundle_version( + *, name: str, version: str, +) -> SkillBundleVersion: ... + + +def get_skill_bundle_version_by_alias( + *, name: str, alias: str, +) -> SkillBundleVersion: ... + + +def get_latest_skill_bundle_version(*, name: str) -> SkillBundleVersion: ... + + +def search_skill_bundle_versions( + *, + name: str, + filter_string: str | None = None, + max_results: int = 100, + order_by: list[str] | None = None, + page_token: str | None = None, +) -> PagedList[SkillBundleVersion]: ... + + +def update_skill_bundle_version( + *, + name: str, + version: str, + display_name: str | None = NOT_SET, + status: str | None = NOT_SET, +) -> SkillBundleVersion: ... + + +def delete_skill_bundle_version(*, name: str, version: str) -> None: ... + + +def set_skill_tag(*, name: str, key: str, value: str) -> None: ... + +def delete_skill_tag(*, name: str, key: str) -> None: ... + +def set_skill_version_tag(*, name: str, version: str, key: str, value: str) -> None: ... + +def delete_skill_version_tag(*, name: str, version: str, key: str) -> None: ... + +def set_skill_alias(*, name: str, alias: str, version: str) -> None: ... + +def delete_skill_alias(*, name: str, alias: str) -> None: ... + +def set_skill_bundle_tag(*, name: str, key: str, value: str) -> None: ... + +def delete_skill_bundle_tag(*, name: str, key: str) -> None: ... + +def set_skill_bundle_version_tag(*, name: str, version: str, key: str, value: str) -> None: ... + +def delete_skill_bundle_version_tag(*, name: str, version: str, key: str) -> None: ... + +def set_skill_bundle_alias(*, name: str, alias: str, version: str) -> None: ... + +def delete_skill_bundle_alias(*, name: str, alias: str) -> None: ... + + +def pull( + *, + name: str | None = None, + bundle: str | None = None, + version: str | None = None, + alias: str | None = None, + destination: str = ".", +) -> str: + """Pull skill or bundle content from registered sources to a + local directory. Specify name for a single skill or bundle + for a skill bundle.""" + + +# Example usage: +version = mlflow.genai.skills.register_skill(name="code-review", version="1.0.0", source_type="git", source="...") +servers = mlflow.genai.skills.search_skills(filter_string="status = 'active'") +``` + +For SDK update methods, `NOT_SET` means "leave unchanged" while `None` +means "clear this nullable field". This mirrors the store-layer update +contract so callers can distinguish partial updates from explicit +nulling. + +`pull` is implemented in the SDK/CLI layer, not the store mixin. The +client calls `get_skill_version` (or resolves an alias) to obtain the +source pointer, then fetches content locally using source-type-specific +logic (git clone, OCI pull, ZIP download, or MLflow artifact download). +This keeps the store as a pure data-access layer. + +## REST API + +The REST API is implemented as a FastAPI router using RESTful nested +resource paths. It is exposed under both `/api/3.0/mlflow/skills` and +`/ajax-api/3.0/mlflow/skills`, plus the corresponding static-prefix +variants, following the MCP Server Registry (RFC-0004) pattern. + +### Skill endpoints + +All paths relative to the logical skills router prefix. + +| Method | Path | Description | +|---|---|---| +| `POST` | `/` | Create a skill | +| `GET` | `/` | Search skills | +| `GET` | `/{name}` | Get skill by name | +| `PATCH` | `/{name}` | Update skill fields | +| `DELETE` | `/{name}` | Hard-delete skill (cascades, subject to references) | +| `POST` | `/{name}/versions` | Create a skill version | +| `GET` | `/{name}/versions` | Search versions | +| `GET` | `/{name}/versions/{version}` | Get a specific version | +| `PATCH` | `/{name}/versions/{version}` | Update version | +| `DELETE` | `/{name}/versions/{version}` | Soft-delete a version (`status='deleted'`) | +| `POST` | `/{name}/tags` | Set a skill-level tag | +| `DELETE` | `/{name}/tags/{key}` | Delete a skill-level tag | +| `POST` | `/{name}/versions/{version}/tags` | Set a version-level tag | +| `DELETE` | `/{name}/versions/{version}/tags/{key}` | Delete a version tag | +| `POST` | `/{name}/aliases` | Set an alias | +| `GET` | `/{name}/aliases/{alias}` | Resolve alias to `SkillVersion` | +| `DELETE` | `/{name}/aliases/{alias}` | Delete an alias | + +Similarly, skill bundle endpoints are exposed under both +`/api/3.0/mlflow/skill-bundles` and +`/ajax-api/3.0/mlflow/skill-bundles`. + +### Skill bundle endpoints + +All paths relative to the logical skill-bundles router prefix. + +| Method | Path | Description | +|---|---|---| +| `POST` | `/` | Create a skill bundle | +| `GET` | `/` | Search skill bundles | +| `GET` | `/{name}` | Get bundle by name | +| `PATCH` | `/{name}` | Update bundle fields | +| `DELETE` | `/{name}` | Hard-delete bundle (cascades versions and memberships) | +| `POST` | `/{name}/versions` | Create a bundle version with members | +| `GET` | `/{name}/versions` | Search bundle versions | +| `GET` | `/{name}/versions/{version}` | Get a specific bundle version | +| `PATCH` | `/{name}/versions/{version}` | Update bundle version status | +| `DELETE` | `/{name}/versions/{version}` | Soft-delete a bundle version (`status='deleted'`) | +| `POST` | `/{name}/tags` | Set a bundle-level tag | +| `DELETE` | `/{name}/tags/{key}` | Delete a bundle-level tag | +| `POST` | `/{name}/versions/{version}/tags` | Set a bundle version tag | +| `DELETE` | `/{name}/versions/{version}/tags/{key}` | Delete a bundle version tag | +| `POST` | `/{name}/aliases` | Set a bundle alias | +| `GET` | `/{name}/aliases/{alias}` | Resolve bundle alias to version | +| `DELETE` | `/{name}/aliases/{alias}` | Delete a bundle alias | + +### Pagination and filtering + +Search endpoints use page-token-based pagination and `filter_string` +expressions following existing MLflow conventions. + +**Skills and bundles:** `name LIKE '%review%'`, `status = 'active'`, +`tags.team = 'platform'` + +**Versions (all entity types):** `status = 'active'`, +`source_type = 'git'`, `tags.approved = 'true'` + +### Request and response models + +Request models contain only the mutable fields; resource identifiers +come from path parameters: + +```python +from pydantic import BaseModel, Field + + +class CreateSkillRequest(BaseModel): + name: str + display_name: str | None = None + description: str | None = None + + +class UpdateSkillRequest(BaseModel): + display_name: str | None = None + description: str | None = None + + +class CreateSkillVersionRequest(BaseModel): + version: str + display_name: str | None = None + source_type: str | None = None + source: str | None = None + subpath: str | None = None + content_digest: str | None = None + + +class UpdateSkillVersionRequest(BaseModel): + display_name: str | None = None + status: str | None = None + + +class CreateSkillBundleRequest(BaseModel): + name: str + display_name: str | None = None + description: str | None = None + + +class UpdateSkillBundleRequest(BaseModel): + display_name: str | None = None + description: str | None = None + + +class SkillMemberRefPayload(BaseModel): + name: str + version: str + member_subpath: str | None = None + + +class CreateSkillBundleVersionRequest(BaseModel): + version: str + display_name: str | None = None + skills: list[SkillMemberRefPayload] | None = None + source_type: str | None = None + source: str | None = None + subpath: str | None = None + content_digest: str | None = None + + +class UpdateSkillBundleVersionRequest(BaseModel): + display_name: str | None = None + status: str | None = None + + +class AliasResponse(BaseModel): + alias: str + version: str + + +class SetAliasRequest(BaseModel): + alias: str + version: str + + +class SetTagRequest(BaseModel): + key: str + value: str + + +class SkillVersionResponse(BaseModel): + name: str + version: str + display_name: str | None = None + source_type: str | None = None + source: str | None = None + subpath: str | None = None + content_digest: str | None = None + status: str = "draft" + aliases: list[str] = Field(default_factory=list) + tags: dict[str, str] = Field(default_factory=dict) + created_by: str | None = None + last_updated_by: str | None = None + creation_timestamp: int | None = None + last_updated_timestamp: int | None = None + + +class SkillResponse(BaseModel): + name: str + display_name: str | None = None + description: str | None = None + status: str | None = None + latest_version: str | None = None + aliases: list[AliasResponse] = Field(default_factory=list) + tags: dict[str, str] = Field(default_factory=dict) + created_by: str | None = None + last_updated_by: str | None = None + creation_timestamp: int | None = None + last_updated_timestamp: int | None = None + + +class SkillBundleVersionResponse(BaseModel): + name: str + version: str + display_name: str | None = None + source_type: str | None = None + source: str | None = None + subpath: str | None = None + content_digest: str | None = None + status: str = "draft" + skills: list[SkillMemberRefPayload] = Field(default_factory=list) + aliases: list[str] = Field(default_factory=list) + tags: dict[str, str] = Field(default_factory=dict) + created_by: str | None = None + last_updated_by: str | None = None + creation_timestamp: int | None = None + last_updated_timestamp: int | None = None + + +class SkillBundleResponse(BaseModel): + name: str + display_name: str | None = None + description: str | None = None + status: str | None = None + latest_version: str | None = None + aliases: list[AliasResponse] = Field(default_factory=list) + tags: dict[str, str] = Field(default_factory=dict) + created_by: str | None = None + last_updated_by: str | None = None + creation_timestamp: int | None = None + last_updated_timestamp: int | None = None +``` + +`Skill.aliases` is modeled as a `dict[str, str]` in the entity layer +for convenience, while REST responses expose aliases as +`list[AliasResponse]` to keep the payload shape explicit and +consistent with the MCP Server Registry (RFC-0004). + +## Python SDK and CLI + +The `mlflow.genai.skills` module exposes top-level functions delegating +to `MlflowClient`, with a 1:1 mapping to the store mixin methods +above. The `mlflow skills` CLI command group provides the same +operations from the command line: + +| CLI subcommand | SDK function | Description | +|---|---|---| +| `mlflow skills register` | `register_skill()` | Register a skill version (auto-creates parent) | +| `mlflow skills get` | `get_skill()` | Get skill metadata | +| `mlflow skills search` | `search_skills()` | Search skills | +| `mlflow skills get-version` | `get_skill_version()` | Get a specific version | +| `mlflow skills update-version` | `update_skill_version()` | Update version status | +| `mlflow skills set-alias` | `set_skill_alias()` | Set a version alias | +| `mlflow skills set-tag` | `set_skill_tag()` | Set a tag | +| `mlflow skills pull` | `pull()` | Pull content to local filesystem | +| `mlflow skills install` | (direct install) | Download and place in harness directory | +| `mlflow skills install-bundle` | (package manager install) | Install bundle via package manager plugin | +| `mlflow skills create-bundle` | `create_skill_bundle()` | Create a skill bundle | +| `mlflow skills create-bundle-version` | `create_skill_bundle_version()` | Create a bundle version with members | +| `mlflow skills get-bundle` | `get_skill_bundle()` | Get bundle metadata | +| `mlflow skills search-bundles` | `search_skill_bundles()` | Search bundles | +| `mlflow skills search-bundle-versions` | `search_skill_bundle_versions()` | Search bundle versions | +| `mlflow skills set-bundle-alias` | `set_skill_bundle_alias()` | Set a bundle alias | +| `mlflow skills set-bundle-tag` | `set_skill_bundle_tag()` | Set a bundle-level tag | +| `mlflow skills set-bundle-version-tag` | `set_skill_bundle_version_tag()` | Set a bundle version tag | +| `mlflow skills update-bundle-version` | `update_skill_bundle_version()` | Update bundle version status | + +**Existing `mlflow skills` CLI group.** MLflow already has an +`mlflow skills` CLI group (`mlflow/cli/skills.py`) with two +subcommands: `list` (list bundled Assistant skills) and `view` +(view details of a bundled skill). These inspect the skills that +ship with the MLflow installation (under `mlflow/assistant/skills/`), +not registry-managed skills. The registry subcommands above extend +this existing group. None of the registry subcommand names conflict +with `list` or `view`, so both sets coexist: `list`/`view` operate +on locally bundled skills, while `search`/`get`/`register`/`pull` +and the other registry subcommands operate on the server-side +registry. + +## Package manager plugin interface + +Package manager plugins are registered via Python entrypoints (group +`mlflow.skill_package_managers`), so third-party plugins can be +installed via `pip install` without modifying MLflow core. + +### Plugin protocol + +```python +class PackageManagerPlugin: + def install_skill( + self, + name: str, + local_path: str, + harness: str | None = None, + scope: str = "project", + ) -> str: + """Install a single skill from a local path. + Returns the installed path. + + Args: + name: skill name for manifest generation + local_path: local directory with skill content + harness: target harness (e.g., "claude-code", "cursor"). + If None, auto-detect from environment. + scope: "project" (cwd) or "user" (home directory) + """ + ... + + def install_bundle( + self, + bundle_name: str, + member_paths: dict[str, str], + harness: str | None = None, + scope: str = "project", + ) -> str: + """Install a bundle of skills from local paths. + member_paths maps skill names to local paths. + Returns the installed path. + + Args: + bundle_name: bundle name for manifest generation + member_paths: {skill_name: local_path} for each member + harness: target harness. If None, auto-detect. + scope: "project" or "user" + """ + ... + + def supported_harnesses(self) -> list[str]: + """Return list of harness identifiers this plugin supports. + E.g., ["claude-code", "cursor", "codex-cli", "copilot"].""" + ... +``` + +### Entrypoint registration + +```toml +# In the package manager plugin's pyproject.toml: +[project.entry-points."mlflow.skill_package_managers"] +apm = "apm_mlflow:ApmPlugin" +lola = "lola_mlflow:LolaPlugin" +``` + +### Source resolution flow + +When `mlflow skills install-bundle` is invoked: + +1. **Resolve:** MLflow calls `get_skill_bundle_version()` (or alias + resolution) to obtain the bundle version and its member list. +2. **Pull:** For each member skill, MLflow pulls content to a local + temporary directory using source-type-aware logic (git clone, OCI + pull, ZIP download, MLflow artifact download). For Git-backed + skills, the package manager can fetch directly from Git if it + supports Git sources natively, avoiding a redundant local pull. +3. **Delegate:** MLflow passes the local paths to the configured + package manager plugin via `install_bundle()`. The plugin handles + harness-specific directory placement and manifest generation. +4. **Manifest:** MLflow writes `mlflow-skills-manifest.json` with + installed registry coordinates for trace integration. + +### Direct install flow + +When `mlflow skills install` is invoked (no package manager needed): + +1. **Resolve:** MLflow calls `get_skill_version()` (or alias + resolution) to obtain the source pointer. +2. **Pull:** MLflow pulls content to a local temporary directory. +3. **Place:** MLflow copies the content to the appropriate + harness-specific directory based on the `--harness` flag: + - `claude-code`: `.claude/skills/{name}/` + - `cursor`: `.cursor/skills/{name}/` + - Other harnesses: configurable via settings +4. **Manifest:** MLflow writes `mlflow-skills-manifest.json`. + +## Pull semantics details + +**Source availability.** The registry stores source pointers but does +not cache or proxy content. If a source is unreachable or the content +has been deleted, pull fails with an error that surfaces the +underlying failure from the source system (e.g., Git clone failure, +OCI pull 404, HTTP download error, MLflow artifact download error). +Source availability is the publisher's responsibility. For assembled +bundle pulls, if one member's source is unavailable, the entire pull +fails rather than producing a partial result. + +**Source authentication.** The registry server stores source pointers +but does not validate source accessibility at registration time and is +not involved in content transfer at pull time. Authentication to +external sources is handled entirely by the client environment: + +| Source type | Authentication mechanism | +|---|---| +| `git` | Standard Git credential resolution: SSH keys (`~/.ssh/`), Git credential helpers (`git-credential-manager`, `git-credential-store`), `.netrc`, and `GIT_SSH_COMMAND`. Private repos work if the caller's Git is configured to access them. | +| `oci` | OCI registry credential resolution: Docker config (`~/.docker/config.json`), registry-specific credential helpers, and container runtime auth. Private registries work if the caller has a valid login session. | +| `zip` | No authentication support. ZIP sources must be publicly accessible URLs. For private content, use `git` or `oci` source types instead. | +| `mlflow` | MLflow artifact storage authentication, using the same credentials as other MLflow API calls. | + +The registry does not store, proxy, or manage source credentials. +Pull failures due to authentication errors are surfaced to the caller +with the underlying error from the source system. + +## skill_context() span attributes + +The `skill_context()` context manager creates a span with the +following attributes: + +| Attribute | Value | Description | +|---|---|---| +| `mlflow.skill.name` | Skill name | Registry name of the active skill | +| `mlflow.skill.version` | Version string | Registered version | +| `mlflow.skill.workspace` | Workspace name | MLflow workspace (defaults to `"default"`) | + +These three attributes form the `{workspace, name, version}` +coordinates that link the span back to a specific skill version in +the registry. + +## SDK and CLI code examples + +### Register skills from an OCI artifact with subpath + +```python +import mlflow +from mlflow.genai.skills import SkillMemberRef + +mlflow.genai.skills.register_skill( + name="code-review", + version="1.0.0", + source_type="oci", + source="ghcr.io/acme/agent-plugin:v1.0.0", + subpath="skills/code-review", +) + +mlflow.genai.skills.register_skill( + name="test-coverage", + version="2.1.0", + source_type="oci", + source="ghcr.io/acme/agent-plugin:v1.0.0", + subpath="skills/test-coverage", +) + +# Assembled bundle: each member has its own source +bundle_version = mlflow.genai.skills.create_skill_bundle_version( + name="pr-workflow", + version="1.0.0", + skills=[ + SkillMemberRef(name="code-review", version="1.0.0"), + SkillMemberRef(name="test-coverage", version="2.1.0"), + ], +) + +# Monolithic bundle from a single OCI image. Embedded member +# versions are registered without their own sources. +mlflow.genai.skills.register_skill( + name="embedded-review", + version="1.0.0", +) + +bundle_version = mlflow.genai.skills.create_skill_bundle_version( + name="pr-workflow-mono", + version="1.0.0", + source_type="oci", + source="ghcr.io/acme/agent-plugin:v1.0.0", + skills=[ + SkillMemberRef(name="embedded-review", version="1.0.0", + member_subpath="skills/embedded-review"), + ], +) +``` + +### Discover and consume skills + +```python +# Search for active skill versions +versions = mlflow.genai.skills.search_skill_versions( + name="code-review", + filter_string="status = 'active'", +) + +# Search for active skill bundles +bundles = mlflow.genai.skills.search_skill_bundles( + filter_string="status = 'active'", +) + +# Get a specific version +version = mlflow.genai.skills.get_skill_version( + name="code-review", + version="1.0.0", +) +# version.source_type == "git" +# version.source == "https://github.com/acme/agent-skills.git@v1.0.0" +# version.subpath == "code-review" + +# Resolve by alias +version = mlflow.genai.skills.get_skill_version_by_alias( + name="code-review", + alias="production", +) + +# Get a bundle version and its pinned members +bundle_version = mlflow.genai.skills.get_skill_bundle_version( + name="pr-workflow", + version="1.0.0", +) +# bundle_version.skills == [SkillMemberRef(name="code-review", version="1.0.0"), ...] + +# Resolve a bundle alias +bundle_version = mlflow.genai.skills.get_skill_bundle_version_by_alias( + name="pr-workflow", + alias="production", +) +``` From 2d913de4a653ec5a760f0e697733c2c1d569d6f6 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Wed, 15 Jul 2026 12:58:58 -0400 Subject: [PATCH 2/7] Incorporate Codex edits and align register_skill with MCP registry 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 --- .../0008-mvp-skill-registry.md | 288 +++++++++--- .../implementation-details.md | 439 +++++++++++++++--- 2 files changed, 591 insertions(+), 136 deletions(-) diff --git a/rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md b/rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md index 3adca18..7448444 100644 --- a/rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md +++ b/rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md @@ -7,8 +7,8 @@ | Author(s) | [Bill Murdock](https://github.com/jwm4) (Red Hat) | | :--------------------- | :-- | -| **Date Last Modified** | 2026-07-13 | -| **AI Assistant(s)** | Claude Code | +| **Date Last Modified** | 2026-07-14 | +| **AI Assistant(s)** | Claude Code, Codex | **Table of contents** @@ -21,6 +21,7 @@ - [Detailed design](#detailed-design) - [Entities and data model](#entities-and-data-model) - [Status and lifecycle](#status-and-lifecycle) + - [Plugin import](#plugin-import) - [Pull semantics](#pull-semantics) - [Workspace scoping](#workspace-scoping) - [Permissions](#permissions) @@ -41,9 +42,10 @@ primary design is metadata-first. It provides enterprise governance on top of existing distribution mechanisms: lifecycle management, usage analytics via traces, and federated discovery across sources. -The registry manages two entity types under the `mlflow.genai.skills` -SDK namespace (CLI: `mlflow skills`), each with full lifecycle -(versioning, aliases, tags, status). Note: MLflow already has an +The registry manages two entity types under the `mlflow.genai` SDK +namespace (CLI: `mlflow skills`), following the top-level public SDK +pattern established by the MCP Server Registry (RFC-0004). Each has +full lifecycle (versioning, aliases, tags, status). Note: MLflow already has an `mlflow skills` CLI group with `list` and `view` subcommands for inspecting bundled Assistant skills. The registry subcommands (`register`, `pull`, `install`, etc.) extend this existing group; @@ -62,11 +64,17 @@ The two entity types: registered content from its source. Harness-specific installation delegates to package managers (APM, Lola, or others via a plugin interface) that already support cross-harness skill installation. - -`mlflow.skill_context()` closes the observability loop by creating -SKILL spans in MLflow traces, annotated with registry coordinates, -enabling adoption tracking, deprecation impact analysis, per-skill -cost attribution, and regression detection. +Existing Claude Code plugins can be imported as monolithic skills-only +bundles: MLflow registers their discovered skills, preserves the plugin +source, and warns about non-skill content that Phase 1 does not register. + +Trace integration supports both manual and automatic instrumentation. +`mlflow.skill_context()` lets SDK applications create SKILL spans +explicitly. For installed skills, the Phase 1 Claude Code autologger +uses the install-time trace manifest to create SKILL spans automatically. +Both paths annotate spans with registry coordinates, enabling adoption +tracking, deprecation impact analysis, per-skill cost attribution, and +regression detection. A follow-up RFC (RFC-0009) will extend skill bundles to include non-skill members (subagents, hooks, MCP server references). @@ -78,7 +86,7 @@ non-skill members (subagents, hooks, MCP server references). ```python import mlflow -mlflow.genai.skills.register_skill( +mlflow.genai.register_skill( name="code-review", version="1.0.0", description="Reviews pull requests for correctness, style, and security", @@ -91,9 +99,9 @@ mlflow.genai.skills.register_skill( ## Create a skill bundle ```python -from mlflow.genai.skills import SkillMemberRef +from mlflow.genai import SkillMemberRef -mlflow.genai.skills.create_skill_bundle_version( +mlflow.genai.create_skill_bundle_version( name="pr-workflow", version="1.0.0", skills=[ @@ -103,13 +111,29 @@ mlflow.genai.skills.create_skill_bundle_version( ) ``` +## Import an existing plugin + +```bash +mlflow skills import \ + --source https://github.com/acme/plugins.git@v1.0.0 \ + --subpath pr-workflow \ + --plugin-format claude-code \ + --bundle-name pr-workflow \ + --version 1.0.0 +``` + +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. + ## Install and use ```bash # Install a skill bundle for Claude Code via a package manager mlflow skills install-bundle --name pr-workflow --alias production -# Or install a single skill directly (file download) +# Or install a single skill through the same package-manager layer mlflow skills install --name code-review --alias production \ --harness claude-code ``` @@ -179,7 +203,7 @@ Registry enables. Each shows both CLI and UI paths. ```python import mlflow - mlflow.genai.skills.register_skill( + mlflow.genai.register_skill( name="code-review", version="1.0.0", description="Reviews pull requests for correctness, style, and security", @@ -213,6 +237,30 @@ Registry enables. Each shows both CLI and UI paths. **UI path:** In the bundle detail page, click "Add Alias" and map `production` to version `1.0.0`. +#### Import an existing plugin as a skills-only bundle + +1. Import a Claude Code plugin from a remotely accessible source: + ```bash + mlflow skills import \ + --source https://github.com/acme/plugins.git@v1.0.0 \ + --subpath pr-workflow \ + --plugin-format claude-code \ + --bundle-name pr-workflow \ + --version 1.0.0 + ``` +2. MLflow fetches the plugin in the client environment and discovers + directories containing SKILL.md entry points. +3. MLflow registers each discovered skill as an embedded, source-less + skill version and records its path as `member_subpath` in a new + monolithic bundle version. The bundle retains the original plugin + source pointer. +4. If the plugin also contains subagents, hooks, or MCP configuration, + MLflow skips those elements and prints a warning that Phase 1 + registers skills only. +5. The created bundle and skills are available through the same + discovery, lifecycle, pull, and installation flows as manually + registered entries. + #### Discover a skill for a specific purpose 1. Search the registry by keyword: @@ -253,7 +301,7 @@ Registry enables. Each shows both CLI and UI paths. content, delegates to the configured package manager (e.g., APM or Lola) for harness-specific installation, and writes a trace manifest (`mlflow-skills-manifest.json`) with installed registry - coordinates. For direct installation without a package manager: + coordinates. A single skill uses the same package-manager layer: ```bash mlflow skills install --name code-review --alias production \ --harness claude-code @@ -497,6 +545,11 @@ new version. The optional `subpath` field identifies content within a shared artifact (used with Git, OCI, and ZIP). The optional `content_digest` field enables integrity verification. +`register_skill()` creates the parent Skill when needed and otherwise +reuses the existing parent. If the target `(name, version)` already +exists, registration fails with an error. This matches the MCP Server +Registry behavior (`register_mcp_server()` in mlflow/mlflow#23696). + #### SkillBundle A skill bundle groups related skills into a governed unit that maps @@ -527,10 +580,9 @@ one of two kinds: - **Monolithic:** has its own source pointer (e.g., a single OCI image or Git repo containing multiple skills) and member references. Skill member versions may omit their own sources when - their content lives inside the bundle artifact; the optional - `member_subpath` on each membership identifies where the member - lives inside the bundle artifact. `pull` fetches the bundle - artifact as a unit. + their content lives inside the bundle artifact. A source-less member + must provide `member_subpath` to identify where it lives inside the + bundle artifact. `pull` fetches the bundle artifact as a unit. A bundle version cannot have both a bundle-level source and skill member versions with their own sources. This avoids confusion about @@ -632,8 +684,52 @@ alias="latest", ...)` is rejected, while `get_skill_version_by_alias(..., alias="latest")` is treated as a convenience alias for `get_latest_skill_version(...)`. +The same rule applies to skill bundles: +`set_skill_bundle_alias(..., alias="latest", ...)` is rejected, while +`get_skill_bundle_version_by_alias(..., alias="latest")` delegates to +`get_latest_skill_bundle_version(...)`. + This aligns with the MCP Server Registry (RFC-0004). +### Plugin import + +`mlflow skills import` is a client-side convenience operation for +registering an existing harness-specific plugin as a monolithic +skills-only bundle. Phase 1 supports the Claude Code plugin layout. +Additional input formats can be added later without changing the +registry data model. + +Before importing, users can call `mlflow skills introspect` or the SDK +`introspect_bundle()` function to preview the skills and skipped content +that MLflow discovers. Introspection is read-only, accepts either a local +path or a remotely accessible source, and does not create registry +records. Import still requires a remote source so the registered bundle +retains a pullable source pointer. + +The client fetches the plugin from a Git, OCI, ZIP, or MLflow artifact +source and inspects it locally. It discovers directories containing a +SKILL.md entry point, creates embedded skill versions without individual +source pointers, and records each directory as the membership +`member_subpath`. It then creates a monolithic bundle version whose +source fields preserve the original plugin location. + +Subagents, hooks, MCP configurations, and other non-skill content remain +in the source artifact but are not registered as entities or members in +Phase 1. The import result reports a warning for each skipped category. +Import does not install the plugin, generate a downstream manifest, or +translate an MLflow bundle into another bundle format. + +The bundle version must have a valid semantic version, supplied by the +caller or read from supported plugin metadata. Embedded skills use the +bundle version. Import never overwrites or reuses an existing skill or +bundle version. The client checks for naming and version conflicts +before creating registry records and fails the import if any target +`(name, version)` already exists. + +See [implementation-details.md: Plugin +import](implementation-details.md#plugin-import) for the SDK return +type, CLI mapping, discovery rules, and conflict behavior. + ### Pull semantics `pull` is a client-side operation. The SDK reads the source pointer @@ -821,13 +917,36 @@ produces a skill stack in the trace tree: Walking up the ancestor chain and collecting SKILL spans reconstructs the skill stack for any span. -#### Autologger compatibility +#### Framework autologger compatibility Because `skill_context()` creates a standard MLflow span, it works -with existing autologgers without modification. When an autologger -(Claude, LangChain, OpenAI, etc.) creates a span inside a -`skill_context()` block, that span automatically becomes a child of -the SKILL span. No changes to the autologgers are needed. +with existing framework autologgers without modification. When a +framework autologger (LangChain, OpenAI, Anthropic, etc.) creates a span +inside a `skill_context()` block, that span automatically becomes a +child of the SKILL span. + +#### Automatic harness instrumentation + +Phase 1 extends the Claude Code autologger to recognize skill +invocations and create SKILL spans automatically. The autologger reads +the `mlflow-skills-manifest.json` written during installation, maps the +harness-local skill name to its registered `{workspace, name, version}` +coordinates, and creates a SKILL span around the invocation. LLM and +tool spans produced while the skill is active become children of that +span. + +Automatic instrumentation runs in the process that owns the active +trace, so it can preserve correct parent-child relationships without +cross-process trace correlation. It does not perform a registry lookup +during invocation. A missing, malformed, or unmatched manifest entry +does not interrupt the agent run or other autologging; it only prevents +creation of a registry-linked SKILL span for that invocation. + +The manifest and instrumentation contract are harness-neutral so other +harness autologgers can adopt them later, but Claude Code integration is +the automatic tracing implementation delivered in Phase 1. See +[implementation-details.md: Automatic trace +instrumentation](implementation-details.md#automatic-trace-instrumentation). #### Registry validation @@ -866,23 +985,47 @@ installation. This avoids duplicating work that projects like [Lola](https://github.com/LobsterTrap/lola) already handle well, and lets the MLflow community benefit from their evolving harness support. -#### Two installation paths - -The registry supports two installation paths: - -1. **Direct install** (`mlflow skills install`): a simple file - download that places skill content in a harness-specific directory. - This handles the common case of installing a single skill from any - supported source type. The `--harness` flag determines the target - directory (e.g., `.claude/skills/` for Claude Code, - `.cursor/skills/` for Cursor). - -2. **Package manager install** (`mlflow skills install-bundle`): for - bundles or when full harness-specific manifest generation is - needed. MLflow resolves registry metadata, pulls content from - MLflow-specific sources (artifact store, OCI) to local paths, and - delegates to a configured package manager plugin for - harness-specific installation. +The Phase 1 plugin boundary is intentionally narrow. MLflow resolves +skills and skills-only bundles into concrete skill sources or local +paths, then passes those skills to an existing package manager for +installation. Phase 1 does not define a generic adapter that translates +MLflow bundle definitions into downstream bundle formats. Translation +of richer bundles containing subagents, hooks, or MCP server references +is deferred to RFC-0009 together with those non-skill member types. + +#### Installation commands + +Both harness-aware installation commands delegate to a configured +package manager plugin: + +1. **Single-skill install** (`mlflow skills install`): MLflow resolves + one registered skill and materializes its content locally, then calls + the plugin's `install_skill()` operation. + +2. **Bundle install** (`mlflow skills install-bundle`): MLflow resolves + a skills-only bundle and materializes its members locally, then calls + the plugin's `install_bundle()` operation. + +MLflow owns registry and source resolution plus the trace manifest. The +package manager owns all harness-specific behavior, including directory +placement and any package-manager or harness manifest generation. Users +can request a target harness through MLflow, which passes that selection +to the plugin. Users who only want to download content without installing +it into a harness use the package-manager-free `mlflow skills pull` +command. + +Two package-manager integration details require follow-up investigation +before the Phase 1 installation contract is finalized: + +- **Harness selection:** Determine whether APM and Lola can reliably + detect the target harness when the caller does not specify one. If the + selected package managers provide this behavior, MLflow can leave the + harness argument optional; otherwise the installation commands must + require an explicit target harness. +- **Reproducible installation:** Determine what lock-file or equivalent + reproducibility support APM and Lola provide. Based on those findings, + decide whether MLflow should rely on package-manager-owned lock data, + generate an intermediate install recipe, or define its own lock file. #### Package manager plugin interface @@ -926,26 +1069,30 @@ When `mlflow skills install-bundle` is invoked: 1. MLflow resolves the bundle version from the registry (by name + version or alias). -2. For each member skill, MLflow pulls content to a local temporary - directory using the same source-type-aware logic as - `mlflow skills pull`. -3. MLflow passes the local paths to the configured package manager - plugin, which handles harness-specific directory placement and - manifest generation. +2. MLflow materializes a local path for each member skill according to + the bundle kind: + - For an assembled bundle, MLflow pulls each member from its own + source to a local temporary directory. + - For a monolithic bundle, MLflow pulls the bundle source once and + resolves each member path from the pulled bundle root and the + member's `member_subpath`. +3. MLflow passes the resulting skill-name-to-local-path mapping to the + configured package manager plugin, which handles harness-specific + directory placement and manifest generation. 4. MLflow writes the `mlflow-skills-manifest.json` trace manifest with installed registry coordinates. -For Git-backed skills, the package manager can also fetch directly -from Git (APM and Lola both support Git sources natively). In this -case, MLflow can provide the Git coordinates from the registry -metadata and let the package manager handle the fetch, avoiding a -redundant local pull. +When `mlflow skills install` is invoked for a single skill, MLflow +resolves the version, pulls its content to a local temporary directory, +passes that path to the configured plugin's `install_skill()` operation, +and writes the trace manifest after installation succeeds. #### Trace manifest -Both installation paths write an `mlflow-skills-manifest.json` file -that records installed registry coordinates. This manifest enables -automatic SKILL span creation by harness autologgers: +Both installation commands write an `mlflow-skills-manifest.json` file +that records installed registry coordinates. In Phase 1, the Claude +Code autologger consumes this manifest for automatic SKILL span +creation: ```json { @@ -965,6 +1112,14 @@ automatic SKILL span creation by harness autologgers: } ``` +Project-scoped installs write the manifest at the project root. +User-scoped installs write it in the MLflow user configuration +directory. Project entries take precedence over user entries with the +same harness-local skill name. See [implementation-details.md: +Automatic trace +instrumentation](implementation-details.md#automatic-trace-instrumentation) +for discovery, matching, span lifecycle, and failure behavior. + ### Implementation details Database schema (table definitions), store interface (method @@ -981,12 +1136,22 @@ in [implementation-details.md](implementation-details.md). `source_type="mlflow"` to store content directly in MLflow artifact storage. +- **Artifact upload atomicity.** Client-side artifact upload and skill + version creation are separate operations. The client performs + best-effort cleanup when version creation fails, but an artifact + backend without deletion support can retain unreferenced uploaded + files until garbage collection. + - **Package manager dependency.** Full harness-specific installation requires a package manager plugin (APM, Lola, or similar). Users - who do not install a package manager can still use direct install - (`mlflow skills install`) for single skills, and `mlflow skills + who do not install a package manager can still use `mlflow skills pull` for harness-agnostic content download. +- **Automatic tracing coverage.** Phase 1 automatic instrumentation is + implemented for Claude Code. Other harnesses can use manual + `skill_context()` instrumentation until their autologgers adopt the + manifest contract. + # Alternatives ## Store skill artifacts only in MLflow (no source pointers) @@ -1056,8 +1221,11 @@ New feature, not a breaking change. Phased rollout: - **Phase 1 (this RFC):** Skill and SkillBundle entities, store, REST API, SDK, CLI, UI, `mlflow skills pull`, - `mlflow skills install` (direct), package manager plugin interface, - and `mlflow.skill_context()` for trace integration. + skills-only plugin import, package-manager-backed single-skill and + bundle installation, the package manager plugin interface, + `mlflow.skill_context()` for manual trace integration, the install-time + trace manifest, and automatic SKILL spans in the Claude Code + autologger. - **Phase 2 (RFC-0009):** Extend skill bundles with non-skill members: subagent definitions, hooks, and MCP server references (cross-registry with RFC-0004). diff --git a/rfcs/0008-mvp-skill-registry/implementation-details.md b/rfcs/0008-mvp-skill-registry/implementation-details.md index ae85e8c..be28de0 100644 --- a/rfcs/0008-mvp-skill-registry/implementation-details.md +++ b/rfcs/0008-mvp-skill-registry/implementation-details.md @@ -233,7 +233,7 @@ class Skill: status: SkillStatus | None = None # read-only, derived from parent-resolved version tags: dict[str, str] = field(default_factory=dict) aliases: dict[str, str] = field(default_factory=dict) # read-only; populated from skill_aliases table, e.g. {"production": "1.2.0"} - latest_version: str | None = None # read-only, highest active semver + latest_version: str | None = None # read-only, shared latest-resolution rule created_by: str | None = None last_updated_by: str | None = None creation_timestamp: int | None = None @@ -342,18 +342,40 @@ skills/code-review/1.0.0/ reference/style-guide.md ``` -The `source` field contains the artifact URI as resolved by MLflow's -artifact storage (e.g., `mlflow-artifacts:/skills/code-review/1.0.0/` -when using the artifact proxy, or a direct artifact-store URI -otherwise). `source_type="mlflow"` means "stored in MLflow-managed -artifact storage," not a specific URI scheme. Pull downloads the -directory tree from the artifact store. The MLflow UI can browse -individual files within a stored skill version when artifact proxying -is enabled. - -The upload API accepts a local directory path and stores each file as -a separate artifact. The `content_digest` is computed over the full -directory contents at upload time. +The `source` field contains the artifact URI resolved by MLflow's +artifact storage (for example, +`mlflow-artifacts:/skills/code-review/1.0.0/sha256-abc123/` when using +the artifact proxy, or a direct artifact-store URI otherwise). +`source_type="mlflow"` means "stored in MLflow-managed artifact +storage," not a specific URI scheme. Pull downloads the directory tree +from the artifact store. The MLflow UI can browse individual files +within a stored skill version when artifact proxying is enabled. + +**Client-side upload flow.** Direct artifact storage is implemented by +the `register_skill(content_path=...)` SDK/CLI convenience path rather +than a new skill-registry upload endpoint: + +1. The client validates the local directory and preflights that the + path can be registered. +2. The client computes `content_digest` over a canonical representation + of the directory. Regular files are ordered by normalized relative + path; both paths and file contents participate in the digest. + Symlinks are rejected, and empty directories are not preserved. +3. The client checks the target `(name, version)`. If the version + already exists, registration fails with an error. This matches + the MCP Server Registry behavior (`register_mcp_server()`). +4. The client uploads each file through MLflow's existing artifact APIs + to a digest-qualified, version-specific artifact prefix. +5. After upload succeeds, the client creates the `SkillVersion` with + `source_type="mlflow"`, the resolved artifact URI, and the computed + digest. + +The upload and registry write are not atomic. If version creation fails, +the client makes a best-effort attempt to delete the uploaded prefix when +the artifact backend supports deletion. Any remaining files are +unreferenced orphaned artifacts and may be removed by artifact-store +garbage collection. A concurrent writer can still win after preflight; +the losing client follows the same cleanup behavior. **Version uniqueness.** The combination of `(name, version)` is unique within a workspace. A skill version represents a single logical @@ -362,16 +384,16 @@ find it but are not part of its identity. **Content integrity.** The optional `content_digest` field stores a digest of the skill content at registration time (e.g., -`sha256:abc123...`). For `source_type="mlflow"`, the server computes -the digest at upload time and stores it on the version; on pull, the +`sha256:abc123...`). For `source_type="mlflow"`, the client computes +the digest before upload and stores it on the version; on pull, the client recomputes the digest over the downloaded content and rejects the result if it does not match, detecting out-of-band modification of the underlying artifact store. For external source types (git, oci, -zip), `content_digest` is client-supplied: for OCI sources, this is -the native image digest; for Git sources, a digest of the file -contents at the pinned commit; for ZIP sources, a digest of the -archive. The registry stores the digest but does not verify it on -read; verification is the consumer's responsibility. +zip), `content_digest` is also client-supplied: for OCI sources, this is +the native image digest; for Git sources, a digest of the file contents +at the pinned commit; for ZIP sources, a digest of the archive. The +registry stores the digest but does not verify it on read; verification +is the consumer's responsibility. **Immutability contract.** `source_type`, `source`, `subpath`, `content_digest`, and `version` are immutable after creation. To point @@ -394,7 +416,7 @@ class SkillBundle: status: SkillStatus | None = None # read-only, derived from parent-resolved version tags: dict[str, str] = field(default_factory=dict) aliases: dict[str, str] = field(default_factory=dict) # read-only; populated from skill_bundle_aliases table - latest_version: str | None = None # read-only, highest active semver + latest_version: str | None = None # read-only, shared latest-resolution rule created_by: str | None = None last_updated_by: str | None = None creation_timestamp: int | None = None @@ -452,8 +474,9 @@ assembled, never both: image or Git repo) that contains the complete bundle. `pull` fetches the bundle artifact as a unit. Member skill versions may omit their own `source` because the bundle artifact is the - authoritative source. The optional membership `member_subpath` - identifies where each member lives inside the bundle artifact. + authoritative source. Every source-less member must provide a + membership `member_subpath` identifying where it lives inside the + bundle artifact. - **Assembled:** has individual member references. Each skill member has its own source. `pull` fetches members individually. If a skill member has no source, `pull` fails rather than producing a partial @@ -462,7 +485,8 @@ assembled, never both: content. The API rejects attempts to set `member_subpath` on a membership whose -member version has its own source. +member version has its own source. It also rejects a source-less member +of a monolithic bundle when `member_subpath` is missing or empty. **Immutability contract.** The member list and source fields of a bundle version are immutable after creation. To change the set of @@ -488,6 +512,9 @@ hard deletes, while `delete_*_version` methods are soft deletes that transition the version to `deleted`. ```python +from mlflow.store.tracking import SEARCH_MAX_RESULTS_DEFAULT + + NOT_SET = object() @@ -511,7 +538,7 @@ class SkillRegistryMixin: def search_skills( self, filter_string: str | None = None, - max_results: int = 100, + max_results: int = SEARCH_MAX_RESULTS_DEFAULT, order_by: list[str] | None = None, page_token: str | None = None, ) -> PagedList[Skill]: @@ -559,7 +586,7 @@ class SkillRegistryMixin: self, name: str, filter_string: str | None = None, - max_results: int = 100, + max_results: int = SEARCH_MAX_RESULTS_DEFAULT, order_by: list[str] | None = None, page_token: str | None = None, ) -> PagedList[SkillVersion]: @@ -627,7 +654,7 @@ class SkillRegistryMixin: def search_skill_bundles( self, filter_string: str | None = None, - max_results: int = 100, + max_results: int = SEARCH_MAX_RESULTS_DEFAULT, order_by: list[str] | None = None, page_token: str | None = None, ) -> PagedList[SkillBundle]: @@ -678,7 +705,7 @@ class SkillRegistryMixin: self, name: str, filter_string: str | None = None, - max_results: int = 100, + max_results: int = SEARCH_MAX_RESULTS_DEFAULT, order_by: list[str] | None = None, page_token: str | None = None, ) -> PagedList[SkillBundleVersion]: @@ -734,17 +761,25 @@ class SkillRegistryMixin: raise NotImplementedError(self.__class__.__name__) ``` +The alias name `latest` is reserved for both skills and skill bundles. +The corresponding `set_*_alias()` methods reject it. Alias lookup with +`latest` delegates to `get_latest_skill_version()` or +`get_latest_skill_bundle_version()` rather than reading a stored alias +row. + For update fields, omitting a parameter leaves the stored value unchanged, while passing `None` to a nullable field explicitly sets the field to `null`. ## SDK convenience functions -The `mlflow.genai.skills` namespace provides convenience functions that -combine store operations, matching the pattern established by -`mlflow.genai.register_mcp_server()` in RFC-0004. +The `mlflow.genai` namespace provides convenience functions that +combine store operations, matching the top-level public SDK pattern +established by `mlflow.genai.register_mcp_server()` in RFC-0004. ```python +from dataclasses import dataclass + import mlflow @@ -761,9 +796,13 @@ def register_skill( content_digest: str | None = None, ) -> SkillVersion: """Register a skill version. Auto-creates the parent Skill if - it does not exist. If content_path is provided, uploads the - local directory to MLflow artifact storage and sets source_type - and source automatically.""" + it does not exist and otherwise reuses the existing parent. If the + version already exists, an MlflowException is raised. This matches + the MCP Server Registry behavior (register_mcp_server). If + content_path is provided, the client uploads the files through + existing MLflow artifact APIs and sets source_type, source, and + content_digest. content_path is mutually exclusive with source_type, + source, subpath, and content_digest.""" def create_skill( @@ -944,6 +983,86 @@ def set_skill_bundle_alias(*, name: str, alias: str, version: str) -> None: ... def delete_skill_bundle_alias(*, name: str, alias: str) -> None: ... +@dataclass(frozen=True) +class PluginImportWarning: + category: str + path: str + message: str + + +@dataclass(frozen=True) +class IntrospectedSkill: + name: str + path: str + + +@dataclass +class PluginIntrospectionResult: + bundle_name: str | None + version: str | None + skills: list[IntrospectedSkill] + warnings: list[PluginImportWarning] + + +@dataclass +class PluginImportResult: + bundle_version: SkillBundleVersion + skill_versions: list[SkillVersion] + warnings: list[PluginImportWarning] + + +def introspect_bundle( + *, + source: str, + plugin_format: str, + source_type: str | None = None, + subpath: str | None = None, +) -> PluginIntrospectionResult: + """Inspect a local or remote plugin without modifying the registry.""" + + +def import_bundle( + *, + source: str, + plugin_format: str, + bundle_name: str | None = None, + version: str | None = None, + source_type: str | None = None, + subpath: str | None = None, +) -> PluginImportResult: + """Import a plugin as a monolithic skills-only bundle. + + Fetches and inspects the plugin in the client environment, registers + discovered skills, preserves the plugin source on the bundle version, + and returns warnings for non-skill content that was skipped. + """ + + +def install_skill( + *, + name: str, + version: str | None = None, + alias: str | None = None, + package_manager: str | None = None, + harness: str | None = None, + scope: str = "project", +) -> str: + """Resolve a skill and install it through a package manager plugin.""" + + +def install_bundle( + *, + name: str, + version: str | None = None, + alias: str | None = None, + package_manager: str | None = None, + harness: str | None = None, + scope: str = "project", +) -> str: + """Resolve a skills-only bundle and install it through a package + manager plugin.""" + + def pull( *, name: str | None = None, @@ -958,8 +1077,8 @@ def pull( # Example usage: -version = mlflow.genai.skills.register_skill(name="code-review", version="1.0.0", source_type="git", source="...") -servers = mlflow.genai.skills.search_skills(filter_string="status = 'active'") +version = mlflow.genai.register_skill(name="code-review", version="1.0.0", source_type="git", source="...") +servers = mlflow.genai.search_skills(filter_string="status = 'active'") ``` For SDK update methods, `NOT_SET` means "leave unchanged" while `None` @@ -980,6 +1099,11 @@ resource paths. It is exposed under both `/api/3.0/mlflow/skills` and `/ajax-api/3.0/mlflow/skills`, plus the corresponding static-prefix variants, following the MCP Server Registry (RFC-0004) pattern. +There is no skill-registry content-upload endpoint. The client-side +`register_skill(content_path=...)` helper uploads through existing +MLflow artifact APIs, then uses the version-creation endpoint below to +store the resulting artifact URI and digest. + ### Skill endpoints All paths relative to the logical skills router prefix. @@ -1194,10 +1318,12 @@ consistent with the MCP Server Registry (RFC-0004). ## Python SDK and CLI -The `mlflow.genai.skills` module exposes top-level functions delegating -to `MlflowClient`, with a 1:1 mapping to the store mixin methods -above. The `mlflow skills` CLI command group provides the same -operations from the command line: +The `mlflow.genai` module exposes the public registry functions, +delegating to `MlflowClient`, plus client-side import, pull, and +package-manager installation operations that compose those registry +functions. Skill-specific entity and request types are also re-exported +from `mlflow.genai`. The `mlflow skills` CLI command group provides the +same operations from the command line: | CLI subcommand | SDK function | Description | |---|---|---| @@ -1209,8 +1335,8 @@ operations from the command line: | `mlflow skills set-alias` | `set_skill_alias()` | Set a version alias | | `mlflow skills set-tag` | `set_skill_tag()` | Set a tag | | `mlflow skills pull` | `pull()` | Pull content to local filesystem | -| `mlflow skills install` | (direct install) | Download and place in harness directory | -| `mlflow skills install-bundle` | (package manager install) | Install bundle via package manager plugin | +| `mlflow skills install` | `install_skill()` | Install one skill through a package manager plugin | +| `mlflow skills install-bundle` | `install_bundle()` | Install a skills-only bundle through a package manager plugin | | `mlflow skills create-bundle` | `create_skill_bundle()` | Create a skill bundle | | `mlflow skills create-bundle-version` | `create_skill_bundle_version()` | Create a bundle version with members | | `mlflow skills get-bundle` | `get_skill_bundle()` | Get bundle metadata | @@ -1220,6 +1346,8 @@ operations from the command line: | `mlflow skills set-bundle-tag` | `set_skill_bundle_tag()` | Set a bundle-level tag | | `mlflow skills set-bundle-version-tag` | `set_skill_bundle_version_tag()` | Set a bundle version tag | | `mlflow skills update-bundle-version` | `update_skill_bundle_version()` | Update bundle version status | +| `mlflow skills introspect` | `introspect_bundle()` | Preview a local or remote plugin without registry writes | +| `mlflow skills import` | `import_bundle()` | Import a plugin as a monolithic skills-only bundle | **Existing `mlflow skills` CLI group.** MLflow already has an `mlflow skills` CLI group (`mlflow/cli/skills.py`) with two @@ -1233,12 +1361,99 @@ on locally bundled skills, while `search`/`get`/`register`/`pull` and the other registry subcommands operate on the server-side registry. +## Plugin import + +Plugin import is implemented in the SDK and CLI layer. There is no +dedicated REST import endpoint: the client fetches and inspects the +source locally, then calls the existing skill and bundle creation APIs. +The registry server does not fetch user-supplied plugin URLs. + +### Read-only preview + +`introspect_bundle()` and `mlflow skills introspect` run the same plugin +discovery used by import but do not create or modify registry records. +They accept either a local path or a remote Git, OCI, ZIP, or MLflow +artifact source and return the discovered skill names and paths, +available plugin name and version metadata, and warnings for skipped +non-skill content. A local path must not specify `source_type`; remote +sources use an explicit source type or the same unambiguous syntax +inference as import. Introspection does not require the plugin to provide +a name or version because those values are only required when importing. + +### Phase 1 input format + +Phase 1 supports the Claude Code plugin layout. The caller passes +`plugin_format="claude-code"`; automatic format detection and additional +input formats are follow-up work. The importer: + +1. Fetches the Git, OCI, ZIP, or MLflow artifact source using the same + source-type-aware logic as `pull`. +2. Applies `subpath`, when provided, to select the plugin root. +3. Reads `.claude-plugin/plugin.json` when present to obtain supported + plugin metadata such as name and version. Explicit `bundle_name` and + `version` arguments take precedence. +4. Discovers skill directories under `skills/` that contain a SKILL.md + entry point. The SKILL.md name is used when present; otherwise the + directory name is used. +5. Detects non-skill plugin content, including subagents, hooks, and MCP + configuration, for warning purposes only. + +The resulting bundle name and version must be available after explicit +arguments and plugin metadata are considered. The version must be a +valid semantic version. When `source_type` is omitted, the client infers +it from the source syntax and fails if the source type is ambiguous. + +### Registration behavior + +For each discovered skill, the importer creates a `SkillVersion` whose +version defaults to the bundle version and whose `source_type`, `source`, +and `subpath` are null. The importer records the skill's plugin-relative +directory as `SkillMemberRef.member_subpath`. + +After registering the embedded skills, the importer creates one +monolithic `SkillBundleVersion` with the original `source_type`, +`source`, and `subpath`, plus member references for all discovered +skills. This preserves a pullable link to the complete original plugin +while keeping Phase 1 registry metadata skills-only. + +The import fails if no skills are discovered. It also preflights all +target `(name, version)` pairs and fails if any skill or bundle version +already exists. It never overwrites or reuses an existing version. A +caller resolves a conflict by choosing a different bundle version or +renaming the conflicting skill before import. + +### Warnings and result + +Subagents, hooks, MCP configurations, and unrecognized content remain +in the plugin artifact but are not registered. Each discovered skipped +category produces a `PluginImportWarning` containing its category, +path, and an explanation that Phase 1 registers skills only. The CLI +prints these warnings after registration. The SDK returns them together +with the created bundle and skill versions in `PluginImportResult`. + +Import only translates an existing plugin into MLflow's skills-only +registry representation. It does not install the plugin, generate a +downstream manifest, or translate an MLflow bundle into a downstream +bundle format. + ## Package manager plugin interface Package manager plugins are registered via Python entrypoints (group `mlflow.skill_package_managers`), so third-party plugins can be installed via `pip install` without modifying MLflow core. +In Phase 1, these plugins receive only resolved skills or skills-only +bundle members. They install those skills using an existing package +manager, but they do not translate MLflow bundle definitions into +downstream bundle formats. Generic translation for bundles containing +non-skill members is deferred to RFC-0009. + +Both `mlflow skills install` and `mlflow skills install-bundle` require +a package manager plugin. The caller can select a plugin explicitly, or +MLflow uses the configured default. If no plugin is selected or +available, installation fails with guidance to install or configure one; +`mlflow skills pull` remains available without a package manager. + ### Plugin protocol ```python @@ -1257,7 +1472,8 @@ class PackageManagerPlugin: name: skill name for manifest generation local_path: local directory with skill content harness: target harness (e.g., "claude-code", "cursor"). - If None, auto-detect from environment. + Behavior when omitted is pending investigation of APM + and Lola harness-detection capabilities. scope: "project" (cwd) or "user" (home directory) """ ... @@ -1276,7 +1492,8 @@ class PackageManagerPlugin: Args: bundle_name: bundle name for manifest generation member_paths: {skill_name: local_path} for each member - harness: target harness. If None, auto-detect. + harness: target harness. Behavior when omitted is pending + investigation of APM and Lola harness-detection capabilities. scope: "project" or "user" """ ... @@ -1296,36 +1513,47 @@ apm = "apm_mlflow:ApmPlugin" lola = "lola_mlflow:LolaPlugin" ``` -### Source resolution flow +### Bundle installation flow When `mlflow skills install-bundle` is invoked: 1. **Resolve:** MLflow calls `get_skill_bundle_version()` (or alias resolution) to obtain the bundle version and its member list. -2. **Pull:** For each member skill, MLflow pulls content to a local - temporary directory using source-type-aware logic (git clone, OCI - pull, ZIP download, MLflow artifact download). For Git-backed - skills, the package manager can fetch directly from Git if it - supports Git sources natively, avoiding a redundant local pull. +2. **Materialize member paths:** + - For an assembled bundle, MLflow pulls each member skill to its own + local temporary directory using source-type-aware logic (Git clone, + OCI pull, ZIP download, or MLflow artifact download). + - For a monolithic bundle, MLflow pulls the bundle-level source once + using the same source-type-aware logic. For each member, it resolves + a local path by joining the pulled bundle root with + `member_subpath`. Every monolithic member must provide a non-empty + `member_subpath`; installation fails if the path is missing, escapes + the pulled bundle root after normalization, or does not contain the + embedded skill. 3. **Delegate:** MLflow passes the local paths to the configured package manager plugin via `install_bundle()`. The plugin handles harness-specific directory placement and manifest generation. 4. **Manifest:** MLflow writes `mlflow-skills-manifest.json` with installed registry coordinates for trace integration. -### Direct install flow - -When `mlflow skills install` is invoked (no package manager needed): - -1. **Resolve:** MLflow calls `get_skill_version()` (or alias - resolution) to obtain the source pointer. -2. **Pull:** MLflow pulls content to a local temporary directory. -3. **Place:** MLflow copies the content to the appropriate - harness-specific directory based on the `--harness` flag: - - `claude-code`: `.claude/skills/{name}/` - - `cursor`: `.cursor/skills/{name}/` - - Other harnesses: configurable via settings -4. **Manifest:** MLflow writes `mlflow-skills-manifest.json`. +### Single-skill installation flow + +When `mlflow skills install` is invoked: + +1. **Resolve:** MLflow calls `get_skill_version()`, alias resolution, or + latest resolution to obtain the registered source pointer. `version` + and `alias` are mutually exclusive; omitting both selects the + system-defined latest version. +2. **Pull:** MLflow pulls the skill content to a local temporary + directory using the same source-type-aware logic as `pull`. +3. **Delegate:** MLflow passes the skill name and local path to the + configured package manager plugin via `install_skill()`. The plugin + owns harness-specific behavior, scope handling, directory placement, + and any generated package-manager or harness metadata. An explicit + harness selection from the caller is passed through to the plugin. +4. **Manifest:** After the plugin reports success, MLflow writes or + updates `mlflow-skills-manifest.json` with the resolved registry + coordinates. ## Pull semantics details @@ -1363,21 +1591,80 @@ following attributes: |---|---|---| | `mlflow.skill.name` | Skill name | Registry name of the active skill | | `mlflow.skill.version` | Version string | Registered version | -| `mlflow.skill.workspace` | Workspace name | MLflow workspace (defaults to `"default"`) | +| `mlflow.skill.workspace` | Workspace name | Resolved from the install manifest, falling back to the current tracking URI's workspace context | These three attributes form the `{workspace, name, version}` coordinates that link the span back to a specific skill version in the registry. +## Automatic trace instrumentation + +Automatic instrumentation uses the install-time +`mlflow-skills-manifest.json` to map harness-local skill invocations to +registered skill coordinates. Phase 1 implements this behavior in the +Claude Code autologger. The manifest format is harness-neutral so other +harness integrations can adopt the same contract later. + +### Manifest writing and discovery + +Installation commands write or update the manifest after all requested +skills have been installed successfully. Each entry is keyed by the +harness-local skill name and contains the registered `workspace`, +`name`, and resolved `version`. Aliases are resolved before the +manifest is written and are not stored in place of versions. + +Project-scoped installation writes the manifest at the project root. +User-scoped installation writes it in the MLflow user configuration +directory. Project entries take precedence over user entries with the +same harness-local skill name. + +For a monolithic bundle, installation writes an entry for every +registered embedded skill resolved through its `member_subpath`. For an +assembled bundle, it writes an entry for every installed member skill. +The bundle itself does not produce a SKILL span because tracing is at +the invoked-skill level. + +### Claude Code invocation matching + +The Phase 1 Claude Code autologger matches harness skill invocations +against manifest entries by skill name. When a match is found, it +creates a span with: + +- span type `SKILL` +- span name equal to the harness-local skill name +- `mlflow.skill.name`, `mlflow.skill.version`, and + `mlflow.skill.workspace` attributes from the manifest + +LLM and tool spans produced while the skill is active become children +of the SKILL span. + +If a matching SKILL span with the same registry coordinates is already +active because application code used `mlflow.skill_context()`, the +autologger reuses that active context and does not create a duplicate +SKILL span. + +### Failure behavior + +Automatic instrumentation does not contact the registry during skill +invocation and does not add runtime latency or create a dependency on +registry availability. + +A missing manifest, malformed manifest, or unmatched skill name never +interrupts the agent run or other autologging; it only prevents +creation of a registry-linked SKILL span for the affected invocation. +Skills copied into a harness without an MLflow installation command +have no manifest entry and are not linked automatically; callers can +still use `mlflow.skill_context()` manually. + ## SDK and CLI code examples ### Register skills from an OCI artifact with subpath ```python import mlflow -from mlflow.genai.skills import SkillMemberRef +from mlflow.genai import SkillMemberRef -mlflow.genai.skills.register_skill( +mlflow.genai.register_skill( name="code-review", version="1.0.0", source_type="oci", @@ -1385,7 +1672,7 @@ mlflow.genai.skills.register_skill( subpath="skills/code-review", ) -mlflow.genai.skills.register_skill( +mlflow.genai.register_skill( name="test-coverage", version="2.1.0", source_type="oci", @@ -1394,7 +1681,7 @@ mlflow.genai.skills.register_skill( ) # Assembled bundle: each member has its own source -bundle_version = mlflow.genai.skills.create_skill_bundle_version( +bundle_version = mlflow.genai.create_skill_bundle_version( name="pr-workflow", version="1.0.0", skills=[ @@ -1405,12 +1692,12 @@ bundle_version = mlflow.genai.skills.create_skill_bundle_version( # Monolithic bundle from a single OCI image. Embedded member # versions are registered without their own sources. -mlflow.genai.skills.register_skill( +mlflow.genai.register_skill( name="embedded-review", version="1.0.0", ) -bundle_version = mlflow.genai.skills.create_skill_bundle_version( +bundle_version = mlflow.genai.create_skill_bundle_version( name="pr-workflow-mono", version="1.0.0", source_type="oci", @@ -1426,18 +1713,18 @@ bundle_version = mlflow.genai.skills.create_skill_bundle_version( ```python # Search for active skill versions -versions = mlflow.genai.skills.search_skill_versions( +versions = mlflow.genai.search_skill_versions( name="code-review", filter_string="status = 'active'", ) # Search for active skill bundles -bundles = mlflow.genai.skills.search_skill_bundles( +bundles = mlflow.genai.search_skill_bundles( filter_string="status = 'active'", ) # Get a specific version -version = mlflow.genai.skills.get_skill_version( +version = mlflow.genai.get_skill_version( name="code-review", version="1.0.0", ) @@ -1446,20 +1733,20 @@ version = mlflow.genai.skills.get_skill_version( # version.subpath == "code-review" # Resolve by alias -version = mlflow.genai.skills.get_skill_version_by_alias( +version = mlflow.genai.get_skill_version_by_alias( name="code-review", alias="production", ) # Get a bundle version and its pinned members -bundle_version = mlflow.genai.skills.get_skill_bundle_version( +bundle_version = mlflow.genai.get_skill_bundle_version( name="pr-workflow", version="1.0.0", ) # bundle_version.skills == [SkillMemberRef(name="code-review", version="1.0.0"), ...] # Resolve a bundle alias -bundle_version = mlflow.genai.skills.get_skill_bundle_version_by_alias( +bundle_version = mlflow.genai.get_skill_bundle_version_by_alias( name="pr-workflow", alias="production", ) From ed3191c5f572c1f48c6c54bfd9f05c0c5383662c Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Wed, 15 Jul 2026 16:30:15 -0400 Subject: [PATCH 3/7] Fix header, grammar, and remove RFC-0009 number references 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 --- .../0008-mvp-skill-registry.md | 32 +++++++++---------- .../implementation-details.md | 8 ++--- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md b/rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md index 7448444..2fd385e 100644 --- a/rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md +++ b/rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md @@ -3,7 +3,7 @@ | start_date | 2026-04-22 | | :----------- | :--------- | | mlflow_issue | https://github.com/mlflow/mlflow/issues/22833 | -| rfc_pr | | +| rfc_pr | https://github.com/mlflow/rfcs/pull/26 | | Author(s) | [Bill Murdock](https://github.com/jwm4) (Red Hat) | | :--------------------- | :-- | @@ -52,8 +52,7 @@ inspecting bundled Assistant skills. The registry subcommands none of the new subcommand names conflict with the existing ones. See [implementation-details.md: Python SDK and CLI](implementation-details.md#python-sdk-and-cli) for details. - -The two entity types: +The two entity types are: - **Skills**: a directory containing a SKILL.md entry point plus supporting files (scripts, templates, reference material) @@ -76,8 +75,8 @@ Both paths annotate spans with registry coordinates, enabling adoption tracking, deprecation impact analysis, per-skill cost attribution, and regression detection. -A follow-up RFC (RFC-0009) will extend skill bundles to include -non-skill members (subagents, hooks, MCP server references). +A follow-up RFC will extend skill bundles to include +non-skill members (e.g., subagents, MCP server references). # Basic example @@ -470,10 +469,10 @@ discovery/search operations. ### Out of scope -- **Non-skill entity types.** Subagent definitions, hooks, and MCP - server references are deferred to a follow-up RFC (RFC-0009) that - will extend skill bundles with non-skill members. The registry - backend is designed to be extensible to these types. +- **Non-skill entity types.** Non-skill members (e.g., subagent + definitions, MCP server references) are deferred to a follow-up RFC + that will extend skill bundles. The registry backend is designed to + be extensible to these types. - **Artifact storage as the only path.** The registry supports both external source pointers (Git, OCI, ZIP) and direct artifact storage (`source_type="mlflow"`). However, it is not an artifact-only store; @@ -563,9 +562,9 @@ members), and direct mapping to the harness plugin concept. Follows the same top-level pattern as Skill: versions, tags, aliases, and derived status. -A follow-up RFC (RFC-0009) will extend skill bundles to include -non-skill members (subagent definitions, hooks, and MCP server -references from RFC-0004), enabling full "plugin"-style bundles. +A follow-up RFC will extend skill bundles to include non-skill +members (e.g., subagents, MCP server references), enabling full +"plugin"-style bundles. The member table schema includes a `member_type` field for forward compatibility with this extension. @@ -990,8 +989,8 @@ skills and skills-only bundles into concrete skill sources or local paths, then passes those skills to an existing package manager for installation. Phase 1 does not define a generic adapter that translates MLflow bundle definitions into downstream bundle formats. Translation -of richer bundles containing subagents, hooks, or MCP server references -is deferred to RFC-0009 together with those non-skill member types. +of richer bundles containing non-skill members (e.g., subagents, MCP +server references) is deferred to the follow-up RFC. #### Installation commands @@ -1226,9 +1225,8 @@ New feature, not a breaking change. Phased rollout: `mlflow.skill_context()` for manual trace integration, the install-time trace manifest, and automatic SKILL spans in the Claude Code autologger. -- **Phase 2 (RFC-0009):** Extend skill bundles with non-skill - members: subagent definitions, hooks, and MCP server references - (cross-registry with RFC-0004). +- **Phase 2 (follow-up RFC):** Extend skill bundles with non-skill + members (e.g., subagents, MCP server references). - **Phase 3 (follow-up):** Usage analytics dashboards, install count tracking, cross-workspace export/import (following cross-registry patterns), shared base extraction with the MCP registry, and richer diff --git a/rfcs/0008-mvp-skill-registry/implementation-details.md b/rfcs/0008-mvp-skill-registry/implementation-details.md index be28de0..6a13e57 100644 --- a/rfcs/0008-mvp-skill-registry/implementation-details.md +++ b/rfcs/0008-mvp-skill-registry/implementation-details.md @@ -152,9 +152,9 @@ FK: `(workspace, bundle_name, bundle_version)` references with RESTRICT delete. The `member_type` column is included for forward compatibility with -RFC-0009, which will extend bundles to include non-skill members -(subagent definitions, hooks, MCP server references). In this RFC, -all members have `member_type='skill'`. +a follow-up RFC that will extend bundles to include non-skill members +(e.g., subagents, MCP server references). In this RFC, all members +have `member_type='skill'`. ### `skill_bundle_tags` @@ -1446,7 +1446,7 @@ In Phase 1, these plugins receive only resolved skills or skills-only bundle members. They install those skills using an existing package manager, but they do not translate MLflow bundle definitions into downstream bundle formats. Generic translation for bundles containing -non-skill members is deferred to RFC-0009. +non-skill members is deferred to the follow-up RFC. Both `mlflow skills install` and `mlflow skills install-bundle` require a package manager plugin. The caller can select a plugin explicitly, or From 2b85e293abe1cdfcc6511233961ddfe71520b901 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Thu, 16 Jul 2026 08:38:12 -0400 Subject: [PATCH 4/7] Resolve package manager investigations, drop Phase 3, fix bundle semantics 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 --- .../0008-mvp-skill-registry.md | 110 +++++++++--------- .../implementation-details.md | 64 +++++----- 2 files changed, 92 insertions(+), 82 deletions(-) diff --git a/rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md b/rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md index 2fd385e..8e8bd92 100644 --- a/rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md +++ b/rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md @@ -55,7 +55,9 @@ CLI](implementation-details.md#python-sdk-and-cli) for details. The two entity types are: - **Skills**: a directory containing a SKILL.md entry point plus - supporting files (scripts, templates, reference material) + supporting files (scripts, templates, reference material). See the + [Agent Skills specification](https://agentskills.io/) for the + complete format definition. - **Skill bundles**: versioned collections that group related skills into a single governed, installable unit @@ -63,9 +65,10 @@ The two entity types are: registered content from its source. Harness-specific installation delegates to package managers (APM, Lola, or others via a plugin interface) that already support cross-harness skill installation. -Existing Claude Code plugins can be imported as monolithic skills-only -bundles: MLflow registers their discovered skills, preserves the plugin -source, and warns about non-skill content that Phase 1 does not register. +Existing Claude Code plugins can be imported as monolithic bundles: +MLflow registers their discovered skills, preserves the plugin source, +and warns about non-skill content that is pulled and installed alongside +the skills but does not receive individual registry entries in Phase 1. Trace integration supports both manual and automatic instrumentation. `mlflow.skill_context()` lets SDK applications create SKILL spans @@ -75,8 +78,8 @@ Both paths annotate spans with registry coordinates, enabling adoption tracking, deprecation impact analysis, per-skill cost attribution, and regression detection. -A follow-up RFC will extend skill bundles to include -non-skill members (e.g., subagents, MCP server references). +A follow-up RFC will add registry entries for non-skill bundle +members (e.g., subagents, MCP server references). # Basic example @@ -236,7 +239,7 @@ Registry enables. Each shows both CLI and UI paths. **UI path:** In the bundle detail page, click "Add Alias" and map `production` to version `1.0.0`. -#### Import an existing plugin as a skills-only bundle +#### Import an existing plugin as a bundle 1. Import a Claude Code plugin from a remotely accessible source: ```bash @@ -254,8 +257,9 @@ Registry enables. Each shows both CLI and UI paths. monolithic bundle version. The bundle retains the original plugin source pointer. 4. If the plugin also contains subagents, hooks, or MCP configuration, - MLflow skips those elements and prints a warning that Phase 1 - registers skills only. + MLflow prints a warning that Phase 1 does not create individual + registry entries for non-skill content. The content remains in the + bundle and is included when the bundle is pulled or installed. 5. The created bundle and skills are available through the same discovery, lifecycle, pull, and installation flows as manually registered entries. @@ -469,10 +473,12 @@ discovery/search operations. ### Out of scope -- **Non-skill entity types.** Non-skill members (e.g., subagent - definitions, MCP server references) are deferred to a follow-up RFC - that will extend skill bundles. The registry backend is designed to - be extensible to these types. +- **Registry entries for non-skill content.** Bundles can contain + non-skill content (e.g., subagents, MCP configurations) that is + pulled and installed alongside skills, but Phase 1 does not create + individual registry entries for non-skill members. A follow-up RFC + will add those entries. The registry backend is designed to be + extensible to these types. - **Artifact storage as the only path.** The registry supports both external source pointers (Git, OCI, ZIP) and direct artifact storage (`source_type="mlflow"`). However, it is not an artifact-only store; @@ -693,8 +699,8 @@ This aligns with the MCP Server Registry (RFC-0004). ### Plugin import `mlflow skills import` is a client-side convenience operation for -registering an existing harness-specific plugin as a monolithic -skills-only bundle. Phase 1 supports the Claude Code plugin layout. +registering an existing harness-specific plugin as a monolithic bundle. +Phase 1 supports the Claude Code plugin layout. Additional input formats can be added later without changing the registry data model. @@ -984,13 +990,14 @@ installation. This avoids duplicating work that projects like [Lola](https://github.com/LobsterTrap/lola) already handle well, and lets the MLflow community benefit from their evolving harness support. -The Phase 1 plugin boundary is intentionally narrow. MLflow resolves -skills and skills-only bundles into concrete skill sources or local -paths, then passes those skills to an existing package manager for -installation. Phase 1 does not define a generic adapter that translates -MLflow bundle definitions into downstream bundle formats. Translation -of richer bundles containing non-skill members (e.g., subagents, MCP -server references) is deferred to the follow-up RFC. +The Phase 1 registry boundary is intentionally narrow. MLflow creates +registry entries only for skills within a bundle; non-skill content +(e.g., subagents, MCP configurations) remains in the bundle source and +is included when the bundle is pulled or installed, but does not receive +individual registry entries. A follow-up RFC will add registry entries +for non-skill member types. MLflow resolves bundles into concrete skill +sources or local paths, then passes those to an existing package manager +for installation. #### Installation commands @@ -1002,29 +1009,33 @@ package manager plugin: the plugin's `install_skill()` operation. 2. **Bundle install** (`mlflow skills install-bundle`): MLflow resolves - a skills-only bundle and materializes its members locally, then calls - the plugin's `install_bundle()` operation. + a bundle and materializes its content locally (including any non-skill + content in monolithic bundles), then calls the plugin's + `install_bundle()` operation. MLflow owns registry and source resolution plus the trace manifest. The package manager owns all harness-specific behavior, including directory -placement and any package-manager or harness manifest generation. Users -can request a target harness through MLflow, which passes that selection -to the plugin. Users who only want to download content without installing -it into a harness use the package-manager-free `mlflow skills pull` -command. - -Two package-manager integration details require follow-up investigation -before the Phase 1 installation contract is finalized: - -- **Harness selection:** Determine whether APM and Lola can reliably - detect the target harness when the caller does not specify one. If the - selected package managers provide this behavior, MLflow can leave the - harness argument optional; otherwise the installation commands must - require an explicit target harness. -- **Reproducible installation:** Determine what lock-file or equivalent - reproducibility support APM and Lola provide. Based on those findings, - decide whether MLflow should rely on package-manager-owned lock data, - generate an intermediate install recipe, or define its own lock file. +placement and any package-manager or harness manifest generation. Both +installation commands require a `--harness` argument, which MLflow +passes to the plugin. Users who only want to download content without +installing it into a harness use the package-manager-free +`mlflow skills pull` command. + +**Harness selection.** The `--harness` argument is required on both +installation commands. While some package managers can auto-detect the +target harness (APM scans for marker directories; Lola installs to all +targets by default), detection behavior varies across plugins and can +produce surprising results. A required argument keeps the MLflow +interface predictable regardless of which plugin is configured. + +**Reproducible installation.** MLflow does not define its own lock file. +Reproducibility support varies by package manager: APM provides a full +lockfile (`apm.lock.yaml`) with resolved commits, content hashes, and +integrity verification; Lola provides version-constraint files +(`.lola-req`) and ref pinning but no lockfile. MLflow documents that +reproducibility depends on the configured package manager plugin and +recommends that production deployments choose a plugin with lockfile +support. #### Package manager plugin interface @@ -1038,7 +1049,7 @@ class PackageManagerPlugin: self, name: str, local_path: str, - harness: str | None = None, + harness: str, scope: str = "project", ) -> str: """Install a single skill from a local path. @@ -1049,7 +1060,7 @@ class PackageManagerPlugin: self, bundle_name: str, member_paths: dict[str, str], - harness: str | None = None, + harness: str, scope: str = "project", ) -> str: """Install a bundle of skills from local paths. @@ -1220,15 +1231,10 @@ New feature, not a breaking change. Phased rollout: - **Phase 1 (this RFC):** Skill and SkillBundle entities, store, REST API, SDK, CLI, UI, `mlflow skills pull`, - skills-only plugin import, package-manager-backed single-skill and + plugin import, package-manager-backed single-skill and bundle installation, the package manager plugin interface, `mlflow.skill_context()` for manual trace integration, the install-time trace manifest, and automatic SKILL spans in the Claude Code autologger. -- **Phase 2 (follow-up RFC):** Extend skill bundles with non-skill - members (e.g., subagents, MCP server references). -- **Phase 3 (follow-up):** Usage analytics dashboards, install count - tracking, cross-workspace export/import (following cross-registry - patterns), shared base extraction with the MCP registry, and richer - evaluation-to-skill query integration (e.g., filtering evaluation - results directly by skill version attributes). +- **Phase 2 (follow-up RFC):** Add individual registry entries for + non-skill bundle members (e.g., subagents, MCP server references). diff --git a/rfcs/0008-mvp-skill-registry/implementation-details.md b/rfcs/0008-mvp-skill-registry/implementation-details.md index 6a13e57..3bb7332 100644 --- a/rfcs/0008-mvp-skill-registry/implementation-details.md +++ b/rfcs/0008-mvp-skill-registry/implementation-details.md @@ -152,9 +152,9 @@ FK: `(workspace, bundle_name, bundle_version)` references with RESTRICT delete. The `member_type` column is included for forward compatibility with -a follow-up RFC that will extend bundles to include non-skill members -(e.g., subagents, MCP server references). In this RFC, all members -have `member_type='skill'`. +a follow-up RFC that will add registry entries for non-skill bundle +members (e.g., subagents, MCP server references). In this RFC, all +registered members have `member_type='skill'`. ### `skill_bundle_tags` @@ -1030,37 +1030,42 @@ def import_bundle( source_type: str | None = None, subpath: str | None = None, ) -> PluginImportResult: - """Import a plugin as a monolithic skills-only bundle. + """Import a plugin as a monolithic bundle. Fetches and inspects the plugin in the client environment, registers discovered skills, preserves the plugin source on the bundle version, - and returns warnings for non-skill content that was skipped. + and returns warnings for non-skill content that is included in the + bundle but does not receive individual registry entries in Phase 1. """ def install_skill( *, name: str, + harness: str, version: str | None = None, alias: str | None = None, package_manager: str | None = None, - harness: str | None = None, scope: str = "project", ) -> str: - """Resolve a skill and install it through a package manager plugin.""" + """Resolve a skill and install it through a package manager plugin. + The harness argument is required to keep behavior predictable + across plugins.""" def install_bundle( *, name: str, + harness: str, version: str | None = None, alias: str | None = None, package_manager: str | None = None, - harness: str | None = None, scope: str = "project", ) -> str: - """Resolve a skills-only bundle and install it through a package - manager plugin.""" + """Resolve a bundle and install it through a package manager plugin. + For monolithic bundles, non-skill content is included in the + installed artifact. The harness argument is required to keep + behavior predictable across plugins.""" def pull( @@ -1336,7 +1341,7 @@ same operations from the command line: | `mlflow skills set-tag` | `set_skill_tag()` | Set a tag | | `mlflow skills pull` | `pull()` | Pull content to local filesystem | | `mlflow skills install` | `install_skill()` | Install one skill through a package manager plugin | -| `mlflow skills install-bundle` | `install_bundle()` | Install a skills-only bundle through a package manager plugin | +| `mlflow skills install-bundle` | `install_bundle()` | Install a bundle through a package manager plugin | | `mlflow skills create-bundle` | `create_skill_bundle()` | Create a skill bundle | | `mlflow skills create-bundle-version` | `create_skill_bundle_version()` | Create a bundle version with members | | `mlflow skills get-bundle` | `get_skill_bundle()` | Get bundle metadata | @@ -1347,7 +1352,7 @@ same operations from the command line: | `mlflow skills set-bundle-version-tag` | `set_skill_bundle_version_tag()` | Set a bundle version tag | | `mlflow skills update-bundle-version` | `update_skill_bundle_version()` | Update bundle version status | | `mlflow skills introspect` | `introspect_bundle()` | Preview a local or remote plugin without registry writes | -| `mlflow skills import` | `import_bundle()` | Import a plugin as a monolithic skills-only bundle | +| `mlflow skills import` | `import_bundle()` | Import a plugin as a monolithic bundle | **Existing `mlflow skills` CLI group.** MLflow already has an `mlflow skills` CLI group (`mlflow/cli/skills.py`) with two @@ -1414,7 +1419,7 @@ After registering the embedded skills, the importer creates one monolithic `SkillBundleVersion` with the original `source_type`, `source`, and `subpath`, plus member references for all discovered skills. This preserves a pullable link to the complete original plugin -while keeping Phase 1 registry metadata skills-only. +while keeping Phase 1 registry entries limited to skills. The import fails if no skills are discovered. It also preflights all target `(name, version)` pairs and fails if any skill or bundle version @@ -1427,14 +1432,16 @@ renaming the conflicting skill before import. Subagents, hooks, MCP configurations, and unrecognized content remain in the plugin artifact but are not registered. Each discovered skipped category produces a `PluginImportWarning` containing its category, -path, and an explanation that Phase 1 registers skills only. The CLI +path, and an explanation that Phase 1 does not create registry entries +for non-skill content (though the content remains in the bundle). The CLI prints these warnings after registration. The SDK returns them together with the created bundle and skill versions in `PluginImportResult`. -Import only translates an existing plugin into MLflow's skills-only -registry representation. It does not install the plugin, generate a -downstream manifest, or translate an MLflow bundle into a downstream -bundle format. +Import translates an existing plugin into MLflow's registry +representation, creating registry entries for discovered skills while +preserving the complete plugin source. It does not install the plugin, +generate a downstream manifest, or translate an MLflow bundle into a +downstream bundle format. ## Package manager plugin interface @@ -1442,11 +1449,11 @@ Package manager plugins are registered via Python entrypoints (group `mlflow.skill_package_managers`), so third-party plugins can be installed via `pip install` without modifying MLflow core. -In Phase 1, these plugins receive only resolved skills or skills-only -bundle members. They install those skills using an existing package -manager, but they do not translate MLflow bundle definitions into -downstream bundle formats. Generic translation for bundles containing -non-skill members is deferred to the follow-up RFC. +In Phase 1, these plugins receive resolved skills or bundle content +(which may include non-skill content in monolithic bundles). They +install the content using an existing package manager. The package +manager handles placement of all content, including non-skill files +that do not have individual registry entries. Both `mlflow skills install` and `mlflow skills install-bundle` require a package manager plugin. The caller can select a plugin explicitly, or @@ -1462,7 +1469,7 @@ class PackageManagerPlugin: self, name: str, local_path: str, - harness: str | None = None, + harness: str, scope: str = "project", ) -> str: """Install a single skill from a local path. @@ -1471,9 +1478,7 @@ class PackageManagerPlugin: Args: name: skill name for manifest generation local_path: local directory with skill content - harness: target harness (e.g., "claude-code", "cursor"). - Behavior when omitted is pending investigation of APM - and Lola harness-detection capabilities. + harness: target harness (e.g., "claude-code", "cursor") scope: "project" (cwd) or "user" (home directory) """ ... @@ -1482,7 +1487,7 @@ class PackageManagerPlugin: self, bundle_name: str, member_paths: dict[str, str], - harness: str | None = None, + harness: str, scope: str = "project", ) -> str: """Install a bundle of skills from local paths. @@ -1492,8 +1497,7 @@ class PackageManagerPlugin: Args: bundle_name: bundle name for manifest generation member_paths: {skill_name: local_path} for each member - harness: target harness. Behavior when omitted is pending - investigation of APM and Lola harness-detection capabilities. + harness: target harness (e.g., "claude-code", "cursor") scope: "project" or "user" """ ... From 4f93db59011bd4f7a0b850873962976880ff547f Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Thu, 16 Jul 2026 10:29:38 -0400 Subject: [PATCH 5/7] Fix internal inconsistencies in main RFC 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 --- rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md b/rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md index 8e8bd92..6e4b12a 100644 --- a/rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md +++ b/rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md @@ -133,7 +133,8 @@ in Phase 1. ```bash # Install a skill bundle for Claude Code via a package manager -mlflow skills install-bundle --name pr-workflow --alias production +mlflow skills install-bundle --name pr-workflow --alias production \ + --harness claude-code # Or install a single skill through the same package-manager layer mlflow skills install --name code-review --alias production \ @@ -298,7 +299,8 @@ Registry enables. Each shows both CLI and UI paths. 1. Install the bundle using a package manager plugin: ```bash - mlflow skills install-bundle --name pr-workflow --alias production + mlflow skills install-bundle --name pr-workflow --alias production \ + --harness claude-code ``` This resolves the bundle from the registry, pulls the bundle content, delegates to the configured package manager (e.g., APM @@ -568,7 +570,7 @@ members), and direct mapping to the harness plugin concept. Follows the same top-level pattern as Skill: versions, tags, aliases, and derived status. -A follow-up RFC will extend skill bundles to include non-skill +A follow-up RFC will add registry entries for non-skill bundle members (e.g., subagents, MCP server references), enabling full "plugin"-style bundles. The member table schema includes a `member_type` field for forward From cc1faa6c70dc532abefa5d039ab6cc8668d0d222 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Thu, 16 Jul 2026 13:10:04 -0400 Subject: [PATCH 6/7] Add resolution lock, harness-local naming, and bundle_path passthrough 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 --- .../0008-mvp-skill-registry.md | 86 ++++++---- .../implementation-details.md | 154 ++++++++++++++---- 2 files changed, 173 insertions(+), 67 deletions(-) diff --git a/rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md b/rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md index 6e4b12a..3706f87 100644 --- a/rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md +++ b/rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md @@ -7,7 +7,7 @@ | Author(s) | [Bill Murdock](https://github.com/jwm4) (Red Hat) | | :--------------------- | :-- | -| **Date Last Modified** | 2026-07-14 | +| **Date Last Modified** | 2026-07-16 | | **AI Assistant(s)** | Claude Code, Codex | **Table of contents** @@ -317,7 +317,7 @@ Registry enables. Each shows both CLI and UI paths. "Skills" tab to filter for traces with SKILL spans. 4. Find the trace for the agent run. Skill invocations appear as SKILL spans in the trace tree, annotated with registry coordinates - (skill name, version, registry). + (skill name, version, workspace). 5. Click a SKILL span to see which registered skill version was used and how long it took. Click the skill name link to navigate to the skill's registry detail page. @@ -707,11 +707,11 @@ Additional input formats can be added later without changing the registry data model. Before importing, users can call `mlflow skills introspect` or the SDK -`introspect_bundle()` function to preview the skills and skipped content -that MLflow discovers. Introspection is read-only, accepts either a local -path or a remotely accessible source, and does not create registry -records. Import still requires a remote source so the registered bundle -retains a pullable source pointer. +`introspect_bundle()` function to preview the skills and unregistered +non-skill content that MLflow discovers. Introspection is read-only, +accepts either a local path or a remotely accessible source, and does not +create registry records. Import still requires a remote source so the +registered bundle retains a pullable source pointer. The client fetches the plugin from a Git, OCI, ZIP, or MLflow artifact source and inspects it locally. It discovers directories containing a @@ -1025,19 +1025,27 @@ installing it into a harness use the package-manager-free **Harness selection.** The `--harness` argument is required on both installation commands. While some package managers can auto-detect the -target harness (APM scans for marker directories; Lola installs to all -targets by default), detection behavior varies across plugins and can -produce surprising results. A required argument keeps the MLflow -interface predictable regardless of which plugin is configured. - -**Reproducible installation.** MLflow does not define its own lock file. -Reproducibility support varies by package manager: APM provides a full -lockfile (`apm.lock.yaml`) with resolved commits, content hashes, and -integrity verification; Lola provides version-constraint files -(`.lola-req`) and ref pinning but no lockfile. MLflow documents that -reproducibility depends on the configured package manager plugin and -recommends that production deployments choose a plugin with lockfile -support. +target harness (APM detects harness signals in the project; Lola installs +to all detected assistants by default), detection behavior varies across +plugins and can produce surprising results. A required argument keeps +the MLflow interface predictable regardless of which plugin is +configured. + +**Reproducible installation.** MLflow defines a small resolution lock, +`mlflow-skills.lock`, that records the exact registry coordinates and +installation inputs selected by an install command: entity type, name, +resolved version, workspace, package manager, harness, and scope. Aliases +are resolved before they are written. Passing `--lock-file` to either +installation command writes or updates this file; `mlflow skills +install --from-lock` replays it by resolving the pinned registry versions +and delegating them to the recorded package manager. + +Package-manager lockfiles complement the MLflow resolution lock rather +than replace it. APM provides `apm.lock.yaml` with resolved commits and +content hashes. Lola provides version-constraint files (`.lola-req`) and +ref pinning but no lockfile. The MLflow lock makes registry resolution +reproducible across both plugins; a package-manager lock can additionally +capture package-manager-specific layout and integrity information. #### Package manager plugin interface @@ -1053,9 +1061,9 @@ class PackageManagerPlugin: local_path: str, harness: str, scope: str = "project", - ) -> str: + ) -> PackageManagerInstallResult: """Install a single skill from a local path. - Returns the installed path.""" + Returns the installed path and harness-local skill name.""" ... def install_bundle( @@ -1063,11 +1071,13 @@ class PackageManagerPlugin: bundle_name: str, member_paths: dict[str, str], harness: str, + bundle_path: str | None = None, scope: str = "project", - ) -> str: - """Install a bundle of skills from local paths. - member_paths maps skill names to local paths. - Returns the installed path.""" + ) -> PackageManagerInstallResult: + """Install a bundle from local paths. For monolithic bundles, + bundle_path is the complete artifact root, including opaque + non-skill content. Returns the installed path and mapping from + registry skill names to harness-local names.""" ... def supported_harnesses(self) -> list[str]: @@ -1086,18 +1096,25 @@ When `mlflow skills install-bundle` is invoked: - For an assembled bundle, MLflow pulls each member from its own source to a local temporary directory. - For a monolithic bundle, MLflow pulls the bundle source once and - resolves each member path from the pulled bundle root and the - member's `member_subpath`. -3. MLflow passes the resulting skill-name-to-local-path mapping to the - configured package manager plugin, which handles harness-specific - directory placement and manifest generation. -4. MLflow writes the `mlflow-skills-manifest.json` trace manifest - with installed registry coordinates. + retains the complete pulled root as `bundle_path`, including opaque + non-skill content, while resolving each skill path from that root + and the member's `member_subpath`. +3. MLflow passes the skill-name-to-local-path mapping and, for a + monolithic bundle, the complete `bundle_path` to the configured + package manager plugin. The plugin handles harness-specific placement + of the entire bundle and returns the actual harness-local name for + each installed skill. +4. MLflow writes `mlflow-skills-manifest.json` using the returned + harness-local names and the corresponding registry coordinates. +5. If requested, MLflow updates the resolution lock with the exact + bundle version and installation inputs. When `mlflow skills install` is invoked for a single skill, MLflow resolves the version, pulls its content to a local temporary directory, passes that path to the configured plugin's `install_skill()` operation, -and writes the trace manifest after installation succeeds. +and writes the trace manifest using the harness-local name returned by +the plugin after installation succeeds. If requested, it then updates +the resolution lock with the exact skill version and installation inputs. #### Trace manifest @@ -1235,6 +1252,7 @@ New feature, not a breaking change. Phased rollout: REST API, SDK, CLI, UI, `mlflow skills pull`, plugin import, package-manager-backed single-skill and bundle installation, the package manager plugin interface, + the `mlflow-skills.lock` resolution lock, `mlflow.skill_context()` for manual trace integration, the install-time trace manifest, and automatic SKILL spans in the Claude Code autologger. diff --git a/rfcs/0008-mvp-skill-registry/implementation-details.md b/rfcs/0008-mvp-skill-registry/implementation-details.md index 3bb7332..fca36e2 100644 --- a/rfcs/0008-mvp-skill-registry/implementation-details.md +++ b/rfcs/0008-mvp-skill-registry/implementation-details.md @@ -1011,6 +1011,30 @@ class PluginImportResult: warnings: list[PluginImportWarning] +@dataclass(frozen=True) +class InstalledSkill: + registry_name: str + harness_local_name: str + installed_path: str + + +@dataclass +class PackageManagerInstallResult: + installed_path: str + skills: list[InstalledSkill] + + +@dataclass(frozen=True) +class MlflowSkillLockEntry: + entity_type: str + name: str + version: str + workspace: str + package_manager: str + harness: str + scope: str + + def introspect_bundle( *, source: str, @@ -1047,10 +1071,12 @@ def install_skill( alias: str | None = None, package_manager: str | None = None, scope: str = "project", -) -> str: + lock_file: str | None = None, +) -> PackageManagerInstallResult: """Resolve a skill and install it through a package manager plugin. The harness argument is required to keep behavior predictable - across plugins.""" + across plugins. If lock_file is provided, record the exact resolved + version and installation inputs for replay.""" def install_bundle( @@ -1061,11 +1087,21 @@ def install_bundle( alias: str | None = None, package_manager: str | None = None, scope: str = "project", -) -> str: + lock_file: str | None = None, +) -> PackageManagerInstallResult: """Resolve a bundle and install it through a package manager plugin. For monolithic bundles, non-skill content is included in the installed artifact. The harness argument is required to keep - behavior predictable across plugins.""" + behavior predictable across plugins. If lock_file is provided, + record the exact resolved version and installation inputs for + replay.""" + + +def install_from_lock( + *, lock_file: str = "mlflow-skills.lock", +) -> list[PackageManagerInstallResult]: + """Replay exact skill and bundle versions from an MLflow resolution + lock using the recorded package manager, harness, and scope.""" def pull( @@ -1342,6 +1378,7 @@ same operations from the command line: | `mlflow skills pull` | `pull()` | Pull content to local filesystem | | `mlflow skills install` | `install_skill()` | Install one skill through a package manager plugin | | `mlflow skills install-bundle` | `install_bundle()` | Install a bundle through a package manager plugin | +| `mlflow skills install --from-lock` | `install_from_lock()` | Replay exact registry versions from an MLflow resolution lock | | `mlflow skills create-bundle` | `create_skill_bundle()` | Create a skill bundle | | `mlflow skills create-bundle-version` | `create_skill_bundle_version()` | Create a bundle version with members | | `mlflow skills get-bundle` | `get_skill_bundle()` | Get bundle metadata | @@ -1379,8 +1416,9 @@ The registry server does not fetch user-supplied plugin URLs. discovery used by import but do not create or modify registry records. They accept either a local path or a remote Git, OCI, ZIP, or MLflow artifact source and return the discovered skill names and paths, -available plugin name and version metadata, and warnings for skipped -non-skill content. A local path must not specify `source_type`; remote +available plugin name and version metadata, and warnings for +unregistered non-skill content. A local path must not specify +`source_type`; remote sources use an explicit source type or the same unambiguous syntax inference as import. Introspection does not require the plugin to provide a name or version because those values are only required when importing. @@ -1443,6 +1481,36 @@ preserving the complete plugin source. It does not install the plugin, generate a downstream manifest, or translate an MLflow bundle into a downstream bundle format. +## MLflow resolution lock + +Package managers receive materialized local paths, so their own +lockfiles cannot by themselves reconstruct which MLflow registry +versions produced those paths. When `lock_file` is supplied to +`install_skill()` or `install_bundle()`, MLflow writes or updates an +`mlflow-skills.lock` resolution lock after installation succeeds. + +Each entry records the entity type (`skill` or `bundle`), name, exact +resolved version, workspace, selected package manager, harness, and +scope. Aliases and `latest` are resolved before writing the entry and +are never stored in place of a version. A bundle entry does not repeat +its members because bundle membership is immutable and can be recovered +from the exact bundle version. + +A resolution lock is scoped to one workspace. Appending an entry from a +different workspace fails, and replay requires the configured MLflow +client to target the recorded workspace. + +`install_from_lock()` reads the entries, resolves the exact versions +through the currently configured MLflow client, materializes their +content, and delegates to the recorded package manager. Normal registry +visibility and lifecycle rules apply during replay, so an unavailable or +deleted version causes the replay to fail rather than silently installing +different content. Package-manager lockfiles may additionally capture +package-manager-specific layout, cached sources, and integrity metadata. +The CLI `--from-lock` mode uses the recorded installation inputs and is +mutually exclusive with name, version, alias, package manager, harness, +scope, and lock-writing options. + ## Package manager plugin interface Package manager plugins are registered via Python entrypoints (group @@ -1453,7 +1521,12 @@ In Phase 1, these plugins receive resolved skills or bundle content (which may include non-skill content in monolithic bundles). They install the content using an existing package manager. The package manager handles placement of all content, including non-skill files -that do not have individual registry entries. +that do not have individual registry entries. It returns the actual +harness-local name of every installed skill so MLflow can write an +accurate trace manifest even when the package manager renames or prefixes +skills. The result must contain exactly one `InstalledSkill` for every +requested registry skill; missing or duplicate mappings fail the install +before MLflow writes its trace manifest or resolution lock. Both `mlflow skills install` and `mlflow skills install-bundle` require a package manager plugin. The caller can select a plugin explicitly, or @@ -1471,12 +1544,12 @@ class PackageManagerPlugin: local_path: str, harness: str, scope: str = "project", - ) -> str: + ) -> PackageManagerInstallResult: """Install a single skill from a local path. - Returns the installed path. + Returns the installed path and harness-local skill name. Args: - name: skill name for manifest generation + name: registry skill name local_path: local directory with skill content harness: target harness (e.g., "claude-code", "cursor") scope: "project" (cwd) or "user" (home directory) @@ -1488,16 +1561,20 @@ class PackageManagerPlugin: bundle_name: str, member_paths: dict[str, str], harness: str, + bundle_path: str | None = None, scope: str = "project", - ) -> str: - """Install a bundle of skills from local paths. - member_paths maps skill names to local paths. - Returns the installed path. + ) -> PackageManagerInstallResult: + """Install a bundle from local paths. member_paths maps registry + skill names to local paths. For a monolithic bundle, bundle_path + is the complete artifact root and must be installed as a unit. + Returns the installed path and harness-local skill names. Args: - bundle_name: bundle name for manifest generation + bundle_name: registry bundle name member_paths: {skill_name: local_path} for each member harness: target harness (e.g., "claude-code", "cursor") + bundle_path: complete monolithic bundle root, or None for an + assembled bundle scope: "project" or "user" """ ... @@ -1528,17 +1605,23 @@ When `mlflow skills install-bundle` is invoked: local temporary directory using source-type-aware logic (Git clone, OCI pull, ZIP download, or MLflow artifact download). - For a monolithic bundle, MLflow pulls the bundle-level source once - using the same source-type-aware logic. For each member, it resolves - a local path by joining the pulled bundle root with - `member_subpath`. Every monolithic member must provide a non-empty - `member_subpath`; installation fails if the path is missing, escapes - the pulled bundle root after normalization, or does not contain the - embedded skill. -3. **Delegate:** MLflow passes the local paths to the configured - package manager plugin via `install_bundle()`. The plugin handles - harness-specific directory placement and manifest generation. -4. **Manifest:** MLflow writes `mlflow-skills-manifest.json` with - installed registry coordinates for trace integration. + using the same source-type-aware logic and retains the complete root + as `bundle_path`, including opaque non-skill content. For each + member, it resolves a local path by joining the pulled bundle root + with `member_subpath`. Every monolithic member must provide a + non-empty `member_subpath`; installation fails if the path is missing, + escapes the pulled bundle root after normalization, or does not + contain the embedded skill. +3. **Delegate:** MLflow passes `member_paths` and, for a monolithic + bundle, `bundle_path` to the configured package manager plugin via + `install_bundle()`. The plugin installs the complete monolithic bundle + or the assembled skills and returns each skill's harness-local name. +4. **Manifest:** MLflow writes `mlflow-skills-manifest.json`, keyed by + the returned harness-local names and populated with the corresponding + registry coordinates. +5. **Resolution lock:** If `lock_file` was supplied, MLflow atomically + updates it with the exact resolved bundle version and installation + inputs after the install and manifest write succeed. ### Single-skill installation flow @@ -1553,11 +1636,15 @@ When `mlflow skills install` is invoked: 3. **Delegate:** MLflow passes the skill name and local path to the configured package manager plugin via `install_skill()`. The plugin owns harness-specific behavior, scope handling, directory placement, - and any generated package-manager or harness metadata. An explicit - harness selection from the caller is passed through to the plugin. + naming, and any generated package-manager or harness metadata. An + explicit harness selection from the caller is passed through to the + plugin, which returns the actual harness-local skill name. 4. **Manifest:** After the plugin reports success, MLflow writes or - updates `mlflow-skills-manifest.json` with the resolved registry - coordinates. + updates `mlflow-skills-manifest.json` under the returned harness-local + name with the resolved registry coordinates. +5. **Resolution lock:** If `lock_file` was supplied, MLflow atomically + updates it with the exact resolved skill version and installation + inputs after the install and manifest write succeed. ## Pull semantics details @@ -1613,9 +1700,10 @@ harness integrations can adopt the same contract later. Installation commands write or update the manifest after all requested skills have been installed successfully. Each entry is keyed by the -harness-local skill name and contains the registered `workspace`, -`name`, and resolved `version`. Aliases are resolved before the -manifest is written and are not stored in place of versions. +harness-local skill name returned by the package manager plugin and +contains the registered `workspace`, `name`, and resolved `version`. +Aliases are resolved before the manifest is written and are not stored +in place of versions. Project-scoped installation writes the manifest at the project root. User-scoped installation writes it in the MLflow user configuration From 4efb1d4e49c54c6f7b1a81e54d3ead3ea05dc615 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Fri, 17 Jul 2026 14:17:07 -0400 Subject: [PATCH 7/7] Add Open questions section with security scan deferral note 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 --- rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md b/rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md index 3706f87..915d384 100644 --- a/rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md +++ b/rfcs/0008-mvp-skill-registry/0008-mvp-skill-registry.md @@ -1258,3 +1258,12 @@ New feature, not a breaking change. Phased rollout: autologger. - **Phase 2 (follow-up RFC):** Add individual registry entries for non-skill bundle members (e.g., subagents, MCP server references). + +# Open questions + +- **Security scan results.** Structured scan metadata on version + entities (scan type, pass/fail status, tool, date) would be valuable + for skill governance. However, the same need applies to MCP servers + (RFC-0004) and other registered assets. This should be addressed as a + cross-registry capability rather than a skill-specific feature, so + that all registries share a consistent scan result model.