From 6d1597d62f018ee4fff9adfee4a073efe79dd594 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Thu, 23 Apr 2026 14:31:24 -0400 Subject: [PATCH 01/19] Add RFC-0005: Skill Registry Propose a governed, metadata-first registry for AI agent skills in MLflow with typed source pointers, lifecycle management, security scan tracking, skill groups, and federated discovery. Co-Authored-By: Claude Opus 4.6 --- .../0005-skill-registry.md | 1302 +++++++++++++++++ 1 file changed, 1302 insertions(+) create mode 100644 rfcs/0005-skill-registry/0005-skill-registry.md diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md new file mode 100644 index 0000000..c17b81c --- /dev/null +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -0,0 +1,1302 @@ +# RFC: Skill Registry + +| start_date | 2026-04-22 | +| :----------- | :--------- | +| mlflow_issue | [mlflow/mlflow#22833](https://github.com/mlflow/mlflow/issues/22833) | +| rfc_pr | | + +| Author(s) | Bill Murdock (Red Hat) | +| :--------------------- | :-- | +| **Date Last Modified** | 2026-04-22 | +| **AI Assistant(s)** | Claude Code (Opus 4.6) | + +# 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.) rather than skill +artifacts directly. It provides enterprise governance on top of existing +skill distribution mechanisms: lifecycle management, security scan +tracking, usage analytics via traces, and federated discovery across +sources. + +The registry also introduces skill groups as a first-class concept, +allowing related skills to be organized into coherent toolboxes or +workflows and discovered as a unit. + +# Basic example + +## Register a skill and publish it + +```python +import mlflow + +# Create the logical skill asset +skill = mlflow.skills.create_skill( + name="code-review", + description="Reviews pull requests for correctness, style, and security", +) + +# Register a version pointing to a Git source +version = mlflow.skills.create_skill_version( + name="code-review", + version="1.0.0", + source_type="git", + source_url="https://github.com/acme/agent-skills/tree/v1.0.0/code-review", + content_hash="sha256:a3f2b8c...", +) +# version.publish_state == "draft" + +# Publish the version so downstream consumers can discover it +mlflow.skills.update_skill_version( + name="code-review", + version="1.0.0", + publish_state="published", +) + +# Set an alias for stable resolution +mlflow.skills.set_skill_alias( + name="code-review", + alias="production", + version="1.0.0", + source_type="git", +) + +# Record a security scan result as a tag +mlflow.skills.set_skill_version_tag( + name="code-review", + version="1.0.0", + source_type="git", + key="scan.prompt-injection.status", + value="pass", +) +mlflow.skills.set_skill_version_tag( + name="code-review", + version="1.0.0", + source_type="git", + key="scan.prompt-injection.date", + value="2026-04-22", +) +``` + +## Create a skill group with a versioned membership snapshot + +```python +from mlflow.entities import SkillGroupVersionMembership + +# Create a group for related skills +group = mlflow.skills.create_skill_group( + name="pr-workflow", + description="End-to-end pull request review workflow", +) + +# Create a group version that pins specific skill versions +group_version = mlflow.skills.create_skill_group_version( + name="pr-workflow", + version="1.0.0", + members=[ + SkillGroupVersionMembership( + group_name="pr-workflow", group_version="1.0.0", + skill_name="code-review", skill_version="1.0.0", skill_source_type="git", + ), + SkillGroupVersionMembership( + group_name="pr-workflow", group_version="1.0.0", + skill_name="test-coverage", skill_version="2.1.0", skill_source_type="git", + ), + SkillGroupVersionMembership( + group_name="pr-workflow", group_version="1.0.0", + skill_name="security-scan", skill_version="1.0.0", skill_source_type="oci", + ), + ], +) + +# Publish the group version +mlflow.skills.update_skill_group_version( + name="pr-workflow", + version="1.0.0", + publish_state="published", +) + +# Set an alias for stable resolution +mlflow.skills.set_skill_group_alias( + name="pr-workflow", + alias="production", + version="1.0.0", +) +``` + +## Discover and consume skills + +```python +# Search for published skills +skills = mlflow.skills.search_skills( + filter_string="publish_state = 'published'", +) + +# Search for active skill groups +groups = mlflow.skills.search_skill_groups( + filter_string="status = 'active'", +) + +# Get a specific version +version = mlflow.skills.get_skill_version( + name="code-review", + version="1.0.0", + source_type="git", +) +# version.source_type == "git" +# version.source_url == "https://github.com/acme/agent-skills/tree/v1.0.0/code-review" + +# Resolve by alias +version = mlflow.skills.get_skill_version_by_alias( + name="code-review", + alias="production", +) + +# Get a group version and its pinned skill versions +group_version = mlflow.skills.get_skill_group_version( + name="pr-workflow", + version="1.0.0", +) +# group_version.members == [SkillGroupVersionMembership(...), ...] + +# Resolve a group alias +group_version = mlflow.skills.get_skill_group_version_by_alias( + name="pr-workflow", + alias="production", +) +``` + +## CLI usage + +```bash +# Register a skill pointing to a Git source +mlflow skills create --name code-review \ + --description "Reviews pull requests" +mlflow skills create-version --name code-review --version 1.0.0 \ + --source-type git \ + --source-url https://github.com/acme/agent-skills/tree/v1.0.0/code-review \ + --content-hash sha256:a3f2b8c... + +# Publish and alias +mlflow skills update-version --name code-review --version 1.0.0 \ + --source-type git --publish-state published +mlflow skills set-alias --name code-review --alias production \ + --version 1.0.0 --source-type git + +# Create a group and a versioned membership snapshot +mlflow skill-groups create --name pr-workflow \ + --description "End-to-end PR review workflow" +mlflow skill-groups create-version --name pr-workflow --version 1.0.0 \ + --member code-review:1.0.0:git \ + --member test-coverage:2.1.0:git \ + --member security-scan:1.0.0:oci +mlflow skill-groups update-version --name pr-workflow --version 1.0.0 \ + --publish-state published +mlflow skill-groups set-alias --name pr-workflow --alias production \ + --version 1.0.0 + +# Search published skills +mlflow skills search --filter "publish_state = 'published'" + +# Search active groups +mlflow skill-groups search --filter "status = 'active'" +``` + +## Motivation + +### The problem + +AI agent skills (reusable tool definitions, workflow steps, and coding +assistant capabilities) are becoming a critical asset class in +enterprise AI platforms. As organizations adopt agentic AI, they +accumulate skills across teams and repositories. + +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. As Databricks engineers +have noted after dogfooding an MLflow skill registry prototype, GitHub +is sufficient for the typical developer use case. + +However, enterprises face governance challenges that Git alone does not +address: + +1. **No publish-state lifecycle.** Git has no concept of "this skill + version is approved for production use" vs. "this is a draft." Teams + resort to branch naming conventions or external tracking to manage + skill promotion. + +2. **No security scan tracking.** Skills may contain executable code or + be vulnerable to prompt injection. There is no standard place to + record whether a skill version has been scanned and what the results + were. + +3. **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. + +4. **No skill grouping.** Skills often work together as coherent + toolboxes or multi-step workflows. Agent harnesses like Claude Code + support plugin-level grouping, but there is no agent-neutral way to + represent these relationships. + +5. **No usage analytics linkage.** MLflow traces can capture skill + metadata, but without a governed registry, there is no way to link + trace data back to a governed skill record to understand adoption + across an organization. + +### Use cases + +1. **Governed registration**: Platform administrators register skill + metadata with typed source pointers to where the skill content lives + (Git, OCI, ZIP). The registry governs; the source system stores. + +2. **Lifecycle management**: Skill versions move through publish states + (draft, published, deprecated, retired) to control downstream + surfacing. This is the governance layer that Git lacks. + +3. **Security scan tracking**: Scan results (prompt injection, code + vulnerabilities, etc.) are recorded as version-level tags. The + registry does not perform scans; it provides the metadata layer for + recording and querying results. + +4. **Skill grouping**: Related skills are organized into groups for + discovery and governance. A skill can belong to multiple groups. + Groups have their own publish state and tags. + +5. **Federated discovery**: Users discover published skills and groups + across all source types from a single search interface, without + requiring skill content to be centralized. + +6. **Usage analytics**: Agent traces record which skill versions were + used. Combined with registry metadata, this enables organizations to + understand adoption and make data-driven promotion decisions. + +### Relationship to other AI asset registries + +The MCP Server Registry proposal +([#22625](https://github.com/mlflow/mlflow/issues/22625)) establishes +the pattern for governed, metadata-first AI asset registries in MLflow. +The skill registry is the next concrete instance of this pattern, +following the same conventions (entity/version/alias/tag, abstract +store, SQL storage, REST API, workspace scoping). + +Skill-specific design decisions include: + +- Typed source pointers (git/oci/zip) instead of an embedded payload + (MCP stores a `server_json` payload; skills point to external content) +- Skill groups as a first-class entity for organizing related skills +- Security scan tracking via tags as an explicit use case + +This RFC focuses on delivering the skill registry. Shared abstractions +across asset types can be extracted once multiple concrete registries +exist and the common patterns are well understood. + +### Out of scope + +- **Skill artifact storage.** The registry stores metadata and source + pointers. Skill content remains in Git, OCI, or other distribution + systems. +- **Skill authoring or development tools.** The registry manages + published skills, not the process of writing them. +- **Skill format specification.** The registry is format-agnostic. It + does not define or enforce what a skill looks like (SKILL.md, plugin + manifests, etc.). +- **Security scanning execution.** The registry records scan results; it + does not perform scans. Scanning tools are separate. +- **Agent harness integration.** How a specific agent harness (Claude + Code, Codex, Cursor, etc.) installs or loads skills from the registry + is outside this RFC. The registry provides the metadata; harness + integration layers consume it. +- **Approval workflows or review gates.** Publish state transitions are + sufficient for initial governance. Approval chains can be built on top + via external systems. +- **Detailed UI/UX design.** This RFC describes the UI surface and + placement but does not specify interaction patterns. + +## Detailed design + +### Entities and data model + +``` +Skill ||--o{ SkillVersion : "has versions" +Skill ||--o{ SkillTag : "has tags" +Skill ||--o{ SkillAlias : "has aliases" +SkillVersion ||--o{ SkillVersionTag : "has tags" +SkillGroup ||--o{ SkillGroupVersion : "has versions" +SkillGroup ||--o{ SkillGroupTag : "has tags" +SkillGroup ||--o{ SkillGroupAlias : "has aliases" +SkillGroupVersion ||--o{ SkillGroupVersionMembership : "contains skills" +SkillGroupVersion ||--o{ SkillGroupVersionTag : "has tags" +SkillGroupVersionMembership }o--|| SkillVersion : "references" +``` + +#### Skill + +The logical governed asset, scoped to a workspace. + +```python +from dataclasses import dataclass, field +from enum import StrEnum + + +class SkillStatus(StrEnum): + ACTIVE = "active" + DEPRECATED = "deprecated" + RETIRED = "retired" + + +@dataclass +class Skill: + name: str + description: str | None = None + workspace: str | None = None + status: SkillStatus = SkillStatus.ACTIVE + tags: dict[str, str] = field(default_factory=dict) + aliases: dict[str, str] = field(default_factory=dict) + last_registered_version: 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 | +|---|---|---| +| `name` | `str` | Stable logical asset name, unique within a workspace | +| `status` | `SkillStatus` | Skill-level lifecycle: `active`, `deprecated`, `retired` | +| `aliases` | `dict[str, str]` | Stable version pointers, e.g. `{"production": "1.2.0"}` | +| `last_registered_version` | `str` | Most recently registered version string | +| `workspace` | `str` | Visibility boundary | + +#### SkillVersion + +A versioned record containing a typed source pointer, publish state, +and tags. + +```python +class SkillPublishState(StrEnum): + DRAFT = "draft" + PUBLISHED = "published" + DEPRECATED = "deprecated" + RETIRED = "retired" + + +class SkillSourceType(StrEnum): + GIT = "git" + OCI = "oci" + ZIP = "zip" + + +@dataclass +class SkillVersion: + name: str + version: str + source_type: SkillSourceType + source_url: str + publish_state: SkillPublishState = SkillPublishState.DRAFT + content_hash: str | None = None + tags: dict[str, str] = field(default_factory=dict) + run_id: str | None = None + 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. Semver recommended but not enforced | +| `source_type` | `SkillSourceType` | Distribution mechanism: `git`, `oci`, `zip` | +| `source_url` | `str` | URL pointing to the skill content in the source system | +| `content_hash` | `str` | Optional content digest for integrity verification (e.g., `sha256:abc123...`) | +| `publish_state` | `SkillPublishState` | Per-version surfacing lifecycle | +| `run_id` | `str` | Optional MLflow run association for trace linkage | + +**Source type extensibility.** The `source_type` enum is intentionally +small for the initial implementation. New source types (e.g., `s3`, +`azure-blob`) can be added without schema changes since the column +stores a string value. + +**Version uniqueness.** The combination of `(name, version, source_type)` +is unique within a workspace. This allows the same skill version to be +registered from multiple distribution mechanisms (e.g., Git and OCI) +without requiring different version strings. + +**Content integrity.** The optional `content_hash` field stores a +digest of the skill content at registration time (e.g., +`sha256:abc123...`). Consumers can use this to verify that the content +at `source_url` has not changed since registration. For OCI sources, +this is the native image digest. For Git sources, this is a hash of +the skill file contents at the pinned commit. For ZIP sources, this is +a hash of the archive. The registry stores the hash but does not +verify it on read; verification is the consumer's responsibility. + +**Immutability contract.** `source_type`, `source_url`, `content_hash`, +and `version` are immutable after creation. To point to different +content, register a new version. Mutable fields (`publish_state`, +`tags`) can be updated independently. + +#### SkillGroup + +The logical group asset, scoped to a workspace. Follows the same +pattern as Skill: a top-level entity with versions, tags, and aliases. + +```python +class SkillGroupStatus(StrEnum): + ACTIVE = "active" + DEPRECATED = "deprecated" + RETIRED = "retired" + + +@dataclass +class SkillGroup: + name: str + description: str | None = None + workspace: str | None = None + status: SkillGroupStatus = SkillGroupStatus.ACTIVE + tags: dict[str, str] = field(default_factory=dict) + aliases: dict[str, str] = field(default_factory=dict) + last_registered_version: 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 +``` + +#### SkillGroupVersion + +A versioned snapshot of a skill group's membership. Each version +captures a specific set of skill versions that work together. + +```python +@dataclass +class SkillGroupVersion: + name: str + version: str + publish_state: SkillPublishState = SkillPublishState.DRAFT + tags: dict[str, str] = field(default_factory=dict) + members: list["SkillGroupVersionMembership"] = 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 +``` + +**Version uniqueness.** The combination of `(name, version)` is unique +within a workspace. + +**Immutability contract.** The membership list of a group version is +immutable after creation. To change the set of skills, register a new +group version. Mutable fields (`publish_state`, `tags`) can be updated +independently. + +#### SkillGroupVersionMembership + +Each membership entry pins a specific skill version (including source +type). + +```python +@dataclass(frozen=True) +class SkillGroupVersionMembership: + group_name: str + group_version: str + skill_name: str + skill_version: str + skill_source_type: str + workspace: str | None = None +``` + +A skill can appear in multiple groups and multiple group versions. +Membership is at the skill version level, so a group version is a +reproducible snapshot of "these specific skill versions work together." + +#### SkillGroupAlias + +```python +@dataclass(frozen=True) +class SkillGroupAlias: + name: str # parent SkillGroup name + alias: str # e.g., "production", "staging" + version: str # group version string this alias points to +``` + +#### SkillAlias and SkillTag + +```python +@dataclass(frozen=True) +class SkillAlias: + name: str # parent Skill name + alias: str # e.g., "production", "staging" + version: str # version string this alias points to + source_type: str # source type this alias points to + +@dataclass(frozen=True) +class SkillTag: + key: str + value: str +``` + +Tags use the same structure for skill-level, version-level, and +group-level tags. The distinction is maintained at the storage and API +layer (separate tables, separate endpoints). + +### Publish state and lifecycle + +#### Per-version publish state + +Each `SkillVersion` has an independent publish state: + +| State | Meaning | Downstream surfacing | +|---|---|---| +| `draft` | Registered but not ready for consumption | Not surfaced | +| `published` | Ready for downstream use | Surfaced to discovery, traces, consumers | +| `deprecated` | Still functional but no longer recommended | Surfaced with deprecation signal | +| `retired` | Preserved for history, no longer active | Not surfaced | + +Allowed transitions: + +| From | To | +|---|---| +| `draft` | `published`, `retired` | +| `published` | `deprecated` | +| `deprecated` | `published`, `retired` | + +`published` cannot return to `draft`. `deprecated` can return to +`published` (re-publish) for cases where a deprecation was premature. + +#### Skill group version publish state + +Each `SkillGroupVersion` has its own publish state lifecycle, following +the same transitions as `SkillVersion`. A group version's publish state +is independent of its member skills' publish states. Publishing a group +version does not require its member skill versions to be published, +though consumers will typically want to verify this. + +#### Skill-level status + +`Skill.status` is a separate lifecycle for the logical asset as a whole +(`active`, `deprecated`, `retired`). Setting a skill to `deprecated` +does not automatically change individual version publish states. + +### Database schema + +Twelve tables, 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 | +| `description` | `String(5000)` | | +| `status` | `String(20)` | default `'active'` | +| `last_registered_version` | `String(256)` | | +| `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, publisher-supplied | +| `source_type` | `String(20)` | PK; `git`, `oci`, `zip`, etc. | +| `source_url` | `String(2048)` | URL to skill content | +| `content_hash` | `String(512)` | optional content digest | +| `publish_state` | `String(20)` | default `'draft'` | +| `run_id` | `String(32)` | optional MLflow run linkage | +| `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. + +#### `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 | +| `source_type` | `String(20)` | 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 | +| `source_type` | `String(20)` | target source type | + +#### `skill_groups` + +| Column | Type | Notes | +|--------|------|-------| +| `workspace` | `String(63)` | PK, default `'default'` | +| `name` | `String(256)` | PK | +| `description` | `String(5000)` | | +| `status` | `String(20)` | default `'active'` | +| `last_registered_version` | `String(256)` | | +| `created_by` | `String(256)` | | +| `last_updated_by` | `String(256)` | | +| `creation_timestamp` | `BigInteger` | millis since epoch | +| `last_updated_timestamp` | `BigInteger` | millis since epoch | + +#### `skill_group_versions` + +| Column | Type | Notes | +|--------|------|-------| +| `workspace` | `String(63)` | PK, FK | +| `name` | `String(256)` | PK, FK | +| `version` | `String(256)` | PK, publisher-supplied | +| `publish_state` | `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_groups`, CASCADE delete. + +#### `skill_group_version_memberships` + +| Column | Type | Notes | +|--------|------|-------| +| `workspace` | `String(63)` | PK | +| `group_name` | `String(256)` | PK, FK to `skill_group_versions` | +| `group_version` | `String(256)` | PK, FK to `skill_group_versions` | +| `skill_name` | `String(256)` | PK, FK to `skill_versions` | +| `skill_version` | `String(256)` | PK, FK to `skill_versions` | +| `skill_source_type` | `String(20)` | PK, FK to `skill_versions` | + +FK: `(workspace, group_name, group_version)` references `skill_group_versions`, CASCADE delete. +FK: `(workspace, skill_name, skill_version, skill_source_type)` references `skill_versions`, RESTRICT delete. + +#### `skill_group_tags` + +| Column | Type | Notes | +|--------|------|-------| +| `workspace` | `String(63)` | PK, FK | +| `name` | `String(256)` | PK, FK | +| `key` | `String(256)` | PK | +| `value` | `Text` | | + +#### `skill_group_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_group_aliases` + +| Column | Type | Notes | +|--------|------|-------| +| `workspace` | `String(63)` | PK, FK | +| `name` | `String(256)` | PK, FK | +| `alias` | `String(256)` | PK | +| `version` | `String(256)` | target group 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. + +### Abstract store interface + +The store interface follows MLflow's abstract store pattern. + +```python +from abc import abstractmethod + + +class AbstractSkillRegistryStore: + # --- Skill operations --- + + @abstractmethod + def create_skill( + self, name: str, description: str | None = None, + ) -> Skill: ... + + @abstractmethod + def get_skill(self, name: str) -> Skill: ... + + @abstractmethod + def search_skills( + self, + filter_string: str | None = None, + max_results: int = 100, + page_token: str | None = None, + ) -> PagedList[Skill]: ... + + @abstractmethod + def update_skill( + self, + name: str, + description: str | None = None, + status: SkillStatus | None = None, + ) -> Skill: ... + + @abstractmethod + def delete_skill(self, name: str) -> None: ... + + # --- SkillVersion operations --- + + @abstractmethod + def create_skill_version( + self, + name: str, + version: str, + source_type: str, + source_url: str, + publish_state: SkillPublishState = SkillPublishState.DRAFT, + content_hash: str | None = None, + run_id: str | None = None, + ) -> SkillVersion: ... + + @abstractmethod + def get_skill_version( + self, name: str, version: str, source_type: str, + ) -> SkillVersion: ... + + @abstractmethod + def get_skill_version_by_alias( + self, name: str, alias: str, + ) -> SkillVersion: ... + + @abstractmethod + def get_latest_skill_version(self, name: str) -> SkillVersion: ... + + @abstractmethod + def search_skill_versions( + self, + name: str, + filter_string: str | None = None, + max_results: int = 100, + page_token: str | None = None, + ) -> PagedList[SkillVersion]: ... + + @abstractmethod + def update_skill_version( + self, + name: str, + version: str, + source_type: str, + publish_state: SkillPublishState | None = None, + ) -> SkillVersion: ... + + @abstractmethod + def delete_skill_version( + self, name: str, version: str, source_type: str, + ) -> None: ... + + # --- Tag operations --- + + @abstractmethod + def set_skill_tag( + self, name: str, key: str, value: str, + ) -> None: ... + + @abstractmethod + def delete_skill_tag(self, name: str, key: str) -> None: ... + + @abstractmethod + def set_skill_version_tag( + self, name: str, version: str, source_type: str, + key: str, value: str, + ) -> None: ... + + @abstractmethod + def delete_skill_version_tag( + self, name: str, version: str, source_type: str, key: str, + ) -> None: ... + + # --- Alias operations --- + + @abstractmethod + def set_skill_alias( + self, name: str, alias: str, version: str, source_type: str, + ) -> None: ... + + @abstractmethod + def delete_skill_alias( + self, name: str, alias: str, + ) -> None: ... + + # --- SkillGroup operations --- + + @abstractmethod + def create_skill_group( + self, name: str, description: str | None = None, + ) -> SkillGroup: ... + + @abstractmethod + def get_skill_group(self, name: str) -> SkillGroup: ... + + @abstractmethod + def search_skill_groups( + self, + filter_string: str | None = None, + max_results: int = 100, + page_token: str | None = None, + ) -> PagedList[SkillGroup]: ... + + @abstractmethod + def update_skill_group( + self, + name: str, + description: str | None = None, + status: SkillGroupStatus | None = None, + ) -> SkillGroup: ... + + @abstractmethod + def delete_skill_group(self, name: str) -> None: ... + + # --- SkillGroupVersion operations --- + + @abstractmethod + def create_skill_group_version( + self, + name: str, + version: str, + members: list[SkillGroupVersionMembership], + publish_state: SkillPublishState = SkillPublishState.DRAFT, + ) -> SkillGroupVersion: ... + + @abstractmethod + def get_skill_group_version( + self, name: str, version: str, + ) -> SkillGroupVersion: ... + + @abstractmethod + def get_skill_group_version_by_alias( + self, name: str, alias: str, + ) -> SkillGroupVersion: ... + + @abstractmethod + def get_latest_skill_group_version( + self, name: str, + ) -> SkillGroupVersion: ... + + @abstractmethod + def search_skill_group_versions( + self, + name: str, + filter_string: str | None = None, + max_results: int = 100, + page_token: str | None = None, + ) -> PagedList[SkillGroupVersion]: ... + + @abstractmethod + def update_skill_group_version( + self, + name: str, + version: str, + publish_state: SkillPublishState | None = None, + ) -> SkillGroupVersion: ... + + @abstractmethod + def delete_skill_group_version( + self, name: str, version: str, + ) -> None: ... + + # --- SkillGroup tag operations --- + + @abstractmethod + def set_skill_group_tag( + self, name: str, key: str, value: str, + ) -> None: ... + + @abstractmethod + def delete_skill_group_tag( + self, name: str, key: str, + ) -> None: ... + + @abstractmethod + def set_skill_group_version_tag( + self, name: str, version: str, + key: str, value: str, + ) -> None: ... + + @abstractmethod + def delete_skill_group_version_tag( + self, name: str, version: str, key: str, + ) -> None: ... + + # --- SkillGroup alias operations --- + + @abstractmethod + def set_skill_group_alias( + self, name: str, alias: str, version: str, + ) -> None: ... + + @abstractmethod + def delete_skill_group_alias( + self, name: str, alias: str, + ) -> None: ... +``` + +### REST API + +The REST API uses RESTful nested resource paths, following the pattern +from the MCP Server Registry proposal. + +#### Skill endpoints + +All paths relative to `/ajax-api/3.0/mlflow/skills`. + +| Method | Path | Description | +|---|---|---| +| `POST` | `/` | Create a skill | +| `GET` | `/` | Search skills | +| `GET` | `/{name}` | Get skill by name | +| `PATCH` | `/{name}` | Update skill fields | +| `DELETE` | `/{name}` | Delete skill (cascades) | +| `POST` | `/{name}/versions` | Create a skill version | +| `GET` | `/{name}/versions` | Search versions | +| `GET` | `/{name}/versions/{version}/{source_type}` | Get a specific version | +| `PATCH` | `/{name}/versions/{version}/{source_type}` | Update version | +| `DELETE` | `/{name}/versions/{version}/{source_type}` | Delete a version | +| `POST` | `/{name}/tags` | Set a skill-level tag | +| `DELETE` | `/{name}/tags/{key}` | Delete a skill-level tag | +| `POST` | `/{name}/versions/{version}/{source_type}/tags` | Set a version-level tag | +| `DELETE` | `/{name}/versions/{version}/{source_type}/tags/{key}` | Delete a version tag | +| `POST` | `/{name}/aliases` | Set an alias | +| `GET` | `/{name}/aliases/{alias}` | Resolve alias to version | +| `DELETE` | `/{name}/aliases/{alias}` | Delete an alias | + +#### Skill group endpoints + +All paths relative to `/ajax-api/3.0/mlflow/skill-groups`. + +| Method | Path | Description | +|---|---|---| +| `POST` | `/` | Create a skill group | +| `GET` | `/` | Search skill groups | +| `GET` | `/{name}` | Get group by name | +| `PATCH` | `/{name}` | Update group fields | +| `DELETE` | `/{name}` | Delete group (cascades versions) | +| `POST` | `/{name}/versions` | Create a group version with members | +| `GET` | `/{name}/versions` | Search group versions | +| `GET` | `/{name}/versions/{version}` | Get a specific group version | +| `PATCH` | `/{name}/versions/{version}` | Update group version publish state | +| `DELETE` | `/{name}/versions/{version}` | Delete a group version | +| `POST` | `/{name}/tags` | Set a group-level tag | +| `DELETE` | `/{name}/tags/{key}` | Delete a group-level tag | +| `POST` | `/{name}/versions/{version}/tags` | Set a group version tag | +| `DELETE` | `/{name}/versions/{version}/tags/{key}` | Delete a group version tag | +| `POST` | `/{name}/aliases` | Set a group alias | +| `GET` | `/{name}/aliases/{alias}` | Resolve group alias to version | +| `DELETE` | `/{name}/aliases/{alias}` | Delete a group alias | + +#### Pagination and filtering + +Search endpoints use page-token-based pagination and `filter_string` +expressions following existing MLflow conventions: + +- `name LIKE '%review%'` +- `publish_state = 'published'` +- `tags.team = 'platform'` +- `source_type = 'git'` + +### Python SDK + +The `mlflow.skills` module exposes top-level functions delegating to +`MlflowClient`: + +```python +import mlflow + +# Skills +mlflow.skills.create_skill(name, description=None) +mlflow.skills.get_skill(name) +mlflow.skills.search_skills(filter_string=None, max_results=100, page_token=None) +mlflow.skills.update_skill(name, description=None, status=None) +mlflow.skills.delete_skill(name) + +# Skill versions +mlflow.skills.create_skill_version(name, version, source_type, source_url, publish_state="draft", content_hash=None, run_id=None) +mlflow.skills.get_skill_version(name, version, source_type) +mlflow.skills.get_skill_version_by_alias(name, alias) +mlflow.skills.get_latest_skill_version(name) +mlflow.skills.search_skill_versions(name, filter_string=None, max_results=100, page_token=None) +mlflow.skills.update_skill_version(name, version, source_type, publish_state=None) +mlflow.skills.delete_skill_version(name, version, source_type) + +# Tags +mlflow.skills.set_skill_tag(name, key, value) +mlflow.skills.delete_skill_tag(name, key) +mlflow.skills.set_skill_version_tag(name, version, source_type, key, value) +mlflow.skills.delete_skill_version_tag(name, version, source_type, key) + +# Aliases +mlflow.skills.set_skill_alias(name, alias, version, source_type) +mlflow.skills.delete_skill_alias(name, alias) + +# Skill groups +mlflow.skills.create_skill_group(name, description=None) +mlflow.skills.get_skill_group(name) +mlflow.skills.search_skill_groups(filter_string=None, max_results=100, page_token=None) +mlflow.skills.update_skill_group(name, description=None, status=None) +mlflow.skills.delete_skill_group(name) + +# Skill group versions +mlflow.skills.create_skill_group_version(name, version, members, publish_state="draft") +mlflow.skills.get_skill_group_version(name, version) +mlflow.skills.get_skill_group_version_by_alias(name, alias) +mlflow.skills.get_latest_skill_group_version(name) +mlflow.skills.search_skill_group_versions(name, filter_string=None, max_results=100, page_token=None) +mlflow.skills.update_skill_group_version(name, version, publish_state=None) +mlflow.skills.delete_skill_group_version(name, version) + +# Skill group tags +mlflow.skills.set_skill_group_tag(name, key, value) +mlflow.skills.delete_skill_group_tag(name, key) +mlflow.skills.set_skill_group_version_tag(name, version, key, value) +mlflow.skills.delete_skill_group_version_tag(name, version, key) + +# Skill group aliases +mlflow.skills.set_skill_group_alias(name, alias, version) +mlflow.skills.delete_skill_group_alias(name, alias) +``` + +### CLI commands + +| Command | Description | +|---|---| +| `mlflow skills create` | Create a skill | +| `mlflow skills get` | Get a skill by name | +| `mlflow skills search` | Search skills | +| `mlflow skills update` | Update skill description or status | +| `mlflow skills delete` | Delete a skill and all versions | +| `mlflow skills create-version` | Create a version with source pointer | +| `mlflow skills get-version` | Get a specific version | +| `mlflow skills get-version-by-alias` | Resolve an alias | +| `mlflow skills get-latest-version` | Get the most recent version | +| `mlflow skills search-versions` | Search versions | +| `mlflow skills update-version` | Update publish state | +| `mlflow skills delete-version` | Delete a version | +| `mlflow skills set-tag` | Set a skill-level tag | +| `mlflow skills delete-tag` | Delete a skill-level tag | +| `mlflow skills set-version-tag` | Set a version-level tag | +| `mlflow skills delete-version-tag` | Delete a version-level tag | +| `mlflow skills set-alias` | Set a version alias | +| `mlflow skills delete-alias` | Delete a version alias | +| `mlflow skill-groups create` | Create a skill group | +| `mlflow skill-groups get` | Get a group by name | +| `mlflow skill-groups search` | Search groups | +| `mlflow skill-groups update` | Update group description or status | +| `mlflow skill-groups delete` | Delete a group and all versions | +| `mlflow skill-groups create-version` | Create a group version with members | +| `mlflow skill-groups get-version` | Get a specific group version | +| `mlflow skill-groups get-version-by-alias` | Resolve a group alias | +| `mlflow skill-groups get-latest-version` | Get the most recent group version | +| `mlflow skill-groups search-versions` | Search group versions | +| `mlflow skill-groups update-version` | Update group version publish state | +| `mlflow skill-groups delete-version` | Delete a group version | +| `mlflow skill-groups set-tag` | Set a group-level tag | +| `mlflow skill-groups delete-tag` | Delete a group-level tag | +| `mlflow skill-groups set-version-tag` | Set a group version tag | +| `mlflow skill-groups delete-version-tag` | Delete a group version tag | +| `mlflow skill-groups set-alias` | Set a group version alias | +| `mlflow skill-groups delete-alias` | Delete a group version alias | + +### Error handling + +| Scenario | Error code | HTTP status | +|---|---|---| +| Skill, version, or group not found | `RESOURCE_DOES_NOT_EXIST` | 404 | +| Duplicate skill name, version, or group | `RESOURCE_ALREADY_EXISTS` | 409 | +| Invalid publish state transition | `INVALID_PARAMETER_VALUE` | 400 | +| Unknown source type | `INVALID_PARAMETER_VALUE` | 400 | +| Alias references non-existent version | `RESOURCE_DOES_NOT_EXIST` | 404 | +| Group version member references non-existent skill version | `RESOURCE_DOES_NOT_EXIST` | 404 | +| Delete skill version referenced by a group version | `INVALID_PARAMETER_VALUE` | 400 | +| Delete skill or group with existing versions | Cascading delete (succeeds) | 200 | + +### Workspace scoping + +All skill registry operations are workspace-scoped, following the model +registry pattern: + +- Workspace is resolved via `resolve_entity_workspace_name()` +- Single-tenant deployments use `"default"` +- All database queries filter by workspace +- The REST API derives workspace from the authenticated caller's context +- Version, tag, alias, and group membership operations inherit workspace + from their parent entity + +Cross-workspace sharing (e.g., a platform team publishing skills +visible to all workspaces) is not addressed by this RFC. This is a +cross-registry concern that applies equally to skills, MCP servers, +and other AI asset registries. It is expected to be solved at the +platform level across all MLflow registries rather than piecemeal in +each one. + +### UI + +The Skills page lives under the GenAI workflow in the MLflow sidebar, +alongside Experiments, Prompts, and other AI asset pages. + +The list view shows skills and skill groups in a card-based or table +layout, with name, description, latest version, status, and tags. Users +can filter by status, source type, and search by name or description. A +toggle switches between individual skills and skill groups. + +The detail view for a skill shows metadata, version list, aliases, tags +(including security scan results), and group memberships. + +The detail view for a skill group shows its description, status, version +list, aliases, and tags. Each group version shows its publish state and +the pinned skill versions it contains. + +### Security scan tracking + +The registry does not perform security scans. It provides a metadata +layer for recording and querying scan results using version-level tags. + +Recommended tag conventions: + +| Tag key | Example value | Description | +|---|---|---| +| `scan.prompt-injection.status` | `pass`, `fail`, `warning` | Scan result | +| `scan.prompt-injection.date` | `2026-04-22` | When the scan was run | +| `scan.prompt-injection.tool` | `garak-0.9` | Which tool performed the scan | +| `scan.code-vuln.status` | `pass` | Code vulnerability scan result | +| `scan.code-vuln.date` | `2026-04-22` | When the scan was run | + +These are conventions, not enforced schema. Organizations can define +additional scan tag prefixes for their own scanning tools and criteria. + +The publish state lifecycle supports scan-gated promotion workflows: +a skill version stays in `draft` until scans pass, then is moved to +`published`. The registry does not enforce this workflow, but the +combination of publish state and scan tags makes it easy to implement. + +### Impact on existing MLflow components + +| Component | Impact | Description | +|---|---|---| +| Database schema | **New tables** | 12 new tables via Alembic migration | +| Tracking server | **New routes** | New FastAPI routers for skills and skill groups | +| Python client | **New module** | `mlflow.skills` module | +| CLI | **New command groups** | `mlflow skills` and `mlflow skill-groups` | +| Model registry | **None** | No changes | +| Other registries | **None** | No changes | +| UI | **New page** | Skills page under GenAI workflow | +| Authentication/RBAC | **Leverages existing** | Uses existing workspace and permission infrastructure | + +## Drawbacks + +- **New database tables.** Twelve new tables and an Alembic migration add + to the schema surface. This is more than a minimal registry, but the + additional tables support versioned skill groups with full tag and + alias support. +- **Pattern duplication.** Some duplication with the MCP Server Registry + until shared abstractions are extracted. The consistent design approach + mitigates this. +- **Source URL validity.** The registry stores source pointers but cannot + guarantee they remain valid. Broken links are possible. The optional + `content_hash` field mitigates content tampering but does not prevent + link rot. This is inherent to a metadata-first design and is the + same tradeoff as any catalog that points to external content. +- **No artifact storage.** Unlike the Databricks skill registry + prototype (which stores skill bundles as MLflow artifacts), this design + does not provide a self-contained backup of skill content. If the + source system goes away, the metadata remains but the content is lost. + +# Alternatives + +## Store skill artifacts directly in MLflow + +Store skill bundles (SKILL.md + scripts + assets) as MLflow artifacts +alongside the metadata, similar to how the Databricks prototype works. + +Rejected because: +- Skills are already versioned and stored in Git, OCI, or other systems. + Duplicating content into MLflow artifact storage adds complexity + without clear value. +- Metadata-first aligns with the MCP Server Registry design, which + stores a `server_json` payload but not the MCP server runtime itself. +- Source pointers federate across distribution mechanisms naturally. + Artifact storage forces centralization. +- Organizations that want artifact backup can use OCI registries, which + already provide versioned, content-addressable storage. + +## Reuse the Model Registry for skills + +Store skill metadata as model registry entries with skill-specific tags. + +Rejected because: +- Model registry uses auto-incremented integer versions; skills use + publisher-supplied version strings. +- Model registry lifecycle (staging/production/archived) does not match + the publish-state lifecycle needed for skills. +- Conceptual confusion: skills are not models. +- No support for skill groups. + +## Build a standalone skill registry outside MLflow + +Build a separate service for skill governance, independent of MLflow. + +Rejected because: +- Duplicates the registry infrastructure MLflow already provides. +- No integration with MLflow traces for usage analytics. +- Forces users to manage another service. +- Contradicts the emerging pattern of MLflow as the governance layer for + AI assets. + +## 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. It is not +rejected as a bad approach; rather, this RFC proposes a governance layer +on top of Git for enterprises that need publish-state lifecycle, security +scan tracking, and federated discovery. The two approaches are +complementary. + +# Adoption strategy + +This is a new feature, not a breaking change. Adoption is incremental: + +**Initial release:** +- Entities, database schema, store implementation, REST API, Python SDK, + CLI, and basic UI. +- Users can register skills with source pointers, manage publish state, + record scan results as tags, organize skills into groups, and discover + published skills. +- Existing MLflow functionality is unaffected. + +**Follow-up:** +- Agent trace integration: traces automatically record which registered + skill version was used, linking back to the registry. +- Usage analytics dashboard based on trace metadata. +- Shared base extraction across AI asset registries (skills, MCP + servers, etc.) once patterns are validated. +- Additional source types as demand emerges. From 06119745e5e64f773aad9c13a756c9ef5836fcb1 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Thu, 23 Apr 2026 14:32:25 -0400 Subject: [PATCH 02/19] Update RFC-0005 metadata with PR and issue links Co-Authored-By: Claude Opus 4.6 --- rfcs/0005-skill-registry/0005-skill-registry.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index c17b81c..cd97274 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -3,7 +3,7 @@ | start_date | 2026-04-22 | | :----------- | :--------- | | mlflow_issue | [mlflow/mlflow#22833](https://github.com/mlflow/mlflow/issues/22833) | -| rfc_pr | | +| rfc_pr | [mlflow/rfcs#10](https://github.com/mlflow/rfcs/pull/10) | | Author(s) | Bill Murdock (Red Hat) | | :--------------------- | :-- | From 94665d64acff04dafa0e8217836936c0813ab4d4 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Mon, 27 Apr 2026 08:35:45 -0400 Subject: [PATCH 03/19] Address review feedback on RFC-0005 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Switch to YAML frontmatter to match repo template - Make ER diagram a mermaid code block - Rename source_url → source (MLflow consistency) - Rename content_hash → content_digest (OCI alignment) - Use SkillSourceType enum consistently for alias/membership types - Fix Skill.aliases type to list[SkillAlias] (was dict, missing source_type) - Fix search_skills example to use search_skill_versions (publish_state is on version) - Clarify alias resolve returns both version and source_type - Specify valid filter fields per search endpoint - Fix delete semantics: skill delete blocked when versions referenced by groups - Drop Databricks dogfooding anecdote - Remove "Relationship to other AI asset registries" section - Remove "Impact on existing MLflow components" table - Condense SDK/CLI sections (defer to store interface + examples) - Tighten drawbacks and trim weaker alternatives Co-Authored-By: Claude Opus 4.6 --- .../0005-skill-registry.md | 316 +++++------------- 1 file changed, 79 insertions(+), 237 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index cd97274..21534d1 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -1,13 +1,14 @@ -# RFC: Skill Registry +--- +start_date: 2026-04-22 +mlflow_issue: https://github.com/mlflow/mlflow/issues/22833 +rfc_pr: https://github.com/mlflow/rfcs/pull/10 +--- -| start_date | 2026-04-22 | -| :----------- | :--------- | -| mlflow_issue | [mlflow/mlflow#22833](https://github.com/mlflow/mlflow/issues/22833) | -| rfc_pr | [mlflow/rfcs#10](https://github.com/mlflow/rfcs/pull/10) | +# RFC: Skill Registry | Author(s) | Bill Murdock (Red Hat) | | :--------------------- | :-- | -| **Date Last Modified** | 2026-04-22 | +| **Date Last Modified** | 2026-04-27 | | **AI Assistant(s)** | Claude Code (Opus 4.6) | # Summary @@ -42,8 +43,8 @@ version = mlflow.skills.create_skill_version( name="code-review", version="1.0.0", source_type="git", - source_url="https://github.com/acme/agent-skills/tree/v1.0.0/code-review", - content_hash="sha256:a3f2b8c...", + source="https://github.com/acme/agent-skills/tree/v1.0.0/code-review", + content_digest="sha256:a3f2b8c...", ) # version.publish_state == "draft" @@ -128,8 +129,9 @@ mlflow.skills.set_skill_group_alias( ## Discover and consume skills ```python -# Search for published skills -skills = mlflow.skills.search_skills( +# Search for published skill versions +versions = mlflow.skills.search_skill_versions( + name="code-review", filter_string="publish_state = 'published'", ) @@ -145,7 +147,7 @@ version = mlflow.skills.get_skill_version( source_type="git", ) # version.source_type == "git" -# version.source_url == "https://github.com/acme/agent-skills/tree/v1.0.0/code-review" +# version.source == "https://github.com/acme/agent-skills/tree/v1.0.0/code-review" # Resolve by alias version = mlflow.skills.get_skill_version_by_alias( @@ -175,8 +177,8 @@ mlflow skills create --name code-review \ --description "Reviews pull requests" mlflow skills create-version --name code-review --version 1.0.0 \ --source-type git \ - --source-url https://github.com/acme/agent-skills/tree/v1.0.0/code-review \ - --content-hash sha256:a3f2b8c... + --source https://github.com/acme/agent-skills/tree/v1.0.0/code-review \ + --content-digest sha256:a3f2b8c... # Publish and alias mlflow skills update-version --name code-review --version 1.0.0 \ @@ -196,8 +198,9 @@ mlflow skill-groups update-version --name pr-workflow --version 1.0.0 \ mlflow skill-groups set-alias --name pr-workflow --alias production \ --version 1.0.0 -# Search published skills -mlflow skills search --filter "publish_state = 'published'" +# Search published skill versions +mlflow skills search-versions --name code-review \ + --filter "publish_state = 'published'" # Search active groups mlflow skill-groups search --filter "status = 'active'" @@ -214,9 +217,7 @@ accumulate skills across teams and repositories. 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. As Databricks engineers -have noted after dogfooding an MLflow skill registry prototype, GitHub -is sufficient for the typical developer use case. +versioning, collaboration, and access control. However, enterprises face governance challenges that Git alone does not address: @@ -272,26 +273,6 @@ address: used. Combined with registry metadata, this enables organizations to understand adoption and make data-driven promotion decisions. -### Relationship to other AI asset registries - -The MCP Server Registry proposal -([#22625](https://github.com/mlflow/mlflow/issues/22625)) establishes -the pattern for governed, metadata-first AI asset registries in MLflow. -The skill registry is the next concrete instance of this pattern, -following the same conventions (entity/version/alias/tag, abstract -store, SQL storage, REST API, workspace scoping). - -Skill-specific design decisions include: - -- Typed source pointers (git/oci/zip) instead of an embedded payload - (MCP stores a `server_json` payload; skills point to external content) -- Skill groups as a first-class entity for organizing related skills -- Security scan tracking via tags as an explicit use case - -This RFC focuses on delivering the skill registry. Shared abstractions -across asset types can be extracted once multiple concrete registries -exist and the common patterns are well understood. - ### Out of scope - **Skill artifact storage.** The registry stores metadata and source @@ -318,7 +299,8 @@ exist and the common patterns are well understood. ### Entities and data model -``` +```mermaid +erDiagram Skill ||--o{ SkillVersion : "has versions" Skill ||--o{ SkillTag : "has tags" Skill ||--o{ SkillAlias : "has aliases" @@ -353,7 +335,7 @@ class Skill: workspace: str | None = None status: SkillStatus = SkillStatus.ACTIVE tags: dict[str, str] = field(default_factory=dict) - aliases: dict[str, str] = field(default_factory=dict) + aliases: list[SkillAlias] = field(default_factory=list) last_registered_version: str | None = None created_by: str | None = None last_updated_by: str | None = None @@ -365,7 +347,7 @@ class Skill: |---|---|---| | `name` | `str` | Stable logical asset name, unique within a workspace | | `status` | `SkillStatus` | Skill-level lifecycle: `active`, `deprecated`, `retired` | -| `aliases` | `dict[str, str]` | Stable version pointers, e.g. `{"production": "1.2.0"}` | +| `aliases` | `list[SkillAlias]` | Stable version pointers, each resolving to a `(version, source_type)` pair | | `last_registered_version` | `str` | Most recently registered version string | | `workspace` | `str` | Visibility boundary | @@ -393,9 +375,9 @@ class SkillVersion: name: str version: str source_type: SkillSourceType - source_url: str + source: str publish_state: SkillPublishState = SkillPublishState.DRAFT - content_hash: str | None = None + content_digest: str | None = None tags: dict[str, str] = field(default_factory=dict) run_id: str | None = None workspace: str | None = None @@ -409,8 +391,8 @@ class SkillVersion: |---|---|---| | `version` | `str` | Publisher-supplied version string. Semver recommended but not enforced | | `source_type` | `SkillSourceType` | Distribution mechanism: `git`, `oci`, `zip` | -| `source_url` | `str` | URL pointing to the skill content in the source system | -| `content_hash` | `str` | Optional content digest for integrity verification (e.g., `sha256:abc123...`) | +| `source` | `str` | Pointer to the skill content in the source system (URL, OCI reference, etc.) | +| `content_digest` | `str` | Optional digest for integrity verification (e.g., `sha256:abc123...`). Aligns with OCI digest terminology | | `publish_state` | `SkillPublishState` | Per-version surfacing lifecycle | | `run_id` | `str` | Optional MLflow run association for trace linkage | @@ -424,19 +406,19 @@ is unique within a workspace. This allows the same skill version to be registered from multiple distribution mechanisms (e.g., Git and OCI) without requiring different version strings. -**Content integrity.** The optional `content_hash` field stores a +**Content integrity.** The optional `content_digest` field stores a digest of the skill content at registration time (e.g., `sha256:abc123...`). Consumers can use this to verify that the content -at `source_url` has not changed since registration. For OCI sources, -this is the native image digest. For Git sources, this is a hash of +at `source` has not changed since registration. For OCI sources, +this is the native image digest. For Git sources, this is a digest of the skill file contents at the pinned commit. For ZIP sources, this is -a hash of the archive. The registry stores the hash but does not +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_url`, `content_hash`, -and `version` are immutable after creation. To point to different -content, register a new version. Mutable fields (`publish_state`, -`tags`) can be updated independently. +**Immutability contract.** `source_type`, `source`, `content_digest`, +and `version` are immutable after creation. To point to different content, +register a new version. Mutable fields (`publish_state`, `tags`) can be +updated independently. #### SkillGroup @@ -457,7 +439,7 @@ class SkillGroup: workspace: str | None = None status: SkillGroupStatus = SkillGroupStatus.ACTIVE tags: dict[str, str] = field(default_factory=dict) - aliases: dict[str, str] = field(default_factory=dict) + aliases: list["SkillGroupAlias"] = field(default_factory=list) last_registered_version: str | None = None created_by: str | None = None last_updated_by: str | None = None @@ -505,7 +487,7 @@ class SkillGroupVersionMembership: group_version: str skill_name: str skill_version: str - skill_source_type: str + skill_source_type: SkillSourceType workspace: str | None = None ``` @@ -528,10 +510,10 @@ class SkillGroupAlias: ```python @dataclass(frozen=True) class SkillAlias: - name: str # parent Skill name - alias: str # e.g., "production", "staging" - version: str # version string this alias points to - source_type: str # source type this alias points to + name: str # parent Skill name + alias: str # e.g., "production", "staging" + version: str # version string this alias points to + source_type: SkillSourceType # source type this alias points to @dataclass(frozen=True) class SkillTag: @@ -608,8 +590,8 @@ workspace-scoped. | `name` | `String(256)` | PK, FK | | `version` | `String(256)` | PK, publisher-supplied | | `source_type` | `String(20)` | PK; `git`, `oci`, `zip`, etc. | -| `source_url` | `String(2048)` | URL to skill content | -| `content_hash` | `String(512)` | optional content digest | +| `source` | `String(2048)` | pointer to skill content | +| `content_digest` | `String(512)` | optional integrity digest | | `publish_state` | `String(20)` | default `'draft'` | | `run_id` | `String(32)` | optional MLflow run linkage | | `created_by` | `String(256)` | | @@ -772,9 +754,9 @@ class AbstractSkillRegistryStore: name: str, version: str, source_type: str, - source_url: str, + source: str, publish_state: SkillPublishState = SkillPublishState.DRAFT, - content_hash: str | None = None, + content_digest: str | None = None, run_id: str | None = None, ) -> SkillVersion: ... @@ -986,7 +968,7 @@ All paths relative to `/ajax-api/3.0/mlflow/skills`. | `POST` | `/{name}/versions/{version}/{source_type}/tags` | Set a version-level tag | | `DELETE` | `/{name}/versions/{version}/{source_type}/tags/{key}` | Delete a version tag | | `POST` | `/{name}/aliases` | Set an alias | -| `GET` | `/{name}/aliases/{alias}` | Resolve alias to version | +| `GET` | `/{name}/aliases/{alias}` | Resolve alias to `SkillVersion` (returns version and source_type) | | `DELETE` | `/{name}/aliases/{alias}` | Delete an alias | #### Skill group endpoints @@ -1016,114 +998,24 @@ All paths relative to `/ajax-api/3.0/mlflow/skill-groups`. #### Pagination and filtering Search endpoints use page-token-based pagination and `filter_string` -expressions following existing MLflow conventions: +expressions following existing MLflow conventions. -- `name LIKE '%review%'` -- `publish_state = 'published'` -- `tags.team = 'platform'` -- `source_type = 'git'` +**Skills and skill groups:** `name LIKE '%review%'`, `status = 'active'`, +`tags.team = 'platform'` -### Python SDK +**Skill versions:** `publish_state = 'published'`, +`source_type = 'git'`, `tags.scan.prompt-injection.status = 'pass'` -The `mlflow.skills` module exposes top-level functions delegating to -`MlflowClient`: +**Skill group versions:** `publish_state = 'published'`, +`tags.approved = 'true'` -```python -import mlflow - -# Skills -mlflow.skills.create_skill(name, description=None) -mlflow.skills.get_skill(name) -mlflow.skills.search_skills(filter_string=None, max_results=100, page_token=None) -mlflow.skills.update_skill(name, description=None, status=None) -mlflow.skills.delete_skill(name) - -# Skill versions -mlflow.skills.create_skill_version(name, version, source_type, source_url, publish_state="draft", content_hash=None, run_id=None) -mlflow.skills.get_skill_version(name, version, source_type) -mlflow.skills.get_skill_version_by_alias(name, alias) -mlflow.skills.get_latest_skill_version(name) -mlflow.skills.search_skill_versions(name, filter_string=None, max_results=100, page_token=None) -mlflow.skills.update_skill_version(name, version, source_type, publish_state=None) -mlflow.skills.delete_skill_version(name, version, source_type) - -# Tags -mlflow.skills.set_skill_tag(name, key, value) -mlflow.skills.delete_skill_tag(name, key) -mlflow.skills.set_skill_version_tag(name, version, source_type, key, value) -mlflow.skills.delete_skill_version_tag(name, version, source_type, key) - -# Aliases -mlflow.skills.set_skill_alias(name, alias, version, source_type) -mlflow.skills.delete_skill_alias(name, alias) - -# Skill groups -mlflow.skills.create_skill_group(name, description=None) -mlflow.skills.get_skill_group(name) -mlflow.skills.search_skill_groups(filter_string=None, max_results=100, page_token=None) -mlflow.skills.update_skill_group(name, description=None, status=None) -mlflow.skills.delete_skill_group(name) - -# Skill group versions -mlflow.skills.create_skill_group_version(name, version, members, publish_state="draft") -mlflow.skills.get_skill_group_version(name, version) -mlflow.skills.get_skill_group_version_by_alias(name, alias) -mlflow.skills.get_latest_skill_group_version(name) -mlflow.skills.search_skill_group_versions(name, filter_string=None, max_results=100, page_token=None) -mlflow.skills.update_skill_group_version(name, version, publish_state=None) -mlflow.skills.delete_skill_group_version(name, version) - -# Skill group tags -mlflow.skills.set_skill_group_tag(name, key, value) -mlflow.skills.delete_skill_group_tag(name, key) -mlflow.skills.set_skill_group_version_tag(name, version, key, value) -mlflow.skills.delete_skill_group_version_tag(name, version, key) - -# Skill group aliases -mlflow.skills.set_skill_group_alias(name, alias, version) -mlflow.skills.delete_skill_group_alias(name, alias) -``` +### Python SDK and CLI -### CLI commands - -| Command | Description | -|---|---| -| `mlflow skills create` | Create a skill | -| `mlflow skills get` | Get a skill by name | -| `mlflow skills search` | Search skills | -| `mlflow skills update` | Update skill description or status | -| `mlflow skills delete` | Delete a skill and all versions | -| `mlflow skills create-version` | Create a version with source pointer | -| `mlflow skills get-version` | Get a specific version | -| `mlflow skills get-version-by-alias` | Resolve an alias | -| `mlflow skills get-latest-version` | Get the most recent version | -| `mlflow skills search-versions` | Search versions | -| `mlflow skills update-version` | Update publish state | -| `mlflow skills delete-version` | Delete a version | -| `mlflow skills set-tag` | Set a skill-level tag | -| `mlflow skills delete-tag` | Delete a skill-level tag | -| `mlflow skills set-version-tag` | Set a version-level tag | -| `mlflow skills delete-version-tag` | Delete a version-level tag | -| `mlflow skills set-alias` | Set a version alias | -| `mlflow skills delete-alias` | Delete a version alias | -| `mlflow skill-groups create` | Create a skill group | -| `mlflow skill-groups get` | Get a group by name | -| `mlflow skill-groups search` | Search groups | -| `mlflow skill-groups update` | Update group description or status | -| `mlflow skill-groups delete` | Delete a group and all versions | -| `mlflow skill-groups create-version` | Create a group version with members | -| `mlflow skill-groups get-version` | Get a specific group version | -| `mlflow skill-groups get-version-by-alias` | Resolve a group alias | -| `mlflow skill-groups get-latest-version` | Get the most recent group version | -| `mlflow skill-groups search-versions` | Search group versions | -| `mlflow skill-groups update-version` | Update group version publish state | -| `mlflow skill-groups delete-version` | Delete a group version | -| `mlflow skill-groups set-tag` | Set a group-level tag | -| `mlflow skill-groups delete-tag` | Delete a group-level tag | -| `mlflow skill-groups set-version-tag` | Set a group version tag | -| `mlflow skill-groups delete-version-tag` | Delete a group version tag | -| `mlflow skill-groups set-alias` | Set a group version alias | -| `mlflow skill-groups delete-alias` | Delete a group version alias | +The `mlflow.skills` module exposes top-level functions delegating to +`MlflowClient`, with a 1:1 mapping to the abstract store methods above. +Two CLI command groups (`mlflow skills` and `mlflow skill-groups`) +provide the same operations from the command line. See the basic +examples at the top of this RFC for usage. ### Error handling @@ -1136,7 +1028,8 @@ mlflow.skills.delete_skill_group_alias(name, alias) | Alias references non-existent version | `RESOURCE_DOES_NOT_EXIST` | 404 | | Group version member references non-existent skill version | `RESOURCE_DOES_NOT_EXIST` | 404 | | Delete skill version referenced by a group version | `INVALID_PARAMETER_VALUE` | 400 | -| Delete skill or group with existing versions | Cascading delete (succeeds) | 200 | +| Delete skill with versions referenced by a group | `INVALID_PARAMETER_VALUE` | 400 | +| Delete skill or group with no group references | Cascading delete (succeeds) | 200 | ### Workspace scoping @@ -1197,89 +1090,38 @@ a skill version stays in `draft` until scans pass, then is moved to `published`. The registry does not enforce this workflow, but the combination of publish state and scan tags makes it easy to implement. -### Impact on existing MLflow components - -| Component | Impact | Description | -|---|---|---| -| Database schema | **New tables** | 12 new tables via Alembic migration | -| Tracking server | **New routes** | New FastAPI routers for skills and skill groups | -| Python client | **New module** | `mlflow.skills` module | -| CLI | **New command groups** | `mlflow skills` and `mlflow skill-groups` | -| Model registry | **None** | No changes | -| Other registries | **None** | No changes | -| UI | **New page** | Skills page under GenAI workflow | -| Authentication/RBAC | **Leverages existing** | Uses existing workspace and permission infrastructure | - ## Drawbacks -- **New database tables.** Twelve new tables and an Alembic migration add - to the schema surface. This is more than a minimal registry, but the - additional tables support versioned skill groups with full tag and - alias support. -- **Pattern duplication.** Some duplication with the MCP Server Registry - until shared abstractions are extracted. The consistent design approach - mitigates this. -- **Source URL validity.** The registry stores source pointers but cannot - guarantee they remain valid. Broken links are possible. The optional - `content_hash` field mitigates content tampering but does not prevent - link rot. This is inherent to a metadata-first design and is the - same tradeoff as any catalog that points to external content. -- **No artifact storage.** Unlike the Databricks skill registry - prototype (which stores skill bundles as MLflow artifacts), this design - does not provide a self-contained backup of skill content. If the - source system goes away, the metadata remains but the content is lost. +- **Source pointer validity.** The registry stores source pointers but + cannot guarantee they remain valid. The optional `content_digest` + field mitigates content tampering but does not prevent link rot. This + is inherent to a metadata-first design. +- **No artifact storage.** This design does not provide a self-contained + backup of skill content. If the source system goes away, the metadata + remains but the content is lost. # Alternatives ## Store skill artifacts directly in MLflow Store skill bundles (SKILL.md + scripts + assets) as MLflow artifacts -alongside the metadata, similar to how the Databricks prototype works. - -Rejected because: -- Skills are already versioned and stored in Git, OCI, or other systems. - Duplicating content into MLflow artifact storage adds complexity - without clear value. -- Metadata-first aligns with the MCP Server Registry design, which - stores a `server_json` payload but not the MCP server runtime itself. -- Source pointers federate across distribution mechanisms naturally. - Artifact storage forces centralization. -- Organizations that want artifact backup can use OCI registries, which - already provide versioned, content-addressable storage. - -## Reuse the Model Registry for skills - -Store skill metadata as model registry entries with skill-specific tags. - -Rejected because: -- Model registry uses auto-incremented integer versions; skills use - publisher-supplied version strings. -- Model registry lifecycle (staging/production/archived) does not match - the publish-state lifecycle needed for skills. -- Conceptual confusion: skills are not models. -- No support for skill groups. - -## Build a standalone skill registry outside MLflow - -Build a separate service for skill governance, independent of MLflow. - -Rejected because: -- Duplicates the registry infrastructure MLflow already provides. -- No integration with MLflow traces for usage analytics. -- Forces users to manage another service. -- Contradicts the emerging pattern of MLflow as the governance layer for - AI assets. +alongside the metadata. + +Rejected because skills are already versioned and stored in Git, OCI, or +other systems. Source pointers federate across distribution mechanisms +naturally; artifact storage forces centralization. Organizations that +want artifact backup can use OCI registries, which already provide +versioned, content-addressable storage. ## 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. It is not -rejected as a bad approach; rather, this RFC proposes a governance layer -on top of Git for enterprises that need publish-state lifecycle, security -scan tracking, and federated discovery. The two approaches are -complementary. +This is sufficient for individual developers and small teams. This RFC +proposes a governance layer on top of Git for enterprises that need +publish-state lifecycle, security scan tracking, and federated discovery. +The two approaches are complementary. # Adoption strategy From 8cbccbdedf4e038588062b0164bad973c242badd Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Mon, 27 Apr 2026 08:50:09 -0400 Subject: [PATCH 04/19] Simplify SkillGroupVersionMembership entity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove redundant group_name/group_version/workspace fields from the entity — parent identity is provided by the enclosing SkillGroupVersion. The DB schema retains those columns as FKs. Co-Authored-By: Claude Opus 4.6 --- rfcs/0005-skill-registry/0005-skill-registry.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index 21534d1..e71965d 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -97,15 +97,12 @@ group_version = mlflow.skills.create_skill_group_version( version="1.0.0", members=[ SkillGroupVersionMembership( - group_name="pr-workflow", group_version="1.0.0", skill_name="code-review", skill_version="1.0.0", skill_source_type="git", ), SkillGroupVersionMembership( - group_name="pr-workflow", group_version="1.0.0", skill_name="test-coverage", skill_version="2.1.0", skill_source_type="git", ), SkillGroupVersionMembership( - group_name="pr-workflow", group_version="1.0.0", skill_name="security-scan", skill_version="1.0.0", skill_source_type="oci", ), ], @@ -478,17 +475,15 @@ independently. #### SkillGroupVersionMembership Each membership entry pins a specific skill version (including source -type). +type). The parent group identity is provided by the enclosing +`SkillGroupVersion`; the storage layer adds those columns as FKs. ```python @dataclass(frozen=True) class SkillGroupVersionMembership: - group_name: str - group_version: str skill_name: str skill_version: str skill_source_type: SkillSourceType - workspace: str | None = None ``` A skill can appear in multiple groups and multiple group versions. From f91df20e8861d0bb30c3bc1892f7d55b6e59c40e Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Mon, 27 Apr 2026 15:59:54 -0400 Subject: [PATCH 05/19] Remove source_type from version PK, add RFC-0006 harness integration Version uniqueness is now (name, version) instead of (name, version, source_type). source_type and source are optional fields on SkillVersion. Cascaded this change through entities, DB schema, store interface, REST API paths, examples, and CLI. Added RFC-0006 covering harness-specific installation with adapters for Claude Code, Codex CLI, Cursor, and Antigravity. Co-Authored-By: Claude Opus 4.6 --- .../0005-skill-registry.md | 470 +++++++++++++----- .../0006-skill-harness-integration.md | 375 ++++++++++++++ 2 files changed, 722 insertions(+), 123 deletions(-) create mode 100644 rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index e71965d..d43c8f2 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -14,16 +14,28 @@ rfc_pr: https://github.com/mlflow/rfcs/pull/10 # 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.) rather than skill -artifacts directly. It provides enterprise governance on top of existing -skill distribution mechanisms: lifecycle management, security scan +AI agent capabilities. The registry stores metadata and typed source +pointers (to Git repos, OCI registries, ZIP archives, etc.) rather +than artifacts directly. It provides enterprise governance on top of +existing distribution mechanisms: lifecycle management, security scan tracking, usage analytics via traces, and federated discovery across sources. -The registry also introduces skill groups as a first-class concept, -allowing related skills to be organized into coherent toolboxes or -workflows and discovered as a unit. +The registry tracks four capability kinds under the `mlflow skills` +namespace: + +- **Skills** (SKILL.md) — reusable agent instructions +- **Agents** (agent .md) — sub-agent definitions +- **MCP servers** (JSON config) — tool server integrations +- **Hooks** (harness-specific) — event-triggered actions + +Skill groups bundle related capabilities of any kind into versioned, +governed units that map to the "plugin" concept in agent harnesses. + +`mlflow skills pull` provides a harness-agnostic way to fetch +registered content from its source. Harness-specific installation +(manifest generation, directory placement) is covered in a companion +RFC (RFC-0006). # Basic example @@ -60,21 +72,18 @@ mlflow.skills.set_skill_alias( name="code-review", alias="production", version="1.0.0", - source_type="git", ) # Record a security scan result as a tag mlflow.skills.set_skill_version_tag( name="code-review", version="1.0.0", - source_type="git", key="scan.prompt-injection.status", value="pass", ) mlflow.skills.set_skill_version_tag( name="code-review", version="1.0.0", - source_type="git", key="scan.prompt-injection.date", value="2026-04-22", ) @@ -97,13 +106,13 @@ group_version = mlflow.skills.create_skill_group_version( version="1.0.0", members=[ SkillGroupVersionMembership( - skill_name="code-review", skill_version="1.0.0", skill_source_type="git", + skill_name="code-review", skill_version="1.0.0", ), SkillGroupVersionMembership( - skill_name="test-coverage", skill_version="2.1.0", skill_source_type="git", + skill_name="test-coverage", skill_version="2.1.0", ), SkillGroupVersionMembership( - skill_name="security-scan", skill_version="1.0.0", skill_source_type="oci", + skill_name="security-scan", skill_version="1.0.0", ), ], ) @@ -123,6 +132,105 @@ mlflow.skills.set_skill_group_alias( ) ``` +## Register other capability kinds + +```python +# Register a sub-agent +mlflow.skills.create_skill( + name="security-auditor", + kind="agent", + description="Security specialist for auth and payment code", +) +mlflow.skills.create_skill_version( + name="security-auditor", + version="1.0.0", + source_type="git", + source="https://github.com/acme/agent-skills/tree/v1.0.0/security-auditor", +) + +# Register an MCP server +mlflow.skills.create_skill( + name="github-mcp", + kind="mcp-server", + description="GitHub integration via MCP", +) +mlflow.skills.create_skill_version( + name="github-mcp", + version="2.0.0", + source_type="oci", + source="ghcr.io/acme/github-mcp:2.0.0", + content_digest="sha256:b4e9f1d...", +) + +# Register a hook +mlflow.skills.create_skill( + name="pre-commit-scan", + kind="hook", + description="Runs security scan before tool commits", +) +mlflow.skills.create_skill_version( + name="pre-commit-scan", + version="1.0.0", + source_type="git", + source="https://github.com/acme/agent-skills/tree/v1.0.0/pre-commit-scan", +) +``` + +## Create a skill group with mixed capability kinds + +```python +from mlflow.entities import SkillGroupVersionMembership + +group = mlflow.skills.create_skill_group( + name="pr-workflow", + description="End-to-end pull request review workflow", +) + +# A group version can bundle skills, agents, MCP servers, and hooks +group_version = mlflow.skills.create_skill_group_version( + name="pr-workflow", + version="1.0.0", + members=[ + SkillGroupVersionMembership( + skill_name="code-review", skill_version="1.0.0", + ), + SkillGroupVersionMembership( + skill_name="security-auditor", skill_version="1.0.0", + ), + SkillGroupVersionMembership( + skill_name="github-mcp", skill_version="2.0.0", + ), + ], +) +``` + +## Pull skills to a local directory + +```python +# Pull a single skill version +mlflow.skills.pull_skill( + name="code-review", + alias="production", + destination="./skills/code-review", +) + +# Pull an entire skill group (all members) +mlflow.skills.pull_skill_group( + name="pr-workflow", + alias="production", + destination="./plugins/pr-workflow", +) +``` + +```bash +# CLI equivalents +mlflow skills pull --name code-review --alias production \ + --destination ./skills/code-review + +mlflow skills pull-group --name pr-workflow --alias production \ + --destination ./plugins/pr-workflow +``` + ## Discover and consume skills ```python @@ -141,7 +249,6 @@ groups = mlflow.skills.search_skill_groups( version = mlflow.skills.get_skill_version( name="code-review", version="1.0.0", - source_type="git", ) # version.source_type == "git" # version.source == "https://github.com/acme/agent-skills/tree/v1.0.0/code-review" @@ -179,17 +286,17 @@ mlflow skills create-version --name code-review --version 1.0.0 \ # Publish and alias mlflow skills update-version --name code-review --version 1.0.0 \ - --source-type git --publish-state published + --publish-state published mlflow skills set-alias --name code-review --alias production \ - --version 1.0.0 --source-type git + --version 1.0.0 # Create a group and a versioned membership snapshot mlflow skill-groups create --name pr-workflow \ --description "End-to-end PR review workflow" mlflow skill-groups create-version --name pr-workflow --version 1.0.0 \ - --member code-review:1.0.0:git \ - --member test-coverage:2.1.0:git \ - --member security-scan:1.0.0:oci + --member code-review:1.0.0 \ + --member test-coverage:2.1.0 \ + --member security-scan:1.0.0 mlflow skill-groups update-version --name pr-workflow --version 1.0.0 \ --publish-state published mlflow skill-groups set-alias --name pr-workflow --alias production \ @@ -207,50 +314,64 @@ mlflow skill-groups search --filter "status = 'active'" ### The problem -AI agent skills (reusable tool definitions, workflow steps, and coding -assistant capabilities) are becoming a critical asset class in -enterprise AI platforms. As organizations adopt agentic AI, they -accumulate skills across teams and repositories. +AI agent capabilities — skills, sub-agents, MCP server configurations, +and hooks — are becoming a critical asset class in enterprise AI +platforms. As organizations adopt agentic AI, they accumulate these +capabilities across teams, repositories, and agent harnesses. + +A cross-harness portable format is emerging around SKILL.md files (for +skills and agents), MCP server configs (for tool integrations), and +hooks (for event-triggered actions). Agent harnesses including Claude +Code, Codex CLI, Cursor, GitHub Copilot, OpenClaw, Kilo Code, and +Antigravity support overlapping subsets of these formats, with SKILL.md +and MCP being the most broadly adopted. -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. +Today, these capabilities 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 publish-state lifecycle.** Git has no concept of "this skill - version is approved for production use" vs. "this is a draft." Teams - resort to branch naming conventions or external tracking to manage - skill promotion. +1. **No publish-state lifecycle.** Git has no concept of "this version + is approved for production use" vs. "this is a draft." Teams resort + to branch naming conventions or external tracking to manage + promotion. 2. **No security scan tracking.** Skills may contain executable code or - be vulnerable to prompt injection. There is no standard place to - record whether a skill version has been scanned and what the results - were. + be vulnerable to prompt injection. Hooks execute arbitrary commands. + There is no standard place to record whether a capability version + has been scanned and what the results were. -3. **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. **Fragmented discovery.** Capabilities may live in multiple Git + repos, OCI registries, or other distribution systems. There is no + single discovery layer across all of these. -4. **No skill grouping.** Skills often work together as coherent - toolboxes or multi-step workflows. Agent harnesses like Claude Code - support plugin-level grouping, but there is no agent-neutral way to - represent these relationships. +4. **No cross-kind grouping.** Agent harnesses like Claude Code and + Codex CLI support plugins that bundle skills, agents, MCP servers, + and hooks together. But there is no agent-neutral way to represent + these bundles for governance and discovery. 5. **No usage analytics linkage.** MLflow traces can capture skill metadata, but without a governed registry, there is no way to link - trace data back to a governed skill record to understand adoption - across an organization. + trace data back to a governed record to understand adoption across + an organization. + +6. **No pull mechanism.** Once a user discovers a capability in the + registry, there is no standard way to fetch its content from the + source system. Users must manually copy source pointers and run + harness-specific install steps. ### Use cases -1. **Governed registration**: Platform administrators register skill - metadata with typed source pointers to where the skill content lives - (Git, OCI, ZIP). The registry governs; the source system stores. +1. **Governed registration**: Platform administrators register + capability metadata with typed source pointers to where the content + lives (Git, OCI, ZIP). The registry governs; the source system + stores. All four capability kinds (skill, agent, mcp-server, hook) + use the same registration model. -2. **Lifecycle management**: Skill versions move through publish states - (draft, published, deprecated, retired) to control downstream +2. **Lifecycle management**: Capability versions move through publish + states (draft, published, deprecated, retired) to control downstream surfacing. This is the governance layer that Git lacks. 3. **Security scan tracking**: Scan results (prompt injection, code @@ -258,37 +379,45 @@ address: registry does not perform scans; it provides the metadata layer for recording and querying results. -4. **Skill grouping**: Related skills are organized into groups for - discovery and governance. A skill can belong to multiple groups. - Groups have their own publish state and tags. +4. **Cross-kind grouping**: Related capabilities of any kind are + organized into skill groups for discovery and governance. A skill + group maps to the "plugin" concept in agent harnesses — for example, + a "pr-workflow" group might bundle a code-review skill, a + security-auditor agent, and a GitHub MCP server. + +5. **Federated discovery**: Users discover published capabilities and + groups across all source types from a single search interface, + filtered by kind, without requiring content to be centralized. -5. **Federated discovery**: Users discover published skills and groups - across all source types from a single search interface, without - requiring skill content to be centralized. +6. **Pull**: `mlflow skills pull` fetches capability content from its + registered source to a local directory. This is source-type-aware + (git clone, OCI pull, ZIP extract) and harness-agnostic. -6. **Usage analytics**: Agent traces record which skill versions were - used. Combined with registry metadata, this enables organizations to - understand adoption and make data-driven promotion decisions. +7. **Usage analytics**: Agent traces record which capability versions + were used. Combined with registry metadata, this enables + organizations to understand adoption and make data-driven promotion + decisions. ### Out of scope -- **Skill artifact storage.** The registry stores metadata and source - pointers. Skill content remains in Git, OCI, or other distribution - systems. -- **Skill authoring or development tools.** The registry manages - published skills, not the process of writing them. -- **Skill format specification.** The registry is format-agnostic. It - does not define or enforce what a skill looks like (SKILL.md, plugin - manifests, etc.). -- **Security scanning execution.** The registry records scan results; it - does not perform scans. Scanning tools are separate. -- **Agent harness integration.** How a specific agent harness (Claude - Code, Codex, Cursor, etc.) installs or loads skills from the registry - is outside this RFC. The registry provides the metadata; harness - integration layers consume it. -- **Approval workflows or review gates.** Publish state transitions are - sufficient for initial governance. Approval chains can be built on top - via external systems. +- **Artifact storage.** The registry stores metadata and source + pointers. Content remains in Git, OCI, or other distribution systems. + `pull` fetches from the source; the registry itself does not store + artifacts. +- **Authoring or development tools.** The registry manages published + capabilities, not the process of writing them. +- **Format specification.** The registry is format-agnostic. It does + not define or enforce what a skill, agent, MCP config, or hook looks + like. +- **Security scanning execution.** The registry records scan results; + it does not perform scans. +- **Harness-specific installation.** How a specific agent harness + (Claude Code, Codex CLI, Cursor, etc.) installs capabilities from + the registry — including manifest generation and directory placement + — is covered in a companion RFC (RFC-0006). This RFC provides the + registry, governance, and `pull`; RFC-0006 provides `install`. +- **Approval workflows or review gates.** Publish state transitions + are sufficient for initial governance. - **Detailed UI/UX design.** This RFC describes the UI surface and placement but does not specify interaction patterns. @@ -319,6 +448,13 @@ from dataclasses import dataclass, field from enum import StrEnum +class SkillKind(StrEnum): + SKILL = "skill" + AGENT = "agent" + MCP_SERVER = "mcp-server" + HOOK = "hook" + + class SkillStatus(StrEnum): ACTIVE = "active" DEPRECATED = "deprecated" @@ -328,6 +464,7 @@ class SkillStatus(StrEnum): @dataclass class Skill: name: str + kind: SkillKind = SkillKind.SKILL description: str | None = None workspace: str | None = None status: SkillStatus = SkillStatus.ACTIVE @@ -343,11 +480,17 @@ class Skill: | Field | Type | Description | |---|---|---| | `name` | `str` | Stable logical asset name, unique within a workspace | +| `kind` | `SkillKind` | Capability type: `skill`, `agent`, `mcp-server`, `hook` | | `status` | `SkillStatus` | Skill-level lifecycle: `active`, `deprecated`, `retired` | -| `aliases` | `list[SkillAlias]` | Stable version pointers, each resolving to a `(version, source_type)` pair | +| `aliases` | `list[SkillAlias]` | Stable version pointers (e.g., `production` → `1.2.0`) | | `last_registered_version` | `str` | Most recently registered version string | | `workspace` | `str` | Visibility boundary | +**Kind extensibility.** The `kind` enum covers the four capability +types with broad cross-harness support. New kinds can be added without +schema changes since the column stores a string value. `kind` is +immutable after creation. + #### SkillVersion A versioned record containing a typed source pointer, publish state, @@ -371,8 +514,8 @@ class SkillSourceType(StrEnum): class SkillVersion: name: str version: str - source_type: SkillSourceType - source: str + source_type: SkillSourceType | None = None + source: str | None = None publish_state: SkillPublishState = SkillPublishState.DRAFT content_digest: str | None = None tags: dict[str, str] = field(default_factory=dict) @@ -387,8 +530,8 @@ class SkillVersion: | Field | Type | Description | |---|---|---| | `version` | `str` | Publisher-supplied version string. Semver recommended but not enforced | -| `source_type` | `SkillSourceType` | Distribution mechanism: `git`, `oci`, `zip` | -| `source` | `str` | Pointer to the skill content in the source system (URL, OCI reference, etc.) | +| `source_type` | `SkillSourceType` | Optional distribution mechanism: `git`, `oci`, `zip` | +| `source` | `str` | Optional pointer to the content in the source system. Required for standalone pull; omit when content is only available via a group-level source | | `content_digest` | `str` | Optional digest for integrity verification (e.g., `sha256:abc123...`). Aligns with OCI digest terminology | | `publish_state` | `SkillPublishState` | Per-version surfacing lifecycle | | `run_id` | `str` | Optional MLflow run association for trace linkage | @@ -398,10 +541,12 @@ small for the initial implementation. New source types (e.g., `s3`, `azure-blob`) can be added without schema changes since the column stores a string value. -**Version uniqueness.** The combination of `(name, version, source_type)` -is unique within a workspace. This allows the same skill version to be -registered from multiple distribution mechanisms (e.g., Git and OCI) -without requiring different version strings. +**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. If the same content is +available from multiple distribution mechanisms (e.g., Git and OCI), +register separate versions or use a group-level source. **Content integrity.** The optional `content_digest` field stores a digest of the skill content at registration time (e.g., @@ -419,8 +564,11 @@ updated independently. #### SkillGroup -The logical group asset, scoped to a workspace. Follows the same -pattern as Skill: a top-level entity with versions, tags, and aliases. +The logical group asset, scoped to a workspace. A skill group bundles +capabilities of any kind (skills, agents, MCP servers, hooks) into a +governed unit that maps to the "plugin" concept in agent harnesses. +Follows the same pattern as Skill: a top-level entity with versions, +tags, and aliases. ```python class SkillGroupStatus(StrEnum): @@ -454,6 +602,9 @@ captures a specific set of skill versions that work together. class SkillGroupVersion: name: str version: str + source_type: SkillSourceType | None = None + source: str | None = None + content_digest: str | None = None publish_state: SkillPublishState = SkillPublishState.DRAFT tags: dict[str, str] = field(default_factory=dict) members: list["SkillGroupVersionMembership"] = field(default_factory=list) @@ -467,10 +618,23 @@ class SkillGroupVersion: **Version uniqueness.** The combination of `(name, version)` is unique within a workspace. -**Immutability contract.** The membership list of a group version is -immutable after creation. To change the set of skills, register a new -group version. Mutable fields (`publish_state`, `tags`) can be updated -independently. +**Group-level source.** A group version can optionally have its own +`source_type`, `source`, and `content_digest`, pointing to a single +artifact (e.g., an OCI image or Git repo) that contains the complete +plugin. When present, `pull` fetches the group artifact as a unit +rather than pulling members individually. This supports distribution +patterns where a plugin is packaged as a single image or repo. + +**Source resolution for pull.** When pulling a group, if the group +version has a source, that source is used. Otherwise, each member is +pulled individually from its own source. Members without a source are +skipped with a warning. When pulling a standalone skill, the skill +version's source is required. + +**Immutability contract.** The membership list and source fields of a +group version are immutable after creation. To change the set of +skills or source pointer, register a new group version. Mutable fields +(`publish_state`, `tags`) can be updated independently. #### SkillGroupVersionMembership @@ -483,7 +647,6 @@ type). The parent group identity is provided by the enclosing class SkillGroupVersionMembership: skill_name: str skill_version: str - skill_source_type: SkillSourceType ``` A skill can appear in multiple groups and multiple group versions. @@ -505,10 +668,9 @@ class SkillGroupAlias: ```python @dataclass(frozen=True) class SkillAlias: - name: str # parent Skill name - alias: str # e.g., "production", "staging" - version: str # version string this alias points to - source_type: SkillSourceType # source type this alias points to + name: str # parent Skill name + alias: str # e.g., "production", "staging" + version: str # version string this alias points to @dataclass(frozen=True) class SkillTag: @@ -569,6 +731,7 @@ workspace-scoped. |--------|------|-------| | `workspace` | `String(63)` | PK, default `'default'` | | `name` | `String(256)` | PK | +| `kind` | `String(20)` | default `'skill'`; `skill`, `agent`, `mcp-server`, `hook` | | `description` | `String(5000)` | | | `status` | `String(20)` | default `'active'` | | `last_registered_version` | `String(256)` | | @@ -584,8 +747,8 @@ workspace-scoped. | `workspace` | `String(63)` | PK, FK | | `name` | `String(256)` | PK, FK | | `version` | `String(256)` | PK, publisher-supplied | -| `source_type` | `String(20)` | PK; `git`, `oci`, `zip`, etc. | -| `source` | `String(2048)` | pointer to skill content | +| `source_type` | `String(20)` | nullable; `git`, `oci`, `zip`, etc. | +| `source` | `String(2048)` | nullable pointer to skill content | | `content_digest` | `String(512)` | optional integrity digest | | `publish_state` | `String(20)` | default `'draft'` | | `run_id` | `String(32)` | optional MLflow run linkage | @@ -612,7 +775,6 @@ FK: `(workspace, name)` references `skills`, CASCADE delete. | `workspace` | `String(63)` | PK, FK | | `name` | `String(256)` | PK, FK | | `version` | `String(256)` | PK, FK | -| `source_type` | `String(20)` | PK, FK | | `key` | `String(256)` | PK | | `value` | `Text` | | @@ -624,7 +786,6 @@ FK: `(workspace, name)` references `skills`, CASCADE delete. | `name` | `String(256)` | PK, FK | | `alias` | `String(256)` | PK | | `version` | `String(256)` | target version string | -| `source_type` | `String(20)` | target source type | #### `skill_groups` @@ -647,6 +808,9 @@ FK: `(workspace, name)` references `skills`, CASCADE delete. | `workspace` | `String(63)` | PK, FK | | `name` | `String(256)` | PK, FK | | `version` | `String(256)` | PK, publisher-supplied | +| `source_type` | `String(20)` | optional; `git`, `oci`, `zip`, etc. | +| `source` | `String(2048)` | optional pointer to group artifact | +| `content_digest` | `String(512)` | optional integrity digest | | `publish_state` | `String(20)` | default `'draft'` | | `created_by` | `String(256)` | | | `last_updated_by` | `String(256)` | | @@ -664,10 +828,9 @@ FK: `(workspace, name)` references `skill_groups`, CASCADE delete. | `group_version` | `String(256)` | PK, FK to `skill_group_versions` | | `skill_name` | `String(256)` | PK, FK to `skill_versions` | | `skill_version` | `String(256)` | PK, FK to `skill_versions` | -| `skill_source_type` | `String(20)` | PK, FK to `skill_versions` | FK: `(workspace, group_name, group_version)` references `skill_group_versions`, CASCADE delete. -FK: `(workspace, skill_name, skill_version, skill_source_type)` references `skill_versions`, RESTRICT delete. +FK: `(workspace, skill_name, skill_version)` references `skill_versions`, RESTRICT delete. #### `skill_group_tags` @@ -716,7 +879,8 @@ class AbstractSkillRegistryStore: @abstractmethod def create_skill( - self, name: str, description: str | None = None, + self, name: str, kind: str = "skill", + description: str | None = None, ) -> Skill: ... @abstractmethod @@ -748,8 +912,8 @@ class AbstractSkillRegistryStore: self, name: str, version: str, - source_type: str, - source: str, + source_type: str | None = None, + source: str | None = None, publish_state: SkillPublishState = SkillPublishState.DRAFT, content_digest: str | None = None, run_id: str | None = None, @@ -757,7 +921,7 @@ class AbstractSkillRegistryStore: @abstractmethod def get_skill_version( - self, name: str, version: str, source_type: str, + self, name: str, version: str, ) -> SkillVersion: ... @abstractmethod @@ -782,13 +946,12 @@ class AbstractSkillRegistryStore: self, name: str, version: str, - source_type: str, publish_state: SkillPublishState | None = None, ) -> SkillVersion: ... @abstractmethod def delete_skill_version( - self, name: str, version: str, source_type: str, + self, name: str, version: str, ) -> None: ... # --- Tag operations --- @@ -803,20 +966,20 @@ class AbstractSkillRegistryStore: @abstractmethod def set_skill_version_tag( - self, name: str, version: str, source_type: str, + self, name: str, version: str, key: str, value: str, ) -> None: ... @abstractmethod def delete_skill_version_tag( - self, name: str, version: str, source_type: str, key: str, + self, name: str, version: str, key: str, ) -> None: ... # --- Alias operations --- @abstractmethod def set_skill_alias( - self, name: str, alias: str, version: str, source_type: str, + self, name: str, alias: str, version: str, ) -> None: ... @abstractmethod @@ -824,6 +987,23 @@ class AbstractSkillRegistryStore: self, name: str, alias: str, ) -> None: ... + # --- Pull operations --- + + @abstractmethod + def pull_skill( + self, name: str, destination: str, + version: str | None = None, + alias: str | None = None, + source_type: str | None = None, + ) -> str: ... + + @abstractmethod + def pull_skill_group( + self, name: str, destination: str, + version: str | None = None, + alias: str | None = None, + ) -> str: ... + # --- SkillGroup operations --- @abstractmethod @@ -862,6 +1042,9 @@ class AbstractSkillRegistryStore: version: str, members: list[SkillGroupVersionMembership], publish_state: SkillPublishState = SkillPublishState.DRAFT, + source_type: str | None = None, + source: str | None = None, + content_digest: str | None = None, ) -> SkillGroupVersion: ... @abstractmethod @@ -955,16 +1138,17 @@ All paths relative to `/ajax-api/3.0/mlflow/skills`. | `DELETE` | `/{name}` | Delete skill (cascades) | | `POST` | `/{name}/versions` | Create a skill version | | `GET` | `/{name}/versions` | Search versions | -| `GET` | `/{name}/versions/{version}/{source_type}` | Get a specific version | -| `PATCH` | `/{name}/versions/{version}/{source_type}` | Update version | -| `DELETE` | `/{name}/versions/{version}/{source_type}` | Delete a version | +| `GET` | `/{name}/versions/{version}` | Get a specific version | +| `PATCH` | `/{name}/versions/{version}` | Update version | +| `DELETE` | `/{name}/versions/{version}` | Delete a version | | `POST` | `/{name}/tags` | Set a skill-level tag | | `DELETE` | `/{name}/tags/{key}` | Delete a skill-level tag | -| `POST` | `/{name}/versions/{version}/{source_type}/tags` | Set a version-level tag | -| `DELETE` | `/{name}/versions/{version}/{source_type}/tags/{key}` | Delete a version 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` (returns version and source_type) | +| `GET` | `/{name}/aliases/{alias}` | Resolve alias to `SkillVersion` | | `DELETE` | `/{name}/aliases/{alias}` | Delete an alias | +| `POST` | `/{name}/pull` | Pull skill content from source to a local destination | #### Skill group endpoints @@ -989,6 +1173,7 @@ All paths relative to `/ajax-api/3.0/mlflow/skill-groups`. | `POST` | `/{name}/aliases` | Set a group alias | | `GET` | `/{name}/aliases/{alias}` | Resolve group alias to version | | `DELETE` | `/{name}/aliases/{alias}` | Delete a group alias | +| `POST` | `/{name}/pull` | Pull all group members from their sources | #### Pagination and filtering @@ -996,7 +1181,7 @@ Search endpoints use page-token-based pagination and `filter_string` expressions following existing MLflow conventions. **Skills and skill groups:** `name LIKE '%review%'`, `status = 'active'`, -`tags.team = 'platform'` +`kind = 'agent'`, `tags.team = 'platform'` **Skill versions:** `publish_state = 'published'`, `source_type = 'git'`, `tags.scan.prompt-injection.status = 'pass'` @@ -1012,6 +1197,40 @@ Two CLI command groups (`mlflow skills` and `mlflow skill-groups`) provide the same operations from the command line. See the basic examples at the top of this RFC for usage. +### Pull semantics + +`pull` resolves a skill or skill group to its source pointer(s) and +fetches content to a local destination directory. It 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 | +| `zip` | HTTP download and extract | + +**Single skill pull.** Fetches the content at the skill version's +`source` to the destination directory. Returns an error if the skill +version has no `source`. + +**Skill group pull.** Source resolution: +1. If the group version has a `source`, fetch the group artifact as a + single unit to the destination directory. +2. Otherwise, pull each member individually from its own `source` to + a subdirectory of the destination, named by the member's skill name. + Members without a `source` are skipped with a warning. + +This supports both distribution patterns: a monolithic plugin artifact +(single OCI image or Git repo) and an assembled plugin (members from +different sources). + +If `content_digest` is set, `pull` verifies the fetched content +matches the digest and returns an error on mismatch. + +`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 covered in RFC-0006. + ### Error handling | Scenario | Error code | HTTP status | @@ -1122,18 +1341,23 @@ The two approaches are complementary. This is a new feature, not a breaking change. Adoption is incremental: -**Initial release:** +**This RFC (RFC-0005):** - Entities, database schema, store implementation, REST API, Python SDK, CLI, and basic UI. -- Users can register skills with source pointers, manage publish state, - record scan results as tags, organize skills into groups, and discover - published skills. +- Users can register capabilities of any kind (skill, agent, mcp-server, + hook), manage publish state, record scan results as tags, organize + capabilities into skill groups, and discover published capabilities. +- `mlflow skills pull` fetches content from registered sources. - Existing MLflow functionality is unaffected. +**Companion RFC (RFC-0006):** +- Harness-specific installation: `mlflow skills install` generates + manifests and places files for specific agent harnesses. +- Initial targets: Claude Code, Codex CLI, Cursor, with additional + harnesses based on demand. + **Follow-up:** - Agent trace integration: traces automatically record which registered - skill version was used, linking back to the registry. + capability version was used, linking back to the registry. - Usage analytics dashboard based on trace metadata. -- Shared base extraction across AI asset registries (skills, MCP - servers, etc.) once patterns are validated. -- Additional source types as demand emerges. +- Additional source types and capability kinds as demand emerges. diff --git a/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md b/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md new file mode 100644 index 0000000..0c1ba91 --- /dev/null +++ b/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md @@ -0,0 +1,375 @@ +--- +start_date: 2026-04-27 +mlflow_issue: https://github.com/mlflow/mlflow/issues/22833 +rfc_pr: https://github.com/mlflow/rfcs/pull/10 +--- + +# RFC: Skill Registry Harness Integration + +| Author(s) | Bill Murdock (Red Hat) | +| :--------------------- | :-- | +| **Date Last Modified** | 2026-04-27 | +| **AI Assistant(s)** | Claude Code (Opus 4.6) | + +# Summary + +Add harness-specific installation to the MLflow Skill Registry +(RFC-0005). Where RFC-0005 provides `mlflow skills pull` to fetch +content from registered sources to a local directory, this RFC adds +`mlflow skills install` to generate harness-specific manifests, place +files in the correct directories, and configure the agent harness to +use the installed capabilities. + +This bridges the gap between "I found a skill group in the registry" +and "my agent harness can use it." + +# Basic example + +## Install a skill group for Claude Code + +```bash +mlflow skills install --group pr-workflow --alias production \ + --harness claude-code +``` + +This resolves the `pr-workflow` skill group, pulls all member +capabilities from their registered sources, and generates: + +``` +.claude/plugins/pr-workflow/ + .claude-plugin/plugin.json # Generated manifest + skills/ + code-review/SKILL.md # Pulled from Git source + agents/ + security-auditor.md # Pulled from Git source + .mcp.json # Generated from mcp-server members +``` + +## Install for other harnesses + +```bash +# Codex CLI (nearly identical to Claude Code) +mlflow skills install --group pr-workflow --alias production \ + --harness codex-cli + +# Cursor +mlflow skills install --group pr-workflow --alias production \ + --harness cursor + +# Antigravity +mlflow skills install --group pr-workflow --alias production \ + --harness antigravity +``` + +## Python SDK + +```python +import mlflow + +mlflow.skills.install_skill_group( + name="pr-workflow", + alias="production", + harness="claude-code", + destination=".", # project root +) +``` + +## Motivation + +### The problem + +RFC-0005 provides a governed registry with `pull` for fetching content +to a local directory. But each agent harness has its own conventions +for where files go, what manifests are needed, and how capabilities +are discovered: + +- **Claude Code / Codex CLI** expect a `plugin.json` manifest, skills + in `skills/`, agents in `agents/`, and MCP configs in `.mcp.json`. +- **Cursor** discovers skills from `.cursor/skills/`, agents from + `.cursor/agents/`, and MCP servers from `.cursor/mcp.json`. +- **Antigravity** discovers skills from `.agent/skills/`. +- **OpenClaw** expects skills in `skills/` directories and uses + `openclaw.plugin.json`. +- **GitHub Copilot** uses `plugin.json` with skills, agents, hooks, + and MCP configs. + +Without harness-specific installation, users must manually: +1. Run `mlflow skills pull` to get the content +2. Create the appropriate manifest files +3. Place files in harness-specific directories +4. Configure the harness to discover the new capabilities + +This is error-prone and discourages adoption. + +### The cross-harness landscape + +The following table summarizes the capability types and installation +conventions across major agent harnesses: + +| Harness | Skills | Agents | MCP | Hooks | Manifest | Install dir | +|---|---|---|---|---|---|---| +| Claude Code | SKILL.md | agent .md | .mcp.json | settings.json | plugin.json | `.claude/plugins/` | +| Codex CLI | SKILL.md | agent .md | .mcp.json | hooks | plugin.json | `.codex/plugins/` | +| Cursor | SKILL.md | agent .md | mcp.json | -- | -- | `.cursor/skills/`, `.cursor/agents/` | +| GitHub Copilot | skills/ | agents/ | .mcp.json | hooks/*.json | plugin.json | project | +| OpenClaw | SKILL.md | -- | -- | plugin hooks | openclaw.plugin.json | `skills/` | +| Kilo Code | SKILL.md | custom modes | mcp.json | -- | -- | project | +| Antigravity | SKILL.md | -- | -- | -- | -- | `.agent/skills/` | +| OpenCode | .md/.ts | agent configs | config | JS events | -- | `.opencode/` | +| Continue | -- | config.yaml | mcpServers/ | -- | -- | `.continue/` | +| Windsurf | -- | -- | mcp_config.json | -- | -- | project | +| Amazon Q | -- | -- | mcp.json | -- | -- | `.amazonq/` | +| Goose | -- | -- | MCP only | -- | -- | config | +| Zed | -- | profiles | settings.json | -- | -- | config | + +Key insight: the SKILL.md file format is portable across harnesses — +only the directory placement and manifest format differ. + +### Out of scope + +- **Registry operations.** Registration, versioning, lifecycle, + search, and governance are covered in RFC-0005. +- **Harness-specific features beyond installation.** This RFC does not + extend harness functionality (e.g., adding hook support to harnesses + that lack it). +- **Automatic harness detection.** The user specifies `--harness` + explicitly. Auto-detection could be a follow-up. + +## Detailed design + +### Harness adapters + +Each supported harness has an adapter that knows how to: + +1. **Map capability kinds to harness paths.** Given the registry's + `kind` field (skill, agent, mcp-server, hook), determine where + each capability's content should be placed. +2. **Generate manifests.** Create harness-specific manifest files + (e.g., `plugin.json`, `.mcp.json`) from registry metadata. +3. **Handle unsupported kinds.** Skip capability kinds the harness + does not support, with a warning. + +```python +from abc import abstractmethod + + +class HarnessAdapter: + @abstractmethod + def install_skill_group( + self, + group_version: SkillGroupVersion, + members: list[tuple[Skill, SkillVersion]], + destination: str, + ) -> str: ... + + @abstractmethod + def supported_kinds(self) -> set[str]: ... +``` + +### Claude Code / Codex CLI adapter + +These two harnesses share nearly identical plugin formats. The adapter +generates: + +**`plugin.json`:** +```json +{ + "name": "pr-workflow", + "version": "1.0.0", + "description": "End-to-end pull request review workflow", + "author": { "name": "Generated by MLflow Skill Registry" } +} +``` + +**Directory layout:** +``` +{destination}/.claude/plugins/{group-name}/ + .claude-plugin/plugin.json + skills/{skill-name}/SKILL.md # kind=skill members + agents/{agent-name}.md # kind=agent members + .mcp.json # kind=mcp-server members, merged +``` + +For Codex CLI, the path uses `.codex/plugins/` instead. + +**MCP server merging.** If the group contains multiple `mcp-server` +members, their configs are merged into a single `.mcp.json` file +using server name as the key: + +```json +{ + "mcpServers": { + "github-mcp": { ... }, + "jira-mcp": { ... } + } +} +``` + +**Hook handling.** `hook` members are placed in the plugin directory. +The adapter generates appropriate entries but does not modify the +user's `settings.json` — the user must explicitly enable hooks for +security reasons. + +### Cursor adapter + +Cursor does not have a plugin bundle format. The adapter places +capabilities directly into Cursor's discovery directories: + +``` +{destination}/.cursor/skills/{skill-name}/SKILL.md # kind=skill +{destination}/.cursor/agents/{agent-name}.md # kind=agent +``` + +For MCP servers, the adapter merges entries into the project's +`.cursor/mcp.json`, adding new servers without overwriting existing +ones. + +Hooks are skipped with a warning (Cursor does not support hooks). + +### Antigravity adapter + +``` +{destination}/.agent/skills/{skill-name}/SKILL.md # kind=skill +``` + +Agents, MCP servers, and hooks are skipped with a warning. + +### Other harness adapters + +Additional adapters (OpenClaw, GitHub Copilot, Kilo Code, OpenCode, +Continue, etc.) follow the same pattern: map kinds to paths, generate +manifests, skip unsupported kinds with warnings. + +New adapters can be contributed without changes to the registry or +the adapter interface. + +### `marketplace.json` generation + +For harnesses that support marketplace catalogs (Claude Code, Codex +CLI), the registry can generate a `marketplace.json` that exposes +skill groups as installable plugins: + +``` +GET /ajax-api/3.0/mlflow/skill-groups/marketplace.json?harness=claude-code +``` + +This enables workflows like: + +```bash +# Add the MLflow registry as a marketplace source +# (in Claude Code settings.json) +{ + "extraKnownMarketplaces": [ + "https://mlflow.example.com/ajax-api/3.0/mlflow/skill-groups/marketplace.json?harness=claude-code" + ] +} + +# Then install directly from the harness +/plugin install pr-workflow@mlflow +``` + +### Store interface + +```python +class AbstractSkillRegistryStore: + # ... (existing methods from RFC-0005) ... + + @abstractmethod + def install_skill( + self, + name: str, + harness: str, + destination: str, + version: str | None = None, + alias: str | None = None, + source_type: str | None = None, + ) -> str: ... + + @abstractmethod + def install_skill_group( + self, + name: str, + harness: str, + destination: str, + version: str | None = None, + alias: str | None = None, + ) -> str: ... + + @abstractmethod + def generate_marketplace( + self, + harness: str, + filter_string: str | None = None, + ) -> dict: ... +``` + +### REST API + +Additional endpoints on the skill and skill group resources: + +| Method | Path | Description | +|---|---|---| +| `POST` | `/ajax-api/3.0/mlflow/skills/{name}/install` | Install a single capability for a harness | +| `POST` | `/ajax-api/3.0/mlflow/skill-groups/{name}/install` | Install a skill group for a harness | +| `GET` | `/ajax-api/3.0/mlflow/skill-groups/marketplace.json` | Generate marketplace catalog for a harness | + +### CLI + +```bash +# Install a single capability +mlflow skills install --name code-review --alias production \ + --harness claude-code --destination . + +# Install a skill group +mlflow skills install --group pr-workflow --alias production \ + --harness claude-code --destination . + +# List supported harnesses +mlflow skills harnesses +``` + +## Drawbacks + +- **Adapter maintenance.** Each harness adapter must be maintained as + harness plugin formats evolve. This is ongoing work. +- **Incomplete coverage.** Not all harnesses support all capability + kinds. Users may be surprised when hooks are silently skipped for + Cursor, or agents are skipped for Antigravity. +- **Manifest format drift.** Generated manifests may not cover all + features of a harness's native plugin format (e.g., Codex CLI's + `interface` block with branding, or OpenClaw's `requires` field). + +# Alternatives + +## Let users write their own install scripts + +Provide only `pull` (RFC-0005) and let users or third parties build +harness-specific tooling. + +Rejected because the gap between "pull" and "working in my harness" +is the main adoption barrier. A first-party install experience is +critical for driving adoption. + +## Generate manifests server-side only + +Serve manifests via the `marketplace.json` endpoint and let harnesses +pull directly, without a client-side `install` command. + +This works for harnesses with marketplace support (Claude Code, Codex +CLI) but not for harnesses that lack marketplace infrastructure +(Cursor, Antigravity, OpenClaw). Both approaches are complementary +and are included in this RFC. + +# Adoption strategy + +**Initial release:** +- Claude Code and Codex CLI adapters (highest impact, nearly identical + format). +- Cursor adapter (second-highest priority for MLflow's user base). +- `marketplace.json` generation for Claude Code / Codex CLI. + +**Follow-up:** +- Additional harness adapters based on demand. +- Automatic harness detection from project structure. +- Bi-directional sync: detect locally installed plugins and register + them in the registry. From dba38044fb52001e5b80b66c31ffa69d3e9ebb17 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Mon, 27 Apr 2026 16:20:19 -0400 Subject: [PATCH 06/19] Expand marketplace integration in RFC-0006 detailed design Move marketplace.json from Alternatives into Detailed Design with full endpoint spec, response format, configuration, and limitations. Co-Authored-By: Claude Opus 4.6 --- .../0006-skill-harness-integration.md | 85 +++++++++++++++---- 1 file changed, 67 insertions(+), 18 deletions(-) diff --git a/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md b/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md index 0c1ba91..bc75e20 100644 --- a/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md +++ b/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md @@ -243,31 +243,90 @@ manifests, skip unsupported kinds with warnings. New adapters can be contributed without changes to the registry or the adapter interface. -### `marketplace.json` generation +### Marketplace integration -For harnesses that support marketplace catalogs (Claude Code, Codex -CLI), the registry can generate a `marketplace.json` that exposes -skill groups as installable plugins: +Some harnesses (Claude Code, Codex CLI) support marketplace catalogs: +a JSON endpoint that lists available plugins so users can browse and +install them natively from within the harness. The registry serves a +`marketplace.json` endpoint that exposes published skill groups as +installable plugins, enabling native harness-driven installation +without requiring the MLflow CLI. + +#### Endpoint ``` GET /ajax-api/3.0/mlflow/skill-groups/marketplace.json?harness=claude-code ``` -This enables workflows like: +Query parameters: + +| Parameter | Required | Description | +|---|---|---| +| `harness` | yes | Target harness (`claude-code`, `codex-cli`) | +| `filter_string` | no | Filter expression (e.g., `tags.team = 'platform'`) | + +The endpoint returns only skill groups whose latest published version +contains at least one member with a kind supported by the target +harness. + +#### Response format + +The response follows the harness's native marketplace schema. For +Claude Code / Codex CLI: + +```json +{ + "plugins": [ + { + "name": "pr-workflow", + "version": "1.0.0", + "description": "End-to-end pull request review workflow", + "author": { "name": "Generated by MLflow Skill Registry" }, + "source": "https://mlflow.example.com/ajax-api/3.0/mlflow/skill-groups/pr-workflow/install?harness=claude-code", + "skills": ["code-review", "test-coverage"], + "agents": ["security-auditor"], + "mcpServers": ["github-mcp"] + } + ] +} +``` + +Each entry is derived from a published skill group version and its +members. The `source` field points to a registry endpoint that serves +the installable plugin bundle. + +#### Configuration + +Users add the registry as a marketplace source in their harness +settings: ```bash -# Add the MLflow registry as a marketplace source -# (in Claude Code settings.json) +# Claude Code settings.json { "extraKnownMarketplaces": [ "https://mlflow.example.com/ajax-api/3.0/mlflow/skill-groups/marketplace.json?harness=claude-code" ] } +``` + +Once configured, users can browse and install registry plugins +natively: -# Then install directly from the harness +``` /plugin install pr-workflow@mlflow ``` +This is the recommended installation path for Claude Code and Codex +CLI users. It provides the most seamless experience and keeps the +harness as the single point of plugin management. + +#### Limitations + +Marketplace integration is only available for harnesses with +marketplace infrastructure (currently Claude Code and Codex CLI). +Harnesses without marketplace support (Cursor, Antigravity, OpenClaw) +use the adapter-based `mlflow skills install` command instead. + ### Store interface ```python @@ -350,16 +409,6 @@ Rejected because the gap between "pull" and "working in my harness" is the main adoption barrier. A first-party install experience is critical for driving adoption. -## Generate manifests server-side only - -Serve manifests via the `marketplace.json` endpoint and let harnesses -pull directly, without a client-side `install` command. - -This works for harnesses with marketplace support (Claude Code, Codex -CLI) but not for harnesses that lack marketplace infrastructure -(Cursor, Antigravity, OpenClaw). Both approaches are complementary -and are included in this RFC. - # Adoption strategy **Initial release:** From 5df5f3529b8abc94355fecb775a7b00c958ca2da Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Wed, 29 Apr 2026 13:02:32 -0400 Subject: [PATCH 07/19] Align with MCP RFC, cross-registry refs, review fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Lifecycle: publish_state → status, 3 states (active/deprecated/ deleted), derived parent status. Aligns with RFC-0004. - Add latest_version_alias to Skill and SkillGroup. - Store: AbstractSkillRegistryStore → SkillRegistryMixin with NotImplementedError. Add order_by to search methods. - Cross-registry membership: rename skill_name/skill_version to member_name/member_version, add registry field (skill/mcp). - Conditional FK for MCP refs → application-layer enforcement. - Pull clarified as client-side, removed from store mixin and REST API. - Dual MCP registration: MCP registry is default, kind=mcp-server reserved for embedded configs only. - SDK namespace: mlflow.skills → mlflow.genai.skills. - Add external skill conventions paragraph. - Add skill groups justification (why not just tags). Co-Authored-By: Claude Opus 4.6 --- .../0005-skill-registry.md | 580 ++++++++++-------- .../0006-skill-harness-integration.md | 18 +- 2 files changed, 335 insertions(+), 263 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index d43c8f2..1e54a5e 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -8,7 +8,7 @@ rfc_pr: https://github.com/mlflow/rfcs/pull/10 | Author(s) | Bill Murdock (Red Hat) | | :--------------------- | :-- | -| **Date Last Modified** | 2026-04-27 | +| **Date Last Modified** | 2026-04-29 | | **AI Assistant(s)** | Claude Code (Opus 4.6) | # Summary @@ -21,8 +21,8 @@ existing distribution mechanisms: lifecycle management, security scan tracking, usage analytics via traces, and federated discovery across sources. -The registry tracks four capability kinds under the `mlflow skills` -namespace: +The registry tracks four capability kinds under the `mlflow.genai.skills` +SDK namespace (CLI: `mlflow skills`): - **Skills** (SKILL.md) — reusable agent instructions - **Agents** (agent .md) — sub-agent definitions @@ -45,43 +45,36 @@ RFC (RFC-0006). import mlflow # Create the logical skill asset -skill = mlflow.skills.create_skill( +skill = mlflow.genai.skills.create_skill( name="code-review", description="Reviews pull requests for correctness, style, and security", ) # Register a version pointing to a Git source -version = mlflow.skills.create_skill_version( +version = mlflow.genai.skills.create_skill_version( name="code-review", version="1.0.0", source_type="git", source="https://github.com/acme/agent-skills/tree/v1.0.0/code-review", content_digest="sha256:a3f2b8c...", ) -# version.publish_state == "draft" - -# Publish the version so downstream consumers can discover it -mlflow.skills.update_skill_version( - name="code-review", - version="1.0.0", - publish_state="published", -) +# version.status == "active" # Set an alias for stable resolution -mlflow.skills.set_skill_alias( +mlflow.genai.skills.set_skill_alias( name="code-review", alias="production", version="1.0.0", ) # Record a security scan result as a tag -mlflow.skills.set_skill_version_tag( +mlflow.genai.skills.set_skill_version_tag( name="code-review", version="1.0.0", key="scan.prompt-injection.status", value="pass", ) -mlflow.skills.set_skill_version_tag( +mlflow.genai.skills.set_skill_version_tag( name="code-review", version="1.0.0", key="scan.prompt-injection.date", @@ -95,37 +88,30 @@ mlflow.skills.set_skill_version_tag( from mlflow.entities import SkillGroupVersionMembership # Create a group for related skills -group = mlflow.skills.create_skill_group( +group = mlflow.genai.skills.create_skill_group( name="pr-workflow", description="End-to-end pull request review workflow", ) # Create a group version that pins specific skill versions -group_version = mlflow.skills.create_skill_group_version( +group_version = mlflow.genai.skills.create_skill_group_version( name="pr-workflow", version="1.0.0", members=[ SkillGroupVersionMembership( - skill_name="code-review", skill_version="1.0.0", + member_name="code-review", member_version="1.0.0", ), SkillGroupVersionMembership( - skill_name="test-coverage", skill_version="2.1.0", + member_name="test-coverage", member_version="2.1.0", ), SkillGroupVersionMembership( - skill_name="security-scan", skill_version="1.0.0", + member_name="security-scan", member_version="1.0.0", ), ], ) -# Publish the group version -mlflow.skills.update_skill_group_version( - name="pr-workflow", - version="1.0.0", - publish_state="published", -) - # Set an alias for stable resolution -mlflow.skills.set_skill_group_alias( +mlflow.genai.skills.set_skill_group_alias( name="pr-workflow", alias="production", version="1.0.0", @@ -136,12 +122,12 @@ mlflow.skills.set_skill_group_alias( ```python # Register a sub-agent -mlflow.skills.create_skill( +mlflow.genai.skills.create_skill( name="security-auditor", kind="agent", description="Security specialist for auth and payment code", ) -mlflow.skills.create_skill_version( +mlflow.genai.skills.create_skill_version( name="security-auditor", version="1.0.0", source_type="git", @@ -149,12 +135,12 @@ mlflow.skills.create_skill_version( ) # Register an MCP server -mlflow.skills.create_skill( +mlflow.genai.skills.create_skill( name="github-mcp", kind="mcp-server", description="GitHub integration via MCP", ) -mlflow.skills.create_skill_version( +mlflow.genai.skills.create_skill_version( name="github-mcp", version="2.0.0", source_type="oci", @@ -163,12 +149,12 @@ mlflow.skills.create_skill_version( ) # Register a hook -mlflow.skills.create_skill( +mlflow.genai.skills.create_skill( name="pre-commit-scan", kind="hook", description="Runs security scan before tool commits", ) -mlflow.skills.create_skill_version( +mlflow.genai.skills.create_skill_version( name="pre-commit-scan", version="1.0.0", source_type="git", @@ -181,24 +167,26 @@ mlflow.skills.create_skill_version( ```python from mlflow.entities import SkillGroupVersionMembership -group = mlflow.skills.create_skill_group( +group = mlflow.genai.skills.create_skill_group( name="pr-workflow", description="End-to-end pull request review workflow", ) -# A group version can bundle skills, agents, MCP servers, and hooks -group_version = mlflow.skills.create_skill_group_version( +# A group version can bundle skills, agents, and MCP server references +group_version = mlflow.genai.skills.create_skill_group_version( name="pr-workflow", version="1.0.0", members=[ SkillGroupVersionMembership( - skill_name="code-review", skill_version="1.0.0", + member_name="code-review", member_version="1.0.0", ), SkillGroupVersionMembership( - skill_name="security-auditor", skill_version="1.0.0", + member_name="security-auditor", member_version="1.0.0", ), + # Reference an MCP server from the MCP registry (RFC-0004) SkillGroupVersionMembership( - skill_name="github-mcp", skill_version="2.0.0", + member_name="github-mcp", member_version="2.0.0", + registry="mcp", ), ], ) @@ -208,14 +196,14 @@ group_version = mlflow.skills.create_skill_group_version( ```python # Pull a single skill version -mlflow.skills.pull_skill( +mlflow.genai.skills.pull_skill( name="code-review", alias="production", destination="./skills/code-review", ) # Pull an entire skill group (all members) -mlflow.skills.pull_skill_group( +mlflow.genai.skills.pull_skill_group( name="pr-workflow", alias="production", destination="./plugins/pr-workflow", @@ -234,19 +222,19 @@ mlflow skills pull-group --name pr-workflow --alias production \ ## Discover and consume skills ```python -# Search for published skill versions -versions = mlflow.skills.search_skill_versions( +# Search for active skill versions +versions = mlflow.genai.skills.search_skill_versions( name="code-review", - filter_string="publish_state = 'published'", + filter_string="status = 'active'", ) # Search for active skill groups -groups = mlflow.skills.search_skill_groups( +groups = mlflow.genai.skills.search_skill_groups( filter_string="status = 'active'", ) # Get a specific version -version = mlflow.skills.get_skill_version( +version = mlflow.genai.skills.get_skill_version( name="code-review", version="1.0.0", ) @@ -254,20 +242,20 @@ version = mlflow.skills.get_skill_version( # version.source == "https://github.com/acme/agent-skills/tree/v1.0.0/code-review" # Resolve by alias -version = mlflow.skills.get_skill_version_by_alias( +version = mlflow.genai.skills.get_skill_version_by_alias( name="code-review", alias="production", ) # Get a group version and its pinned skill versions -group_version = mlflow.skills.get_skill_group_version( +group_version = mlflow.genai.skills.get_skill_group_version( name="pr-workflow", version="1.0.0", ) # group_version.members == [SkillGroupVersionMembership(...), ...] # Resolve a group alias -group_version = mlflow.skills.get_skill_group_version_by_alias( +group_version = mlflow.genai.skills.get_skill_group_version_by_alias( name="pr-workflow", alias="production", ) @@ -284,9 +272,7 @@ mlflow skills create-version --name code-review --version 1.0.0 \ --source https://github.com/acme/agent-skills/tree/v1.0.0/code-review \ --content-digest sha256:a3f2b8c... -# Publish and alias -mlflow skills update-version --name code-review --version 1.0.0 \ - --publish-state published +# Alias mlflow skills set-alias --name code-review --alias production \ --version 1.0.0 @@ -296,15 +282,14 @@ mlflow skill-groups create --name pr-workflow \ mlflow skill-groups create-version --name pr-workflow --version 1.0.0 \ --member code-review:1.0.0 \ --member test-coverage:2.1.0 \ - --member security-scan:1.0.0 -mlflow skill-groups update-version --name pr-workflow --version 1.0.0 \ - --publish-state published + --member security-scan:1.0.0 \ + --member mcp:github-mcp:2.0.0 mlflow skill-groups set-alias --name pr-workflow --alias production \ --version 1.0.0 -# Search published skill versions +# Search active skill versions mlflow skills search-versions --name code-review \ - --filter "publish_state = 'published'" + --filter "status = 'active'" # Search active groups mlflow skill-groups search --filter "status = 'active'" @@ -319,12 +304,24 @@ and hooks — are becoming a critical asset class in enterprise AI platforms. As organizations adopt agentic AI, they accumulate these capabilities across teams, repositories, and agent harnesses. -A cross-harness portable format is emerging around SKILL.md files (for -skills and agents), MCP server configs (for tool integrations), and -hooks (for event-triggered actions). Agent harnesses including Claude -Code, Codex CLI, Cursor, GitHub Copilot, OpenClaw, Kilo Code, and -Antigravity support overlapping subsets of these formats, with SKILL.md -and MCP being the most broadly adopted. +A cross-harness portable format is emerging around these capabilities. +The registry is format-agnostic but is designed to interoperate with +the conventions gaining adoption across agent harnesses: + +- **SKILL.md** — a markdown file with structured instructions for the + agent. Supported by Claude Code, Codex CLI, Cursor, GitHub Copilot, + OpenClaw, Kilo Code, and Antigravity. This is the most broadly + portable format for skills and agents. +- **MCP server configs** — JSON configuration for Model Context + Protocol servers. MCP is a universal tool extension protocol + supported by nearly all major harnesses. +- **Hooks** — event-triggered shell commands or scripts. Less + standardized; Claude Code and Codex CLI have the most mature hook + support. +- **Plugin bundles** — harness-specific packaging of skills, agents, + MCP configs, and hooks into a single installable unit. Claude Code + and Codex CLI use `plugin.json` manifests; other harnesses use + directory conventions. Today, these capabilities are managed as ad-hoc files in Git repositories. This works well for individual developers and small @@ -333,8 +330,8 @@ teams. GitHub provides versioning, collaboration, and access control. However, enterprises face governance challenges that Git alone does not address: -1. **No publish-state lifecycle.** Git has no concept of "this version - is approved for production use" vs. "this is a draft." Teams resort +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. @@ -370,8 +367,8 @@ address: stores. All four capability kinds (skill, agent, mcp-server, hook) use the same registration model. -2. **Lifecycle management**: Capability versions move through publish - states (draft, published, deprecated, retired) to control downstream +2. **Lifecycle management**: Capability versions move through status + states (active, deprecated, deleted) to control downstream surfacing. This is the governance layer that Git lacks. 3. **Security scan tracking**: Scan results (prompt injection, code @@ -416,8 +413,8 @@ address: the registry — including manifest generation and directory placement — is covered in a companion RFC (RFC-0006). This RFC provides the registry, governance, and `pull`; RFC-0006 provides `install`. -- **Approval workflows or review gates.** Publish state transitions - are sufficient for initial governance. +- **Approval workflows or review gates.** Status transitions are + sufficient for initial governance. - **Detailed UI/UX design.** This RFC describes the UI surface and placement but does not specify interaction patterns. @@ -434,9 +431,10 @@ SkillVersion ||--o{ SkillVersionTag : "has tags" SkillGroup ||--o{ SkillGroupVersion : "has versions" SkillGroup ||--o{ SkillGroupTag : "has tags" SkillGroup ||--o{ SkillGroupAlias : "has aliases" -SkillGroupVersion ||--o{ SkillGroupVersionMembership : "contains skills" +SkillGroupVersion ||--o{ SkillGroupVersionMembership : "contains members" SkillGroupVersion ||--o{ SkillGroupVersionTag : "has tags" -SkillGroupVersionMembership }o--|| SkillVersion : "references" +SkillGroupVersionMembership }o--o| SkillVersion : "references (registry=skill)" +SkillGroupVersionMembership }o--o| MCPServerVersion : "references (registry=mcp)" ``` #### Skill @@ -458,7 +456,7 @@ class SkillKind(StrEnum): class SkillStatus(StrEnum): ACTIVE = "active" DEPRECATED = "deprecated" - RETIRED = "retired" + DELETED = "deleted" @dataclass @@ -471,6 +469,7 @@ class Skill: tags: dict[str, str] = field(default_factory=dict) aliases: list[SkillAlias] = field(default_factory=list) last_registered_version: str | None = None + latest_version_alias: str | None = None created_by: str | None = None last_updated_by: str | None = None creation_timestamp: int | None = None @@ -481,9 +480,10 @@ class Skill: |---|---|---| | `name` | `str` | Stable logical asset name, unique within a workspace | | `kind` | `SkillKind` | Capability type: `skill`, `agent`, `mcp-server`, `hook` | -| `status` | `SkillStatus` | Skill-level lifecycle: `active`, `deprecated`, `retired` | +| `status` | `SkillStatus` | Read-only, derived from the latest version's status | | `aliases` | `list[SkillAlias]` | Stable version pointers (e.g., `production` → `1.2.0`) | -| `last_registered_version` | `str` | Most recently registered version string | +| `last_registered_version` | `str` | Most recently registered version string (read-only, auto-updated) | +| `latest_version_alias` | `str` | Optional alias name to resolve as "latest" (e.g., `"production"`). If unset, `get_latest_skill_version` falls back to `creation_timestamp` | | `workspace` | `str` | Visibility boundary | **Kind extensibility.** The `kind` enum covers the four capability @@ -491,19 +491,26 @@ types with broad cross-harness support. New kinds can be added without schema changes since the column stores a string value. `kind` is immutable after creation. -#### SkillVersion +**MCP servers: two registration paths.** The MCP server registry +(RFC-0004) is the default and recommended path for registering MCP +servers. It provides deployment tracking via hosted bindings, +deduplication across skill groups, and the full MCP governance model. +Skill groups reference MCP registry entries via `registry="mcp"` in +their membership. -A versioned record containing a typed source pointer, publish state, -and tags. +`kind=mcp-server` in this registry is reserved for MCP configs that +are embedded in a group-level artifact (e.g., an OCI image containing +a complete plugin with an `.mcp.json` file). These are not +independently managed and exist only as part of their containing +artifact. Standalone MCP servers should always be registered in the +MCP registry, not as skills. -```python -class SkillPublishState(StrEnum): - DRAFT = "draft" - PUBLISHED = "published" - DEPRECATED = "deprecated" - RETIRED = "retired" +#### SkillVersion +A versioned record containing a typed source pointer, status, and +tags. +```python class SkillSourceType(StrEnum): GIT = "git" OCI = "oci" @@ -516,9 +523,10 @@ class SkillVersion: version: str source_type: SkillSourceType | None = None source: str | None = None - publish_state: SkillPublishState = SkillPublishState.DRAFT + status: SkillStatus = SkillStatus.ACTIVE content_digest: str | None = None tags: dict[str, str] = field(default_factory=dict) + aliases: list[str] = field(default_factory=list) run_id: str | None = None workspace: str | None = None created_by: str | None = None @@ -533,7 +541,8 @@ class SkillVersion: | `source_type` | `SkillSourceType` | Optional distribution mechanism: `git`, `oci`, `zip` | | `source` | `str` | Optional pointer to the content in the source system. Required for standalone pull; omit when content is only available via a group-level source | | `content_digest` | `str` | Optional digest for integrity verification (e.g., `sha256:abc123...`). Aligns with OCI digest terminology | -| `publish_state` | `SkillPublishState` | Per-version surfacing lifecycle | +| `status` | `SkillStatus` | Per-version lifecycle: `active`, `deprecated`, `deleted` | +| `aliases` | `list[str]` | Alias names currently pointing at this version (read-only, projected from alias table) | | `run_id` | `str` | Optional MLflow run association for trace linkage | **Source type extensibility.** The `source_type` enum is intentionally @@ -559,7 +568,7 @@ verify it on read; verification is the consumer's responsibility. **Immutability contract.** `source_type`, `source`, `content_digest`, and `version` are immutable after creation. To point to different content, -register a new version. Mutable fields (`publish_state`, `tags`) can be +register a new version. Mutable fields (`status`, `tags`) can be updated independently. #### SkillGroup @@ -571,27 +580,47 @@ Follows the same pattern as Skill: a top-level entity with versions, tags, and aliases. ```python -class SkillGroupStatus(StrEnum): - ACTIVE = "active" - DEPRECATED = "deprecated" - RETIRED = "retired" - - @dataclass class SkillGroup: name: str description: str | None = None workspace: str | None = None - status: SkillGroupStatus = SkillGroupStatus.ACTIVE + status: SkillStatus = SkillStatus.ACTIVE tags: dict[str, str] = field(default_factory=dict) aliases: list["SkillGroupAlias"] = field(default_factory=list) last_registered_version: str | None = None + latest_version_alias: 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 ``` +`SkillGroup.status` is read-only, derived from the latest group +version's status. `latest_version_alias` works the same as on `Skill`. + +**Why groups instead of tags?** Tags on individual skills could +express "these skills are related" but cannot provide: + +- **Versioned membership snapshots.** A group version pins specific + member versions, so "pr-workflow v1.0.0" always means the same set + of capabilities. Tags are mutable and cannot capture a reproducible + point-in-time combination. +- **Cross-registry references.** A group version can reference both + skill registry members and MCP server registry members (RFC-0004). + Tags on individual skills cannot express this cross-registry + relationship. +- **Group-level source.** A group version can have its own source + pointer (e.g., a single OCI image containing a complete plugin). + Tags cannot carry source metadata. +- **Independent lifecycle.** A group version has its own status, + aliases, and tags. The group can be deprecated independently of its + members. With tags, lifecycle management would have to be inferred + from individual skill states. +- **Plugin mapping.** Agent harnesses (Claude Code, Codex CLI) model + plugins as bundles of capabilities with a manifest. Skill groups + map directly to this concept; tags do not. + #### SkillGroupVersion A versioned snapshot of a skill group's membership. Each version @@ -605,9 +634,10 @@ class SkillGroupVersion: source_type: SkillSourceType | None = None source: str | None = None content_digest: str | None = None - publish_state: SkillPublishState = SkillPublishState.DRAFT + status: SkillStatus = SkillStatus.ACTIVE tags: dict[str, str] = field(default_factory=dict) members: list["SkillGroupVersionMembership"] = 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 @@ -634,24 +664,56 @@ version's source is required. **Immutability contract.** The membership list and source fields of a group version are immutable after creation. To change the set of skills or source pointer, register a new group version. Mutable fields -(`publish_state`, `tags`) can be updated independently. +(`status`, `tags`) can be updated independently. #### SkillGroupVersionMembership -Each membership entry pins a specific skill version (including source -type). The parent group identity is provided by the enclosing -`SkillGroupVersion`; the storage layer adds those columns as FKs. +Each membership entry pins a specific versioned asset from either the +skill registry or the MCP server registry (RFC-0004). The `registry` +field indicates which registry the member comes from. The parent group +identity is provided by the enclosing `SkillGroupVersion`; the storage +layer adds those columns as FKs. ```python @dataclass(frozen=True) class SkillGroupVersionMembership: - skill_name: str - skill_version: str + member_name: str + member_version: str + registry: str = "skill" # "skill" or "mcp" ``` -A skill can appear in multiple groups and multiple group versions. -Membership is at the skill version level, so a group version is a -reproducible snapshot of "these specific skill versions work together." +| Field | Type | Description | +|---|---|---| +| `member_name` | `str` | Name of the member asset in the target registry | +| `member_version` | `str` | Version of the member asset | +| `registry` | `str` | Which registry the member comes from: `skill` (this registry) or `mcp` (MCP server registry, RFC-0004) | + +When `registry="skill"`, the member references a `SkillVersion` in +this registry. When `registry="mcp"`, the member references an +`MCPServerVersion` in the MCP server registry (RFC-0004). This +cross-registry reference enables: + +- **Deduplication.** Two skill groups that both need `github-mcp` + reference the same MCP registry entry. No duplicate configs. +- **Runtime status.** The MCP registry tracks deployment state via + hosted bindings (`is_deployed`, `endpoint_url`). Install-time + tooling can check whether a referenced MCP server is already + running rather than starting a duplicate. +- **Single source of truth.** MCP server definitions are governed in + the MCP registry; skill groups reference them rather than carrying + standalone copies. + +A member can appear in multiple groups and multiple group versions. +Membership is at the version level, so a group version is a +reproducible snapshot of "these specific asset versions work together." + +**Group-level source and embedded MCP configs.** When a group version +has a group-level source (e.g., a single OCI image containing a +complete plugin), the artifact may include MCP configs alongside +skills and agents. In this case, MCP servers do not need separate +membership entries or MCP registry references — they are part of the +artifact. Cross-registry MCP references are for the case where MCP +servers are independently registered and managed. #### SkillGroupAlias @@ -682,43 +744,51 @@ Tags use the same structure for skill-level, version-level, and group-level tags. The distinction is maintained at the storage and API layer (separate tables, separate endpoints). -### Publish state and lifecycle +### Status and lifecycle + +This lifecycle aligns with the MCP Server Registry (RFC-0004). -#### Per-version publish state +#### Per-version status -Each `SkillVersion` has an independent publish state: +Each `SkillVersion` and `SkillGroupVersion` has an independent status: | State | Meaning | Downstream surfacing | |---|---|---| -| `draft` | Registered but not ready for consumption | Not surfaced | -| `published` | Ready for downstream use | Surfaced to discovery, traces, consumers | +| `active` | Ready for downstream use | Surfaced to discovery, traces, consumers | | `deprecated` | Still functional but no longer recommended | Surfaced with deprecation signal | -| `retired` | Preserved for history, no longer active | Not surfaced | +| `deleted` | Soft-deleted; preserved for history, no longer active | Not surfaced | + +New versions default to `active` upon creation. Allowed transitions: | From | To | |---|---| -| `draft` | `published`, `retired` | -| `published` | `deprecated` | -| `deprecated` | `published`, `retired` | +| `active` | `deprecated` | +| `deprecated` | `active`, `deleted` | + +`deprecated` can return to `active` (re-activate) for cases where a +deprecation was premature. + +#### Skill-level and group-level status -`published` cannot return to `draft`. `deprecated` can return to -`published` (re-publish) for cases where a deprecation was premature. +`Skill.status` and `SkillGroup.status` are read-only, derived from the +latest version's status. This follows the MCP Server Registry pattern +where the parent entity's status reflects its latest version. -#### Skill group version publish state +#### `latest_version_alias` resolution -Each `SkillGroupVersion` has its own publish state lifecycle, following -the same transitions as `SkillVersion`. A group version's publish state -is independent of its member skills' publish states. Publishing a group -version does not require its member skill versions to be published, -though consumers will typically want to verify this. +`get_latest_skill_version(name)` resolves the "latest" version: -#### Skill-level status +1. If `Skill.latest_version_alias` is set, resolve that alias to a + version. +2. If unset, fall back to the version with the most recent + `creation_timestamp`. -`Skill.status` is a separate lifecycle for the logical asset as a whole -(`active`, `deprecated`, `retired`). Setting a skill to `deprecated` -does not automatically change individual version publish states. +`latest_version_alias` is mutable via `update_skill()`. It stores an +alias name (e.g., `"production"`), providing a level of indirection: +the user says "latest means whatever `production` points to." The same +pattern applies to `SkillGroup` and `get_latest_skill_group_version`. ### Database schema @@ -733,8 +803,8 @@ workspace-scoped. | `name` | `String(256)` | PK | | `kind` | `String(20)` | default `'skill'`; `skill`, `agent`, `mcp-server`, `hook` | | `description` | `String(5000)` | | -| `status` | `String(20)` | default `'active'` | | `last_registered_version` | `String(256)` | | +| `latest_version_alias` | `String(256)` | optional; alias name to resolve as "latest" | | `created_by` | `String(256)` | | | `last_updated_by` | `String(256)` | | | `creation_timestamp` | `BigInteger` | millis since epoch | @@ -750,7 +820,7 @@ workspace-scoped. | `source_type` | `String(20)` | nullable; `git`, `oci`, `zip`, etc. | | `source` | `String(2048)` | nullable pointer to skill content | | `content_digest` | `String(512)` | optional integrity digest | -| `publish_state` | `String(20)` | default `'draft'` | +| `status` | `String(20)` | default `'active'` | | `run_id` | `String(32)` | optional MLflow run linkage | | `created_by` | `String(256)` | | | `last_updated_by` | `String(256)` | | @@ -794,8 +864,8 @@ FK: `(workspace, name)` references `skills`, CASCADE delete. | `workspace` | `String(63)` | PK, default `'default'` | | `name` | `String(256)` | PK | | `description` | `String(5000)` | | -| `status` | `String(20)` | default `'active'` | | `last_registered_version` | `String(256)` | | +| `latest_version_alias` | `String(256)` | optional; alias name to resolve as "latest" | | `created_by` | `String(256)` | | | `last_updated_by` | `String(256)` | | | `creation_timestamp` | `BigInteger` | millis since epoch | @@ -811,7 +881,7 @@ FK: `(workspace, name)` references `skills`, CASCADE delete. | `source_type` | `String(20)` | optional; `git`, `oci`, `zip`, etc. | | `source` | `String(2048)` | optional pointer to group artifact | | `content_digest` | `String(512)` | optional integrity digest | -| `publish_state` | `String(20)` | default `'draft'` | +| `status` | `String(20)` | default `'active'` | | `created_by` | `String(256)` | | | `last_updated_by` | `String(256)` | | | `creation_timestamp` | `BigInteger` | millis since epoch | @@ -826,11 +896,20 @@ FK: `(workspace, name)` references `skill_groups`, CASCADE delete. | `workspace` | `String(63)` | PK | | `group_name` | `String(256)` | PK, FK to `skill_group_versions` | | `group_version` | `String(256)` | PK, FK to `skill_group_versions` | -| `skill_name` | `String(256)` | PK, FK to `skill_versions` | -| `skill_version` | `String(256)` | PK, FK to `skill_versions` | +| `member_name` | `String(256)` | PK | +| `member_version` | `String(256)` | PK | +| `registry` | `String(20)` | PK, default `'skill'`; `skill` or `mcp` | FK: `(workspace, group_name, group_version)` references `skill_group_versions`, CASCADE delete. -FK: `(workspace, skill_name, skill_version)` references `skill_versions`, RESTRICT delete. +FK: `(workspace, member_name, member_version)` references `skill_versions`, RESTRICT delete. Only applies when `registry='skill'`. + +**Cross-registry references (`registry='mcp'`).** There is no +database-level FK for MCP registry references. Referential integrity +is enforced at the application layer: the store validates that the +referenced `MCPServerVersion` exists when creating a group version +and returns `RESOURCE_DOES_NOT_EXIST` if it does not. This avoids +deployment-ordering dependencies between RFC-0004 and RFC-0005 +migrations and allows either registry to be deployed independently. #### `skill_group_tags` @@ -866,258 +945,243 @@ primary key components. Single-tenant deployments use `'default'`. **Timestamps.** Set at the application layer via `get_current_time_millis()`, not via DDL defaults. -### Abstract store interface +### Store interface -The store interface follows MLflow's abstract store pattern. +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 don't support skills +(e.g., `FileStore`) to work without stubbing every method. ```python -from abc import abstractmethod - - -class AbstractSkillRegistryStore: +class SkillRegistryMixin: # --- Skill operations --- - @abstractmethod def create_skill( self, name: str, kind: str = "skill", description: str | None = None, - ) -> Skill: ... + ) -> Skill: + raise NotImplementedError - @abstractmethod - def get_skill(self, name: str) -> Skill: ... + def get_skill(self, name: str) -> Skill: + raise NotImplementedError - @abstractmethod 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]: ... + ) -> PagedList[Skill]: + raise NotImplementedError - @abstractmethod def update_skill( self, name: str, description: str | None = None, - status: SkillStatus | None = None, - ) -> Skill: ... + latest_version_alias: str | None = None, + ) -> Skill: + raise NotImplementedError - @abstractmethod - def delete_skill(self, name: str) -> None: ... + def delete_skill(self, name: str) -> None: + raise NotImplementedError # --- SkillVersion operations --- - @abstractmethod def create_skill_version( self, name: str, version: str, source_type: str | None = None, source: str | None = None, - publish_state: SkillPublishState = SkillPublishState.DRAFT, content_digest: str | None = None, run_id: str | None = None, - ) -> SkillVersion: ... + ) -> SkillVersion: + raise NotImplementedError - @abstractmethod def get_skill_version( self, name: str, version: str, - ) -> SkillVersion: ... + ) -> SkillVersion: + raise NotImplementedError - @abstractmethod def get_skill_version_by_alias( self, name: str, alias: str, - ) -> SkillVersion: ... + ) -> SkillVersion: + raise NotImplementedError - @abstractmethod - def get_latest_skill_version(self, name: str) -> SkillVersion: ... + def get_latest_skill_version(self, name: str) -> SkillVersion: + raise NotImplementedError - @abstractmethod 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]: ... + ) -> PagedList[SkillVersion]: + raise NotImplementedError - @abstractmethod def update_skill_version( self, name: str, version: str, - publish_state: SkillPublishState | None = None, - ) -> SkillVersion: ... + status: SkillStatus | None = None, + ) -> SkillVersion: + raise NotImplementedError - @abstractmethod def delete_skill_version( self, name: str, version: str, - ) -> None: ... + ) -> None: + raise NotImplementedError # --- Tag operations --- - @abstractmethod def set_skill_tag( self, name: str, key: str, value: str, - ) -> None: ... + ) -> None: + raise NotImplementedError - @abstractmethod - def delete_skill_tag(self, name: str, key: str) -> None: ... + def delete_skill_tag(self, name: str, key: str) -> None: + raise NotImplementedError - @abstractmethod def set_skill_version_tag( self, name: str, version: str, key: str, value: str, - ) -> None: ... + ) -> None: + raise NotImplementedError - @abstractmethod def delete_skill_version_tag( self, name: str, version: str, key: str, - ) -> None: ... + ) -> None: + raise NotImplementedError # --- Alias operations --- - @abstractmethod def set_skill_alias( self, name: str, alias: str, version: str, - ) -> None: ... + ) -> None: + raise NotImplementedError - @abstractmethod def delete_skill_alias( self, name: str, alias: str, - ) -> None: ... - - # --- Pull operations --- - - @abstractmethod - def pull_skill( - self, name: str, destination: str, - version: str | None = None, - alias: str | None = None, - source_type: str | None = None, - ) -> str: ... - - @abstractmethod - def pull_skill_group( - self, name: str, destination: str, - version: str | None = None, - alias: str | None = None, - ) -> str: ... + ) -> None: + raise NotImplementedError # --- SkillGroup operations --- - @abstractmethod def create_skill_group( self, name: str, description: str | None = None, - ) -> SkillGroup: ... + ) -> SkillGroup: + raise NotImplementedError - @abstractmethod - def get_skill_group(self, name: str) -> SkillGroup: ... + def get_skill_group(self, name: str) -> SkillGroup: + raise NotImplementedError - @abstractmethod def search_skill_groups( self, filter_string: str | None = None, max_results: int = 100, + order_by: list[str] | None = None, page_token: str | None = None, - ) -> PagedList[SkillGroup]: ... + ) -> PagedList[SkillGroup]: + raise NotImplementedError - @abstractmethod def update_skill_group( self, name: str, description: str | None = None, - status: SkillGroupStatus | None = None, - ) -> SkillGroup: ... + latest_version_alias: str | None = None, + ) -> SkillGroup: + raise NotImplementedError - @abstractmethod - def delete_skill_group(self, name: str) -> None: ... + def delete_skill_group(self, name: str) -> None: + raise NotImplementedError # --- SkillGroupVersion operations --- - @abstractmethod def create_skill_group_version( self, name: str, version: str, members: list[SkillGroupVersionMembership], - publish_state: SkillPublishState = SkillPublishState.DRAFT, source_type: str | None = None, source: str | None = None, content_digest: str | None = None, - ) -> SkillGroupVersion: ... + ) -> SkillGroupVersion: + raise NotImplementedError - @abstractmethod def get_skill_group_version( self, name: str, version: str, - ) -> SkillGroupVersion: ... + ) -> SkillGroupVersion: + raise NotImplementedError - @abstractmethod def get_skill_group_version_by_alias( self, name: str, alias: str, - ) -> SkillGroupVersion: ... + ) -> SkillGroupVersion: + raise NotImplementedError - @abstractmethod def get_latest_skill_group_version( self, name: str, - ) -> SkillGroupVersion: ... + ) -> SkillGroupVersion: + raise NotImplementedError - @abstractmethod def search_skill_group_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[SkillGroupVersion]: ... + ) -> PagedList[SkillGroupVersion]: + raise NotImplementedError - @abstractmethod def update_skill_group_version( self, name: str, version: str, - publish_state: SkillPublishState | None = None, - ) -> SkillGroupVersion: ... + status: SkillStatus | None = None, + ) -> SkillGroupVersion: + raise NotImplementedError - @abstractmethod def delete_skill_group_version( self, name: str, version: str, - ) -> None: ... + ) -> None: + raise NotImplementedError # --- SkillGroup tag operations --- - @abstractmethod def set_skill_group_tag( self, name: str, key: str, value: str, - ) -> None: ... + ) -> None: + raise NotImplementedError - @abstractmethod def delete_skill_group_tag( self, name: str, key: str, - ) -> None: ... + ) -> None: + raise NotImplementedError - @abstractmethod def set_skill_group_version_tag( self, name: str, version: str, key: str, value: str, - ) -> None: ... + ) -> None: + raise NotImplementedError - @abstractmethod def delete_skill_group_version_tag( self, name: str, version: str, key: str, - ) -> None: ... + ) -> None: + raise NotImplementedError # --- SkillGroup alias operations --- - @abstractmethod def set_skill_group_alias( self, name: str, alias: str, version: str, - ) -> None: ... + ) -> None: + raise NotImplementedError - @abstractmethod def delete_skill_group_alias( self, name: str, alias: str, - ) -> None: ... + ) -> None: + raise NotImplementedError ``` ### REST API @@ -1148,7 +1212,6 @@ All paths relative to `/ajax-api/3.0/mlflow/skills`. | `POST` | `/{name}/aliases` | Set an alias | | `GET` | `/{name}/aliases/{alias}` | Resolve alias to `SkillVersion` | | `DELETE` | `/{name}/aliases/{alias}` | Delete an alias | -| `POST` | `/{name}/pull` | Pull skill content from source to a local destination | #### Skill group endpoints @@ -1164,7 +1227,7 @@ All paths relative to `/ajax-api/3.0/mlflow/skill-groups`. | `POST` | `/{name}/versions` | Create a group version with members | | `GET` | `/{name}/versions` | Search group versions | | `GET` | `/{name}/versions/{version}` | Get a specific group version | -| `PATCH` | `/{name}/versions/{version}` | Update group version publish state | +| `PATCH` | `/{name}/versions/{version}` | Update group version status | | `DELETE` | `/{name}/versions/{version}` | Delete a group version | | `POST` | `/{name}/tags` | Set a group-level tag | | `DELETE` | `/{name}/tags/{key}` | Delete a group-level tag | @@ -1173,7 +1236,6 @@ All paths relative to `/ajax-api/3.0/mlflow/skill-groups`. | `POST` | `/{name}/aliases` | Set a group alias | | `GET` | `/{name}/aliases/{alias}` | Resolve group alias to version | | `DELETE` | `/{name}/aliases/{alias}` | Delete a group alias | -| `POST` | `/{name}/pull` | Pull all group members from their sources | #### Pagination and filtering @@ -1183,24 +1245,32 @@ expressions following existing MLflow conventions. **Skills and skill groups:** `name LIKE '%review%'`, `status = 'active'`, `kind = 'agent'`, `tags.team = 'platform'` -**Skill versions:** `publish_state = 'published'`, +**Skill versions:** `status = 'active'`, `source_type = 'git'`, `tags.scan.prompt-injection.status = 'pass'` -**Skill group versions:** `publish_state = 'published'`, +**Skill group versions:** `status = 'active'`, `tags.approved = 'true'` ### Python SDK and CLI -The `mlflow.skills` module exposes top-level functions delegating to -`MlflowClient`, with a 1:1 mapping to the abstract store methods above. +The `mlflow.genai.skills` module exposes top-level functions delegating to +`MlflowClient`, with a 1:1 mapping to the store mixin methods above. Two CLI command groups (`mlflow skills` and `mlflow skill-groups`) provide the same operations from the command line. See the basic examples at the top of this RFC for usage. +`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). This keeps the store as a +pure data-access layer. + ### Pull semantics -`pull` resolves a skill or skill group to its source pointer(s) and -fetches content to a local destination directory. It is +`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 | @@ -1237,12 +1307,13 @@ directories. Harness-specific installation is covered in RFC-0006. |---|---|---| | Skill, version, or group not found | `RESOURCE_DOES_NOT_EXIST` | 404 | | Duplicate skill name, version, or group | `RESOURCE_ALREADY_EXISTS` | 409 | -| Invalid publish state transition | `INVALID_PARAMETER_VALUE` | 400 | +| Invalid status transition | `INVALID_PARAMETER_VALUE` | 400 | | Unknown source type | `INVALID_PARAMETER_VALUE` | 400 | | Alias references non-existent version | `RESOURCE_DOES_NOT_EXIST` | 404 | -| Group version member references non-existent skill version | `RESOURCE_DOES_NOT_EXIST` | 404 | +| Group version member references non-existent version (skill or MCP) | `RESOURCE_DOES_NOT_EXIST` | 404 | | Delete skill version referenced by a group version | `INVALID_PARAMETER_VALUE` | 400 | | Delete skill with versions referenced by a group | `INVALID_PARAMETER_VALUE` | 400 | +| Delete MCP server version referenced by a group version | `INVALID_PARAMETER_VALUE` | 400 | | Delete skill or group with no group references | Cascading delete (succeeds) | 200 | ### Workspace scoping @@ -1278,8 +1349,8 @@ The detail view for a skill shows metadata, version list, aliases, tags (including security scan results), and group memberships. The detail view for a skill group shows its description, status, version -list, aliases, and tags. Each group version shows its publish state and -the pinned skill versions it contains. +list, aliases, and tags. Each group version shows its status and the +pinned member versions it contains. ### Security scan tracking @@ -1299,10 +1370,11 @@ Recommended tag conventions: These are conventions, not enforced schema. Organizations can define additional scan tag prefixes for their own scanning tools and criteria. -The publish state lifecycle supports scan-gated promotion workflows: -a skill version stays in `draft` until scans pass, then is moved to -`published`. The registry does not enforce this workflow, but the -combination of publish state and scan tags makes it easy to implement. +The status lifecycle supports scan-gated deprecation workflows: +organizations can deprecate versions that fail scans and use scan +result tags to filter for safe versions. The registry does not enforce +this workflow, but the combination of status and scan tags makes it +easy to implement. ## Drawbacks @@ -1334,7 +1406,7 @@ management. This is sufficient for individual developers and small teams. This RFC proposes a governance layer on top of Git for enterprises that need -publish-state lifecycle, security scan tracking, and federated discovery. +status lifecycle, security scan tracking, and federated discovery. The two approaches are complementary. # Adoption strategy @@ -1345,8 +1417,8 @@ This is a new feature, not a breaking change. Adoption is incremental: - Entities, database schema, store implementation, REST API, Python SDK, CLI, and basic UI. - Users can register capabilities of any kind (skill, agent, mcp-server, - hook), manage publish state, record scan results as tags, organize - capabilities into skill groups, and discover published capabilities. + hook), manage status lifecycle, record scan results as tags, organize + capabilities into skill groups, and discover active capabilities. - `mlflow skills pull` fetches content from registered sources. - Existing MLflow functionality is unaffected. diff --git a/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md b/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md index bc75e20..6ef8f7c 100644 --- a/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md +++ b/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md @@ -8,7 +8,7 @@ rfc_pr: https://github.com/mlflow/rfcs/pull/10 | Author(s) | Bill Murdock (Red Hat) | | :--------------------- | :-- | -| **Date Last Modified** | 2026-04-27 | +| **Date Last Modified** | 2026-04-29 | | **AI Assistant(s)** | Claude Code (Opus 4.6) | # Summary @@ -66,7 +66,7 @@ mlflow skills install --group pr-workflow --alias production \ ```python import mlflow -mlflow.skills.install_skill_group( +mlflow.genai.skills.install_skill_group( name="pr-workflow", alias="production", harness="claude-code", @@ -330,10 +330,9 @@ use the adapter-based `mlflow skills install` command instead. ### Store interface ```python -class AbstractSkillRegistryStore: +class SkillRegistryMixin: # ... (existing methods from RFC-0005) ... - @abstractmethod def install_skill( self, name: str, @@ -342,9 +341,9 @@ class AbstractSkillRegistryStore: version: str | None = None, alias: str | None = None, source_type: str | None = None, - ) -> str: ... + ) -> str: + raise NotImplementedError - @abstractmethod def install_skill_group( self, name: str, @@ -352,14 +351,15 @@ class AbstractSkillRegistryStore: destination: str, version: str | None = None, alias: str | None = None, - ) -> str: ... + ) -> str: + raise NotImplementedError - @abstractmethod def generate_marketplace( self, harness: str, filter_string: str | None = None, - ) -> dict: ... + ) -> dict: + raise NotImplementedError ``` ### REST API From 69d7c2b417e21d9b43cf366acfb0caa50c60b9e2 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Wed, 29 Apr 2026 13:13:29 -0400 Subject: [PATCH 08/19] Add MLflow artifact storage as explicit future source type Document `mlflow` as a deferred source type in the extensibility section and adoption strategy follow-up, per review feedback. Co-Authored-By: Claude Opus 4.6 --- rfcs/0005-skill-registry/0005-skill-registry.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index 1e54a5e..8be2138 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -547,8 +547,13 @@ class SkillVersion: **Source type extensibility.** The `source_type` enum is intentionally small for the initial implementation. New source types (e.g., `s3`, -`azure-blob`) can be added without schema changes since the column -stores a string value. +`azure-blob`, `mlflow`) can be added without schema changes since the +column stores a string value. In particular, an `mlflow` source type +would allow the registry to store skill content directly in MLflow's +artifact storage, providing a natural UI upload flow and keeping the +door open for MLflow-native packaging. This is deferred from the +initial implementation to keep the registry metadata-first, but can be +added as a follow-up without breaking changes. **Version uniqueness.** The combination of `(name, version)` is unique within a workspace. A skill version represents a single logical @@ -1432,4 +1437,7 @@ This is a new feature, not a breaking change. Adoption is incremental: - Agent trace integration: traces automatically record which registered capability version was used, linking back to the registry. - Usage analytics dashboard based on trace metadata. -- Additional source types and capability kinds as demand emerges. +- Additional source types as demand emerges, including an `mlflow` + source type for storing skill content directly in MLflow artifact + storage (see "Source type extensibility" in the data model section). +- Additional capability kinds as demand emerges. From b799da21b4fea1585cb29352b3cd9051377f56f3 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Wed, 29 Apr 2026 14:31:08 -0400 Subject: [PATCH 09/19] Add persona-based use cases, scan tag convention, and permissions model - Replace abstract use case bullets with end-to-end persona flows (platform admin, developer, security engineer) - Expand security scan tracking with structured tag namespace convention (scan.{type}.{field}) and documented fields/examples - Add permissions section mapping operations to MLflow's READ/EDIT/MANAGE levels, with status transitions and alias management requiring MANAGE Co-Authored-By: Claude Opus 4.6 --- .../0005-skill-registry.md | 163 ++++++++++++------ 1 file changed, 111 insertions(+), 52 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index 8be2138..20b535a 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -67,7 +67,7 @@ mlflow.genai.skills.set_skill_alias( version="1.0.0", ) -# Record a security scan result as a tag +# Record security scan results (see "Security scan tracking" for convention) mlflow.genai.skills.set_skill_version_tag( name="code-review", version="1.0.0", @@ -78,7 +78,13 @@ mlflow.genai.skills.set_skill_version_tag( name="code-review", version="1.0.0", key="scan.prompt-injection.date", - value="2026-04-22", + value="2026-04-29", +) +mlflow.genai.skills.set_skill_version_tag( + name="code-review", + version="1.0.0", + key="scan.prompt-injection.tool", + value="promptfoo/1.2.0", ) ``` @@ -361,39 +367,28 @@ address: ### Use cases -1. **Governed registration**: Platform administrators register - capability metadata with typed source pointers to where the content - lives (Git, OCI, ZIP). The registry governs; the source system - stores. All four capability kinds (skill, agent, mcp-server, hook) - use the same registration model. - -2. **Lifecycle management**: Capability versions move through status - states (active, deprecated, deleted) to control downstream - surfacing. This is the governance layer that Git lacks. - -3. **Security scan tracking**: Scan results (prompt injection, code - vulnerabilities, etc.) are recorded as version-level tags. The - registry does not perform scans; it provides the metadata layer for - recording and querying results. - -4. **Cross-kind grouping**: Related capabilities of any kind are - organized into skill groups for discovery and governance. A skill - group maps to the "plugin" concept in agent harnesses — for example, - a "pr-workflow" group might bundle a code-review skill, a - security-auditor agent, and a GitHub MCP server. - -5. **Federated discovery**: Users discover published capabilities and - groups across all source types from a single search interface, - filtered by kind, without requiring content to be centralized. - -6. **Pull**: `mlflow skills pull` fetches capability content from its - registered source to a local directory. This is source-type-aware - (git clone, OCI pull, ZIP extract) and harness-agnostic. - -7. **Usage analytics**: Agent traces record which capability versions - were used. Combined with registry metadata, this enables - organizations to understand adoption and make data-driven promotion - decisions. +**Platform administrator** — A platform admin at Acme Corp registers +their team's code-review skill, pointing to its Git source. They +create a version, record a prompt-injection scan result as a tag, and +group it with a security-auditor agent and a GitHub MCP server into a +"pr-workflow" skill group. They set the group's `production` alias to +the tested version. When a newer version introduces a vulnerability, +they deprecate it — downstream consumers resolving `production` are +unaffected because the alias still points to the safe version. + +**Developer** — A developer starting a new project searches the +registry for active skills filtered by `kind = 'skill'`. They find +the `pr-workflow` group, resolve its `production` alias, and run +`mlflow skills pull --group pr-workflow --alias production` to fetch +all member content locally. They can also browse and install directly +from their agent harness if marketplace integration is configured +([RFC-0006](../0006-skill-harness-integration/0006-skill-harness-integration.md)). + +**Security engineer** — A security engineer queries scan tags across +all skill versions to find capabilities that haven't been scanned +recently (`tags.scan.prompt-injection.date < '2026-01-01'`). They +deprecate versions that fail re-scanning and track compliance posture +across the organization's registered capabilities. ### Out of scope @@ -1340,6 +1335,36 @@ and other AI asset registries. It is expected to be solved at the platform level across all MLflow registries rather than piecemeal in each one. +### Permissions + +The skill registry integrates with MLflow's existing permission +framework (READ / EDIT / MANAGE), applied at the `Skill` and +`SkillGroup` level. Versions, tags, aliases, and memberships inherit +permissions from their parent entity. + +| Permission | Operations | +|---|---| +| `READ` | Search skills and groups, get versions, resolve aliases, list tags and memberships | +| `EDIT` | Create skills and groups, create versions, set and delete tags, update description | +| `MANAGE` | Status transitions (deprecate, delete), set and delete aliases, delete versions, delete skills and groups, manage permissions | + +Key design choices: + +- **Status transitions require MANAGE.** Deprecating or deleting a + capability version affects all downstream consumers. This is a + governance action, not a routine edit, and should require elevated + permissions. +- **Alias management requires MANAGE.** Aliases like `production` + control which version downstream consumers resolve to. Changing an + alias has the same blast radius as a status transition. +- **Tag edits require EDIT.** Tags (including scan result tags) are + operational metadata. Requiring MANAGE for scan tags would create + friction for CI/CD scan integrations that need to record results + automatically. +- **Creator gets MANAGE.** When a user creates a skill or group, they + automatically receive MANAGE permission, following the MLflow model + registry pattern. + ### UI The Skills page lives under the GenAI workflow in the MLflow sidebar, @@ -1360,26 +1385,60 @@ pinned member versions it contains. ### Security scan tracking The registry does not perform security scans. It provides a metadata -layer for recording and querying scan results using version-level tags. +layer for recording and querying scan results using version-level tags +with a reserved `scan.*` namespace. -Recommended tag conventions: +**Tag namespace convention.** All security scan tags use the pattern +`scan.{scan-type}.{field}`, where `{scan-type}` identifies the scan +(e.g., `prompt-injection`, `code-vulnerability`, `secrets-detection`) +and `{field}` is one of the following defined keys: -| Tag key | Example value | Description | +| Field | Expected values | Description | |---|---|---| -| `scan.prompt-injection.status` | `pass`, `fail`, `warning` | Scan result | -| `scan.prompt-injection.date` | `2026-04-22` | When the scan was run | -| `scan.prompt-injection.tool` | `garak-0.9` | Which tool performed the scan | -| `scan.code-vuln.status` | `pass` | Code vulnerability scan result | -| `scan.code-vuln.date` | `2026-04-22` | When the scan was run | - -These are conventions, not enforced schema. Organizations can define -additional scan tag prefixes for their own scanning tools and criteria. - -The status lifecycle supports scan-gated deprecation workflows: -organizations can deprecate versions that fail scans and use scan -result tags to filter for safe versions. The registry does not enforce -this workflow, but the combination of status and scan tags makes it -easy to implement. +| `status` | `pass`, `fail`, `error` | Scan outcome | +| `date` | ISO 8601 date (e.g., `2026-04-29`) | When the scan was run | +| `tool` | Tool name/version (e.g., `promptfoo/1.2.0`) | Which tool performed the scan | +| `details` | URL or free text | Link to full results or summary | + +**Example tags on a skill version:** + +| Tag key | Value | +|---|---| +| `scan.prompt-injection.status` | `pass` | +| `scan.prompt-injection.date` | `2026-04-29` | +| `scan.prompt-injection.tool` | `promptfoo/1.2.0` | +| `scan.code-vulnerability.status` | `fail` | +| `scan.code-vulnerability.date` | `2026-04-28` | +| `scan.code-vulnerability.tool` | `semgrep/1.67.0` | +| `scan.code-vulnerability.details` | `https://scans.acme.com/results/abc123` | + +**Convention, not schema.** These are documented conventions, not +server-enforced schema. The registry does not validate that `status` +is one of the expected values or that `date` is a valid ISO 8601 +string. This is a deliberate tradeoff: the scan tool landscape is +evolving rapidly, and a flexible convention allows organizations to +adopt new scan types without schema changes. Organizations can define +additional `scan.{type}` prefixes for their own scanning tools. + +**UI rendering.** The convention gives the UI enough structure to +detect `scan.*.status` tags and render a scan summary (e.g., a green +check or red X per scan type) without requiring a dedicated entity. + +**Querying.** Scan results are queryable using the existing filter +syntax: `tags.scan.prompt-injection.status = 'pass'` or +`tags.scan.code-vulnerability.date < '2026-01-01'`. + +**Scan-gated workflows.** The status lifecycle supports scan-gated +deprecation: organizations can deprecate versions that fail scans and +use scan result tags to filter for safe versions. The registry does +not enforce this workflow, but the combination of status and scan tags +makes it straightforward to implement. + +**Future evolution.** If scan patterns stabilize and the convention +proves insufficient (e.g., organizations need server-side validation, +separate permissions for scan results, or richer scan metadata), +structured scan metadata can be added as a first-class entity in a +follow-up without breaking the tag-based approach. ## Drawbacks From 0dd8ea4ebbc92b1e718f90d945968ffd44691b21 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Wed, 29 Apr 2026 14:36:44 -0400 Subject: [PATCH 10/19] Fix section heading: remove stale 'publish' reference Co-Authored-By: Claude Opus 4.6 --- rfcs/0005-skill-registry/0005-skill-registry.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index 20b535a..cabc323 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -39,7 +39,7 @@ RFC (RFC-0006). # Basic example -## Register a skill and publish it +## Register a skill ```python import mlflow From a6d573d7268f36605bca22d4f8d31bbc8071fbe8 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Thu, 30 Apr 2026 14:32:28 -0400 Subject: [PATCH 11/19] Add MLflow artifact storage, alias audit trail, export/import follow-up - Promote source_type="mlflow" from deferred to first-class: directory tree storage in MLflow artifact store, consistent with model artifacts - Add alias audit trail: append-only history tables, entity definitions, store methods, and REST endpoints for both skills and skill groups - Add cross-workspace export/import as explicit follow-up item - Update RFC-0006 to use source-agnostic language Co-Authored-By: Claude Opus 4.6 --- .../0005-skill-registry.md | 151 +++++++++++++++--- .../0006-skill-harness-integration.md | 4 +- 2 files changed, 130 insertions(+), 25 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index cabc323..ca91987 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -392,10 +392,10 @@ across the organization's registered capabilities. ### Out of scope -- **Artifact storage.** The registry stores metadata and source - pointers. Content remains in Git, OCI, or other distribution systems. - `pull` fetches from the source; the registry itself does not store - artifacts. +- **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 capabilities, not the process of writing them. - **Format specification.** The registry is format-agnostic. It does @@ -422,10 +422,12 @@ erDiagram Skill ||--o{ SkillVersion : "has versions" Skill ||--o{ SkillTag : "has tags" Skill ||--o{ SkillAlias : "has aliases" +SkillAlias ||--o{ SkillAliasHistory : "has history" SkillVersion ||--o{ SkillVersionTag : "has tags" SkillGroup ||--o{ SkillGroupVersion : "has versions" SkillGroup ||--o{ SkillGroupTag : "has tags" SkillGroup ||--o{ SkillGroupAlias : "has aliases" +SkillGroupAlias ||--o{ SkillGroupAliasHistory : "has history" SkillGroupVersion ||--o{ SkillGroupVersionMembership : "contains members" SkillGroupVersion ||--o{ SkillGroupVersionTag : "has tags" SkillGroupVersionMembership }o--o| SkillVersion : "references (registry=skill)" @@ -542,13 +544,37 @@ class SkillVersion: **Source type extensibility.** The `source_type` enum is intentionally small for the initial implementation. New source types (e.g., `s3`, -`azure-blob`, `mlflow`) can be added without schema changes since the -column stores a string value. In particular, an `mlflow` source type -would allow the registry to store skill content directly in MLflow's -artifact storage, providing a natural UI upload flow and keeping the -door open for MLflow-native packaging. This is deferred from the -initial implementation to keep the registry metadata-first, but can be -added as a follow-up without breaking changes. +`azure-blob`) can be added without schema changes since the column +stores a string value. + +**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 MLflow artifact URI (e.g., +`mlflow-artifacts:/skills/code-review/1.0.0/`). Pull downloads the +directory tree from the artifact store. The MLflow UI can browse +individual files within a stored skill version. + +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 @@ -744,6 +770,32 @@ Tags use the same structure for skill-level, version-level, and group-level tags. The distinction is maintained at the storage and API layer (separate tables, separate endpoints). +#### Alias audit trail + +Alias changes are auditable. Every call to `set_skill_alias`, +`delete_skill_alias`, `set_skill_group_alias`, or +`delete_skill_group_alias` appends a record to an append-only history +table. This supports governance questions like "who promoted this to +production and when?" or "what was production pointing to before the +incident?" + +```python +@dataclass(frozen=True) +class SkillAliasHistory: + name: str # parent Skill name + alias: str # e.g., "production" + old_version: str | None # previous target (None if alias was created) + new_version: str | None # new target (None if alias was deleted) + changed_by: str | None + timestamp: int | None # millis since epoch +``` + +History is recorded automatically by the store on every alias +mutation. The same structure applies to `SkillGroupAliasHistory`. + +History records are read-only and append-only. They cannot be modified +or deleted through the API. + ### Status and lifecycle This lifecycle aligns with the MCP Server Registry (RFC-0004). @@ -857,6 +909,20 @@ FK: `(workspace, name)` references `skills`, CASCADE delete. | `alias` | `String(256)` | PK | | `version` | `String(256)` | target version string | +#### `skill_alias_history` + +| Column | Type | Notes | +|--------|------|-------| +| `workspace` | `String(63)` | FK | +| `name` | `String(256)` | FK | +| `alias` | `String(256)` | | +| `old_version` | `String(256)` | nullable; null on alias creation | +| `new_version` | `String(256)` | nullable; null on alias deletion | +| `changed_by` | `String(256)` | | +| `timestamp` | `BigInteger` | millis since epoch; PK with workspace, name, alias | + +Append-only. No updates or deletes through the API. + #### `skill_groups` | Column | Type | Notes | @@ -939,6 +1005,20 @@ migrations and allows either registry to be deployed independently. | `alias` | `String(256)` | PK | | `version` | `String(256)` | target group version string | +#### `skill_group_alias_history` + +| Column | Type | Notes | +|--------|------|-------| +| `workspace` | `String(63)` | FK | +| `name` | `String(256)` | FK | +| `alias` | `String(256)` | | +| `old_version` | `String(256)` | nullable; null on alias creation | +| `new_version` | `String(256)` | nullable; null on alias deletion | +| `changed_by` | `String(256)` | | +| `timestamp` | `BigInteger` | millis since epoch; PK with workspace, name, alias | + +Append-only. No updates or deletes through the API. + **Workspace handling.** All tables use `(workspace, ...)` as the leading primary key components. Single-tenant deployments use `'default'`. @@ -1067,6 +1147,15 @@ class SkillRegistryMixin: ) -> None: raise NotImplementedError + def get_skill_alias_history( + self, + name: str, + alias: str | None = None, + max_results: int = 100, + page_token: str | None = None, + ) -> PagedList[SkillAliasHistory]: + raise NotImplementedError + # --- SkillGroup operations --- def create_skill_group( @@ -1182,6 +1271,15 @@ class SkillRegistryMixin: self, name: str, alias: str, ) -> None: raise NotImplementedError + + def get_skill_group_alias_history( + self, + name: str, + alias: str | None = None, + max_results: int = 100, + page_token: str | None = None, + ) -> PagedList[SkillGroupAliasHistory]: + raise NotImplementedError ``` ### REST API @@ -1212,6 +1310,8 @@ All paths relative to `/ajax-api/3.0/mlflow/skills`. | `POST` | `/{name}/aliases` | Set an alias | | `GET` | `/{name}/aliases/{alias}` | Resolve alias to `SkillVersion` | | `DELETE` | `/{name}/aliases/{alias}` | Delete an alias | +| `GET` | `/{name}/aliases/history` | Get alias change history (all aliases) | +| `GET` | `/{name}/aliases/{alias}/history` | Get alias change history (specific alias) | #### Skill group endpoints @@ -1236,6 +1336,8 @@ All paths relative to `/ajax-api/3.0/mlflow/skill-groups`. | `POST` | `/{name}/aliases` | Set a group alias | | `GET` | `/{name}/aliases/{alias}` | Resolve group alias to version | | `DELETE` | `/{name}/aliases/{alias}` | Delete a group alias | +| `GET` | `/{name}/aliases/history` | Get alias change history (all aliases) | +| `GET` | `/{name}/aliases/{alias}/history` | Get alias change history (specific alias) | #### Pagination and filtering @@ -1452,16 +1554,16 @@ follow-up without breaking the tag-based approach. # Alternatives -## Store skill artifacts directly in MLflow +## Store skill artifacts only in MLflow (no source pointers) -Store skill bundles (SKILL.md + scripts + assets) as MLflow artifacts -alongside the metadata. +Make MLflow artifact storage the sole storage mechanism, with no +support for external source pointers. -Rejected because skills are already versioned and stored in Git, OCI, or -other systems. Source pointers federate across distribution mechanisms -naturally; artifact storage forces centralization. Organizations that -want artifact backup can use OCI registries, which already provide -versioned, content-addressable storage. +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) @@ -1483,6 +1585,8 @@ This is a new feature, not a breaking change. Adoption is incremental: - Users can register capabilities of any kind (skill, agent, mcp-server, hook), manage status lifecycle, record scan results as tags, organize capabilities into skill groups, and discover active capabilities. +- Source types include `git`, `oci`, `zip`, and `mlflow` (direct + artifact storage). - `mlflow skills pull` fetches content from registered sources. - Existing MLflow functionality is unaffected. @@ -1496,7 +1600,8 @@ This is a new feature, not a breaking change. Adoption is incremental: - Agent trace integration: traces automatically record which registered capability version was used, linking back to the registry. - Usage analytics dashboard based on trace metadata. -- Additional source types as demand emerges, including an `mlflow` - source type for storing skill content directly in MLflow artifact - storage (see "Source type extensibility" in the data model section). -- Additional capability kinds as demand emerges. +- Additional source types and capability kinds as demand emerges. +- Cross-workspace export/import for promoting assets between + workspaces or instances. This should follow whatever pattern the + other MLflow registries adopt rather than designing a serialization + format in isolation. diff --git a/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md b/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md index 6ef8f7c..e680932 100644 --- a/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md +++ b/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md @@ -39,9 +39,9 @@ capabilities from their registered sources, and generates: .claude/plugins/pr-workflow/ .claude-plugin/plugin.json # Generated manifest skills/ - code-review/SKILL.md # Pulled from Git source + code-review/SKILL.md # Pulled from registered source agents/ - security-auditor.md # Pulled from Git source + security-auditor.md # Pulled from registered source .mcp.json # Generated from mcp-server members ``` From dbde6f2a4209d892b8ee35da2380d2a22265eb90 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Thu, 7 May 2026 10:32:38 -0400 Subject: [PATCH 12/19] Add DRAFT status to align with MCP Registry RFC lifecycle Co-Authored-By: Claude Opus 4.6 --- .../0005-skill-registry.md | 63 +++++++++++-------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index ca91987..b7a9eff 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -58,16 +58,10 @@ version = mlflow.genai.skills.create_skill_version( source="https://github.com/acme/agent-skills/tree/v1.0.0/code-review", content_digest="sha256:a3f2b8c...", ) -# version.status == "active" +# version.status == "draft" -# Set an alias for stable resolution -mlflow.genai.skills.set_skill_alias( - name="code-review", - alias="production", - version="1.0.0", -) - -# Record security scan results (see "Security scan tracking" for convention) +# Record security scan results while still in draft +# (see "Security scan tracking" for convention) mlflow.genai.skills.set_skill_version_tag( name="code-review", version="1.0.0", @@ -86,6 +80,20 @@ mlflow.genai.skills.set_skill_version_tag( key="scan.prompt-injection.tool", value="promptfoo/1.2.0", ) + +# Activate the version once it's ready for downstream use +mlflow.genai.skills.update_skill_version( + name="code-review", + version="1.0.0", + status="active", +) + +# Set an alias for stable resolution +mlflow.genai.skills.set_skill_alias( + name="code-review", + alias="production", + version="1.0.0", +) ``` ## Create a skill group with a versioned membership snapshot @@ -451,6 +459,7 @@ class SkillKind(StrEnum): class SkillStatus(StrEnum): + DRAFT = "draft" ACTIVE = "active" DEPRECATED = "deprecated" DELETED = "deleted" @@ -462,7 +471,7 @@ class Skill: kind: SkillKind = SkillKind.SKILL description: str | None = None workspace: str | None = None - status: SkillStatus = SkillStatus.ACTIVE + status: SkillStatus = SkillStatus.DRAFT tags: dict[str, str] = field(default_factory=dict) aliases: list[SkillAlias] = field(default_factory=list) last_registered_version: str | None = None @@ -477,7 +486,7 @@ class Skill: |---|---|---| | `name` | `str` | Stable logical asset name, unique within a workspace | | `kind` | `SkillKind` | Capability type: `skill`, `agent`, `mcp-server`, `hook` | -| `status` | `SkillStatus` | Read-only, derived from the latest version's status | +| `status` | `SkillStatus` | Read-only, derived from the latest version's status: `draft`, `active`, `deprecated`, `deleted` | | `aliases` | `list[SkillAlias]` | Stable version pointers (e.g., `production` → `1.2.0`) | | `last_registered_version` | `str` | Most recently registered version string (read-only, auto-updated) | | `latest_version_alias` | `str` | Optional alias name to resolve as "latest" (e.g., `"production"`). If unset, `get_latest_skill_version` falls back to `creation_timestamp` | @@ -520,7 +529,7 @@ class SkillVersion: version: str source_type: SkillSourceType | None = None source: str | None = None - status: SkillStatus = SkillStatus.ACTIVE + status: SkillStatus = SkillStatus.DRAFT content_digest: str | None = None tags: dict[str, str] = field(default_factory=dict) aliases: list[str] = field(default_factory=list) @@ -538,7 +547,7 @@ class SkillVersion: | `source_type` | `SkillSourceType` | Optional distribution mechanism: `git`, `oci`, `zip` | | `source` | `str` | Optional pointer to the content in the source system. Required for standalone pull; omit when content is only available via a group-level source | | `content_digest` | `str` | Optional digest for integrity verification (e.g., `sha256:abc123...`). Aligns with OCI digest terminology | -| `status` | `SkillStatus` | Per-version lifecycle: `active`, `deprecated`, `deleted` | +| `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) | | `run_id` | `str` | Optional MLflow run association for trace linkage | @@ -611,7 +620,7 @@ class SkillGroup: name: str description: str | None = None workspace: str | None = None - status: SkillStatus = SkillStatus.ACTIVE + status: SkillStatus = SkillStatus.DRAFT tags: dict[str, str] = field(default_factory=dict) aliases: list["SkillGroupAlias"] = field(default_factory=list) last_registered_version: str | None = None @@ -660,7 +669,7 @@ class SkillGroupVersion: source_type: SkillSourceType | None = None source: str | None = None content_digest: str | None = None - status: SkillStatus = SkillStatus.ACTIVE + status: SkillStatus = SkillStatus.DRAFT tags: dict[str, str] = field(default_factory=dict) members: list["SkillGroupVersionMembership"] = field(default_factory=list) aliases: list[str] = field(default_factory=list) @@ -806,21 +815,25 @@ Each `SkillVersion` and `SkillGroupVersion` has an independent status: | 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 for history, no longer active | Not surfaced | -New versions default to `active` upon creation. +New versions default to `draft` upon creation. Allowed transitions: | From | To | |---|---| +| `draft` | `active`, `deleted` | | `active` | `deprecated` | | `deprecated` | `active`, `deleted` | -`deprecated` can return to `active` (re-activate) for cases where a -deprecation was premature. +`draft` allows a version to be registered, tagged with scan results, +and reviewed before being made visible to consumers. `deprecated` can +return to `active` (re-activate) for cases where a deprecation was +premature. #### Skill-level and group-level status @@ -872,7 +885,7 @@ workspace-scoped. | `source_type` | `String(20)` | nullable; `git`, `oci`, `zip`, etc. | | `source` | `String(2048)` | nullable pointer to skill content | | `content_digest` | `String(512)` | optional integrity digest | -| `status` | `String(20)` | default `'active'` | +| `status` | `String(20)` | default `'draft'` | | `run_id` | `String(32)` | optional MLflow run linkage | | `created_by` | `String(256)` | | | `last_updated_by` | `String(256)` | | @@ -947,7 +960,7 @@ Append-only. No updates or deletes through the API. | `source_type` | `String(20)` | optional; `git`, `oci`, `zip`, etc. | | `source` | `String(2048)` | optional pointer to group artifact | | `content_digest` | `String(512)` | optional integrity digest | -| `status` | `String(20)` | default `'active'` | +| `status` | `String(20)` | default `'draft'` | | `created_by` | `String(256)` | | | `last_updated_by` | `String(256)` | | | `creation_timestamp` | `BigInteger` | millis since epoch | @@ -1448,14 +1461,14 @@ permissions from their parent entity. |---|---| | `READ` | Search skills and groups, get versions, resolve aliases, list tags and memberships | | `EDIT` | Create skills and groups, create versions, set and delete tags, update description | -| `MANAGE` | Status transitions (deprecate, delete), set and delete aliases, delete versions, delete skills and groups, manage permissions | +| `MANAGE` | Status transitions (activate, deprecate, delete), set and delete aliases, delete versions, delete skills and groups, manage permissions | Key design choices: -- **Status transitions require MANAGE.** Deprecating or deleting a - capability version affects all downstream consumers. This is a - governance action, not a routine edit, and should require elevated - permissions. +- **Status transitions require MANAGE.** Activating, deprecating, or + deleting a capability version affects all downstream consumers. This + is a governance action, not a routine edit, and should require + elevated permissions. - **Alias management requires MANAGE.** Aliases like `production` control which version downstream consumers resolve to. Changing an alias has the same blast radius as a status transition. From a8446ecdc8d923875890d89487b6443169618b00 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Tue, 12 May 2026 16:40:57 -0400 Subject: [PATCH 13/19] Align with merged MCP Registry RFC: latest_version, unpublish transition - Replace latest_version_alias (alias indirection) with latest_version (direct version string) to match MCP RFC pattern - Add active -> draft (unpublish) status transition to match MCP RFC - Reserve "latest" alias name, matching MCP RFC convention - Update fallback to ignore draft versions in latest resolution - Update Date Last Modified to 2026-05-12 Co-Authored-By: Claude Opus 4.6 --- .../0005-skill-registry.md | 49 ++++++++++--------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index b7a9eff..ac69c4a 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -8,7 +8,7 @@ rfc_pr: https://github.com/mlflow/rfcs/pull/10 | Author(s) | Bill Murdock (Red Hat) | | :--------------------- | :-- | -| **Date Last Modified** | 2026-04-29 | +| **Date Last Modified** | 2026-05-12 | | **AI Assistant(s)** | Claude Code (Opus 4.6) | # Summary @@ -475,7 +475,7 @@ class Skill: tags: dict[str, str] = field(default_factory=dict) aliases: list[SkillAlias] = field(default_factory=list) last_registered_version: str | None = None - latest_version_alias: str | None = None + latest_version: str | None = None created_by: str | None = None last_updated_by: str | None = None creation_timestamp: int | None = None @@ -489,7 +489,7 @@ class Skill: | `status` | `SkillStatus` | Read-only, derived from the latest version's status: `draft`, `active`, `deprecated`, `deleted` | | `aliases` | `list[SkillAlias]` | Stable version pointers (e.g., `production` → `1.2.0`) | | `last_registered_version` | `str` | Most recently registered version string (read-only, auto-updated) | -| `latest_version_alias` | `str` | Optional alias name to resolve as "latest" (e.g., `"production"`). If unset, `get_latest_skill_version` falls back to `creation_timestamp` | +| `latest_version` | `str` | Optional explicit version string to resolve as "latest". If unset, `get_latest_skill_version` falls back to the most recently created non-`draft` version | | `workspace` | `str` | Visibility boundary | **Kind extensibility.** The `kind` enum covers the four capability @@ -624,7 +624,7 @@ class SkillGroup: tags: dict[str, str] = field(default_factory=dict) aliases: list["SkillGroupAlias"] = field(default_factory=list) last_registered_version: str | None = None - latest_version_alias: str | None = None + latest_version: str | None = None created_by: str | None = None last_updated_by: str | None = None creation_timestamp: int | None = None @@ -632,7 +632,7 @@ class SkillGroup: ``` `SkillGroup.status` is read-only, derived from the latest group -version's status. `latest_version_alias` works the same as on `Skill`. +version's status. `latest_version` works the same as on `Skill`. **Why groups instead of tags?** Tags on individual skills could express "these skills are related" but cannot provide: @@ -827,13 +827,14 @@ Allowed transitions: | From | To | |---|---| | `draft` | `active`, `deleted` | -| `active` | `deprecated` | +| `active` | `draft`, `deprecated` | | `deprecated` | `active`, `deleted` | `draft` allows a version to be registered, tagged with scan results, -and reviewed before being made visible to consumers. `deprecated` can -return to `active` (re-activate) for cases where a deprecation was -premature. +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. #### Skill-level and group-level status @@ -841,19 +842,23 @@ premature. latest version's status. This follows the MCP Server Registry pattern where the parent entity's status reflects its latest version. -#### `latest_version_alias` resolution +#### `latest_version` resolution `get_latest_skill_version(name)` resolves the "latest" version: -1. If `Skill.latest_version_alias` is set, resolve that alias to a - version. -2. If unset, fall back to the version with the most recent - `creation_timestamp`. +1. If `Skill.latest_version` is set, resolve directly to that version. +2. If unset, fall back to the most recently created non-`draft` + version. Draft versions are ignored so that staging a draft does + not change downstream `latest` resolution or the derived skill + status. -`latest_version_alias` is mutable via `update_skill()`. It stores an -alias name (e.g., `"production"`), providing a level of indirection: -the user says "latest means whatever `production` points to." The same -pattern applies to `SkillGroup` and `get_latest_skill_group_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(...)`. + +`latest_version` is mutable via `update_skill()`. The same pattern +applies to `SkillGroup` and `get_latest_skill_group_version`. ### Database schema @@ -869,7 +874,7 @@ workspace-scoped. | `kind` | `String(20)` | default `'skill'`; `skill`, `agent`, `mcp-server`, `hook` | | `description` | `String(5000)` | | | `last_registered_version` | `String(256)` | | -| `latest_version_alias` | `String(256)` | optional; alias name to resolve as "latest" | +| `latest_version` | `String(256)` | optional; explicit version string to resolve as "latest" | | `created_by` | `String(256)` | | | `last_updated_by` | `String(256)` | | | `creation_timestamp` | `BigInteger` | millis since epoch | @@ -944,7 +949,7 @@ Append-only. No updates or deletes through the API. | `name` | `String(256)` | PK | | `description` | `String(5000)` | | | `last_registered_version` | `String(256)` | | -| `latest_version_alias` | `String(256)` | optional; alias name to resolve as "latest" | +| `latest_version` | `String(256)` | optional; explicit version string to resolve as "latest" | | `created_by` | `String(256)` | | | `last_updated_by` | `String(256)` | | | `creation_timestamp` | `BigInteger` | millis since epoch | @@ -1071,7 +1076,7 @@ class SkillRegistryMixin: self, name: str, description: str | None = None, - latest_version_alias: str | None = None, + latest_version: str | None = None, ) -> Skill: raise NotImplementedError @@ -1192,7 +1197,7 @@ class SkillRegistryMixin: self, name: str, description: str | None = None, - latest_version_alias: str | None = None, + latest_version: str | None = None, ) -> SkillGroup: raise NotImplementedError From 119816d471e797ade7303c1ad5741aad75afd989 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Sun, 17 May 2026 17:03:10 -0400 Subject: [PATCH 14/19] Address Khaled's review: register convenience, remove mcp-server kind, unify pull RFC-0005: - Add register_skill() SDK convenience function matching MCP RFC's register_mcp_server() pattern, with content_path for artifact upload - Remove kind=mcp-server from SkillKind (MCP servers belong in MCP registry; embedded configs are artifact content for harness adapters) - Unify pull/pull-group into single pull command with --group flag - Add shared base extraction note to adoption strategy - Update basic examples to use register_skill() RFC-0006: - Drop install POST endpoints (install is client-side) - Keep marketplace.json GET endpoint - Update SDK example to use unified install() Co-Authored-By: Claude Opus 4.6 --- .../0005-skill-registry.md | 138 ++++++++++-------- .../0006-skill-harness-integration.md | 58 +++----- 2 files changed, 96 insertions(+), 100 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index ac69c4a..8afb6b9 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -21,16 +21,17 @@ existing distribution mechanisms: lifecycle management, security scan tracking, usage analytics via traces, and federated discovery across sources. -The registry tracks four capability kinds under the `mlflow.genai.skills` +The registry tracks three capability kinds under the `mlflow.genai.skills` SDK namespace (CLI: `mlflow skills`): - **Skills** (SKILL.md) — reusable agent instructions - **Agents** (agent .md) — sub-agent definitions -- **MCP servers** (JSON config) — tool server integrations - **Hooks** (harness-specific) — event-triggered actions -Skill groups bundle related capabilities of any kind into versioned, -governed units that map to the "plugin" concept in agent harnesses. +Skill groups bundle related capabilities into versioned, governed units +that map to the "plugin" concept in agent harnesses. Groups can also +reference MCP servers from the MCP Server Registry (RFC-0004) via +cross-registry membership. `mlflow skills pull` provides a harness-agnostic way to fetch registered content from its source. Harness-specific installation @@ -44,16 +45,12 @@ RFC (RFC-0006). ```python import mlflow -# Create the logical skill asset -skill = mlflow.genai.skills.create_skill( - name="code-review", - description="Reviews pull requests for correctness, style, and security", -) - -# Register a version pointing to a Git source -version = mlflow.genai.skills.create_skill_version( +# Register a skill version pointing to a Git source. +# The parent Skill entity is auto-created if it doesn't exist. +version = 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/tree/v1.0.0/code-review", content_digest="sha256:a3f2b8c...", @@ -136,47 +133,27 @@ mlflow.genai.skills.set_skill_group_alias( ```python # Register a sub-agent -mlflow.genai.skills.create_skill( +mlflow.genai.skills.register_skill( name="security-auditor", + version="1.0.0", kind="agent", description="Security specialist for auth and payment code", -) -mlflow.genai.skills.create_skill_version( - name="security-auditor", - version="1.0.0", source_type="git", source="https://github.com/acme/agent-skills/tree/v1.0.0/security-auditor", ) -# Register an MCP server -mlflow.genai.skills.create_skill( - name="github-mcp", - kind="mcp-server", - description="GitHub integration via MCP", -) -mlflow.genai.skills.create_skill_version( - name="github-mcp", - version="2.0.0", - source_type="oci", - source="ghcr.io/acme/github-mcp:2.0.0", - content_digest="sha256:b4e9f1d...", -) - # Register a hook -mlflow.genai.skills.create_skill( +mlflow.genai.skills.register_skill( name="pre-commit-scan", + version="1.0.0", kind="hook", description="Runs security scan before tool commits", -) -mlflow.genai.skills.create_skill_version( - name="pre-commit-scan", - version="1.0.0", source_type="git", source="https://github.com/acme/agent-skills/tree/v1.0.0/pre-commit-scan", ) ``` -## Create a skill group with mixed capability kinds +## Create a skill group with cross-registry references ```python from mlflow.entities import SkillGroupVersionMembership @@ -210,15 +187,15 @@ group_version = mlflow.genai.skills.create_skill_group_version( ```python # Pull a single skill version -mlflow.genai.skills.pull_skill( +mlflow.genai.skills.pull( name="code-review", alias="production", destination="./skills/code-review", ) # Pull an entire skill group (all members) -mlflow.genai.skills.pull_skill_group( - name="pr-workflow", +mlflow.genai.skills.pull( + group="pr-workflow", alias="production", destination="./plugins/pr-workflow", ) @@ -229,7 +206,7 @@ mlflow.genai.skills.pull_skill_group( mlflow skills pull --name code-review --alias production \ --destination ./skills/code-review -mlflow skills pull-group --name pr-workflow --alias production \ +mlflow skills pull --group pr-workflow --alias production \ --destination ./plugins/pr-workflow ``` @@ -278,10 +255,10 @@ group_version = mlflow.genai.skills.get_skill_group_version_by_alias( ## CLI usage ```bash -# Register a skill pointing to a Git source -mlflow skills create --name code-review \ - --description "Reviews pull requests" -mlflow skills create-version --name code-review --version 1.0.0 \ +# Register a skill pointing to a Git source. +# The parent Skill entity is auto-created if it doesn't exist. +mlflow skills register --name code-review --version 1.0.0 \ + --description "Reviews pull requests" \ --source-type git \ --source https://github.com/acme/agent-skills/tree/v1.0.0/code-review \ --content-digest sha256:a3f2b8c... @@ -454,7 +431,6 @@ from enum import StrEnum class SkillKind(StrEnum): SKILL = "skill" AGENT = "agent" - MCP_SERVER = "mcp-server" HOOK = "hook" @@ -485,31 +461,24 @@ class Skill: | Field | Type | Description | |---|---|---| | `name` | `str` | Stable logical asset name, unique within a workspace | -| `kind` | `SkillKind` | Capability type: `skill`, `agent`, `mcp-server`, `hook` | +| `kind` | `SkillKind` | Capability type: `skill`, `agent`, `hook` | | `status` | `SkillStatus` | Read-only, derived from the latest version's status: `draft`, `active`, `deprecated`, `deleted` | | `aliases` | `list[SkillAlias]` | Stable version pointers (e.g., `production` → `1.2.0`) | | `last_registered_version` | `str` | Most recently registered version string (read-only, auto-updated) | | `latest_version` | `str` | Optional explicit version string to resolve as "latest". If unset, `get_latest_skill_version` falls back to the most recently created non-`draft` version | | `workspace` | `str` | Visibility boundary | -**Kind extensibility.** The `kind` enum covers the four capability +**Kind extensibility.** The `kind` enum covers the three capability types with broad cross-harness support. New kinds can be added without schema changes since the column stores a string value. `kind` is immutable after creation. -**MCP servers: two registration paths.** The MCP server registry -(RFC-0004) is the default and recommended path for registering MCP -servers. It provides deployment tracking via hosted bindings, -deduplication across skill groups, and the full MCP governance model. -Skill groups reference MCP registry entries via `registry="mcp"` in -their membership. - -`kind=mcp-server` in this registry is reserved for MCP configs that -are embedded in a group-level artifact (e.g., an OCI image containing -a complete plugin with an `.mcp.json` file). These are not -independently managed and exist only as part of their containing -artifact. Standalone MCP servers should always be registered in the -MCP registry, not as skills. +**MCP servers.** MCP servers are registered in the MCP Server Registry +(RFC-0004), not in this registry. Skill groups can reference MCP +registry entries via `registry="mcp"` in their membership. MCP configs +embedded in group-level artifacts (e.g., `.mcp.json` inside an OCI +image) are treated as artifact content discovered by harness adapters +during installation (RFC-0006), not as separately registered entities. #### SkillVersion @@ -871,7 +840,7 @@ workspace-scoped. |--------|------|-------| | `workspace` | `String(63)` | PK, default `'default'` | | `name` | `String(256)` | PK | -| `kind` | `String(20)` | default `'skill'`; `skill`, `agent`, `mcp-server`, `hook` | +| `kind` | `String(20)` | default `'skill'`; `skill`, `agent`, `hook` | | `description` | `String(5000)` | | | `last_registered_version` | `String(256)` | | | `latest_version` | `String(256)` | optional; explicit version string to resolve as "latest" | @@ -1300,6 +1269,42 @@ class SkillRegistryMixin: raise NotImplementedError ``` +### 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 +def register_skill( + name: str, + version: str, + kind: str = "skill", + description: str | None = None, + source_type: str | None = None, + source: str | None = None, + content_path: str | None = None, + content_digest: str | None = None, + run_id: 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 pull( + name: str | None = None, + group: str | None = None, + version: str | None = None, + alias: str | None = None, + destination: str = ".", +) -> str: + """Pull skill or group content from registered sources to a + local directory. Specify name for a single skill or group for + a skill group.""" +``` + ### REST API The REST API uses RESTful nested resource paths, following the pattern @@ -1600,8 +1605,8 @@ This is a new feature, not a breaking change. Adoption is incremental: **This RFC (RFC-0005):** - Entities, database schema, store implementation, REST API, Python SDK, CLI, and basic UI. -- Users can register capabilities of any kind (skill, agent, mcp-server, - hook), manage status lifecycle, record scan results as tags, organize +- Users can register capabilities of any kind (skill, agent, hook), + manage status lifecycle, record scan results as tags, organize capabilities into skill groups, and discover active capabilities. - Source types include `git`, `oci`, `zip`, and `mlflow` (direct artifact storage). @@ -1623,3 +1628,8 @@ This is a new feature, not a breaking change. Adoption is incremental: workspaces or instances. This should follow whatever pattern the other MLflow registries adopt rather than designing a serialization format in isolation. +- Shared base extraction: the MCP Registry RFC identifies extracting + common registry infrastructure (store patterns, permission models, + alias management) as a future phase. The skill registry + implementation should coordinate with that effort where practical + to reduce duplication. diff --git a/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md b/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md index e680932..36591c7 100644 --- a/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md +++ b/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md @@ -66,8 +66,8 @@ mlflow skills install --group pr-workflow --alias production \ ```python import mlflow -mlflow.genai.skills.install_skill_group( - name="pr-workflow", +mlflow.genai.skills.install( + group="pr-workflow", alias="production", harness="claude-code", destination=".", # project root @@ -327,49 +327,35 @@ marketplace infrastructure (currently Claude Code and Codex CLI). Harnesses without marketplace support (Cursor, Antigravity, OpenClaw) use the adapter-based `mlflow skills install` command instead. -### Store interface +### SDK interface -```python -class SkillRegistryMixin: - # ... (existing methods from RFC-0005) ... - - def install_skill( - self, - name: str, - harness: str, - destination: str, - version: str | None = None, - alias: str | None = None, - source_type: str | None = None, - ) -> str: - raise NotImplementedError - - def install_skill_group( - self, - name: str, - harness: str, - destination: str, - version: str | None = None, - alias: str | None = None, - ) -> str: - raise NotImplementedError +Installation is a client-side operation: the SDK resolves the skill or +group from the registry, pulls content from registered sources, and +writes harness-specific manifests and files to the local filesystem. +No server-side install endpoint is needed. - def generate_marketplace( - self, - harness: str, - filter_string: str | None = None, - ) -> dict: - raise NotImplementedError +```python +def install( + name: str | None = None, + group: str | None = None, + harness: str = "claude-code", + destination: str = ".", + version: str | None = None, + alias: str | None = None, +) -> str: + """Install a skill or skill group for a specific harness. + Resolves from the registry, pulls content, generates + harness-specific manifests, and places files in the correct + directories.""" ``` ### REST API -Additional endpoints on the skill and skill group resources: +The only server-side endpoint is the marketplace catalog, which +harnesses query to discover available plugins. | Method | Path | Description | |---|---|---| -| `POST` | `/ajax-api/3.0/mlflow/skills/{name}/install` | Install a single capability for a harness | -| `POST` | `/ajax-api/3.0/mlflow/skill-groups/{name}/install` | Install a skill group for a harness | | `GET` | `/ajax-api/3.0/mlflow/skill-groups/marketplace.json` | Generate marketplace catalog for a harness | ### CLI From 71c57506e14af70c8ef91000723e0c977a1da3b5 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Sun, 17 May 2026 17:54:10 -0400 Subject: [PATCH 15/19] Address Matt's review: clarify artifact scope, content integrity, lock file RFC-0005 changes: - Clarify summary: artifact storage is supported but metadata-first - Clarify source_type="mlflow" means MLflow-managed storage, not a specific URI scheme - Merge content integrity into one section with server-side validation for mlflow sources and client-side for external sources - Remove guidance to register separate versions for different sources - Rename SkillGroupVersionMembership to SkillGroupVersionMember - Unified UI list view showing skills and groups together - Add install count tracking to follow-up roadmap RFC-0006 changes: - Add lock file section for reproducible installs (mlflow-skills.lock) - Add Python entrypoint support for third-party harness adapters Co-Authored-By: Claude Opus 4.6 --- .../0005-skill-registry.md | 81 +++++++++-------- .../0006-skill-harness-integration.md | 88 ++++++++++++++++++- 2 files changed, 131 insertions(+), 38 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index 8afb6b9..5e90d44 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -8,16 +8,17 @@ rfc_pr: https://github.com/mlflow/rfcs/pull/10 | Author(s) | Bill Murdock (Red Hat) | | :--------------------- | :-- | -| **Date Last Modified** | 2026-05-12 | +| **Date Last Modified** | 2026-05-17 | | **AI Assistant(s)** | Claude Code (Opus 4.6) | # Summary Add a Skill Registry to MLflow: a governed, metadata-first registry for AI agent capabilities. The registry stores metadata and typed source -pointers (to Git repos, OCI registries, ZIP archives, etc.) rather -than artifacts directly. It provides enterprise governance on top of -existing distribution mechanisms: lifecycle management, security scan +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, security scan tracking, usage analytics via traces, and federated discovery across sources. @@ -96,7 +97,7 @@ mlflow.genai.skills.set_skill_alias( ## Create a skill group with a versioned membership snapshot ```python -from mlflow.entities import SkillGroupVersionMembership +from mlflow.entities import SkillGroupVersionMember # Create a group for related skills group = mlflow.genai.skills.create_skill_group( @@ -109,13 +110,13 @@ group_version = mlflow.genai.skills.create_skill_group_version( name="pr-workflow", version="1.0.0", members=[ - SkillGroupVersionMembership( + SkillGroupVersionMember( member_name="code-review", member_version="1.0.0", ), - SkillGroupVersionMembership( + SkillGroupVersionMember( member_name="test-coverage", member_version="2.1.0", ), - SkillGroupVersionMembership( + SkillGroupVersionMember( member_name="security-scan", member_version="1.0.0", ), ], @@ -156,7 +157,7 @@ mlflow.genai.skills.register_skill( ## Create a skill group with cross-registry references ```python -from mlflow.entities import SkillGroupVersionMembership +from mlflow.entities import SkillGroupVersionMember group = mlflow.genai.skills.create_skill_group( name="pr-workflow", @@ -168,14 +169,14 @@ group_version = mlflow.genai.skills.create_skill_group_version( name="pr-workflow", version="1.0.0", members=[ - SkillGroupVersionMembership( + SkillGroupVersionMember( member_name="code-review", member_version="1.0.0", ), - SkillGroupVersionMembership( + SkillGroupVersionMember( member_name="security-auditor", member_version="1.0.0", ), # Reference an MCP server from the MCP registry (RFC-0004) - SkillGroupVersionMembership( + SkillGroupVersionMember( member_name="github-mcp", member_version="2.0.0", registry="mcp", ), @@ -243,7 +244,7 @@ group_version = mlflow.genai.skills.get_skill_group_version( name="pr-workflow", version="1.0.0", ) -# group_version.members == [SkillGroupVersionMembership(...), ...] +# group_version.members == [SkillGroupVersionMember(...), ...] # Resolve a group alias group_version = mlflow.genai.skills.get_skill_group_version_by_alias( @@ -413,10 +414,10 @@ SkillGroup ||--o{ SkillGroupVersion : "has versions" SkillGroup ||--o{ SkillGroupTag : "has tags" SkillGroup ||--o{ SkillGroupAlias : "has aliases" SkillGroupAlias ||--o{ SkillGroupAliasHistory : "has history" -SkillGroupVersion ||--o{ SkillGroupVersionMembership : "contains members" +SkillGroupVersion ||--o{ SkillGroupVersionMember : "contains members" SkillGroupVersion ||--o{ SkillGroupVersionTag : "has tags" -SkillGroupVersionMembership }o--o| SkillVersion : "references (registry=skill)" -SkillGroupVersionMembership }o--o| MCPServerVersion : "references (registry=mcp)" +SkillGroupVersionMember }o--o| SkillVersion : "references (registry=skill)" +SkillGroupVersionMember }o--o| MCPServerVersion : "references (registry=mcp)" ``` #### Skill @@ -545,10 +546,14 @@ skills/code-review/1.0.0/ reference/style-guide.md ``` -The `source` field contains the MLflow artifact URI (e.g., -`mlflow-artifacts:/skills/code-review/1.0.0/`). Pull downloads the +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. +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 @@ -557,18 +562,20 @@ 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. If the same content is -available from multiple distribution mechanisms (e.g., Git and OCI), -register separate versions or use a group-level source. +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...`). Consumers can use this to verify that the content -at `source` has not changed since registration. For OCI sources, -this is the native image digest. For Git sources, this is a digest of -the skill file contents at the pinned commit. For ZIP sources, this is -a digest of the archive. The registry stores the digest but does not -verify it on read; verification is the consumer's responsibility. +`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`, `content_digest`, and `version` are immutable after creation. To point to different content, @@ -640,7 +647,7 @@ class SkillGroupVersion: content_digest: str | None = None status: SkillStatus = SkillStatus.DRAFT tags: dict[str, str] = field(default_factory=dict) - members: list["SkillGroupVersionMembership"] = field(default_factory=list) + members: list["SkillGroupVersionMember"] = field(default_factory=list) aliases: list[str] = field(default_factory=list) workspace: str | None = None created_by: str | None = None @@ -670,7 +677,7 @@ group version are immutable after creation. To change the set of skills or source pointer, register a new group version. Mutable fields (`status`, `tags`) can be updated independently. -#### SkillGroupVersionMembership +#### SkillGroupVersionMember Each membership entry pins a specific versioned asset from either the skill registry or the MCP server registry (RFC-0004). The `registry` @@ -680,7 +687,7 @@ layer adds those columns as FKs. ```python @dataclass(frozen=True) -class SkillGroupVersionMembership: +class SkillGroupVersionMember: member_name: str member_version: str registry: str = "skill" # "skill" or "mcp" @@ -1179,7 +1186,7 @@ class SkillRegistryMixin: self, name: str, version: str, - members: list[SkillGroupVersionMembership], + members: list[SkillGroupVersionMember], source_type: str | None = None, source: str | None = None, content_digest: str | None = None, @@ -1495,10 +1502,10 @@ Key design choices: The Skills page lives under the GenAI workflow in the MLflow sidebar, alongside Experiments, Prompts, and other AI asset pages. -The list view shows skills and skill groups in a card-based or table -layout, with name, description, latest version, status, and tags. Users -can filter by status, source type, and search by name or description. A -toggle switches between individual skills and skill groups. +The list view shows skills and skill groups together, with name, +description, latest version, status, and tags. Users can filter by +type (skill, group), status, source type, and search by name or +description. The detail view for a skill shows metadata, version list, aliases, tags (including security scan results), and group memberships. @@ -1623,6 +1630,8 @@ This is a new feature, not a breaking change. Adoption is incremental: - Agent trace integration: traces automatically record which registered capability version was used, linking back to the registry. - Usage analytics dashboard based on trace metadata. +- Install count tracking and surfacing in the UI, enabling users to + sort or rank capabilities by adoption. - Additional source types and capability kinds as demand emerges. - Cross-workspace export/import for promoting assets between workspaces or instances. This should follow whatever pattern the diff --git a/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md b/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md index 36591c7..690c446 100644 --- a/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md +++ b/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md @@ -8,7 +8,7 @@ rfc_pr: https://github.com/mlflow/rfcs/pull/10 | Author(s) | Bill Murdock (Red Hat) | | :--------------------- | :-- | -| **Date Last Modified** | 2026-04-29 | +| **Date Last Modified** | 2026-05-17 | | **AI Assistant(s)** | Claude Code (Opus 4.6) | # Summary @@ -241,7 +241,11 @@ Continue, etc.) follow the same pattern: map kinds to paths, generate manifests, skip unsupported kinds with warnings. New adapters can be contributed without changes to the registry or -the adapter interface. +the adapter interface. Adapters are registered via Python entrypoints +(group `mlflow.skill_harness_adapters`), so third-party adapters can +be installed via `pip install` without modifying MLflow core. MLflow +ships builtin adapters for Claude Code, Codex CLI, and Cursor; +additional harnesses are community-contributed. ### Marketplace integration @@ -373,6 +377,86 @@ mlflow skills install --group pr-workflow --alias production \ mlflow skills harnesses ``` +### Lock file + +A project can check in an `mlflow-skills.lock` file that records the +exact resolved skills, versions, sources, and harness so that +`mlflow skills install` with no arguments reproduces the same local +setup. This is analogous to `package-lock.json` in Node.js or +`poetry.lock` in Python. + +#### Format + +```json +{ + "harness": "claude-code", + "locked_at": "2026-05-17T21:00:00Z", + "entries": [ + { + "type": "group", + "name": "pr-workflow", + "version": "1.0.0", + "alias": "production", + "members": [ + { + "name": "code-review", + "version": "1.0.0", + "source_type": "git", + "source": "https://github.com/acme/agent-skills/tree/v1.0.0/code-review", + "content_digest": "sha256:a3f2b8c..." + }, + { + "name": "security-auditor", + "version": "1.0.0", + "source_type": "git", + "source": "https://github.com/acme/agent-skills/tree/v1.0.0/security-auditor", + "content_digest": "sha256:d7e4a1b..." + }, + { + "name": "github-mcp", + "version": "2.0.0", + "registry": "mcp" + } + ] + } + ] +} +``` + +#### Workflow + +```bash +# First install: resolves from registry and writes lock file +mlflow skills install --group pr-workflow --alias production \ + --harness claude-code --lock + +# Subsequent installs: reads lock file, no registry resolution needed +mlflow skills install + +# Update: re-resolves from registry and updates lock file +mlflow skills install --group pr-workflow --alias production \ + --harness claude-code --lock --update +``` + +The lock file records enough information to reproduce the install +without contacting the registry: source URIs, exact versions, and +content digests. This supports airgapped environments and ensures +reproducible setups across team members. + +#### SDK + +```python +mlflow.genai.skills.install( + group="pr-workflow", + alias="production", + harness="claude-code", + lock=True, +) + +# Install from lock file +mlflow.genai.skills.install() +``` + ## Drawbacks - **Adapter maintenance.** Each harness adapter must be maintained as From 764be423c69df9d20ebd562d864db2981c4f539d Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Tue, 19 May 2026 08:10:35 -0400 Subject: [PATCH 16/19] Add subpath field to separate artifact location from content path For OCI and ZIP source types, multiple skills may share a single artifact. The new subpath field identifies where each skill lives within the artifact. Not used for Git (tree URLs encode the path) or MLflow artifacts (path is scoped at upload time). Co-Authored-By: Claude Opus 4.6 --- .../0005-skill-registry.md | 92 ++++++++++++++++--- 1 file changed, 80 insertions(+), 12 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index 5e90d44..8e3fcdd 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -184,6 +184,44 @@ group_version = mlflow.genai.skills.create_skill_group_version( ) ``` +## Register skills from an OCI artifact with subpath + +```python +# Register individual skills that live inside a shared OCI image. +# The subpath identifies each skill's location within the image. +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", +) + +# Create a group with a group-level OCI source +group_version = mlflow.genai.skills.create_skill_group_version( + name="pr-workflow", + version="1.0.0", + source_type="oci", + source="ghcr.io/acme/agent-plugin:v1.0.0", + members=[ + SkillGroupVersionMember( + member_name="code-review", member_version="1.0.0", + ), + SkillGroupVersionMember( + member_name="test-coverage", member_version="2.1.0", + ), + ], +) +``` + ## Pull skills to a local directory ```python @@ -264,6 +302,13 @@ mlflow skills register --name code-review --version 1.0.0 \ --source https://github.com/acme/agent-skills/tree/v1.0.0/code-review \ --content-digest sha256:a3f2b8c... +# Register a skill from an OCI image with subpath +mlflow skills register --name code-review --version 1.0.0 \ + --description "Reviews pull requests" \ + --source-type oci \ + --source ghcr.io/acme/agent-plugin:v1.0.0 \ + --subpath skills/code-review + # Alias mlflow skills set-alias --name code-review --alias production \ --version 1.0.0 @@ -499,6 +544,7 @@ class SkillVersion: version: str 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) @@ -516,6 +562,7 @@ class SkillVersion: | `version` | `str` | Publisher-supplied version string. Semver recommended but not enforced | | `source_type` | `SkillSourceType` | Optional distribution mechanism: `git`, `oci`, `zip` | | `source` | `str` | Optional pointer to the content in the source system. Required for standalone pull; omit when content is only available via a group-level source | +| `subpath` | `str` | Optional path within the artifact where this skill's content lives. Used with OCI and ZIP source types when multiple skills share a single artifact. Not needed for Git (use tree URLs) or MLflow artifacts (path is scoped at upload) | | `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) | @@ -526,6 +573,17 @@ small for the initial implementation. New source types (e.g., `s3`, `azure-blob`) can be added without schema changes since the column stores a string value. +**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` | Not used. Git tree URLs already encode the repository, ref, and path in a single `source` string (e.g., `https://github.com/acme/skills/tree/v1.0.0/code-review`). | +| `mlflow` | Not used. The artifact path is scoped to the specific skill version at upload time. | + **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 @@ -577,9 +635,9 @@ 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`, `content_digest`, -and `version` are immutable after creation. To point to different content, -register a new version. Mutable fields (`status`, `tags`) can be +**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 (`status`, `tags`) can be updated independently. #### SkillGroup @@ -644,6 +702,7 @@ class SkillGroupVersion: version: str 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) @@ -660,11 +719,14 @@ class SkillGroupVersion: within a workspace. **Group-level source.** A group version can optionally have its own -`source_type`, `source`, and `content_digest`, pointing to a single -artifact (e.g., an OCI image or Git repo) that contains the complete -plugin. When present, `pull` fetches the group artifact as a unit -rather than pulling members individually. This supports distribution -patterns where a plugin is packaged as a single image or repo. +`source_type`, `source`, `subpath`, and `content_digest`, pointing to +a single artifact (e.g., an OCI image or Git repo) that contains the +complete plugin. When present, `pull` fetches the group artifact as a +unit rather than pulling members individually. This supports +distribution patterns where a plugin is packaged as a single image or +repo. Individual members within a group-level artifact use `subpath` +on their `SkillVersion` to identify their location within the +artifact. **Source resolution for pull.** When pulling a group, if the group version has a source, that source is used. Otherwise, each member is @@ -865,6 +927,7 @@ workspace-scoped. | `version` | `String(256)` | PK, publisher-supplied | | `source_type` | `String(20)` | nullable; `git`, `oci`, `zip`, etc. | | `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'` | | `run_id` | `String(32)` | optional MLflow run linkage | @@ -940,6 +1003,7 @@ Append-only. No updates or deletes through the API. | `version` | `String(256)` | PK, publisher-supplied | | `source_type` | `String(20)` | optional; `git`, `oci`, `zip`, etc. | | `source` | `String(2048)` | optional pointer to group 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)` | | @@ -1067,6 +1131,7 @@ class SkillRegistryMixin: version: str, source_type: str | None = None, source: str | None = None, + subpath: str | None = None, content_digest: str | None = None, run_id: str | None = None, ) -> SkillVersion: @@ -1189,6 +1254,7 @@ class SkillRegistryMixin: members: list[SkillGroupVersionMember], source_type: str | None = None, source: str | None = None, + subpath: str | None = None, content_digest: str | None = None, ) -> SkillGroupVersion: raise NotImplementedError @@ -1290,6 +1356,7 @@ def register_skill( 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, run_id: str | None = None, @@ -1408,12 +1475,13 @@ 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 | -| `zip` | HTTP download and extract | +| `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 | **Single skill pull.** Fetches the content at the skill version's -`source` to the destination directory. Returns an error if the skill -version has no `source`. +`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`. **Skill group pull.** Source resolution: 1. If the group version has a `source`, fetch the group artifact as a From da18b634588de9719e2bb610b801923400676457 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Tue, 19 May 2026 08:17:06 -0400 Subject: [PATCH 17/19] Add mlflow to SkillSourceType enum and field table Co-Authored-By: Claude Opus 4.6 --- rfcs/0005-skill-registry/0005-skill-registry.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index 8e3fcdd..2cd318b 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -536,6 +536,7 @@ class SkillSourceType(StrEnum): GIT = "git" OCI = "oci" ZIP = "zip" + MLFLOW = "mlflow" @dataclass @@ -560,7 +561,7 @@ class SkillVersion: | Field | Type | Description | |---|---|---| | `version` | `str` | Publisher-supplied version string. Semver recommended but not enforced | -| `source_type` | `SkillSourceType` | Optional distribution mechanism: `git`, `oci`, `zip` | +| `source_type` | `SkillSourceType` | Optional distribution mechanism: `git`, `oci`, `zip`, `mlflow` | | `source` | `str` | Optional pointer to the content in the source system. Required for standalone pull; omit when content is only available via a group-level source | | `subpath` | `str` | Optional path within the artifact where this skill's content lives. Used with OCI and ZIP source types when multiple skills share a single artifact. Not needed for Git (use tree URLs) or MLflow artifacts (path is scoped at upload) | | `content_digest` | `str` | Optional digest for integrity verification (e.g., `sha256:abc123...`). Aligns with OCI digest terminology | From 5c60df3966fb40ae7cfc7dbd52d3c40fc2b88607 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Tue, 19 May 2026 08:19:13 -0400 Subject: [PATCH 18/19] Remove error handling section per review feedback The MCP Registry RFC (RFC-0004) has no error handling section. Consistent with that precedent, this detail is better left to implementation. Reference copy saved in error-handling-reference.md (not checked in). Co-Authored-By: Claude Opus 4.6 --- rfcs/0005-skill-registry/0005-skill-registry.md | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index 2cd318b..30dd7e7 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -1502,21 +1502,6 @@ matches the digest and returns an error on mismatch. harness-specific manifests or place files in harness-specific directories. Harness-specific installation is covered in RFC-0006. -### Error handling - -| Scenario | Error code | HTTP status | -|---|---|---| -| Skill, version, or group not found | `RESOURCE_DOES_NOT_EXIST` | 404 | -| Duplicate skill name, version, or group | `RESOURCE_ALREADY_EXISTS` | 409 | -| Invalid status transition | `INVALID_PARAMETER_VALUE` | 400 | -| Unknown source type | `INVALID_PARAMETER_VALUE` | 400 | -| Alias references non-existent version | `RESOURCE_DOES_NOT_EXIST` | 404 | -| Group version member references non-existent version (skill or MCP) | `RESOURCE_DOES_NOT_EXIST` | 404 | -| Delete skill version referenced by a group version | `INVALID_PARAMETER_VALUE` | 400 | -| Delete skill with versions referenced by a group | `INVALID_PARAMETER_VALUE` | 400 | -| Delete MCP server version referenced by a group version | `INVALID_PARAMETER_VALUE` | 400 | -| Delete skill or group with no group references | Cascading delete (succeeds) | 200 | - ### Workspace scoping All skill registry operations are workspace-scoped, following the model From 13d040bdf599515a63f0941e33ac0019856e706d Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Tue, 19 May 2026 08:21:38 -0400 Subject: [PATCH 19/19] Trim workspace scoping and adoption strategy sections Workspace scoping now references MLflow's existing patterns instead of enumerating implementation details. Adoption strategy condensed to a three-phase summary. Co-Authored-By: Claude Opus 4.6 --- .../0005-skill-registry.md | 60 +++---------------- 1 file changed, 9 insertions(+), 51 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index 30dd7e7..c803f2c 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -1504,22 +1504,10 @@ directories. Harness-specific installation is covered in RFC-0006. ### Workspace scoping -All skill registry operations are workspace-scoped, following the model -registry pattern: - -- Workspace is resolved via `resolve_entity_workspace_name()` -- Single-tenant deployments use `"default"` -- All database queries filter by workspace -- The REST API derives workspace from the authenticated caller's context -- Version, tag, alias, and group membership operations inherit workspace - from their parent entity - -Cross-workspace sharing (e.g., a platform team publishing skills -visible to all workspaces) is not addressed by this RFC. This is a -cross-registry concern that applies equally to skills, MCP servers, -and other AI asset registries. It is expected to be solved at the -platform level across all MLflow registries rather than piecemeal in -each one. +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 @@ -1661,38 +1649,8 @@ The two approaches are complementary. # Adoption strategy -This is a new feature, not a breaking change. Adoption is incremental: - -**This RFC (RFC-0005):** -- Entities, database schema, store implementation, REST API, Python SDK, - CLI, and basic UI. -- Users can register capabilities of any kind (skill, agent, hook), - manage status lifecycle, record scan results as tags, organize - capabilities into skill groups, and discover active capabilities. -- Source types include `git`, `oci`, `zip`, and `mlflow` (direct - artifact storage). -- `mlflow skills pull` fetches content from registered sources. -- Existing MLflow functionality is unaffected. - -**Companion RFC (RFC-0006):** -- Harness-specific installation: `mlflow skills install` generates - manifests and places files for specific agent harnesses. -- Initial targets: Claude Code, Codex CLI, Cursor, with additional - harnesses based on demand. - -**Follow-up:** -- Agent trace integration: traces automatically record which registered - capability version was used, linking back to the registry. -- Usage analytics dashboard based on trace metadata. -- Install count tracking and surfacing in the UI, enabling users to - sort or rank capabilities by adoption. -- Additional source types and capability kinds as demand emerges. -- Cross-workspace export/import for promoting assets between - workspaces or instances. This should follow whatever pattern the - other MLflow registries adopt rather than designing a serialization - format in isolation. -- Shared base extraction: the MCP Registry RFC identifies extracting - common registry infrastructure (store patterns, permission models, - alias management) as a future phase. The skill registry - implementation should coordinate with that effort where practical - to reduce duplication. +New feature, not a breaking change. Phased rollout: + +- **Phase 1 (this RFC):** Registry entities, store, REST API, SDK, CLI, UI, and `mlflow skills pull`. +- **Phase 2 (RFC-0006):** Harness-specific `mlflow skills install` for Claude Code, Codex CLI, and Cursor. +- **Phase 3 (follow-up):** Trace integration and usage analytics, install count tracking, cross-workspace export/import (following cross-registry patterns), and shared base extraction with the MCP registry.