From 6d1597d62f018ee4fff9adfee4a073efe79dd594 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Thu, 23 Apr 2026 14:31:24 -0400 Subject: [PATCH 01/52] 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/52] 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/52] 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/52] 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/52] 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/52] 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/52] 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/52] 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/52] 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/52] 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/52] 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/52] 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/52] 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/52] 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/52] 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/52] 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/52] 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/52] 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/52] 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. From bd26ad82f74abb9301f3d9f14af772cafb2ea88f Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Wed, 27 May 2026 16:42:43 -0400 Subject: [PATCH 20/52] Add trace integration: mlflow.skill_context() and harness hooks RFC-0005: Add skill_context() context manager that creates SKILL spans with registry coordinates, supporting nested skill stacks. Strengthen motivation item on trace-to-skill linkage. Move trace integration from Phase 3 to Phase 1 in adoption strategy. RFC-0006: Add harness trace integration via install-time manifest and Claude Code PreToolUse/PostToolUse hooks on the Skill tool for automatic SKILL span creation. Co-Authored-By: Claude Opus 4.6 --- .../0005-skill-registry.md | 172 +++++++++++++++++- .../0006-skill-harness-integration.md | 139 +++++++++++++- 2 files changed, 302 insertions(+), 9 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index c803f2c..f68b302 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-05-17 | +| **Date Last Modified** | 2026-05-27 | | **AI Assistant(s)** | Claude Code (Opus 4.6) | # Summary @@ -386,10 +386,15 @@ address: 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 record to understand adoption across - an organization. +5. **No trace-to-skill linkage.** MLflow already traces agent + conversations (Claude Code via `mlflow autolog claude`, SDK + applications via framework autologgers such as + `mlflow.langchain.autolog()` and `mlflow.anthropic.autolog()`). These traces capture LLM calls, + tool use, and token consumption, but there is no way to know which + governed, versioned skill was active during any part of a trace. + Without a registry, organizations cannot answer questions like + "which skill versions are most used?" or "show me all traces where + the deprecated code-review v1.0 was loaded." 6. **No pull mechanism.** Once a user discovers a capability in the registry, there is no standard way to fetch its content from the @@ -1614,6 +1619,157 @@ 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. +### Trace integration + +MLflow already traces agent conversations across multiple frameworks: +Claude Code (via `mlflow autolog claude`), SDK applications (via +framework autologgers such as `mlflow.langchain.autolog()` and +`mlflow.anthropic.autolog()`), and others. These +traces capture LLM calls, tool use, and timing as a tree of spans. +The skill registry closes the observability loop by letting agent +developers indicate which registered skill is active during each +part of a trace. + +#### `mlflow.skill_context()` context manager + +The primary instrumentation API is a context manager that creates a +span of type `SKILL` and attaches registry coordinates as span +attributes: + +```python +with mlflow.skill_context(name="code-review", version="1.0.0") as span: + # All spans created inside this block (including those from + # autologgers) become children of this SKILL span. + result = llm.chat([{"role": "user", "content": "Review this code..."}]) +``` + +The context manager creates a span with the following attributes: + +| Attribute | Value | Description | +|---|---|---| +| `mlflow.skill.name` | Skill name | Registry name of the active skill | +| `mlflow.skill.version` | Version string | Registered version | +| `mlflow.skill.registry` | Workspace name | MLflow workspace (defaults to `"default"`) | + +These three attributes form the `{workspace, name, version}` +coordinates that link the span back to a specific skill version in +the registry. + +#### Skill stacks via nesting + +Skills can invoke other skills. Because `skill_context()` creates a +real span, nesting context managers naturally produces a skill stack +in the trace tree. Consider an agent that uses a "code-review" skill, +which internally invokes a "style-check" skill: + +```python +import mlflow + +def run_code_review(diff: str): + with mlflow.skill_context(name="code-review", version="1.0.0"): + # First LLM call: analyze the diff + analysis = llm.chat([ + {"role": "user", "content": f"Review this diff:\n{diff}"} + ]) + + # Invoke a sub-skill for style checking + style_issues = run_style_check(diff) + + # Second LLM call: synthesize final review + review = llm.chat([ + {"role": "user", "content": f"Summarize: {analysis}, {style_issues}"} + ]) + return review + +def run_style_check(code: str): + with mlflow.skill_context(name="style-check", version="2.0.0"): + return llm.chat([ + {"role": "user", "content": f"Check style:\n{code}"} + ]) +``` + +The resulting trace tree: + +``` +Trace: tr-abc123 +| ++-- Span: "code-review" (type: SKILL) +| | mlflow.skill.name = "code-review" +| | mlflow.skill.version = "1.0.0" +| | +| +-- Span: ChatCompletion (type: LLM) +| | "Review this diff: ..." +| | +| +-- Span: "style-check" (type: SKILL) +| | | mlflow.skill.name = "style-check" +| | | mlflow.skill.version = "2.0.0" +| | | +| | +-- Span: ChatCompletion (type: LLM) +| | "Check style: ..." +| | +| +-- Span: ChatCompletion (type: LLM) +| "Summarize: ..." +``` + +For any span in the tree, walking up the ancestor chain and +collecting SKILL-type spans reconstructs the skill stack. For the +"Check style" LLM call, the stack is +`[code-review@1.0.0, style-check@2.0.0]`. For the "Summarize" LLM +call, the stack is just `[code-review@1.0.0]` because it executes +after the style-check block exits. + +#### What this enables + +With skill-annotated traces, organizations can answer questions that +are impossible without trace-to-registry linkage: + +- **Adoption tracking.** "Which skill versions are most used across + the organization?" Query for SKILL spans grouped by name and + version. +- **Deprecation impact.** "Show me all traces where the deprecated + code-review v1.0 was loaded." Filter traces by + `mlflow.skill.name` and `mlflow.skill.version`. +- **Per-skill cost attribution.** Each SKILL span contains all child + spans. Aggregate token usage and latency per skill, including or + excluding sub-skills. +- **Regression detection.** "Did error rates change after upgrading + style-check from v1.0 to v2.0?" Compare trace outcomes across + skill versions. + +#### Autologger compatibility + +Because `skill_context()` creates a standard MLflow span, it works +with existing autologgers without modification. When an autologger +(Claude, LangChain, OpenAI, etc.) creates a span inside a +`skill_context()` block, that span automatically becomes a child of +the SKILL span. No changes to the autologgers are needed. + +For harness-specific integration (e.g., Claude Code automatically +wrapping skill loads in `skill_context()` spans), see RFC-0006. + +#### Registry validation + +`skill_context()` does not validate that the named skill exists in +the registry at call time. Validating on every invocation would add +latency and create a hard dependency on registry availability. The +trace records the `{workspace, name, version}` coordinates +regardless; the MLflow UI performs a best-effort lookup when +displaying traces and shows a "not found in registry" indicator if +the coordinates do not resolve. + +#### Relationship to MCP trace linking + +The MCP Registry (RFC-0004) provides `link_mcp_server_versions_to_trace()` +for after-the-fact, trace-level association between traces and MCP +server versions. Skill trace integration takes a different approach: +span-level, inline annotation via context managers. The span-based +approach is a better fit for skills because skills are ambient (active +during inference rather than handling discrete requests) and can nest +(a skill invoking a sub-skill). MCP servers have clearer +request/response boundaries that make after-the-fact linking more +natural. Both approaches produce trace metadata that the MLflow UI +can display together. + ## Drawbacks - **Source pointer validity.** The registry stores source pointers but @@ -1651,6 +1807,6 @@ The two approaches are complementary. 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. +- **Phase 1 (this RFC):** Registry entities, store, REST API, SDK, CLI, UI, `mlflow skills pull`, and `mlflow.skill_context()` for trace integration. +- **Phase 2 (RFC-0006):** Harness-specific `mlflow skills install` for Claude Code, Codex CLI, and Cursor. Automatic `skill_context()` wrapping in harness-specific autologgers. +- **Phase 3 (follow-up):** Usage analytics dashboards, install count tracking, cross-workspace export/import (following cross-registry patterns), and shared base extraction with the MCP registry. 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 690c446..0cf6579 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-05-17 | +| **Date Last Modified** | 2026-05-27 | | **AI Assistant(s)** | Claude Code (Opus 4.6) | # Summary @@ -457,6 +457,139 @@ mlflow.genai.skills.install( mlflow.genai.skills.install() ``` +### Trace integration + +RFC-0005 defines `mlflow.skill_context()`, a context manager that +creates SKILL spans in MLflow traces (see RFC-0005, Trace +integration). Agent developers using the Python SDK can call +`skill_context()` directly in their code. This section describes how +harness-specific installation can automate that instrumentation so +users get skill-annotated traces without writing any tracing code. + +#### Install-time manifest + +When `mlflow skills install` places files for a harness, it also +writes a manifest that maps installed skill names to their registry +coordinates: + +**`mlflow-skills-manifest.json`:** +```json +{ + "manifest_version": "1.0", + "skills": { + "code-review": { + "name": "code-review", + "version": "1.0.0", + "registry": "default" + }, + "security-auditor": { + "name": "security-auditor", + "version": "1.0.0", + "registry": "default" + } + } +} +``` + +The manifest is keyed by the skill's local name (the name the harness +uses to invoke it). The value provides the `{registry, name, version}` +coordinates that link back to the skill registry. This file is used +by trace hooks to annotate spans with registry coordinates without +requiring a registry lookup at runtime. + +#### Claude Code: hook-based instrumentation + +Claude Code invokes skills via a built-in `Skill` tool, which fires +`PreToolUse` and `PostToolUse` hook events. The `mlflow skills install` +command can configure hooks that create SKILL spans automatically +when a registered skill is invoked. + +**Hook configuration** (added to `.claude/settings.json`): + +```json +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Skill", + "hooks": [ + { + "type": "command", + "command": "mlflow skills trace-start --manifest .mlflow-skills-manifest.json" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Skill", + "hooks": [ + { + "type": "command", + "command": "mlflow skills trace-end --manifest .mlflow-skills-manifest.json" + } + ] + } + ] + } +} +``` + +The `PreToolUse` hook receives the skill name in its input, looks it +up in the manifest to get registry coordinates, and opens a SKILL +span via the `mlflow autolog claude` trace pipeline. The `PostToolUse` +hook closes the span. Because these hooks integrate with the same +tracing mechanism that `mlflow autolog claude` already uses, SKILL +spans appear as part of the existing trace tree alongside LLM and +tool call spans. + +The hook commands shown above are illustrative. The exact CLI +subcommands and their integration with the `mlflow autolog claude` +trace pipeline are implementation details. + +**Hook installation behavior.** `mlflow skills install` writes the +manifest automatically. It does not modify `settings.json` by +default. Instead, it prints instructions showing the hook +configuration to add. Users can opt in with `--install-hooks` to +have the installer merge hook entries into `settings.json`. This +follows the same security principle as hook handling for plugin +members: users must explicitly enable hooks. + +#### Agent SDK: direct instrumentation + +For developers building agents with the Claude Agent SDK or other +Python frameworks, the recommended approach is to use +`mlflow.skill_context()` directly (see RFC-0005). The Agent SDK's +hook system also supports Python callbacks, so a similar automatic +approach is possible: + +```python +from claude_code_sdk import ClaudeAgentOptions + +async def on_skill_start(input_data, tool_use_id, context): + skill_name = input_data["tool_input"].get("skill") + # Look up registry coordinates from manifest + # Open mlflow.skill_context() span + return {} + +options = ClaudeAgentOptions( + hooks={"PreToolUse": [{"matcher": "Skill", "hook": on_skill_start}]} +) +``` + +Because Agent SDK hooks run in-process, they can call +`mlflow.skill_context()` directly, creating SKILL spans in the +same trace tree as the autologger spans. + +#### Other harnesses + +Trace integration depends on each harness exposing a hook or event +mechanism for skill invocation. Harnesses that support pre/post tool +use hooks (Codex CLI, GitHub Copilot) can follow the same pattern as +Claude Code. Harnesses without hook support cannot be automatically +instrumented; users of those harnesses can still use +`mlflow.skill_context()` manually in SDK-based agent code. + ## Drawbacks - **Adapter maintenance.** Each harness adapter must be maintained as @@ -486,6 +619,10 @@ critical for driving adoption. format). - Cursor adapter (second-highest priority for MLflow's user base). - `marketplace.json` generation for Claude Code / Codex CLI. +- Install-time manifest (`mlflow-skills-manifest.json`) for trace + integration. +- Claude Code trace hooks for automatic SKILL span creation via + `PreToolUse`/`PostToolUse` on the `Skill` tool. **Follow-up:** - Additional harness adapters based on demand. From d16abe0cc7dede485ec021d508783c5461b91b03 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Thu, 28 May 2026 10:18:29 -0400 Subject: [PATCH 21/52] Restructure entity model: separate types, rename SkillGroup to SkillBundle Split the single Skill entity (with kind field) into three separate entity types: Skill, Subagent, Hook. Rename SkillGroup to SkillBundle with typed member lists (skills, subagents, hooks, mcp_servers) replacing the SkillGroupVersionMember entity. Use "subagent" to avoid collision with a future agent registry. Updates span both RFCs: entity definitions, DB schema, store interface, REST API, SDK convenience functions, CLI, and harness adapter interfaces. Co-Authored-By: Claude Opus 4.6 --- .../0005-skill-registry.md | 846 +++++++++++------- .../deferred-review-items.md | 27 + .../0006-skill-harness-integration.md | 97 +- 3 files changed, 600 insertions(+), 370 deletions(-) create mode 100644 rfcs/0005-skill-registry/deferred-review-items.md diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index f68b302..4c9419d 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -22,17 +22,20 @@ on top of existing distribution mechanisms: lifecycle management, security scan tracking, usage analytics via traces, and federated discovery across sources. -The registry tracks three capability kinds under the `mlflow.genai.skills` -SDK namespace (CLI: `mlflow skills`): +The registry manages three entity types under the `mlflow.genai.skills` +SDK namespace (CLI: `mlflow skills`), each with full lifecycle +(versioning, aliases, tags, status): -- **Skills** (SKILL.md) — reusable agent instructions -- **Agents** (agent .md) — sub-agent definitions -- **Hooks** (harness-specific) — event-triggered actions +- **Skills** — a directory containing a SKILL.md entry point plus + supporting files (scripts, templates, reference material) +- **Subagents** — sub-agent definitions that can be invoked by a + parent agent +- **Hooks** — event-triggered actions (harness-specific) -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. +Skill bundles group related capabilities into versioned, governed +units that map to the "plugin" concept in agent harnesses. Bundles +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 @@ -94,92 +97,74 @@ mlflow.genai.skills.set_skill_alias( ) ``` -## Create a skill group with a versioned membership snapshot +## Create a skill bundle with a versioned membership snapshot ```python -from mlflow.entities import SkillGroupVersionMember - -# Create a group for related skills -group = mlflow.genai.skills.create_skill_group( +# Create a bundle for related capabilities +bundle = mlflow.genai.skills.create_skill_bundle( name="pr-workflow", description="End-to-end pull request review workflow", ) -# Create a group version that pins specific skill versions -group_version = mlflow.genai.skills.create_skill_group_version( +# Create a bundle version that pins specific versions +bundle_version = mlflow.genai.skills.create_skill_bundle_version( name="pr-workflow", version="1.0.0", - members=[ - SkillGroupVersionMember( - member_name="code-review", member_version="1.0.0", - ), - SkillGroupVersionMember( - member_name="test-coverage", member_version="2.1.0", - ), - SkillGroupVersionMember( - member_name="security-scan", member_version="1.0.0", - ), + skills=[ + ("code-review", "1.0.0"), + ("test-coverage", "2.1.0"), + ], + subagents=[ + ("security-auditor", "1.0.0"), ], ) # Set an alias for stable resolution -mlflow.genai.skills.set_skill_group_alias( +mlflow.genai.skills.set_skill_bundle_alias( name="pr-workflow", alias="production", version="1.0.0", ) ``` -## Register other capability kinds +## Register other capability types ```python -# Register a sub-agent -mlflow.genai.skills.register_skill( +# Register a subagent +mlflow.genai.skills.register_subagent( name="security-auditor", version="1.0.0", - kind="agent", description="Security specialist for auth and payment code", source_type="git", source="https://github.com/acme/agent-skills/tree/v1.0.0/security-auditor", ) # Register a hook -mlflow.genai.skills.register_skill( +mlflow.genai.skills.register_hook( name="pre-commit-scan", version="1.0.0", - kind="hook", description="Runs security scan before tool commits", source_type="git", source="https://github.com/acme/agent-skills/tree/v1.0.0/pre-commit-scan", ) ``` -## Create a skill group with cross-registry references +## Create a skill bundle with cross-registry references ```python -from mlflow.entities import SkillGroupVersionMember - -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, and MCP server references -group_version = mlflow.genai.skills.create_skill_group_version( +# A bundle version can include skills, subagents, hooks, and MCP servers +bundle_version = mlflow.genai.skills.create_skill_bundle_version( name="pr-workflow", version="1.0.0", - members=[ - SkillGroupVersionMember( - member_name="code-review", member_version="1.0.0", - ), - SkillGroupVersionMember( - member_name="security-auditor", member_version="1.0.0", - ), - # Reference an MCP server from the MCP registry (RFC-0004) - SkillGroupVersionMember( - member_name="github-mcp", member_version="2.0.0", - registry="mcp", - ), + skills=[ + ("code-review", "1.0.0"), + ], + subagents=[ + ("security-auditor", "1.0.0"), + ], + # Reference MCP servers from the MCP registry (RFC-0004) + mcp_servers=[ + ("github-mcp", "2.0.0"), ], ) ``` @@ -205,19 +190,15 @@ mlflow.genai.skills.register_skill( subpath="skills/test-coverage", ) -# Create a group with a group-level OCI source -group_version = mlflow.genai.skills.create_skill_group_version( +# Create a bundle with a bundle-level OCI source +bundle_version = mlflow.genai.skills.create_skill_bundle_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", - ), + skills=[ + ("code-review", "1.0.0"), + ("test-coverage", "2.1.0"), ], ) ``` @@ -232,9 +213,9 @@ mlflow.genai.skills.pull( destination="./skills/code-review", ) -# Pull an entire skill group (all members) +# Pull an entire skill bundle (all members) mlflow.genai.skills.pull( - group="pr-workflow", + bundle="pr-workflow", alias="production", destination="./plugins/pr-workflow", ) @@ -245,7 +226,7 @@ mlflow.genai.skills.pull( mlflow skills pull --name code-review --alias production \ --destination ./skills/code-review -mlflow skills pull --group pr-workflow --alias production \ +mlflow skills pull --bundle pr-workflow --alias production \ --destination ./plugins/pr-workflow ``` @@ -258,8 +239,8 @@ versions = mlflow.genai.skills.search_skill_versions( filter_string="status = 'active'", ) -# Search for active skill groups -groups = mlflow.genai.skills.search_skill_groups( +# Search for active skill bundles +bundles = mlflow.genai.skills.search_skill_bundles( filter_string="status = 'active'", ) @@ -277,15 +258,17 @@ version = mlflow.genai.skills.get_skill_version_by_alias( alias="production", ) -# Get a group version and its pinned skill versions -group_version = mlflow.genai.skills.get_skill_group_version( +# Get a bundle version and its pinned members +bundle_version = mlflow.genai.skills.get_skill_bundle_version( name="pr-workflow", version="1.0.0", ) -# group_version.members == [SkillGroupVersionMember(...), ...] +# bundle_version.skills == [("code-review", "1.0.0"), ...] +# bundle_version.subagents == [("security-auditor", "1.0.0"), ...] +# bundle_version.mcp_servers == [("github-mcp", "2.0.0"), ...] -# Resolve a group alias -group_version = mlflow.genai.skills.get_skill_group_version_by_alias( +# Resolve a bundle alias +bundle_version = mlflow.genai.skills.get_skill_bundle_version_by_alias( name="pr-workflow", alias="production", ) @@ -313,23 +296,36 @@ mlflow skills register --name code-review --version 1.0.0 \ mlflow skills set-alias --name code-review --alias production \ --version 1.0.0 -# Create a group and a versioned membership snapshot -mlflow skill-groups create --name pr-workflow \ +# Register a subagent +mlflow subagents register --name security-auditor --version 1.0.0 \ + --description "Security specialist for auth and payment code" \ + --source-type git \ + --source https://github.com/acme/agent-skills/tree/v1.0.0/security-auditor + +# Register a hook +mlflow hooks register --name pre-commit-scan --version 1.0.0 \ + --description "Runs security scan before tool commits" \ + --source-type git \ + --source https://github.com/acme/agent-skills/tree/v1.0.0/pre-commit-scan + +# Create a bundle and a versioned membership snapshot +mlflow skill-bundles 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 \ - --member test-coverage:2.1.0 \ - --member security-scan:1.0.0 \ - --member mcp:github-mcp:2.0.0 -mlflow skill-groups set-alias --name pr-workflow --alias production \ +mlflow skill-bundles create-version --name pr-workflow --version 1.0.0 \ + --skill code-review:1.0.0 \ + --skill test-coverage:2.1.0 \ + --subagent security-auditor:1.0.0 \ + --hook pre-commit-scan:1.0.0 \ + --mcp-server github-mcp:2.0.0 +mlflow skill-bundles set-alias --name pr-workflow --alias production \ --version 1.0.0 # Search active skill versions mlflow skills search-versions --name code-review \ --filter "status = 'active'" -# Search active groups -mlflow skill-groups search --filter "status = 'active'" +# Search active bundles +mlflow skill-bundles search --filter "status = 'active'" ``` ## Motivation @@ -348,14 +344,14 @@ 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. + portable format for skills and subagents. - **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, +- **Plugin bundles** — harness-specific packaging of skills, subagents, MCP configs, and hooks into a single installable unit. Claude Code and Codex CLI use `plugin.json` manifests; other harnesses use directory conventions. @@ -381,10 +377,10 @@ address: repos, OCI registries, or other distribution systems. There is no single discovery layer across all of these. -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. +4. **No cross-type bundling.** Agent harnesses like Claude Code and + Codex CLI support plugins that bundle skills, subagents, MCP + servers, and hooks together. But there is no agent-neutral way to + represent these bundles for governance and discovery. 5. **No trace-to-skill linkage.** MLflow already traces agent conversations (Claude Code via `mlflow autolog claude`, SDK @@ -406,16 +402,16 @@ address: **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 +bundle it with a security-auditor subagent and a GitHub MCP server +into a "pr-workflow" skill bundle. They set the bundle'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 +registry for active skills. They find the `pr-workflow` bundle, +resolve its `production` alias, and run +`mlflow skills pull --bundle 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)). @@ -435,8 +431,8 @@ across the organization's registered capabilities. - **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. + not define or enforce what a skill, subagent, 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 @@ -457,34 +453,35 @@ across the organization's registered capabilities. erDiagram Skill ||--o{ SkillVersion : "has versions" Skill ||--o{ SkillTag : "has tags" +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{ SkillGroupVersionMember : "contains members" -SkillGroupVersion ||--o{ SkillGroupVersionTag : "has tags" -SkillGroupVersionMember }o--o| SkillVersion : "references (registry=skill)" -SkillGroupVersionMember }o--o| MCPServerVersion : "references (registry=mcp)" +Subagent ||--o{ SubagentVersion : "has versions" +Subagent ||--o{ SubagentTag : "has tags" +Subagent ||--o{ SubagentAlias : "has aliases" +Hook ||--o{ HookVersion : "has versions" +Hook ||--o{ HookTag : "has tags" +Hook ||--o{ HookAlias : "has aliases" +SkillBundle ||--o{ SkillBundleVersion : "has versions" +SkillBundle ||--o{ SkillBundleTag : "has tags" +SkillBundle ||--o{ SkillBundleAlias : "has aliases" +SkillBundleVersion }o--o{ SkillVersion : "skills" +SkillBundleVersion }o--o{ SubagentVersion : "subagents" +SkillBundleVersion }o--o{ HookVersion : "hooks" +SkillBundleVersion }o--o{ MCPServerVersion : "mcp_servers" ``` #### Skill -The logical governed asset, scoped to a workspace. +A skill is a directory containing a SKILL.md entry point plus +supporting files (scripts, templates, reference material). The +`Skill` entity is the logical governed asset, scoped to a workspace. ```python from dataclasses import dataclass, field from enum import StrEnum -class SkillKind(StrEnum): - SKILL = "skill" - AGENT = "agent" - HOOK = "hook" - - class SkillStatus(StrEnum): DRAFT = "draft" ACTIVE = "active" @@ -495,7 +492,6 @@ class SkillStatus(StrEnum): @dataclass class Skill: name: str - kind: SkillKind = SkillKind.SKILL description: str | None = None workspace: str | None = None status: SkillStatus = SkillStatus.DRAFT @@ -512,24 +508,18 @@ class Skill: | Field | Type | Description | |---|---|---| | `name` | `str` | Stable logical asset name, unique within a workspace | -| `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`) | +| `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 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.** 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. +(RFC-0004), not in this registry. Skill bundles can reference MCP +registry entries in their `mcp_servers` list. MCP configs embedded in +bundle-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 @@ -646,23 +636,22 @@ read; verification is the consumer's responsibility. to different content, register a new version. Mutable fields (`status`, `tags`) can be updated independently. -#### SkillGroup +#### Subagent -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. +A subagent is a sub-agent definition that can be invoked by a parent +agent. The `Subagent` entity follows the same structure as `Skill`: +a top-level governed asset with versions, tags, aliases, and full +lifecycle management. ```python @dataclass -class SkillGroup: +class Subagent: name: str description: str | None = None workspace: str | None = None status: SkillStatus = SkillStatus.DRAFT tags: dict[str, str] = field(default_factory=dict) - aliases: list["SkillGroupAlias"] = field(default_factory=list) + aliases: list["SubagentAlias"] = field(default_factory=list) last_registered_version: str | None = None latest_version: str | None = None created_by: str | None = None @@ -671,39 +660,101 @@ class SkillGroup: last_updated_timestamp: int | None = None ``` -`SkillGroup.status` is read-only, derived from the latest group +`SubagentVersion` follows the same structure as `SkillVersion`: +`name`, `version`, `source_type`, `source`, `subpath`, +`content_digest`, `status`, `tags`, and timestamps. Version +uniqueness is `(name, version)` within a workspace, and the same +immutability contract applies. + +#### Hook + +A hook is an event-triggered action (e.g., a shell command that runs +before a commit). The `Hook` entity follows the same structure as +`Skill` and `Subagent`. + +```python +@dataclass +class Hook: + name: str + description: str | None = None + workspace: str | None = None + status: SkillStatus = SkillStatus.DRAFT + tags: dict[str, str] = field(default_factory=dict) + aliases: list["HookAlias"] = field(default_factory=list) + last_registered_version: str | None = None + latest_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 +``` + +`HookVersion` follows the same structure as `SkillVersion`. + +**Shared patterns.** All three entity types (Skill, Subagent, Hook) +share the same version, tag, alias, and lifecycle patterns. The +store interface, REST API, and SDK expose parallel operations for +each type. The database uses parallel table sets (see Database +schema). + +#### SkillBundle + +A skill bundle groups related capabilities (skills, subagents, hooks, +and MCP servers) into a governed unit that maps to the "plugin" +concept in agent harnesses. Follows the same top-level pattern as +Skill: versions, tags, and aliases. + +```python +@dataclass +class SkillBundle: + name: str + description: str | None = None + workspace: str | None = None + status: SkillStatus = SkillStatus.DRAFT + tags: dict[str, str] = field(default_factory=dict) + aliases: list["SkillBundleAlias"] = field(default_factory=list) + last_registered_version: str | None = None + latest_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 +``` + +`SkillBundle.status` is read-only, derived from the latest bundle version's status. `latest_version` works the same as on `Skill`. -**Why groups instead of tags?** Tags on individual skills could +**Why bundles instead of tags?** Tags on individual skills could express "these skills are related" but cannot provide: -- **Versioned membership snapshots.** A group version pins specific +- **Versioned membership snapshots.** A bundle 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 +- **Cross-registry references.** A bundle 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 +- **Bundle-level source.** A bundle 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. +- **Independent lifecycle.** A bundle version has its own status, + aliases, and tags. The bundle 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 + plugins as bundles of capabilities with a manifest. Skill bundles map directly to this concept; tags do not. -#### SkillGroupVersion +#### SkillBundleVersion -A versioned snapshot of a skill group's membership. Each version -captures a specific set of skill versions that work together. +A versioned snapshot of a skill bundle's membership. Each version +captures a specific set of capabilities that work together, organized +by type. ```python @dataclass -class SkillGroupVersion: +class SkillBundleVersion: name: str version: str source_type: SkillSourceType | None = None @@ -712,7 +763,10 @@ class SkillGroupVersion: content_digest: str | None = None status: SkillStatus = SkillStatus.DRAFT tags: dict[str, str] = field(default_factory=dict) - members: list["SkillGroupVersionMember"] = field(default_factory=list) + skills: list[tuple[str, str]] = field(default_factory=list) + subagents: list[tuple[str, str]] = field(default_factory=list) + hooks: list[tuple[str, str]] = field(default_factory=list) + mcp_servers: list[tuple[str, str]] = field(default_factory=list) aliases: list[str] = field(default_factory=list) workspace: str | None = None created_by: str | None = None @@ -721,87 +775,70 @@ class SkillGroupVersion: last_updated_timestamp: int | None = None ``` +Each member list contains `(name, version)` tuples. The `skills`, +`subagents`, and `hooks` lists reference entities in this registry. +The `mcp_servers` list references entries in the MCP Server Registry +(RFC-0004). + **Version uniqueness.** The combination of `(name, version)` is unique within a workspace. -**Group-level source.** A group version can optionally have its own +**Bundle-level source.** A bundle version can optionally have its own `source_type`, `source`, `subpath`, and `content_digest`, pointing to a single artifact (e.g., an OCI image or Git repo) that contains the -complete plugin. When present, `pull` fetches the group artifact as a +complete plugin. When present, `pull` fetches the bundle 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 +repo. Individual members within a bundle-level artifact use `subpath` +on their version entities to identify their location within the artifact. -**Source resolution for pull.** When pulling a group, if the group +**Source resolution for pull.** When pulling a bundle, if the bundle 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 -(`status`, `tags`) can be updated independently. - -#### SkillGroupVersionMember - -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 SkillGroupVersionMember: - member_name: str - member_version: str - registry: str = "skill" # "skill" or "mcp" -``` - -| 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) | +**Immutability contract.** The member lists and source fields of a +bundle version are immutable after creation. To change the set of +members or source pointer, register a new bundle version. Mutable +fields (`status`, `tags`) can be updated independently. 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` +- **Deduplication.** Two bundles 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 + the MCP registry; skill bundles 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 +A member can appear in multiple bundles and multiple bundle versions. +Membership is at the version level, so a bundle version is a reproducible snapshot of "these specific 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. +**Bundle-level source and embedded MCP configs.** When a bundle +version has a bundle-level source (e.g., a single OCI image +containing a complete plugin), the artifact may include MCP configs +alongside skills and subagents. 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 +#### SkillBundleAlias ```python @dataclass(frozen=True) -class SkillGroupAlias: - name: str # parent SkillGroup name +class SkillBundleAlias: + name: str # parent SkillBundle name alias: str # e.g., "production", "staging" - version: str # group version string this alias points to + version: str # bundle version string this alias points to ``` #### SkillAlias and SkillTag @@ -819,18 +856,21 @@ class SkillTag: value: str ``` +Subagent and Hook follow the same alias and tag patterns (e.g., +`SubagentAlias`, `SubagentTag`, `HookAlias`, `HookTag`). + 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). +bundle-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?" +`delete_skill_alias`, `set_skill_bundle_alias`, or +`delete_skill_bundle_alias` (and the corresponding subagent and hook +variants) 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) @@ -844,7 +884,8 @@ class SkillAliasHistory: ``` History is recorded automatically by the store on every alias -mutation. The same structure applies to `SkillGroupAliasHistory`. +mutation. The same structure applies to `SkillBundleAliasHistory`, +`SubagentAliasHistory`, and `HookAliasHistory`. History records are read-only and append-only. They cannot be modified or deleted through the API. @@ -855,7 +896,8 @@ This lifecycle aligns with the MCP Server Registry (RFC-0004). #### Per-version status -Each `SkillVersion` and `SkillGroupVersion` has an independent status: +Each `SkillVersion`, `SubagentVersion`, `HookVersion`, and +`SkillBundleVersion` has an independent status: | State | Meaning | Downstream surfacing | |---|---|---| @@ -880,11 +922,12 @@ 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 +#### Entity-level status -`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.status`, `Subagent.status`, `Hook.status`, and +`SkillBundle.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. #### `latest_version` resolution @@ -902,11 +945,12 @@ alias="latest", ...)` is rejected, while 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`. +applies to `Subagent`, `Hook`, `SkillBundle`, and their corresponding +`get_latest_*_version` methods. ### Database schema -Twelve tables, created via a single Alembic migration. All tables are +Tables are created via a single Alembic migration. All tables are workspace-scoped. #### `skills` @@ -915,7 +959,6 @@ workspace-scoped. |--------|------|-------| | `workspace` | `String(63)` | PK, default `'default'` | | `name` | `String(256)` | PK | -| `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" | @@ -986,7 +1029,23 @@ FK: `(workspace, name)` references `skills`, CASCADE delete. Append-only. No updates or deletes through the API. -#### `skill_groups` +#### Subagent tables + +The `subagents`, `subagent_versions`, `subagent_tags`, +`subagent_version_tags`, `subagent_aliases`, and +`subagent_alias_history` tables follow the same structure as the +corresponding skill tables above (without `kind`). FK relationships +mirror the skill tables: `subagent_versions` references `subagents` +with CASCADE delete, etc. + +#### Hook tables + +The `hooks`, `hook_versions`, `hook_tags`, `hook_version_tags`, +`hook_aliases`, and `hook_alias_history` tables follow the same +structure as the corresponding skill tables. FK relationships mirror +the skill tables. + +#### `skill_bundles` | Column | Type | Notes | |--------|------|-------| @@ -1000,7 +1059,7 @@ Append-only. No updates or deletes through the API. | `creation_timestamp` | `BigInteger` | millis since epoch | | `last_updated_timestamp` | `BigInteger` | millis since epoch | -#### `skill_group_versions` +#### `skill_bundle_versions` | Column | Type | Notes | |--------|------|-------| @@ -1008,7 +1067,7 @@ Append-only. No updates or deletes through the API. | `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 | +| `source` | `String(2048)` | optional pointer to bundle artifact | | `subpath` | `String(2048)` | nullable; path within the artifact | | `content_digest` | `String(512)` | optional integrity digest | | `status` | `String(20)` | default `'draft'` | @@ -1017,31 +1076,35 @@ Append-only. No updates or deletes through the API. | `creation_timestamp` | `BigInteger` | millis since epoch | | `last_updated_timestamp` | `BigInteger` | millis since epoch | -FK: `(workspace, name)` references `skill_groups`, CASCADE delete. +FK: `(workspace, name)` references `skill_bundles`, CASCADE delete. -#### `skill_group_version_memberships` +#### `skill_bundle_version_members` | 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` | +| `bundle_name` | `String(256)` | PK, FK to `skill_bundle_versions` | +| `bundle_version` | `String(256)` | PK, FK to `skill_bundle_versions` | +| `member_type` | `String(20)` | PK; `skill`, `subagent`, `hook`, or `mcp` | | `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, member_name, member_version)` references `skill_versions`, RESTRICT delete. Only applies when `registry='skill'`. +FK: `(workspace, bundle_name, bundle_version)` references `skill_bundle_versions`, CASCADE delete. + +The `member_type` column distinguishes member categories. When +`member_type` is `skill`, a FK to `skill_versions` enforces +referential integrity with RESTRICT delete. Similarly for `subagent` +(FK to `subagent_versions`) and `hook` (FK to `hook_versions`). -**Cross-registry references (`registry='mcp'`).** There is no +**Cross-registry references (`member_type='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 +referenced `MCPServerVersion` exists when creating a bundle 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` +#### `skill_bundle_tags` | Column | Type | Notes | |--------|------|-------| @@ -1050,7 +1113,7 @@ migrations and allows either registry to be deployed independently. | `key` | `String(256)` | PK | | `value` | `Text` | | -#### `skill_group_version_tags` +#### `skill_bundle_version_tags` | Column | Type | Notes | |--------|------|-------| @@ -1060,16 +1123,16 @@ migrations and allows either registry to be deployed independently. | `key` | `String(256)` | PK | | `value` | `Text` | | -#### `skill_group_aliases` +#### `skill_bundle_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 | +| `version` | `String(256)` | target bundle version string | -#### `skill_group_alias_history` +#### `skill_bundle_alias_history` | Column | Type | Notes | |--------|------|-------| @@ -1101,7 +1164,7 @@ class SkillRegistryMixin: # --- Skill operations --- def create_skill( - self, name: str, kind: str = "skill", + self, name: str, description: str | None = None, ) -> Skill: raise NotImplementedError @@ -1179,7 +1242,7 @@ class SkillRegistryMixin: ) -> None: raise NotImplementedError - # --- Tag operations --- + # --- Skill tag operations --- def set_skill_tag( self, name: str, key: str, value: str, @@ -1200,7 +1263,7 @@ class SkillRegistryMixin: ) -> None: raise NotImplementedError - # --- Alias operations --- + # --- Skill alias operations --- def set_skill_alias( self, name: str, alias: str, version: str, @@ -1221,130 +1284,223 @@ class SkillRegistryMixin: ) -> PagedList[SkillAliasHistory]: raise NotImplementedError - # --- SkillGroup operations --- + # --- Subagent operations --- + # Same shape as Skill: create, get, search, update, delete, + # plus version, tag, and alias operations. + + def create_subagent( + self, name: str, + description: str | None = None, + ) -> Subagent: + raise NotImplementedError + + def get_subagent(self, name: str) -> Subagent: + raise NotImplementedError - def create_skill_group( + def search_subagents( + self, + filter_string: str | None = None, + max_results: int = 100, + order_by: list[str] | None = None, + page_token: str | None = None, + ) -> PagedList[Subagent]: + raise NotImplementedError + + def update_subagent( + self, name: str, + description: str | None = None, + latest_version: str | None = None, + ) -> Subagent: + raise NotImplementedError + + def delete_subagent(self, name: str) -> None: + raise NotImplementedError + + def create_subagent_version( + self, name: str, 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, + ) -> SubagentVersion: + raise NotImplementedError + + # Remaining subagent version, tag, and alias operations + # follow the same pattern as skill operations above. + + # --- Hook operations --- + # Same shape as Skill: create, get, search, update, delete, + # plus version, tag, and alias operations. + + def create_hook( + self, name: str, + description: str | None = None, + ) -> Hook: + raise NotImplementedError + + def get_hook(self, name: str) -> Hook: + raise NotImplementedError + + def search_hooks( + self, + filter_string: str | None = None, + max_results: int = 100, + order_by: list[str] | None = None, + page_token: str | None = None, + ) -> PagedList[Hook]: + raise NotImplementedError + + def update_hook( + self, name: str, + description: str | None = None, + latest_version: str | None = None, + ) -> Hook: + raise NotImplementedError + + def delete_hook(self, name: str) -> None: + raise NotImplementedError + + def create_hook_version( + self, name: str, 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, + ) -> HookVersion: + raise NotImplementedError + + # Remaining hook version, tag, and alias operations + # follow the same pattern as skill operations above. + + # --- SkillBundle operations --- + + def create_skill_bundle( self, name: str, description: str | None = None, - ) -> SkillGroup: + ) -> SkillBundle: raise NotImplementedError - def get_skill_group(self, name: str) -> SkillGroup: + def get_skill_bundle(self, name: str) -> SkillBundle: raise NotImplementedError - def search_skill_groups( + def search_skill_bundles( self, filter_string: str | None = None, max_results: int = 100, order_by: list[str] | None = None, page_token: str | None = None, - ) -> PagedList[SkillGroup]: + ) -> PagedList[SkillBundle]: raise NotImplementedError - def update_skill_group( + def update_skill_bundle( self, name: str, description: str | None = None, latest_version: str | None = None, - ) -> SkillGroup: + ) -> SkillBundle: raise NotImplementedError - def delete_skill_group(self, name: str) -> None: + def delete_skill_bundle(self, name: str) -> None: raise NotImplementedError - # --- SkillGroupVersion operations --- + # --- SkillBundleVersion operations --- - def create_skill_group_version( + def create_skill_bundle_version( self, name: str, version: str, - members: list[SkillGroupVersionMember], + skills: list[tuple[str, str]] | None = None, + subagents: list[tuple[str, str]] | None = None, + hooks: list[tuple[str, str]] | None = None, + mcp_servers: list[tuple[str, str]] | None = None, source_type: str | None = None, source: str | None = None, subpath: str | None = None, content_digest: str | None = None, - ) -> SkillGroupVersion: + ) -> SkillBundleVersion: raise NotImplementedError - def get_skill_group_version( + def get_skill_bundle_version( self, name: str, version: str, - ) -> SkillGroupVersion: + ) -> SkillBundleVersion: raise NotImplementedError - def get_skill_group_version_by_alias( + def get_skill_bundle_version_by_alias( self, name: str, alias: str, - ) -> SkillGroupVersion: + ) -> SkillBundleVersion: raise NotImplementedError - def get_latest_skill_group_version( + def get_latest_skill_bundle_version( self, name: str, - ) -> SkillGroupVersion: + ) -> SkillBundleVersion: raise NotImplementedError - def search_skill_group_versions( + def search_skill_bundle_versions( self, name: str, filter_string: str | None = None, max_results: int = 100, order_by: list[str] | None = None, page_token: str | None = None, - ) -> PagedList[SkillGroupVersion]: + ) -> PagedList[SkillBundleVersion]: raise NotImplementedError - def update_skill_group_version( + def update_skill_bundle_version( self, name: str, version: str, status: SkillStatus | None = None, - ) -> SkillGroupVersion: + ) -> SkillBundleVersion: raise NotImplementedError - def delete_skill_group_version( + def delete_skill_bundle_version( self, name: str, version: str, ) -> None: raise NotImplementedError - # --- SkillGroup tag operations --- + # --- SkillBundle tag operations --- - def set_skill_group_tag( + def set_skill_bundle_tag( self, name: str, key: str, value: str, ) -> None: raise NotImplementedError - def delete_skill_group_tag( + def delete_skill_bundle_tag( self, name: str, key: str, ) -> None: raise NotImplementedError - def set_skill_group_version_tag( + def set_skill_bundle_version_tag( self, name: str, version: str, key: str, value: str, ) -> None: raise NotImplementedError - def delete_skill_group_version_tag( + def delete_skill_bundle_version_tag( self, name: str, version: str, key: str, ) -> None: raise NotImplementedError - # --- SkillGroup alias operations --- + # --- SkillBundle alias operations --- - def set_skill_group_alias( + def set_skill_bundle_alias( self, name: str, alias: str, version: str, ) -> None: raise NotImplementedError - def delete_skill_group_alias( + def delete_skill_bundle_alias( self, name: str, alias: str, ) -> None: raise NotImplementedError - def get_skill_group_alias_history( + def get_skill_bundle_alias_history( self, name: str, alias: str | None = None, max_results: int = 100, page_token: str | None = None, - ) -> PagedList[SkillGroupAliasHistory]: + ) -> PagedList[SkillBundleAliasHistory]: raise NotImplementedError ``` @@ -1358,7 +1514,6 @@ combine store operations, matching the pattern established by def register_skill( name: str, version: str, - kind: str = "skill", description: str | None = None, source_type: str | None = None, source: str | None = None, @@ -1373,16 +1528,46 @@ def register_skill( and source automatically.""" +def register_subagent( + name: str, + version: str, + 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, +) -> SubagentVersion: + """Register a subagent version. Auto-creates the parent + Subagent if it does not exist.""" + + +def register_hook( + name: str, + version: str, + 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, +) -> HookVersion: + """Register a hook version. Auto-creates the parent Hook if + it does not exist.""" + + def pull( name: str | None = None, - group: str | None = None, + bundle: 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.""" + """Pull skill, subagent, hook, or bundle content from registered + sources to a local directory. Specify name for a single + capability or bundle for a skill bundle.""" ``` ### REST API @@ -1416,29 +1601,41 @@ All paths relative to `/ajax-api/3.0/mlflow/skills`. | `GET` | `/{name}/aliases/history` | Get alias change history (all aliases) | | `GET` | `/{name}/aliases/{alias}/history` | Get alias change history (specific alias) | -#### Skill group endpoints +#### Subagent endpoints + +All paths relative to `/ajax-api/3.0/mlflow/subagents`. Same +structure as skill endpoints: CRUD on subagents and subagent versions, +plus tags, aliases, and alias history. + +#### Hook endpoints + +All paths relative to `/ajax-api/3.0/mlflow/hooks`. Same structure as +skill endpoints: CRUD on hooks and hook versions, plus tags, aliases, +and alias history. + +#### Skill bundle endpoints -All paths relative to `/ajax-api/3.0/mlflow/skill-groups`. +All paths relative to `/ajax-api/3.0/mlflow/skill-bundles`. | 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 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 | -| `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 | +| `POST` | `/` | Create a skill bundle | +| `GET` | `/` | Search skill bundles | +| `GET` | `/{name}` | Get bundle by name | +| `PATCH` | `/{name}` | Update bundle fields | +| `DELETE` | `/{name}` | Delete bundle (cascades versions) | +| `POST` | `/{name}/versions` | Create a bundle version with members | +| `GET` | `/{name}/versions` | Search bundle versions | +| `GET` | `/{name}/versions/{version}` | Get a specific bundle version | +| `PATCH` | `/{name}/versions/{version}` | Update bundle version status | +| `DELETE` | `/{name}/versions/{version}` | Delete a bundle version | +| `POST` | `/{name}/tags` | Set a bundle-level tag | +| `DELETE` | `/{name}/tags/{key}` | Delete a bundle-level tag | +| `POST` | `/{name}/versions/{version}/tags` | Set a bundle version tag | +| `DELETE` | `/{name}/versions/{version}/tags/{key}` | Delete a bundle version tag | +| `POST` | `/{name}/aliases` | Set a bundle alias | +| `GET` | `/{name}/aliases/{alias}` | Resolve bundle alias to version | +| `DELETE` | `/{name}/aliases/{alias}` | Delete a bundle alias | | `GET` | `/{name}/aliases/history` | Get alias change history (all aliases) | | `GET` | `/{name}/aliases/{alias}/history` | Get alias change history (specific alias) | @@ -1447,28 +1644,30 @@ All paths relative to `/ajax-api/3.0/mlflow/skill-groups`. Search endpoints use page-token-based pagination and `filter_string` expressions following existing MLflow conventions. -**Skills and skill groups:** `name LIKE '%review%'`, `status = 'active'`, -`kind = 'agent'`, `tags.team = 'platform'` +**Skills, subagents, hooks, and bundles:** `name LIKE '%review%'`, +`status = 'active'`, `tags.team = 'platform'` -**Skill versions:** `status = 'active'`, +**Versions (all entity types):** `status = 'active'`, `source_type = 'git'`, `tags.scan.prompt-injection.status = 'pass'` -**Skill group versions:** `status = 'active'`, +**Skill bundle versions:** `status = 'active'`, `tags.approved = 'true'` ### Python SDK and CLI The `mlflow.genai.skills` module exposes top-level functions delegating to `MlflowClient`, with a 1:1 mapping to the store mixin methods above. -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. +CLI command groups (`mlflow skills`, `mlflow subagents`, +`mlflow hooks`, and `mlflow skill-bundles`) 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. +client calls `get_skill_version` (or the corresponding subagent/hook +method, 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 @@ -1489,11 +1688,11 @@ source-type-aware: 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 +**Skill bundle pull.** Source resolution: +1. If the bundle version has a `source`, fetch the bundle 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. + a subdirectory of the destination, named by the member's name. Members without a `source` are skipped with a warning. This supports both distribution patterns: a monolithic plugin artifact @@ -1517,15 +1716,15 @@ should be solved at the platform level across all MLflow registries. ### Permissions The skill registry integrates with MLflow's existing permission -framework (READ / EDIT / MANAGE), applied at the `Skill` and -`SkillGroup` level. Versions, tags, aliases, and memberships inherit -permissions from their parent entity. +framework (READ / EDIT / MANAGE), applied at the `Skill`, `Subagent`, +`Hook`, and `SkillBundle` 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 (activate, deprecate, delete), set and delete aliases, delete versions, delete skills and groups, manage permissions | +| `READ` | Search entities, get versions, resolve aliases, list tags and memberships | +| `EDIT` | Create entities, create versions, set and delete tags, update description | +| `MANAGE` | Status transitions (activate, deprecate, delete), set and delete aliases, delete versions, delete entities, manage permissions | Key design choices: @@ -1540,26 +1739,27 @@ Key design choices: 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. +- **Creator gets MANAGE.** When a user creates an entity (skill, + subagent, hook, or bundle), they automatically receive MANAGE + permission, following the MLflow model registry pattern. ### UI The Skills page lives under the GenAI workflow in the MLflow sidebar, alongside Experiments, Prompts, and other AI asset pages. -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 list view shows skills, subagents, hooks, and bundles together, +with name, description, latest version, status, and tags. Users can +filter by type (skill, subagent, hook, bundle), 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. +The detail view for a skill, subagent, or hook shows metadata, version +list, aliases, tags (including security scan results), and bundle +memberships. -The detail view for a skill group shows its description, status, version -list, aliases, and tags. Each group version shows its status and the -pinned member versions it contains. +The detail view for a skill bundle shows its description, status, +version list, aliases, and tags. Each bundle version shows its status +and the pinned member versions it contains. ### Security scan tracking @@ -1807,6 +2007,6 @@ The two approaches are complementary. New feature, not a breaking change. Phased rollout: -- **Phase 1 (this RFC):** Registry entities, store, REST API, SDK, CLI, UI, `mlflow skills pull`, and `mlflow.skill_context()` for trace integration. +- **Phase 1 (this RFC):** Registry entities (Skill, Subagent, Hook, SkillBundle), store, REST API, SDK, CLI, UI, `mlflow skills pull`, and `mlflow.skill_context()` for trace integration. - **Phase 2 (RFC-0006):** Harness-specific `mlflow skills install` for Claude Code, Codex CLI, and Cursor. Automatic `skill_context()` wrapping in harness-specific autologgers. - **Phase 3 (follow-up):** Usage analytics dashboards, install count tracking, cross-workspace export/import (following cross-registry patterns), and shared base extraction with the MCP registry. diff --git a/rfcs/0005-skill-registry/deferred-review-items.md b/rfcs/0005-skill-registry/deferred-review-items.md new file mode 100644 index 0000000..e051b66 --- /dev/null +++ b/rfcs/0005-skill-registry/deferred-review-items.md @@ -0,0 +1,27 @@ +# Deferred Review Items + +Open items from Matt's May 15 review on PR #10 that still need replies or RFC changes. + +## ~~Subpath field (comment 3251079063)~~ DONE + +Added `subpath` as an optional field on `SkillVersion` and `SkillGroupVersion`. Used for OCI and ZIP source types when multiple skills share a single artifact. Not used for Git (tree URLs encode repo+ref+path) or MLflow artifacts (path scoped at upload). Reply posted 2026-05-19. + +## ~~Error handling section (comment 3251079530)~~ DONE + +Removed entirely. RFC-0004 has no equivalent section. Reply posted 2026-05-19. + +## ~~Workspace scoping / Adoption strategy (comment 3251079711)~~ DONE + +Workspace scoping trimmed to reference existing patterns. Adoption strategy condensed to three-phase summary. Reply posted 2026-05-19. + +## Security scan tracking (comments 3251079589, 3251079660) + +Two related comments. Matt suggests scan metadata should either be a first-class structured field or dropped from the RFC, rather than modeled as plain tags. Also notes the query example for scan metadata may not be supported by MLflow's existing filter syntax. + +## ~~Naming (comment 3251078919)~~ DONE + +Resolved: split the single `Skill(kind=...)` entity into three +separate entity types (Skill, Subagent, Hook). SkillGroup renamed to +SkillBundle. SkillBundleVersion uses typed member lists instead of a +SkillGroupVersionMember entity. See `naming-and-scope-discussion.md` +for the full discussion history. 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 0cf6579..25bdac2 100644 --- a/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md +++ b/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md @@ -20,19 +20,19 @@ content from registered sources to a local directory, this RFC adds 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" +This bridges the gap between "I found a skill bundle in the registry" and "my agent harness can use it." # Basic example -## Install a skill group for Claude Code +## Install a skill bundle for Claude Code ```bash -mlflow skills install --group pr-workflow --alias production \ +mlflow skills install --bundle pr-workflow --alias production \ --harness claude-code ``` -This resolves the `pr-workflow` skill group, pulls all member +This resolves the `pr-workflow` skill bundle, pulls all member capabilities from their registered sources, and generates: ``` @@ -49,15 +49,15 @@ capabilities from their registered sources, and generates: ```bash # Codex CLI (nearly identical to Claude Code) -mlflow skills install --group pr-workflow --alias production \ +mlflow skills install --bundle pr-workflow --alias production \ --harness codex-cli # Cursor -mlflow skills install --group pr-workflow --alias production \ +mlflow skills install --bundle pr-workflow --alias production \ --harness cursor # Antigravity -mlflow skills install --group pr-workflow --alias production \ +mlflow skills install --bundle pr-workflow --alias production \ --harness antigravity ``` @@ -67,7 +67,7 @@ mlflow skills install --group pr-workflow --alias production \ import mlflow mlflow.genai.skills.install( - group="pr-workflow", + bundle="pr-workflow", alias="production", harness="claude-code", destination=".", # project root @@ -141,13 +141,13 @@ only the directory placement and manifest format differ. 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. +1. **Map member types to harness paths.** Given the bundle's member + types (skill, subagent, hook, mcp_server), determine where each + member'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. +3. **Handle unsupported types.** Skip member types the harness does + not support, with a warning. ```python from abc import abstractmethod @@ -155,15 +155,18 @@ from abc import abstractmethod class HarnessAdapter: @abstractmethod - def install_skill_group( + def install_skill_bundle( self, - group_version: SkillGroupVersion, - members: list[tuple[Skill, SkillVersion]], + bundle_version: SkillBundleVersion, + skills: list[tuple[Skill, SkillVersion]], + subagents: list[tuple[Subagent, SubagentVersion]], + hooks: list[tuple[Hook, HookVersion]], + mcp_servers: list[tuple[str, str]], destination: str, ) -> str: ... @abstractmethod - def supported_kinds(self) -> set[str]: ... + def supported_member_types(self) -> set[str]: ... ``` ### Claude Code / Codex CLI adapter @@ -183,16 +186,16 @@ generates: **Directory layout:** ``` -{destination}/.claude/plugins/{group-name}/ +{destination}/.claude/plugins/{bundle-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 + skills/{skill-name}/SKILL.md # skill members + agents/{agent-name}.md # subagent members + .mcp.json # mcp_server members, merged ``` For Codex CLI, the path uses `.codex/plugins/` instead. -**MCP server merging.** If the group contains multiple `mcp-server` +**MCP server merging.** If the bundle contains multiple `mcp-server` members, their configs are merged into a single `.mcp.json` file using server name as the key: @@ -216,8 +219,8 @@ 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 +{destination}/.cursor/skills/{skill-name}/SKILL.md # skill members +{destination}/.cursor/agents/{agent-name}.md # subagent members ``` For MCP servers, the adapter merges entries into the project's @@ -229,16 +232,16 @@ Hooks are skipped with a warning (Cursor does not support hooks). ### Antigravity adapter ``` -{destination}/.agent/skills/{skill-name}/SKILL.md # kind=skill +{destination}/.agent/skills/{skill-name}/SKILL.md # skill members ``` -Agents, MCP servers, and hooks are skipped with a warning. +Subagents, 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. +Continue, etc.) follow the same pattern: map member types to paths, +generate manifests, skip unsupported types with warnings. New adapters can be contributed without changes to the registry or the adapter interface. Adapters are registered via Python entrypoints @@ -252,14 +255,14 @@ additional harnesses are community-contributed. 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 +`marketplace.json` endpoint that exposes published skill bundles 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 +GET /ajax-api/3.0/mlflow/skill-bundles/marketplace.json?harness=claude-code ``` Query parameters: @@ -269,8 +272,8 @@ Query parameters: | `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 +The endpoint returns only skill bundles whose latest published version +contains at least one member with a type supported by the target harness. #### Response format @@ -286,7 +289,7 @@ Claude Code / Codex CLI: "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", + "source": "https://mlflow.example.com/ajax-api/3.0/mlflow/skill-bundles/pr-workflow/install?harness=claude-code", "skills": ["code-review", "test-coverage"], "agents": ["security-auditor"], "mcpServers": ["github-mcp"] @@ -295,7 +298,7 @@ Claude Code / Codex CLI: } ``` -Each entry is derived from a published skill group version and its +Each entry is derived from a published skill bundle version and its members. The `source` field points to a registry endpoint that serves the installable plugin bundle. @@ -308,7 +311,7 @@ settings: # Claude Code settings.json { "extraKnownMarketplaces": [ - "https://mlflow.example.com/ajax-api/3.0/mlflow/skill-groups/marketplace.json?harness=claude-code" + "https://mlflow.example.com/ajax-api/3.0/mlflow/skill-bundles/marketplace.json?harness=claude-code" ] } ``` @@ -334,20 +337,20 @@ use the adapter-based `mlflow skills install` command instead. ### SDK interface Installation is a client-side operation: the SDK resolves the skill or -group from the registry, pulls content from registered sources, and +bundle 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. ```python def install( name: str | None = None, - group: str | None = None, + bundle: 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. + """Install a skill or skill bundle for a specific harness. Resolves from the registry, pulls content, generates harness-specific manifests, and places files in the correct directories.""" @@ -360,7 +363,7 @@ harnesses query to discover available plugins. | Method | Path | Description | |---|---|---| -| `GET` | `/ajax-api/3.0/mlflow/skill-groups/marketplace.json` | Generate marketplace catalog for a harness | +| `GET` | `/ajax-api/3.0/mlflow/skill-bundles/marketplace.json` | Generate marketplace catalog for a harness | ### CLI @@ -369,8 +372,8 @@ harnesses query to discover available plugins. mlflow skills install --name code-review --alias production \ --harness claude-code --destination . -# Install a skill group -mlflow skills install --group pr-workflow --alias production \ +# Install a skill bundle +mlflow skills install --bundle pr-workflow --alias production \ --harness claude-code --destination . # List supported harnesses @@ -393,7 +396,7 @@ setup. This is analogous to `package-lock.json` in Node.js or "locked_at": "2026-05-17T21:00:00Z", "entries": [ { - "type": "group", + "type": "bundle", "name": "pr-workflow", "version": "1.0.0", "alias": "production", @@ -427,14 +430,14 @@ setup. This is analogous to `package-lock.json` in Node.js or ```bash # First install: resolves from registry and writes lock file -mlflow skills install --group pr-workflow --alias production \ +mlflow skills install --bundle 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 \ +mlflow skills install --bundle pr-workflow --alias production \ --harness claude-code --lock --update ``` @@ -447,7 +450,7 @@ reproducible setups across team members. ```python mlflow.genai.skills.install( - group="pr-workflow", + bundle="pr-workflow", alias="production", harness="claude-code", lock=True, @@ -595,8 +598,8 @@ instrumented; users of those harnesses can still use - **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. + types. Users may be surprised when hooks are silently skipped for + Cursor, or subagents 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). From 7c69349c9cfb8765cae419fe094d9dbf877d996e Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Thu, 28 May 2026 12:45:44 -0400 Subject: [PATCH 22/52] Address Matt's review: replace tag-based scan tracking with structured ScanResult fields Replaces the tag-based security scan convention with a structured ScanResult entity on version types. ScanStatus enum (pass/fail/warning/error) enables cross-scanner filtering. Upsert semantics per (version, scan_type). Adds scan result DB tables, store methods, and REST endpoints for all version types. Co-Authored-By: Claude Opus 4.6 --- .../0005-skill-registry.md | 290 +++++++++++------- .../deferred-review-items.md | 8 +- 2 files changed, 191 insertions(+), 107 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index 4c9419d..0a0f7d7 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -61,25 +61,14 @@ version = mlflow.genai.skills.register_skill( ) # version.status == "draft" -# Record security scan results while still in draft -# (see "Security scan tracking" for convention) -mlflow.genai.skills.set_skill_version_tag( +# Record a security scan result while still in draft +mlflow.genai.skills.set_skill_version_scan_result( name="code-review", version="1.0.0", - key="scan.prompt-injection.status", - value="pass", -) -mlflow.genai.skills.set_skill_version_tag( - name="code-review", - version="1.0.0", - key="scan.prompt-injection.date", - 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", + scan_type="prompt-injection", + status="pass", + date="2026-04-29", + tool="promptfoo/1.2.0", ) # Activate the version once it's ready for downstream use @@ -401,7 +390,7 @@ address: **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 +create a version, record a prompt-injection scan result, and bundle it with a security-auditor subagent and a GitHub MCP server into a "pr-workflow" skill bundle. They set the bundle's `production` alias to the tested version. When a newer version introduces a vulnerability, @@ -416,11 +405,11 @@ 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. +**Security engineer** — A security engineer queries scan results across +all skill versions to find capabilities that failed scanning +(`scan_results.status = 'fail'`) or that haven't been scanned recently. +They deprecate versions that fail re-scanning and track compliance +posture across the organization's registered capabilities. ### Out of scope @@ -489,6 +478,23 @@ class SkillStatus(StrEnum): DELETED = "deleted" +class ScanStatus(StrEnum): + PASS = "pass" + FAIL = "fail" + WARNING = "warning" + ERROR = "error" + + +@dataclass +class ScanResult: + scan_type: str + status: ScanStatus + date: str + tool: str | None = None + details_url: str | None = None + details: str | None = None + + @dataclass class Skill: name: str @@ -544,6 +550,7 @@ class SkillVersion: status: SkillStatus = SkillStatus.DRAFT content_digest: str | None = None tags: dict[str, str] = field(default_factory=dict) + scan_results: list[ScanResult] = field(default_factory=list) aliases: list[str] = field(default_factory=list) run_id: str | None = None workspace: str | None = None @@ -561,6 +568,7 @@ class SkillVersion: | `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` | +| `scan_results` | `list[ScanResult]` | Structured scan results, one per `scan_type` (read-only, projected from scan results table) | | `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 | @@ -633,8 +641,8 @@ read; verification is the consumer's responsibility. **Immutability contract.** `source_type`, `source`, `subpath`, `content_digest`, and `version` are immutable after creation. To point -to different content, register a new version. Mutable fields (`status`, `tags`) can be -updated independently. +to different content, register a new version. Mutable fields (`status`, +`tags`, `scan_results`) can be updated independently. #### Subagent @@ -662,9 +670,9 @@ class Subagent: `SubagentVersion` follows the same structure as `SkillVersion`: `name`, `version`, `source_type`, `source`, `subpath`, -`content_digest`, `status`, `tags`, and timestamps. Version -uniqueness is `(name, version)` within a workspace, and the same -immutability contract applies. +`content_digest`, `status`, `tags`, `scan_results`, and timestamps. +Version uniqueness is `(name, version)` within a workspace, and the +same immutability contract applies. #### Hook @@ -689,13 +697,14 @@ class Hook: last_updated_timestamp: int | None = None ``` -`HookVersion` follows the same structure as `SkillVersion`. +`HookVersion` follows the same structure as `SkillVersion` (including +`scan_results`). **Shared patterns.** All three entity types (Skill, Subagent, Hook) -share the same version, tag, alias, and lifecycle patterns. The -store interface, REST API, and SDK expose parallel operations for -each type. The database uses parallel table sets (see Database -schema). +share the same version, tag, alias, scan result, and lifecycle +patterns. The store interface, REST API, and SDK expose parallel +operations for each type. The database uses parallel table sets (see +Database schema). #### SkillBundle @@ -763,6 +772,7 @@ class SkillBundleVersion: content_digest: str | None = None status: SkillStatus = SkillStatus.DRAFT tags: dict[str, str] = field(default_factory=dict) + scan_results: list[ScanResult] = field(default_factory=list) skills: list[tuple[str, str]] = field(default_factory=list) subagents: list[tuple[str, str]] = field(default_factory=list) hooks: list[tuple[str, str]] = field(default_factory=list) @@ -916,7 +926,7 @@ Allowed transitions: | `active` | `draft`, `deprecated` | | `deprecated` | `active`, `deleted` | -`draft` allows a version to be registered, tagged with scan results, +`draft` allows a version to be registered, scanned, 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` @@ -1006,6 +1016,23 @@ FK: `(workspace, name)` references `skills`, CASCADE delete. | `key` | `String(256)` | PK | | `value` | `Text` | | +#### `skill_version_scan_results` + +| Column | Type | Notes | +|--------|------|-------| +| `workspace` | `String(63)` | PK, FK | +| `name` | `String(256)` | PK, FK | +| `version` | `String(256)` | PK, FK | +| `scan_type` | `String(256)` | PK | +| `status` | `String(20)` | `pass`, `fail`, `warning`, `error` | +| `date` | `String(32)` | ISO 8601 | +| `tool` | `String(256)` | nullable | +| `details_url` | `String(2048)` | nullable | +| `details` | `Text` | nullable | + +FK: `(workspace, name, version)` references `skill_versions`, CASCADE +delete. PK includes `scan_type` for upsert-per-scan-type semantics. + #### `skill_aliases` | Column | Type | Notes | @@ -1032,18 +1059,18 @@ Append-only. No updates or deletes through the API. #### Subagent tables The `subagents`, `subagent_versions`, `subagent_tags`, -`subagent_version_tags`, `subagent_aliases`, and -`subagent_alias_history` tables follow the same structure as the -corresponding skill tables above (without `kind`). FK relationships -mirror the skill tables: `subagent_versions` references `subagents` -with CASCADE delete, etc. +`subagent_version_tags`, `subagent_version_scan_results`, +`subagent_aliases`, and `subagent_alias_history` tables follow the +same structure as the corresponding skill tables above. FK +relationships mirror the skill tables: `subagent_versions` references +`subagents` with CASCADE delete, etc. #### Hook tables The `hooks`, `hook_versions`, `hook_tags`, `hook_version_tags`, -`hook_aliases`, and `hook_alias_history` tables follow the same -structure as the corresponding skill tables. FK relationships mirror -the skill tables. +`hook_version_scan_results`, `hook_aliases`, and `hook_alias_history` +tables follow the same structure as the corresponding skill tables. +FK relationships mirror the skill tables. #### `skill_bundles` @@ -1123,6 +1150,12 @@ migrations and allows either registry to be deployed independently. | `key` | `String(256)` | PK | | `value` | `Text` | | +#### `skill_bundle_version_scan_results` + +Same structure as `skill_version_scan_results`. FK: +`(workspace, name, version)` references `skill_bundle_versions`, +CASCADE delete. + #### `skill_bundle_aliases` | Column | Type | Notes | @@ -1263,6 +1296,22 @@ class SkillRegistryMixin: ) -> None: raise NotImplementedError + # --- Skill scan result operations --- + + def set_skill_version_scan_result( + self, name: str, version: str, + scan_type: str, status: str, date: str, + tool: str | None = None, + details_url: str | None = None, + details: str | None = None, + ) -> ScanResult: + raise NotImplementedError + + def delete_skill_version_scan_result( + self, name: str, version: str, scan_type: str, + ) -> None: + raise NotImplementedError + # --- Skill alias operations --- def set_skill_alias( @@ -1286,7 +1335,7 @@ class SkillRegistryMixin: # --- Subagent operations --- # Same shape as Skill: create, get, search, update, delete, - # plus version, tag, and alias operations. + # plus version, tag, scan result, and alias operations. def create_subagent( self, name: str, @@ -1326,12 +1375,12 @@ class SkillRegistryMixin: ) -> SubagentVersion: raise NotImplementedError - # Remaining subagent version, tag, and alias operations - # follow the same pattern as skill operations above. + # Remaining subagent version, tag, scan result, and alias + # operations follow the same pattern as skill operations above. # --- Hook operations --- # Same shape as Skill: create, get, search, update, delete, - # plus version, tag, and alias operations. + # plus version, tag, scan result, and alias operations. def create_hook( self, name: str, @@ -1371,8 +1420,8 @@ class SkillRegistryMixin: ) -> HookVersion: raise NotImplementedError - # Remaining hook version, tag, and alias operations - # follow the same pattern as skill operations above. + # Remaining hook version, tag, scan result, and alias + # operations follow the same pattern as skill operations above. # --- SkillBundle operations --- @@ -1482,6 +1531,22 @@ class SkillRegistryMixin: ) -> None: raise NotImplementedError + # --- SkillBundle scan result operations --- + + def set_skill_bundle_version_scan_result( + self, name: str, version: str, + scan_type: str, status: str, date: str, + tool: str | None = None, + details_url: str | None = None, + details: str | None = None, + ) -> ScanResult: + raise NotImplementedError + + def delete_skill_bundle_version_scan_result( + self, name: str, version: str, scan_type: str, + ) -> None: + raise NotImplementedError + # --- SkillBundle alias operations --- def set_skill_bundle_alias( @@ -1595,6 +1660,9 @@ All paths relative to `/ajax-api/3.0/mlflow/skills`. | `DELETE` | `/{name}/tags/{key}` | Delete a skill-level tag | | `POST` | `/{name}/versions/{version}/tags` | Set a version-level tag | | `DELETE` | `/{name}/versions/{version}/tags/{key}` | Delete a version tag | +| `GET` | `/{name}/versions/{version}/scan-results` | List scan results | +| `PUT` | `/{name}/versions/{version}/scan-results/{scan_type}` | Set scan result (upsert) | +| `DELETE` | `/{name}/versions/{version}/scan-results/{scan_type}` | Delete scan result | | `POST` | `/{name}/aliases` | Set an alias | | `GET` | `/{name}/aliases/{alias}` | Resolve alias to `SkillVersion` | | `DELETE` | `/{name}/aliases/{alias}` | Delete an alias | @@ -1605,13 +1673,13 @@ All paths relative to `/ajax-api/3.0/mlflow/skills`. All paths relative to `/ajax-api/3.0/mlflow/subagents`. Same structure as skill endpoints: CRUD on subagents and subagent versions, -plus tags, aliases, and alias history. +plus tags, scan results, aliases, and alias history. #### Hook endpoints All paths relative to `/ajax-api/3.0/mlflow/hooks`. Same structure as -skill endpoints: CRUD on hooks and hook versions, plus tags, aliases, -and alias history. +skill endpoints: CRUD on hooks and hook versions, plus tags, scan +results, aliases, and alias history. #### Skill bundle endpoints @@ -1633,6 +1701,9 @@ All paths relative to `/ajax-api/3.0/mlflow/skill-bundles`. | `DELETE` | `/{name}/tags/{key}` | Delete a bundle-level tag | | `POST` | `/{name}/versions/{version}/tags` | Set a bundle version tag | | `DELETE` | `/{name}/versions/{version}/tags/{key}` | Delete a bundle version tag | +| `GET` | `/{name}/versions/{version}/scan-results` | List scan results | +| `PUT` | `/{name}/versions/{version}/scan-results/{scan_type}` | Set scan result (upsert) | +| `DELETE` | `/{name}/versions/{version}/scan-results/{scan_type}` | Delete scan result | | `POST` | `/{name}/aliases` | Set a bundle alias | | `GET` | `/{name}/aliases/{alias}` | Resolve bundle alias to version | | `DELETE` | `/{name}/aliases/{alias}` | Delete a bundle alias | @@ -1648,7 +1719,8 @@ expressions following existing MLflow conventions. `status = 'active'`, `tags.team = 'platform'` **Versions (all entity types):** `status = 'active'`, -`source_type = 'git'`, `tags.scan.prompt-injection.status = 'pass'` +`source_type = 'git'`, `scan_results.status = 'pass'`, +`scan_results.scan_type = 'prompt-injection'` **Skill bundle versions:** `status = 'active'`, `tags.approved = 'true'` @@ -1735,10 +1807,10 @@ Key design choices: - **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. +- **Tag edits require EDIT.** Tags are operational metadata. +- **Scan result edits require EDIT.** Requiring MANAGE for scan results + would create friction for CI/CD scan integrations that need to record + results automatically. - **Creator gets MANAGE.** When a user creates an entity (skill, subagent, hook, or bundle), they automatically receive MANAGE permission, following the MLflow model registry pattern. @@ -1754,7 +1826,7 @@ filter by type (skill, subagent, hook, bundle), status, source type, and search by name or description. The detail view for a skill, subagent, or hook shows metadata, version -list, aliases, tags (including security scan results), and bundle +list, aliases, tags, scan results, and bundle memberships. The detail view for a skill bundle shows its description, status, @@ -1763,61 +1835,69 @@ and the 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 -with a reserved `scan.*` namespace. +The registry does not perform security scans. It provides a structured +metadata layer for recording and querying scan results on any version +entity (skill, subagent, hook, or bundle). -**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: +**ScanResult.** Each scan result is a structured record on a version: -| Field | Expected values | Description | -|---|---|---| -| `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 | +| Field | Type | Required | Description | +|---|---|---|---| +| `scan_type` | `str` | yes | What was scanned for: `prompt-injection`, `code-vulnerability`, `secrets-detection`, etc. Free string, not an enum, so organizations can define custom scan types | +| `status` | `ScanStatus` | yes | `pass`, `fail`, `warning`, `error` | +| `date` | `str` | yes | ISO 8601 date when the scan was run (e.g., `2026-04-29`) | +| `tool` | `str` | no | Tool name and version (e.g., `promptfoo/1.2.0`) | +| `details_url` | `str` | no | Link to full scan report | +| `details` | `str` | no | Free-text summary, error message, or scanner-specific output | -**Example tags on a skill version:** +**ScanStatus enum.** The `status` field uses a small fixed enum so +that "show me all versions that passed all scans" works across +scanners without knowing each tool's output format. Scanners that +produce nuanced output (e.g., risk ratings) map to the enum based on +the organization's threshold and put the detailed score in `details`. -| 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'`. +**Upsert semantics.** One result per `(version, scan_type)`. +Re-scanning overwrites the previous result for that scan type. This +keeps the model simple and avoids ambiguity about which result is +current. + +**Example scan results on a skill version:** + +```python +mlflow.genai.skills.set_skill_version_scan_result( + name="code-review", + version="1.0.0", + scan_type="prompt-injection", + status="pass", + date="2026-04-29", + tool="promptfoo/1.2.0", +) +mlflow.genai.skills.set_skill_version_scan_result( + name="code-review", + version="1.0.0", + scan_type="code-vulnerability", + status="fail", + date="2026-04-28", + tool="semgrep/1.67.0", + details_url="https://scans.acme.com/results/abc123", +) +``` + +**UI rendering.** Structured fields give the UI reliable data to +render a scan summary (e.g., a green check or red X per scan type) +without parsing tag conventions. + +**Querying.** Scan results support structured filter expressions: +`scan_results.status = 'pass'`, +`scan_results.scan_type = 'prompt-injection'`. Date comparisons are +supported on the structured `date` field, unlike tags which only +support equality. **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. +filter for safe versions using `scan_results.status`. The registry +does not enforce this workflow, but the combination of version status +and scan results makes it straightforward to implement. ### Trace integration diff --git a/rfcs/0005-skill-registry/deferred-review-items.md b/rfcs/0005-skill-registry/deferred-review-items.md index e051b66..dae06cb 100644 --- a/rfcs/0005-skill-registry/deferred-review-items.md +++ b/rfcs/0005-skill-registry/deferred-review-items.md @@ -14,9 +14,13 @@ Removed entirely. RFC-0004 has no equivalent section. Reply posted 2026-05-19. Workspace scoping trimmed to reference existing patterns. Adoption strategy condensed to three-phase summary. Reply posted 2026-05-19. -## Security scan tracking (comments 3251079589, 3251079660) +## ~~Security scan tracking (comments 3251079589, 3251079660)~~ DONE -Two related comments. Matt suggests scan metadata should either be a first-class structured field or dropped from the RFC, rather than modeled as plain tags. Also notes the query example for scan metadata may not be supported by MLflow's existing filter syntax. +Resolved: replaced tag-based scan convention with structured +`ScanResult` fields on version entities. `ScanStatus` enum +(pass/fail/warning/error) enables cross-scanner filtering. Upsert +semantics per (version, scan_type). Broken date comparison query +example removed. Reply posted 2026-05-28. ## ~~Naming (comment 3251078919)~~ DONE From 33312921e00597ccc98634434884b58e4f8c47f9 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Thu, 28 May 2026 12:49:29 -0400 Subject: [PATCH 23/52] Remove deferred-review-items from PR, add .gitignore for .local/ Move local working documents (deferred-review-items, naming discussion, error handling reference) to .local/ directory. Add .gitignore to exclude .local/ and .DS_Store from version control. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 2 ++ .../deferred-review-items.md | 31 ------------------- 2 files changed, 2 insertions(+), 31 deletions(-) create mode 100644 .gitignore delete mode 100644 rfcs/0005-skill-registry/deferred-review-items.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f7d380 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.local/ +.DS_Store diff --git a/rfcs/0005-skill-registry/deferred-review-items.md b/rfcs/0005-skill-registry/deferred-review-items.md deleted file mode 100644 index dae06cb..0000000 --- a/rfcs/0005-skill-registry/deferred-review-items.md +++ /dev/null @@ -1,31 +0,0 @@ -# Deferred Review Items - -Open items from Matt's May 15 review on PR #10 that still need replies or RFC changes. - -## ~~Subpath field (comment 3251079063)~~ DONE - -Added `subpath` as an optional field on `SkillVersion` and `SkillGroupVersion`. Used for OCI and ZIP source types when multiple skills share a single artifact. Not used for Git (tree URLs encode repo+ref+path) or MLflow artifacts (path scoped at upload). Reply posted 2026-05-19. - -## ~~Error handling section (comment 3251079530)~~ DONE - -Removed entirely. RFC-0004 has no equivalent section. Reply posted 2026-05-19. - -## ~~Workspace scoping / Adoption strategy (comment 3251079711)~~ DONE - -Workspace scoping trimmed to reference existing patterns. Adoption strategy condensed to three-phase summary. Reply posted 2026-05-19. - -## ~~Security scan tracking (comments 3251079589, 3251079660)~~ DONE - -Resolved: replaced tag-based scan convention with structured -`ScanResult` fields on version entities. `ScanStatus` enum -(pass/fail/warning/error) enables cross-scanner filtering. Upsert -semantics per (version, scan_type). Broken date comparison query -example removed. Reply posted 2026-05-28. - -## ~~Naming (comment 3251078919)~~ DONE - -Resolved: split the single `Skill(kind=...)` entity into three -separate entity types (Skill, Subagent, Hook). SkillGroup renamed to -SkillBundle. SkillBundleVersion uses typed member lists instead of a -SkillGroupVersionMember entity. See `naming-and-scope-discussion.md` -for the full discussion history. From a8100f3c647e743daf950d5a9962c0b450172d6f Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Thu, 28 May 2026 12:51:47 -0400 Subject: [PATCH 24/52] Remove .gitignore from PR Co-Authored-By: Claude Opus 4.6 --- .gitignore | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .gitignore diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 7f7d380..0000000 --- a/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -.local/ -.DS_Store From dea06bf45e281799537bfaa7e660ae4d11329947 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Thu, 28 May 2026 14:18:43 -0400 Subject: [PATCH 25/52] Drop last_registered_version field per review feedback Redundant with latest_version and sorting versions by creation_timestamp. Removed from all entity types (Skill, Subagent, Hook, SkillBundle) and DB schema tables. Co-Authored-By: Claude Opus 4.6 --- rfcs/0005-skill-registry/0005-skill-registry.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index 0a0f7d7..c861733 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -503,7 +503,6 @@ class Skill: 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 latest_version: str | None = None created_by: str | None = None last_updated_by: str | None = None @@ -516,7 +515,6 @@ class Skill: | `name` | `str` | Stable logical asset name, unique within a workspace | | `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 | @@ -660,7 +658,6 @@ class Subagent: status: SkillStatus = SkillStatus.DRAFT tags: dict[str, str] = field(default_factory=dict) aliases: list["SubagentAlias"] = field(default_factory=list) - last_registered_version: str | None = None latest_version: str | None = None created_by: str | None = None last_updated_by: str | None = None @@ -689,7 +686,6 @@ class Hook: status: SkillStatus = SkillStatus.DRAFT tags: dict[str, str] = field(default_factory=dict) aliases: list["HookAlias"] = field(default_factory=list) - last_registered_version: str | None = None latest_version: str | None = None created_by: str | None = None last_updated_by: str | None = None @@ -722,7 +718,6 @@ class SkillBundle: status: SkillStatus = SkillStatus.DRAFT tags: dict[str, str] = field(default_factory=dict) aliases: list["SkillBundleAlias"] = field(default_factory=list) - last_registered_version: str | None = None latest_version: str | None = None created_by: str | None = None last_updated_by: str | None = None @@ -970,7 +965,6 @@ workspace-scoped. | `workspace` | `String(63)` | PK, default `'default'` | | `name` | `String(256)` | PK | | `description` | `String(5000)` | | -| `last_registered_version` | `String(256)` | | | `latest_version` | `String(256)` | optional; explicit version string to resolve as "latest" | | `created_by` | `String(256)` | | | `last_updated_by` | `String(256)` | | @@ -1079,7 +1073,6 @@ FK relationships mirror the skill tables. | `workspace` | `String(63)` | PK, default `'default'` | | `name` | `String(256)` | PK | | `description` | `String(5000)` | | -| `last_registered_version` | `String(256)` | | | `latest_version` | `String(256)` | optional; explicit version string to resolve as "latest" | | `created_by` | `String(256)` | | | `last_updated_by` | `String(256)` | | From 544e78e49beb4365aae2cb04f352fe481506d3b8 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Thu, 28 May 2026 14:22:18 -0400 Subject: [PATCH 26/52] Add display_name field to all entity types per review feedback Adds mutable display_name to Skill, SkillVersion, Subagent, Hook, SkillBundle, SkillBundleVersion, their DB tables, store methods, and SDK convenience functions. Consistent with MCP registry schema. Co-Authored-By: Claude Opus 4.6 --- .../0005-skill-registry.md | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index c861733..08d5fd1 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -498,6 +498,7 @@ class ScanResult: @dataclass class Skill: name: str + display_name: str | None = None description: str | None = None workspace: str | None = None status: SkillStatus = SkillStatus.DRAFT @@ -513,6 +514,7 @@ class Skill: | Field | Type | Description | |---|---|---| | `name` | `str` | Stable logical asset name, unique within a workspace | +| `display_name` | `str` | Mutable human-readable label for UI display | | `status` | `SkillStatus` | Read-only, derived from the latest version's status: `draft`, `active`, `deprecated`, `deleted` | | `aliases` | `list[SkillAlias]` | Stable version pointers (e.g., `production` -> `1.2.0`) | | `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 | @@ -542,6 +544,7 @@ class SkillSourceType(StrEnum): class SkillVersion: name: str version: str + display_name: str | None = None source_type: SkillSourceType | None = None source: str | None = None subpath: str | None = None @@ -561,6 +564,7 @@ class SkillVersion: | Field | Type | Description | |---|---|---| | `version` | `str` | Publisher-supplied version string. Semver recommended but not enforced | +| `display_name` | `str` | Mutable human-readable label for UI display | | `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) | @@ -639,8 +643,8 @@ read; verification is the consumer's responsibility. **Immutability contract.** `source_type`, `source`, `subpath`, `content_digest`, and `version` are immutable after creation. To point -to different content, register a new version. Mutable fields (`status`, -`tags`, `scan_results`) can be updated independently. +to different content, register a new version. Mutable fields (`display_name`, +`status`, `tags`, `scan_results`) can be updated independently. #### Subagent @@ -653,6 +657,7 @@ lifecycle management. @dataclass class Subagent: name: str + display_name: str | None = None description: str | None = None workspace: str | None = None status: SkillStatus = SkillStatus.DRAFT @@ -681,6 +686,7 @@ before a commit). The `Hook` entity follows the same structure as @dataclass class Hook: name: str + display_name: str | None = None description: str | None = None workspace: str | None = None status: SkillStatus = SkillStatus.DRAFT @@ -713,6 +719,7 @@ Skill: versions, tags, and aliases. @dataclass class SkillBundle: name: str + display_name: str | None = None description: str | None = None workspace: str | None = None status: SkillStatus = SkillStatus.DRAFT @@ -761,6 +768,7 @@ by type. class SkillBundleVersion: name: str version: str + display_name: str | None = None source_type: SkillSourceType | None = None source: str | None = None subpath: str | None = None @@ -807,7 +815,7 @@ version's source is required. **Immutability contract.** The member lists and source fields of a bundle version are immutable after creation. To change the set of members or source pointer, register a new bundle version. Mutable -fields (`status`, `tags`) can be updated independently. +fields (`display_name`, `status`, `tags`) can be updated independently. When `registry="skill"`, the member references a `SkillVersion` in this registry. When `registry="mcp"`, the member references an @@ -964,6 +972,7 @@ workspace-scoped. |--------|------|-------| | `workspace` | `String(63)` | PK, default `'default'` | | `name` | `String(256)` | PK | +| `display_name` | `String(256)` | mutable human-readable label | | `description` | `String(5000)` | | | `latest_version` | `String(256)` | optional; explicit version string to resolve as "latest" | | `created_by` | `String(256)` | | @@ -978,6 +987,7 @@ workspace-scoped. | `workspace` | `String(63)` | PK, FK | | `name` | `String(256)` | PK, FK | | `version` | `String(256)` | PK, publisher-supplied | +| `display_name` | `String(256)` | mutable human-readable label | | `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 | @@ -1072,6 +1082,7 @@ FK relationships mirror the skill tables. |--------|------|-------| | `workspace` | `String(63)` | PK, default `'default'` | | `name` | `String(256)` | PK | +| `display_name` | `String(256)` | mutable human-readable label | | `description` | `String(5000)` | | | `latest_version` | `String(256)` | optional; explicit version string to resolve as "latest" | | `created_by` | `String(256)` | | @@ -1086,6 +1097,7 @@ FK relationships mirror the skill tables. | `workspace` | `String(63)` | PK, FK | | `name` | `String(256)` | PK, FK | | `version` | `String(256)` | PK, publisher-supplied | +| `display_name` | `String(256)` | mutable human-readable label | | `source_type` | `String(20)` | optional; `git`, `oci`, `zip`, etc. | | `source` | `String(2048)` | optional pointer to bundle artifact | | `subpath` | `String(2048)` | nullable; path within the artifact | @@ -1191,6 +1203,7 @@ class SkillRegistryMixin: def create_skill( self, name: str, + display_name: str | None = None, description: str | None = None, ) -> Skill: raise NotImplementedError @@ -1210,6 +1223,7 @@ class SkillRegistryMixin: def update_skill( self, name: str, + display_name: str | None = None, description: str | None = None, latest_version: str | None = None, ) -> Skill: @@ -1224,6 +1238,7 @@ class SkillRegistryMixin: self, name: str, version: str, + display_name: str | None = None, source_type: str | None = None, source: str | None = None, subpath: str | None = None, @@ -1332,6 +1347,7 @@ class SkillRegistryMixin: def create_subagent( self, name: str, + display_name: str | None = None, description: str | None = None, ) -> Subagent: raise NotImplementedError @@ -1350,6 +1366,7 @@ class SkillRegistryMixin: def update_subagent( self, name: str, + display_name: str | None = None, description: str | None = None, latest_version: str | None = None, ) -> Subagent: @@ -1360,6 +1377,7 @@ class SkillRegistryMixin: def create_subagent_version( self, name: str, version: str, + display_name: str | None = None, source_type: str | None = None, source: str | None = None, subpath: str | None = None, @@ -1377,6 +1395,7 @@ class SkillRegistryMixin: def create_hook( self, name: str, + display_name: str | None = None, description: str | None = None, ) -> Hook: raise NotImplementedError @@ -1395,6 +1414,7 @@ class SkillRegistryMixin: def update_hook( self, name: str, + display_name: str | None = None, description: str | None = None, latest_version: str | None = None, ) -> Hook: @@ -1405,6 +1425,7 @@ class SkillRegistryMixin: def create_hook_version( self, name: str, version: str, + display_name: str | None = None, source_type: str | None = None, source: str | None = None, subpath: str | None = None, @@ -1419,7 +1440,9 @@ class SkillRegistryMixin: # --- SkillBundle operations --- def create_skill_bundle( - self, name: str, description: str | None = None, + self, name: str, + display_name: str | None = None, + description: str | None = None, ) -> SkillBundle: raise NotImplementedError @@ -1438,6 +1461,7 @@ class SkillRegistryMixin: def update_skill_bundle( self, name: str, + display_name: str | None = None, description: str | None = None, latest_version: str | None = None, ) -> SkillBundle: @@ -1452,6 +1476,7 @@ class SkillRegistryMixin: self, name: str, version: str, + display_name: str | None = None, skills: list[tuple[str, str]] | None = None, subagents: list[tuple[str, str]] | None = None, hooks: list[tuple[str, str]] | None = None, @@ -1572,6 +1597,7 @@ combine store operations, matching the pattern established by def register_skill( name: str, version: str, + display_name: str | None = None, description: str | None = None, source_type: str | None = None, source: str | None = None, @@ -1589,6 +1615,7 @@ def register_skill( def register_subagent( name: str, version: str, + display_name: str | None = None, description: str | None = None, source_type: str | None = None, source: str | None = None, @@ -1604,6 +1631,7 @@ def register_subagent( def register_hook( name: str, version: str, + display_name: str | None = None, description: str | None = None, source_type: str | None = None, source: str | None = None, From 1b9fb12d58178464ccdbfd830624f3451dd1cf9e Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Thu, 28 May 2026 15:11:12 -0400 Subject: [PATCH 27/52] Drop run_id field from version entities per review feedback run_id can be expressed via tags, consistent with how MLflow prompts handle experiment linkage. Removed from SkillVersion dataclass, field table, DB schema, store methods, and SDK convenience functions. Co-Authored-By: Claude Opus 4.6 --- rfcs/0005-skill-registry/0005-skill-registry.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index 08d5fd1..3a05a78 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -553,7 +553,6 @@ class SkillVersion: tags: dict[str, str] = field(default_factory=dict) scan_results: list[ScanResult] = field(default_factory=list) aliases: list[str] = field(default_factory=list) - run_id: str | None = None workspace: str | None = None created_by: str | None = None last_updated_by: str | None = None @@ -572,7 +571,6 @@ class SkillVersion: | `status` | `SkillStatus` | Per-version lifecycle: `draft`, `active`, `deprecated`, `deleted` | | `scan_results` | `list[ScanResult]` | Structured scan results, one per `scan_type` (read-only, projected from scan results table) | | `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 small for the initial implementation. New source types (e.g., `s3`, @@ -993,7 +991,6 @@ workspace-scoped. | `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 | | `created_by` | `String(256)` | | | `last_updated_by` | `String(256)` | | | `creation_timestamp` | `BigInteger` | millis since epoch | @@ -1243,7 +1240,6 @@ class SkillRegistryMixin: source: str | None = None, subpath: str | None = None, content_digest: str | None = None, - run_id: str | None = None, ) -> SkillVersion: raise NotImplementedError @@ -1382,7 +1378,6 @@ class SkillRegistryMixin: source: str | None = None, subpath: str | None = None, content_digest: str | None = None, - run_id: str | None = None, ) -> SubagentVersion: raise NotImplementedError @@ -1430,7 +1425,6 @@ class SkillRegistryMixin: source: str | None = None, subpath: str | None = None, content_digest: str | None = None, - run_id: str | None = None, ) -> HookVersion: raise NotImplementedError @@ -1604,7 +1598,6 @@ def register_skill( subpath: 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 @@ -1622,7 +1615,6 @@ def register_subagent( subpath: str | None = None, content_path: str | None = None, content_digest: str | None = None, - run_id: str | None = None, ) -> SubagentVersion: """Register a subagent version. Auto-creates the parent Subagent if it does not exist.""" @@ -1638,7 +1630,6 @@ def register_hook( subpath: str | None = None, content_path: str | None = None, content_digest: str | None = None, - run_id: str | None = None, ) -> HookVersion: """Register a hook version. Auto-creates the parent Hook if it does not exist.""" From 8d2228787cb69a63be37ee62c3bc67672a1568d7 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Thu, 28 May 2026 15:40:45 -0400 Subject: [PATCH 28/52] Add prompts to out-of-scope section per review feedback Clarifies that prompts (template strings loaded at runtime) and skills (file sets installed by harnesses) serve different audiences and belong in separate registries. Co-Authored-By: Claude Opus 4.6 --- rfcs/0005-skill-registry/0005-skill-registry.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index 3a05a78..4fc47aa 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -433,6 +433,12 @@ posture across the organization's registered capabilities. sufficient for initial governance. - **Detailed UI/UX design.** This RFC describes the UI surface and placement but does not specify interaction patterns. +- **Prompts.** MLflow's prompt registry manages template strings with + variable placeholders, loaded at runtime by custom code via + `mlflow.genai.load_prompt()`. Skills are structurally different + (sets of files installed by a harness) and serve a different + audience (harness-based agents vs. custom agentic code). The two + registries are complementary but separate. ## Detailed design From 6f4440920f2eb9a56547000306403297fb0323bb Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Thu, 28 May 2026 15:45:05 -0400 Subject: [PATCH 29/52] Add source authentication section to pull semantics Documents how each source type handles auth: Git via standard credential resolution, OCI via Docker config, ZIP requires public URLs, MLflow via existing API credentials. Registry does not validate source accessibility at registration time. Co-Authored-By: Claude Opus 4.6 --- rfcs/0005-skill-registry/0005-skill-registry.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index 4fc47aa..1938e58 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -1792,6 +1792,22 @@ different sources). If `content_digest` is set, `pull` verifies the fetched content matches the digest and returns an error on mismatch. +**Source authentication.** The registry server stores source pointers +but does not validate source accessibility at registration time and is +not involved in content transfer at pull time. Authentication to +external sources is handled entirely by the client environment: + +| Source type | Authentication mechanism | +|---|---| +| `git` | Standard Git credential resolution: SSH keys (`~/.ssh/`), Git credential helpers (`git-credential-manager`, `git-credential-store`), `.netrc`, and `GIT_SSH_COMMAND`. Private repos work if the caller's Git is configured to access them. | +| `oci` | OCI registry credential resolution: Docker config (`~/.docker/config.json`), registry-specific credential helpers, and container runtime auth. Private registries work if the caller has a valid login session. | +| `zip` | No authentication support. ZIP sources must be publicly accessible URLs. For private content, use `git` or `oci` source types instead. | +| `mlflow` | MLflow artifact storage authentication, using the same credentials as other MLflow API calls. | + +The registry does not store, proxy, or manage source credentials. +Pull failures due to authentication errors are surfaced to the caller +with the underlying error from the source system. + `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. From e5c6289735a813da69cac408731a5be5fa916b26 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Thu, 28 May 2026 15:48:27 -0400 Subject: [PATCH 30/52] Clarify that source is only optional for bundle-level artifact members Source is required for standalone pull. It may be omitted only when the version's content lives within a bundle-level artifact. 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 1938e58..b06a356 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -571,7 +571,7 @@ class SkillVersion: | `version` | `str` | Publisher-supplied version string. Semver recommended but not enforced | | `display_name` | `str` | Mutable human-readable label for UI display | | `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 | +| `source` | `str` | Pointer to the content in the source system. Required for standalone pull. May be omitted only when the version's content lives within a bundle-level artifact (identified by `subpath` on the member) | | `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` | From 8e0a029b153942c7d523ebb58690ed704d657586 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Thu, 28 May 2026 15:54:37 -0400 Subject: [PATCH 31/52] Clarify marketplace install endpoint behavior Server-side install resolves bundle, pulls content, assembles plugin. Only works for public sources and MLflow artifact storage. Private sources require the CLI path with local credentials. Co-Authored-By: Claude Opus 4.6 --- .../0006-skill-harness-integration.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 25bdac2..b37daae 100644 --- a/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md +++ b/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md @@ -302,6 +302,18 @@ Each entry is derived from a published skill bundle version and its members. The `source` field points to a registry endpoint that serves the installable plugin bundle. +**Install endpoint behavior.** When a harness fetches the `source` URL, +the registry server resolves the bundle version, pulls member content +from their registered sources, generates harness-specific manifests, +and serves the assembled plugin as a downloadable archive. This is a +server-side equivalent of `mlflow skills install`. Because the server +performs the content fetch, this flow only works for bundles whose +sources are publicly accessible or stored in MLflow artifact storage +(`source_type="mlflow"`). Bundles with private external sources (private +Git repos, authenticated OCI registries) are not included in the +marketplace catalog; users install those via the CLI +(`mlflow skills install`), which uses the caller's local credentials. + #### Configuration Users add the registry as a marketplace source in their harness From dcbdfeb9e5e31db7ce193ae8b4c3db1ab27a5197 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Thu, 28 May 2026 16:01:45 -0400 Subject: [PATCH 32/52] Lock file replay checks version status against registry Governance actions (deprecation, deletion) now take effect even for existing lock files. The registry is always contacted for status verification. Co-Authored-By: Claude Opus 4.6 --- .../0006-skill-harness-integration.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 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 b37daae..ef66418 100644 --- a/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md +++ b/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md @@ -453,10 +453,11 @@ mlflow skills install --bundle 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. +The lock file records source URIs, exact versions, and content +digests for reproducible installs across team members. Lock file +replay still contacts the registry to verify version status, so +governance actions (deprecation, deletion) take effect even for +existing lock files. #### SDK From 8ad04a6543144309ad3f18920dd0dc87e770633a Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Thu, 28 May 2026 16:03:26 -0400 Subject: [PATCH 33/52] Add source availability section to pull semantics Documents pull failure behavior when sources are unreachable or deleted. Bundle pulls fail entirely rather than producing partial results. Co-Authored-By: Claude Opus 4.6 --- rfcs/0005-skill-registry/0005-skill-registry.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index b06a356..2411060 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -1792,6 +1792,15 @@ different sources). If `content_digest` is set, `pull` verifies the fetched content matches the digest and returns an error on mismatch. +**Source availability.** The registry stores source pointers but does +not cache or proxy content. If a source is unreachable or the content +has been deleted, pull fails with an error that surfaces the +underlying failure from the source system (e.g., Git clone failure, +OCI pull 404, HTTP download error). Source availability is the +publisher's responsibility. For bundle pulls, if one member's source +is unavailable, the entire pull fails rather than producing a partial +result. + **Source authentication.** The registry server stores source pointers but does not validate source accessibility at registration time and is not involved in content transfer at pull time. Authentication to From b6b93e28f079bff33c6484aae145ddd749cacca3 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Thu, 28 May 2026 16:09:28 -0400 Subject: [PATCH 34/52] Expand MCP server config generation in install flow Documents how adapter resolves connection details from MCPAccessBinding, handles embedded .mcp.json in bundle artifacts, and notes that credentials are the user's responsibility. Co-Authored-By: Claude Opus 4.6 --- .../0006-skill-harness-integration.md | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 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 ef66418..0f8946b 100644 --- a/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md +++ b/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md @@ -195,9 +195,18 @@ generates: For Codex CLI, the path uses `.codex/plugins/` instead. -**MCP server merging.** If the bundle contains multiple `mcp-server` -members, their configs are merged into a single `.mcp.json` file -using server name as the key: +**MCP server config generation.** When a bundle references MCP servers +in its `mcp_servers` member list, the adapter generates `.mcp.json` +entries from MCP registry metadata. For each referenced server, the +adapter resolves the `MCPServerVersion` from the MCP registry +(RFC-0004) and looks for an `MCPAccessBinding` targeting that version +or alias. If a binding exists, the adapter uses its `endpoint_url` and +`transport_type` as the connection target. If multiple bindings exist +for the same server, the adapter uses the first binding targeting the +referenced version or alias. If no binding exists, the adapter falls +back to the connection details in `server_json` (e.g., `remotes[]`). + +Entries are merged into a single `.mcp.json` using server name as key: ```json { @@ -208,6 +217,20 @@ using server name as the key: } ``` +**Embedded MCP configs.** When a bundle has a bundle-level source and +the artifact already contains a `.mcp.json`, those embedded configs +are used as-is for any MCP servers not in the `mcp_servers` member +list. If the same server name appears in both the embedded config and +the `mcp_servers` list, the registry-generated entry takes precedence +(the registry is the governed source of truth). + +**MCP server credentials.** The adapter generates connection config +but does not configure credentials, certificates, or authorization +headers. These are the user's responsibility. The adapter logs a +warning when it generates an entry for a server that uses +authenticated transport, so users know to complete the setup +manually. + **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 From 75aba447725b29ec1bc6c3daa672d5ef664944f4 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Thu, 28 May 2026 16:18:59 -0400 Subject: [PATCH 35/52] Clarify bundle-level source artifact expectations Bundle artifact may or may not be harness-ready. Adapters generate manifests from registry metadata regardless. Artifact layout correctness is the publisher's responsibility. Co-Authored-By: Claude Opus 4.6 --- rfcs/0005-skill-registry/0005-skill-registry.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index 2411060..163239d 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -810,6 +810,18 @@ repo. Individual members within a bundle-level artifact use `subpath` on their version entities to identify their location within the artifact. +The bundle artifact is a generic package of content (skill files, +agent definitions, hook scripts). It may or may not be +harness-ready; the adapter does not assume either way. Harness +adapters (RFC-0006) generate manifests (`plugin.json`, +`.mcp.json`) from registry metadata at install time, since the +registry is the governed source of truth. If the artifact +contents disagree with the declared members (e.g., a `subpath` +points to a missing directory), pull succeeds but install fails +when the adapter cannot find the expected content. Correctness of +the artifact layout is the publisher's responsibility; the registry +does not validate artifact contents at registration time. + **Source resolution for pull.** When pulling a bundle, if the bundle version has a source, that source is used. Otherwise, each member is pulled individually from its own source. Members without a source are From 32018799255a9cbc671827aaf1a374a37788a37b Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Mon, 1 Jun 2026 14:21:51 -0400 Subject: [PATCH 36/52] Remove alias history from RFC-0005 Drop SkillAliasHistory, SubagentAliasHistory, HookAliasHistory, and SkillBundleAliasHistory per team meeting decision. This removes the alias audit trail section, DB tables, store methods, and REST endpoints. Alias history can be re-proposed as a cross-registry feature later. Co-Authored-By: Claude Opus 4.6 --- .../0005-skill-registry.md | 86 +------------------ 1 file changed, 4 insertions(+), 82 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index 163239d..8b69f3b 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -892,33 +892,6 @@ Tags use the same structure for skill-level, version-level, and bundle-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_bundle_alias`, or -`delete_skill_bundle_alias` (and the corresponding subagent and hook -variants) 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 `SkillBundleAliasHistory`, -`SubagentAliasHistory`, and `HookAliasHistory`. - -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). @@ -1061,25 +1034,11 @@ delete. PK includes `scan_type` for upsert-per-scan-type semantics. | `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. - #### Subagent tables The `subagents`, `subagent_versions`, `subagent_tags`, `subagent_version_tags`, `subagent_version_scan_results`, -`subagent_aliases`, and `subagent_alias_history` tables follow the +and `subagent_aliases` tables follow the same structure as the corresponding skill tables above. FK relationships mirror the skill tables: `subagent_versions` references `subagents` with CASCADE delete, etc. @@ -1087,7 +1046,7 @@ relationships mirror the skill tables: `subagent_versions` references #### Hook tables The `hooks`, `hook_versions`, `hook_tags`, `hook_version_tags`, -`hook_version_scan_results`, `hook_aliases`, and `hook_alias_history` +`hook_version_scan_results`, and `hook_aliases` tables follow the same structure as the corresponding skill tables. FK relationships mirror the skill tables. @@ -1185,20 +1144,6 @@ CASCADE delete. | `alias` | `String(256)` | PK | | `version` | `String(256)` | target bundle version string | -#### `skill_bundle_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'`. @@ -1346,15 +1291,6 @@ 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 - # --- Subagent operations --- # Same shape as Skill: create, get, search, update, delete, # plus version, tag, scan result, and alias operations. @@ -1589,14 +1525,6 @@ class SkillRegistryMixin: ) -> None: raise NotImplementedError - def get_skill_bundle_alias_history( - self, - name: str, - alias: str | None = None, - max_results: int = 100, - page_token: str | None = None, - ) -> PagedList[SkillBundleAliasHistory]: - raise NotImplementedError ``` ### SDK convenience functions @@ -1696,20 +1624,17 @@ 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) | - #### Subagent endpoints All paths relative to `/ajax-api/3.0/mlflow/subagents`. Same structure as skill endpoints: CRUD on subagents and subagent versions, -plus tags, scan results, aliases, and alias history. +plus tags, scan results, and aliases. #### Hook endpoints All paths relative to `/ajax-api/3.0/mlflow/hooks`. Same structure as skill endpoints: CRUD on hooks and hook versions, plus tags, scan -results, aliases, and alias history. +results, and aliases. #### Skill bundle endpoints @@ -1737,9 +1662,6 @@ All paths relative to `/ajax-api/3.0/mlflow/skill-bundles`. | `POST` | `/{name}/aliases` | Set a bundle alias | | `GET` | `/{name}/aliases/{alias}` | Resolve bundle alias to version | | `DELETE` | `/{name}/aliases/{alias}` | Delete a bundle 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 Search endpoints use page-token-based pagination and `filter_string` From 729b9852673256c791f5b4195c31cf3ee7045af9 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Mon, 1 Jun 2026 15:21:23 -0400 Subject: [PATCH 37/52] Add bundle import to RFC-0006 Adds the reverse of install: import takes an existing harness-specific artifact (local path, git, OCI, or ZIP), introspects it to identify skills, subagents, hooks, and MCP servers, and registers them all in the registry as a skill bundle. Extends the HarnessAdapter interface with introspect_bundle, and adds SDK, CLI, and UI surfaces. Co-Authored-By: Claude Opus 4.6 --- .../0006-skill-harness-integration.md | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) 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 0f8946b..7907b81 100644 --- a/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md +++ b/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md @@ -61,6 +61,23 @@ mlflow skills install --bundle pr-workflow --alias production \ --harness antigravity ``` +## Import an existing plugin as a skill bundle + +```bash +# Register all elements from an existing Claude Code plugin +mlflow skills import --source ./my-claude-plugin \ + --harness claude-code --bundle-name my-plugin --version 1.0.0 + +# Or from a remote source (git, OCI, ZIP) +mlflow skills import --source https://github.com/acme/plugins/tree/v1.0.0/pr-workflow \ + --harness claude-code +``` + +This fetches the artifact (or reads it locally), inspects it to +identify skills, subagents, hooks, and MCP servers, and registers +each element in the registry along with a skill bundle that ties +them together. + ## Python SDK ```python @@ -72,6 +89,14 @@ mlflow.genai.skills.install( harness="claude-code", destination=".", # project root ) + +# Import an existing harness-specific plugin into the registry +mlflow.genai.skills.import_bundle( + source="./my-claude-plugin", + harness="claude-code", + bundle_name="my-plugin", + version="1.0.0", +) ``` ## Motivation @@ -148,11 +173,30 @@ Each supported harness has an adapter that knows how to: (e.g., `plugin.json`, `.mcp.json`) from registry metadata. 3. **Handle unsupported types.** Skip member types the harness does not support, with a warning. +4. **Introspect existing bundles.** Given a harness-specific artifact + (e.g., a Claude Code plugin directory), identify the individual + capabilities it contains and their types. ```python from abc import abstractmethod +@dataclass +class IntrospectedMember: + name: str + kind: str # "skill", "subagent", "hook", "mcp_server" + source_path: str + description: str | None = None + metadata: dict[str, str] | None = None + + +@dataclass +class IntrospectedBundle: + name: str + description: str | None = None + members: list[IntrospectedMember] = field(default_factory=list) + + class HarnessAdapter: @abstractmethod def install_skill_bundle( @@ -165,6 +209,11 @@ class HarnessAdapter: destination: str, ) -> str: ... + @abstractmethod + def introspect_bundle( + self, source: str, + ) -> IntrospectedBundle: ... + @abstractmethod def supported_member_types(self) -> set[str]: ... ``` @@ -273,6 +322,112 @@ 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. +### Bundle import + +Installation takes registry metadata and produces a harness-specific +artifact. Bundle import is the reverse: it takes an existing +harness-specific artifact and registers all its elements in the +registry, creating individual capability entries and a skill bundle +that ties them together. + +#### Motivation + +Teams that already have skills, subagents, and hooks organized as +harness-specific artifacts (e.g., a Claude Code plugin directory, a +Cursor project) should not have to manually decompose and register +each element. Bundle import lets them point at an existing artifact +and register everything in one operation. + +#### Contract + +The import operation takes three inputs: + +- **source**: a reference to the artifact. Supports the same source + types as skill bundle registration: local path, git URL, OCI + reference, or ZIP URL. The import operation fetches the artifact + from the source before introspection. +- **harness**: the harness format to interpret the artifact as (e.g., + `claude-code`, `cursor`). If omitted, the system attempts + auto-detection by probing each registered adapter. +- **bundle_name**: the name for the resulting skill bundle. If + omitted, the adapter derives a name from the artifact (e.g., from + `plugin.json` or the directory name). + +The import operation: + +1. Calls the adapter's `introspect_bundle` method, which parses the + artifact and returns an `IntrospectedBundle` listing each + discovered member with its type, source path, and any metadata the + adapter can extract. +2. For each member, registers the individual capability in the + registry (skill, subagent, hook, or MCP server) if it does not + already exist, with source pointing to the member's location + within the artifact. +3. Creates a skill bundle and bundle version whose members reference + all the registered capabilities. +4. Returns the created bundle and a summary of what was registered vs. + what already existed. + +If a capability with the same name already exists in the registry, +import does not overwrite it. Instead, it creates a new version if +the content has changed, or reuses the existing version if it has not. +The bundle version references whichever version is current after +import. + +#### Conflict handling + +When an existing registry entry conflicts with an imported element +(e.g., same name but different source), the import operation reports +the conflict and skips that element. The caller can resolve conflicts +by renaming elements or using the `--force` flag to overwrite. + +#### SDK + +```python +mlflow.genai.skills.import_bundle( + source="./my-claude-plugin", + harness="claude-code", + bundle_name="my-plugin", + version="1.0.0", +) +``` + +#### CLI + +```bash +# Import from a local directory +mlflow skills import --source ./my-claude-plugin \ + --harness claude-code --bundle-name my-plugin --version 1.0.0 + +# Import from a git repo +mlflow skills import --source https://github.com/acme/plugins/tree/v1.0.0/pr-workflow \ + --harness claude-code + +# Import from an OCI registry +mlflow skills import --source oci://registry.example.com/plugins/review:latest \ + --harness claude-code + +# Import from a ZIP URL +mlflow skills import --source https://example.com/plugins/review-v1.zip \ + --harness claude-code + +# Auto-detect harness format +mlflow skills import --source ./my-plugin +``` + +#### UI + +The UI provides an import flow where users can: + +1. Specify a source (local path or URL) and optionally select a + harness format. +2. Preview the introspected elements before confirming registration. +3. Resolve any naming conflicts with existing registry entries. +4. Confirm the import, which creates all entries and the bundle. + +The preview step calls `introspect_bundle` without writing to the +registry, so users can see what will be registered before committing. + ### Marketplace integration Some harnesses (Claude Code, Codex CLI) support marketplace catalogs: @@ -389,6 +544,17 @@ def install( Resolves from the registry, pulls content, generates harness-specific manifests, and places files in the correct directories.""" + + +def import_bundle( + source: str, + harness: str | None = None, + bundle_name: str | None = None, + version: str | None = None, +) -> SkillBundle: + """Import an existing harness-specific artifact into the registry. + Introspects the artifact, registers individual capabilities, + and creates a skill bundle referencing them all.""" ``` ### REST API @@ -411,6 +577,10 @@ mlflow skills install --name code-review --alias production \ mlflow skills install --bundle pr-workflow --alias production \ --harness claude-code --destination . +# Import an existing plugin into the registry +mlflow skills import --source ./my-claude-plugin \ + --harness claude-code --bundle-name my-plugin --version 1.0.0 + # List supported harnesses mlflow skills harnesses ``` From 6fd76065223f8f1773230781068c9cb1c17ee927 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Mon, 1 Jun 2026 15:25:02 -0400 Subject: [PATCH 38/52] Remove security scan tracking from RFC-0005 Drop ScanResult, ScanStatus, scan_results fields, scan result DB tables, store methods, REST endpoints, filter examples, permissions, UI references, and the full security scan tracking section per team meeting decision. Co-Authored-By: Claude Opus 4.6 --- .../0005-skill-registry.md | 229 ++---------------- 1 file changed, 25 insertions(+), 204 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index 8b69f3b..ae16cd7 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -18,9 +18,8 @@ AI agent capabilities. The registry stores metadata and typed source pointers (to Git repos, OCI registries, ZIP archives, etc.). It can also store content directly via MLflow artifact storage, but the primary design is metadata-first. It provides enterprise governance -on top of existing distribution mechanisms: lifecycle management, security scan -tracking, usage analytics via traces, and federated discovery across -sources. +on top of existing distribution mechanisms: lifecycle management, +usage analytics via traces, and federated discovery across sources. The registry manages three entity types under the `mlflow.genai.skills` SDK namespace (CLI: `mlflow skills`), each with full lifecycle @@ -61,16 +60,6 @@ version = mlflow.genai.skills.register_skill( ) # version.status == "draft" -# Record a security scan result while still in draft -mlflow.genai.skills.set_skill_version_scan_result( - name="code-review", - version="1.0.0", - scan_type="prompt-injection", - status="pass", - date="2026-04-29", - tool="promptfoo/1.2.0", -) - # Activate the version once it's ready for downstream use mlflow.genai.skills.update_skill_version( name="code-review", @@ -357,12 +346,7 @@ address: 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. 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.** Capabilities may live in multiple Git +2. **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. @@ -390,8 +374,8 @@ address: **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, and -bundle it with a security-auditor subagent and a GitHub MCP server +create a version and bundle it with a security-auditor subagent +and a GitHub MCP server into a "pr-workflow" skill bundle. They set the bundle's `production` alias to the tested version. When a newer version introduces a vulnerability, they deprecate it — downstream consumers resolving `production` are @@ -405,12 +389,6 @@ 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 results across -all skill versions to find capabilities that failed scanning -(`scan_results.status = 'fail'`) or that haven't been scanned recently. -They deprecate versions that fail re-scanning and track compliance -posture across the organization's registered capabilities. - ### Out of scope - **Artifact storage as the only path.** The registry supports both @@ -422,8 +400,6 @@ posture across the organization's registered capabilities. - **Format specification.** The registry is format-agnostic. It does not define or enforce what a skill, subagent, 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 @@ -484,23 +460,6 @@ class SkillStatus(StrEnum): DELETED = "deleted" -class ScanStatus(StrEnum): - PASS = "pass" - FAIL = "fail" - WARNING = "warning" - ERROR = "error" - - -@dataclass -class ScanResult: - scan_type: str - status: ScanStatus - date: str - tool: str | None = None - details_url: str | None = None - details: str | None = None - - @dataclass class Skill: name: str @@ -557,7 +516,6 @@ class SkillVersion: status: SkillStatus = SkillStatus.DRAFT content_digest: str | None = None tags: dict[str, str] = field(default_factory=dict) - scan_results: list[ScanResult] = field(default_factory=list) aliases: list[str] = field(default_factory=list) workspace: str | None = None created_by: str | None = None @@ -575,7 +533,6 @@ class SkillVersion: | `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` | -| `scan_results` | `list[ScanResult]` | Structured scan results, one per `scan_type` (read-only, projected from scan results table) | | `aliases` | `list[str]` | Alias names currently pointing at this version (read-only, projected from alias table) | **Source type extensibility.** The `source_type` enum is intentionally @@ -648,7 +605,7 @@ read; verification is the consumer's responsibility. **Immutability contract.** `source_type`, `source`, `subpath`, `content_digest`, and `version` are immutable after creation. To point to different content, register a new version. Mutable fields (`display_name`, -`status`, `tags`, `scan_results`) can be updated independently. +`status`, `tags`) can be updated independently. #### Subagent @@ -676,7 +633,7 @@ class Subagent: `SubagentVersion` follows the same structure as `SkillVersion`: `name`, `version`, `source_type`, `source`, `subpath`, -`content_digest`, `status`, `tags`, `scan_results`, and timestamps. +`content_digest`, `status`, `tags`, and timestamps. Version uniqueness is `(name, version)` within a workspace, and the same immutability contract applies. @@ -703,12 +660,10 @@ class Hook: last_updated_timestamp: int | None = None ``` -`HookVersion` follows the same structure as `SkillVersion` (including -`scan_results`). +`HookVersion` follows the same structure as `SkillVersion`. **Shared patterns.** All three entity types (Skill, Subagent, Hook) -share the same version, tag, alias, scan result, and lifecycle -patterns. The store interface, REST API, and SDK expose parallel +share the same version, tag, alias, and lifecycle patterns. The store interface, REST API, and SDK expose parallel operations for each type. The database uses parallel table sets (see Database schema). @@ -779,7 +734,6 @@ class SkillBundleVersion: content_digest: str | None = None status: SkillStatus = SkillStatus.DRAFT tags: dict[str, str] = field(default_factory=dict) - scan_results: list[ScanResult] = field(default_factory=list) skills: list[tuple[str, str]] = field(default_factory=list) subagents: list[tuple[str, str]] = field(default_factory=list) hooks: list[tuple[str, str]] = field(default_factory=list) @@ -918,8 +872,8 @@ Allowed transitions: | `active` | `draft`, `deprecated` | | `deprecated` | `active`, `deleted` | -`draft` allows a version to be registered, scanned, -and reviewed before being made visible to consumers. `active` can +`draft` allows a version to be registered and reviewed before being +made visible to consumers. `active` can return to `draft` (unpublish) for cases where a version needs to be pulled back for further review. `deprecated` can return to `active` (re-activate) for cases where a deprecation was premature. @@ -1008,23 +962,6 @@ FK: `(workspace, name)` references `skills`, CASCADE delete. | `key` | `String(256)` | PK | | `value` | `Text` | | -#### `skill_version_scan_results` - -| Column | Type | Notes | -|--------|------|-------| -| `workspace` | `String(63)` | PK, FK | -| `name` | `String(256)` | PK, FK | -| `version` | `String(256)` | PK, FK | -| `scan_type` | `String(256)` | PK | -| `status` | `String(20)` | `pass`, `fail`, `warning`, `error` | -| `date` | `String(32)` | ISO 8601 | -| `tool` | `String(256)` | nullable | -| `details_url` | `String(2048)` | nullable | -| `details` | `Text` | nullable | - -FK: `(workspace, name, version)` references `skill_versions`, CASCADE -delete. PK includes `scan_type` for upsert-per-scan-type semantics. - #### `skill_aliases` | Column | Type | Notes | @@ -1037,8 +974,7 @@ delete. PK includes `scan_type` for upsert-per-scan-type semantics. #### Subagent tables The `subagents`, `subagent_versions`, `subagent_tags`, -`subagent_version_tags`, `subagent_version_scan_results`, -and `subagent_aliases` tables follow the +`subagent_version_tags`, and `subagent_aliases` tables follow the same structure as the corresponding skill tables above. FK relationships mirror the skill tables: `subagent_versions` references `subagents` with CASCADE delete, etc. @@ -1046,7 +982,7 @@ relationships mirror the skill tables: `subagent_versions` references #### Hook tables The `hooks`, `hook_versions`, `hook_tags`, `hook_version_tags`, -`hook_version_scan_results`, and `hook_aliases` +and `hook_aliases` tables follow the same structure as the corresponding skill tables. FK relationships mirror the skill tables. @@ -1129,12 +1065,6 @@ migrations and allows either registry to be deployed independently. | `key` | `String(256)` | PK | | `value` | `Text` | | -#### `skill_bundle_version_scan_results` - -Same structure as `skill_version_scan_results`. FK: -`(workspace, name, version)` references `skill_bundle_versions`, -CASCADE delete. - #### `skill_bundle_aliases` | Column | Type | Notes | @@ -1263,22 +1193,6 @@ class SkillRegistryMixin: ) -> None: raise NotImplementedError - # --- Skill scan result operations --- - - def set_skill_version_scan_result( - self, name: str, version: str, - scan_type: str, status: str, date: str, - tool: str | None = None, - details_url: str | None = None, - details: str | None = None, - ) -> ScanResult: - raise NotImplementedError - - def delete_skill_version_scan_result( - self, name: str, version: str, scan_type: str, - ) -> None: - raise NotImplementedError - # --- Skill alias operations --- def set_skill_alias( @@ -1293,7 +1207,7 @@ class SkillRegistryMixin: # --- Subagent operations --- # Same shape as Skill: create, get, search, update, delete, - # plus version, tag, scan result, and alias operations. + # plus version, tag, and alias operations. def create_subagent( self, name: str, @@ -1335,12 +1249,12 @@ class SkillRegistryMixin: ) -> SubagentVersion: raise NotImplementedError - # Remaining subagent version, tag, scan result, and alias - # operations follow the same pattern as skill operations above. + # Remaining subagent version, tag, and alias operations + # follow the same pattern as skill operations above. # --- Hook operations --- # Same shape as Skill: create, get, search, update, delete, - # plus version, tag, scan result, and alias operations. + # plus version, tag, and alias operations. def create_hook( self, name: str, @@ -1382,8 +1296,8 @@ class SkillRegistryMixin: ) -> HookVersion: raise NotImplementedError - # Remaining hook version, tag, scan result, and alias - # operations follow the same pattern as skill operations above. + # Remaining hook version, tag, and alias operations + # follow the same pattern as skill operations above. # --- SkillBundle operations --- @@ -1497,22 +1411,6 @@ class SkillRegistryMixin: ) -> None: raise NotImplementedError - # --- SkillBundle scan result operations --- - - def set_skill_bundle_version_scan_result( - self, name: str, version: str, - scan_type: str, status: str, date: str, - tool: str | None = None, - details_url: str | None = None, - details: str | None = None, - ) -> ScanResult: - raise NotImplementedError - - def delete_skill_bundle_version_scan_result( - self, name: str, version: str, scan_type: str, - ) -> None: - raise NotImplementedError - # --- SkillBundle alias operations --- def set_skill_bundle_alias( @@ -1618,9 +1516,6 @@ All paths relative to `/ajax-api/3.0/mlflow/skills`. | `DELETE` | `/{name}/tags/{key}` | Delete a skill-level tag | | `POST` | `/{name}/versions/{version}/tags` | Set a version-level tag | | `DELETE` | `/{name}/versions/{version}/tags/{key}` | Delete a version tag | -| `GET` | `/{name}/versions/{version}/scan-results` | List scan results | -| `PUT` | `/{name}/versions/{version}/scan-results/{scan_type}` | Set scan result (upsert) | -| `DELETE` | `/{name}/versions/{version}/scan-results/{scan_type}` | Delete scan result | | `POST` | `/{name}/aliases` | Set an alias | | `GET` | `/{name}/aliases/{alias}` | Resolve alias to `SkillVersion` | | `DELETE` | `/{name}/aliases/{alias}` | Delete an alias | @@ -1628,13 +1523,13 @@ All paths relative to `/ajax-api/3.0/mlflow/skills`. All paths relative to `/ajax-api/3.0/mlflow/subagents`. Same structure as skill endpoints: CRUD on subagents and subagent versions, -plus tags, scan results, and aliases. +plus tags and aliases. #### Hook endpoints All paths relative to `/ajax-api/3.0/mlflow/hooks`. Same structure as -skill endpoints: CRUD on hooks and hook versions, plus tags, scan -results, and aliases. +skill endpoints: CRUD on hooks and hook versions, plus tags and +aliases. #### Skill bundle endpoints @@ -1656,9 +1551,6 @@ All paths relative to `/ajax-api/3.0/mlflow/skill-bundles`. | `DELETE` | `/{name}/tags/{key}` | Delete a bundle-level tag | | `POST` | `/{name}/versions/{version}/tags` | Set a bundle version tag | | `DELETE` | `/{name}/versions/{version}/tags/{key}` | Delete a bundle version tag | -| `GET` | `/{name}/versions/{version}/scan-results` | List scan results | -| `PUT` | `/{name}/versions/{version}/scan-results/{scan_type}` | Set scan result (upsert) | -| `DELETE` | `/{name}/versions/{version}/scan-results/{scan_type}` | Delete scan result | | `POST` | `/{name}/aliases` | Set a bundle alias | | `GET` | `/{name}/aliases/{alias}` | Resolve bundle alias to version | | `DELETE` | `/{name}/aliases/{alias}` | Delete a bundle alias | @@ -1671,8 +1563,7 @@ expressions following existing MLflow conventions. `status = 'active'`, `tags.team = 'platform'` **Versions (all entity types):** `status = 'active'`, -`source_type = 'git'`, `scan_results.status = 'pass'`, -`scan_results.scan_type = 'prompt-injection'` +`source_type = 'git'` **Skill bundle versions:** `status = 'active'`, `tags.approved = 'true'` @@ -1785,9 +1676,6 @@ Key design choices: control which version downstream consumers resolve to. Changing an alias has the same blast radius as a status transition. - **Tag edits require EDIT.** Tags are operational metadata. -- **Scan result edits require EDIT.** Requiring MANAGE for scan results - would create friction for CI/CD scan integrations that need to record - results automatically. - **Creator gets MANAGE.** When a user creates an entity (skill, subagent, hook, or bundle), they automatically receive MANAGE permission, following the MLflow model registry pattern. @@ -1803,79 +1691,12 @@ filter by type (skill, subagent, hook, bundle), status, source type, and search by name or description. The detail view for a skill, subagent, or hook shows metadata, version -list, aliases, tags, scan results, and bundle -memberships. +list, aliases, tags, and bundle memberships. The detail view for a skill bundle shows its description, status, version list, aliases, and tags. Each bundle version shows its status and the pinned member versions it contains. -### Security scan tracking - -The registry does not perform security scans. It provides a structured -metadata layer for recording and querying scan results on any version -entity (skill, subagent, hook, or bundle). - -**ScanResult.** Each scan result is a structured record on a version: - -| Field | Type | Required | Description | -|---|---|---|---| -| `scan_type` | `str` | yes | What was scanned for: `prompt-injection`, `code-vulnerability`, `secrets-detection`, etc. Free string, not an enum, so organizations can define custom scan types | -| `status` | `ScanStatus` | yes | `pass`, `fail`, `warning`, `error` | -| `date` | `str` | yes | ISO 8601 date when the scan was run (e.g., `2026-04-29`) | -| `tool` | `str` | no | Tool name and version (e.g., `promptfoo/1.2.0`) | -| `details_url` | `str` | no | Link to full scan report | -| `details` | `str` | no | Free-text summary, error message, or scanner-specific output | - -**ScanStatus enum.** The `status` field uses a small fixed enum so -that "show me all versions that passed all scans" works across -scanners without knowing each tool's output format. Scanners that -produce nuanced output (e.g., risk ratings) map to the enum based on -the organization's threshold and put the detailed score in `details`. - -**Upsert semantics.** One result per `(version, scan_type)`. -Re-scanning overwrites the previous result for that scan type. This -keeps the model simple and avoids ambiguity about which result is -current. - -**Example scan results on a skill version:** - -```python -mlflow.genai.skills.set_skill_version_scan_result( - name="code-review", - version="1.0.0", - scan_type="prompt-injection", - status="pass", - date="2026-04-29", - tool="promptfoo/1.2.0", -) -mlflow.genai.skills.set_skill_version_scan_result( - name="code-review", - version="1.0.0", - scan_type="code-vulnerability", - status="fail", - date="2026-04-28", - tool="semgrep/1.67.0", - details_url="https://scans.acme.com/results/abc123", -) -``` - -**UI rendering.** Structured fields give the UI reliable data to -render a scan summary (e.g., a green check or red X per scan type) -without parsing tag conventions. - -**Querying.** Scan results support structured filter expressions: -`scan_results.status = 'pass'`, -`scan_results.scan_type = 'prompt-injection'`. Date comparisons are -supported on the structured `date` field, unlike tags which only -support equality. - -**Scan-gated workflows.** The status lifecycle supports scan-gated -deprecation: organizations can deprecate versions that fail scans and -filter for safe versions using `scan_results.status`. The registry -does not enforce this workflow, but the combination of version status -and scan results makes it straightforward to implement. - ### Trace integration MLflow already traces agent conversations across multiple frameworks: @@ -2057,7 +1878,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 -status lifecycle, security scan tracking, and federated discovery. +status lifecycle and federated discovery. The two approaches are complementary. # Adoption strategy From cd921b7af4dd5d960ec610f556e17aa64c818519 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Mon, 1 Jun 2026 15:36:41 -0400 Subject: [PATCH 39/52] Extract implementation details into separate files Move database schema, store interface, SDK convenience functions, REST API endpoints, pagination/filtering, and Python SDK/CLI mapping from RFC-0005 into implementation-details.md. Move detailed adapter layouts, MCP config generation, SDK/CLI/REST signatures, lock file SDK, and trace instrumentation from RFC-0006 into its own implementation-details.md. Main RFCs now focus on design rationale with pointers to implementation specs. RFC-0005: 1890 -> 1217 lines. RFC-0006: 840 -> 584 lines. Co-Authored-By: Claude Opus 4.6 --- .../0005-skill-registry.md | 683 +---------------- .../implementation-details.md | 689 ++++++++++++++++++ .../0006-skill-harness-integration.md | 314 +------- .../implementation-details.md | 300 ++++++++ 4 files changed, 1023 insertions(+), 963 deletions(-) create mode 100644 rfcs/0005-skill-registry/implementation-details.md create mode 100644 rfcs/0006-skill-harness-integration/implementation-details.md diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index ae16cd7..d1bff82 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -904,685 +904,12 @@ convenience alias for `get_latest_skill_version(...)`. applies to `Subagent`, `Hook`, `SkillBundle`, and their corresponding `get_latest_*_version` methods. -### Database schema - -Tables are created via a single Alembic migration. All tables are -workspace-scoped. - -#### `skills` - -| Column | Type | Notes | -|--------|------|-------| -| `workspace` | `String(63)` | PK, default `'default'` | -| `name` | `String(256)` | PK | -| `display_name` | `String(256)` | mutable human-readable label | -| `description` | `String(5000)` | | -| `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 | -| `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 | -| `display_name` | `String(256)` | mutable human-readable label | -| `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'` | -| `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 | -| `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 | - -#### Subagent tables - -The `subagents`, `subagent_versions`, `subagent_tags`, -`subagent_version_tags`, and `subagent_aliases` tables follow the -same structure as the corresponding skill tables above. FK -relationships mirror the skill tables: `subagent_versions` references -`subagents` with CASCADE delete, etc. - -#### Hook tables - -The `hooks`, `hook_versions`, `hook_tags`, `hook_version_tags`, -and `hook_aliases` -tables follow the same structure as the corresponding skill tables. -FK relationships mirror the skill tables. - -#### `skill_bundles` - -| Column | Type | Notes | -|--------|------|-------| -| `workspace` | `String(63)` | PK, default `'default'` | -| `name` | `String(256)` | PK | -| `display_name` | `String(256)` | mutable human-readable label | -| `description` | `String(5000)` | | -| `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 | -| `last_updated_timestamp` | `BigInteger` | millis since epoch | - -#### `skill_bundle_versions` - -| Column | Type | Notes | -|--------|------|-------| -| `workspace` | `String(63)` | PK, FK | -| `name` | `String(256)` | PK, FK | -| `version` | `String(256)` | PK, publisher-supplied | -| `display_name` | `String(256)` | mutable human-readable label | -| `source_type` | `String(20)` | optional; `git`, `oci`, `zip`, etc. | -| `source` | `String(2048)` | optional pointer to bundle artifact | -| `subpath` | `String(2048)` | nullable; path within the artifact | -| `content_digest` | `String(512)` | optional integrity digest | -| `status` | `String(20)` | default `'draft'` | -| `created_by` | `String(256)` | | -| `last_updated_by` | `String(256)` | | -| `creation_timestamp` | `BigInteger` | millis since epoch | -| `last_updated_timestamp` | `BigInteger` | millis since epoch | - -FK: `(workspace, name)` references `skill_bundles`, CASCADE delete. - -#### `skill_bundle_version_members` - -| Column | Type | Notes | -|--------|------|-------| -| `workspace` | `String(63)` | PK | -| `bundle_name` | `String(256)` | PK, FK to `skill_bundle_versions` | -| `bundle_version` | `String(256)` | PK, FK to `skill_bundle_versions` | -| `member_type` | `String(20)` | PK; `skill`, `subagent`, `hook`, or `mcp` | -| `member_name` | `String(256)` | PK | -| `member_version` | `String(256)` | PK | - -FK: `(workspace, bundle_name, bundle_version)` references `skill_bundle_versions`, CASCADE delete. - -The `member_type` column distinguishes member categories. When -`member_type` is `skill`, a FK to `skill_versions` enforces -referential integrity with RESTRICT delete. Similarly for `subagent` -(FK to `subagent_versions`) and `hook` (FK to `hook_versions`). - -**Cross-registry references (`member_type='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 bundle 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_bundle_tags` - -| Column | Type | Notes | -|--------|------|-------| -| `workspace` | `String(63)` | PK, FK | -| `name` | `String(256)` | PK, FK | -| `key` | `String(256)` | PK | -| `value` | `Text` | | - -#### `skill_bundle_version_tags` - -| Column | Type | Notes | -|--------|------|-------| -| `workspace` | `String(63)` | PK, FK | -| `name` | `String(256)` | PK, FK | -| `version` | `String(256)` | PK, FK | -| `key` | `String(256)` | PK | -| `value` | `Text` | | - -#### `skill_bundle_aliases` - -| Column | Type | Notes | -|--------|------|-------| -| `workspace` | `String(63)` | PK, FK | -| `name` | `String(256)` | PK, FK | -| `alias` | `String(256)` | PK | -| `version` | `String(256)` | target bundle version string | - -**Workspace handling.** All tables use `(workspace, ...)` as the leading -primary key components. Single-tenant deployments use `'default'`. - -**Timestamps.** Set at the application layer via -`get_current_time_millis()`, not via DDL defaults. - -### Store interface - -The store interface follows the mixin pattern established by the MCP -Server Registry (RFC-0004). Methods raise `NotImplementedError` rather -than using `@abstractmethod`, allowing stores that don't support skills -(e.g., `FileStore`) to work without stubbing every method. +### Implementation details -```python -class SkillRegistryMixin: - # --- Skill operations --- - - def create_skill( - self, name: str, - display_name: str | None = None, - description: str | None = None, - ) -> Skill: - raise NotImplementedError - - def get_skill(self, name: str) -> Skill: - raise NotImplementedError - - def search_skills( - self, - filter_string: str | None = None, - max_results: int = 100, - order_by: list[str] | None = None, - page_token: str | None = None, - ) -> PagedList[Skill]: - raise NotImplementedError - - def update_skill( - self, - name: str, - display_name: str | None = None, - description: str | None = None, - latest_version: str | None = None, - ) -> Skill: - raise NotImplementedError - - def delete_skill(self, name: str) -> None: - raise NotImplementedError - - # --- SkillVersion operations --- - - def create_skill_version( - self, - name: str, - version: str, - display_name: str | None = None, - source_type: str | None = None, - source: str | None = None, - subpath: str | None = None, - content_digest: str | None = None, - ) -> SkillVersion: - raise NotImplementedError - - def get_skill_version( - self, name: str, version: str, - ) -> SkillVersion: - raise NotImplementedError - - def get_skill_version_by_alias( - self, name: str, alias: str, - ) -> SkillVersion: - raise NotImplementedError - - def get_latest_skill_version(self, name: str) -> SkillVersion: - raise NotImplementedError - - def search_skill_versions( - self, - name: str, - filter_string: str | None = None, - max_results: int = 100, - order_by: list[str] | None = None, - page_token: str | None = None, - ) -> PagedList[SkillVersion]: - raise NotImplementedError - - def update_skill_version( - self, - name: str, - version: str, - status: SkillStatus | None = None, - ) -> SkillVersion: - raise NotImplementedError - - def delete_skill_version( - self, name: str, version: str, - ) -> None: - raise NotImplementedError - - # --- Skill tag operations --- - - def set_skill_tag( - self, name: str, key: str, value: str, - ) -> None: - raise NotImplementedError - - def delete_skill_tag(self, name: str, key: str) -> None: - raise NotImplementedError - - def set_skill_version_tag( - self, name: str, version: str, - key: str, value: str, - ) -> None: - raise NotImplementedError - - def delete_skill_version_tag( - self, name: str, version: str, key: str, - ) -> None: - raise NotImplementedError - - # --- Skill alias operations --- - - def set_skill_alias( - self, name: str, alias: str, version: str, - ) -> None: - raise NotImplementedError - - def delete_skill_alias( - self, name: str, alias: str, - ) -> None: - raise NotImplementedError - - # --- Subagent operations --- - # Same shape as Skill: create, get, search, update, delete, - # plus version, tag, and alias operations. - - def create_subagent( - self, name: str, - display_name: str | None = None, - description: str | None = None, - ) -> Subagent: - raise NotImplementedError - - def get_subagent(self, name: str) -> Subagent: - raise NotImplementedError - - def search_subagents( - self, - filter_string: str | None = None, - max_results: int = 100, - order_by: list[str] | None = None, - page_token: str | None = None, - ) -> PagedList[Subagent]: - raise NotImplementedError - - def update_subagent( - self, name: str, - display_name: str | None = None, - description: str | None = None, - latest_version: str | None = None, - ) -> Subagent: - raise NotImplementedError - - def delete_subagent(self, name: str) -> None: - raise NotImplementedError - - def create_subagent_version( - self, name: str, version: str, - display_name: str | None = None, - source_type: str | None = None, - source: str | None = None, - subpath: str | None = None, - content_digest: str | None = None, - ) -> SubagentVersion: - raise NotImplementedError - - # Remaining subagent version, tag, and alias operations - # follow the same pattern as skill operations above. - - # --- Hook operations --- - # Same shape as Skill: create, get, search, update, delete, - # plus version, tag, and alias operations. - - def create_hook( - self, name: str, - display_name: str | None = None, - description: str | None = None, - ) -> Hook: - raise NotImplementedError - - def get_hook(self, name: str) -> Hook: - raise NotImplementedError - - def search_hooks( - self, - filter_string: str | None = None, - max_results: int = 100, - order_by: list[str] | None = None, - page_token: str | None = None, - ) -> PagedList[Hook]: - raise NotImplementedError - - def update_hook( - self, name: str, - display_name: str | None = None, - description: str | None = None, - latest_version: str | None = None, - ) -> Hook: - raise NotImplementedError - - def delete_hook(self, name: str) -> None: - raise NotImplementedError - - def create_hook_version( - self, name: str, version: str, - display_name: str | None = None, - source_type: str | None = None, - source: str | None = None, - subpath: str | None = None, - content_digest: str | None = None, - ) -> HookVersion: - raise NotImplementedError - - # Remaining hook version, tag, and alias operations - # follow the same pattern as skill operations above. - - # --- SkillBundle operations --- - - def create_skill_bundle( - self, name: str, - display_name: str | None = None, - description: str | None = None, - ) -> SkillBundle: - raise NotImplementedError - - def get_skill_bundle(self, name: str) -> SkillBundle: - raise NotImplementedError - - def search_skill_bundles( - self, - filter_string: str | None = None, - max_results: int = 100, - order_by: list[str] | None = None, - page_token: str | None = None, - ) -> PagedList[SkillBundle]: - raise NotImplementedError - - def update_skill_bundle( - self, - name: str, - display_name: str | None = None, - description: str | None = None, - latest_version: str | None = None, - ) -> SkillBundle: - raise NotImplementedError - - def delete_skill_bundle(self, name: str) -> None: - raise NotImplementedError - - # --- SkillBundleVersion operations --- - - def create_skill_bundle_version( - self, - name: str, - version: str, - display_name: str | None = None, - skills: list[tuple[str, str]] | None = None, - subagents: list[tuple[str, str]] | None = None, - hooks: list[tuple[str, str]] | None = None, - mcp_servers: list[tuple[str, str]] | None = None, - source_type: str | None = None, - source: str | None = None, - subpath: str | None = None, - content_digest: str | None = None, - ) -> SkillBundleVersion: - raise NotImplementedError - - def get_skill_bundle_version( - self, name: str, version: str, - ) -> SkillBundleVersion: - raise NotImplementedError - - def get_skill_bundle_version_by_alias( - self, name: str, alias: str, - ) -> SkillBundleVersion: - raise NotImplementedError - - def get_latest_skill_bundle_version( - self, name: str, - ) -> SkillBundleVersion: - raise NotImplementedError - - def search_skill_bundle_versions( - self, - name: str, - filter_string: str | None = None, - max_results: int = 100, - order_by: list[str] | None = None, - page_token: str | None = None, - ) -> PagedList[SkillBundleVersion]: - raise NotImplementedError - - def update_skill_bundle_version( - self, - name: str, - version: str, - status: SkillStatus | None = None, - ) -> SkillBundleVersion: - raise NotImplementedError - - def delete_skill_bundle_version( - self, name: str, version: str, - ) -> None: - raise NotImplementedError - - # --- SkillBundle tag operations --- - - def set_skill_bundle_tag( - self, name: str, key: str, value: str, - ) -> None: - raise NotImplementedError - - def delete_skill_bundle_tag( - self, name: str, key: str, - ) -> None: - raise NotImplementedError - - def set_skill_bundle_version_tag( - self, name: str, version: str, - key: str, value: str, - ) -> None: - raise NotImplementedError - - def delete_skill_bundle_version_tag( - self, name: str, version: str, key: str, - ) -> None: - raise NotImplementedError - - # --- SkillBundle alias operations --- - - def set_skill_bundle_alias( - self, name: str, alias: str, version: str, - ) -> None: - raise NotImplementedError - - def delete_skill_bundle_alias( - self, name: str, alias: str, - ) -> None: - 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, - display_name: str | None = None, - description: str | None = None, - source_type: str | None = None, - source: str | None = None, - subpath: str | None = None, - content_path: str | None = None, - content_digest: str | None = None, -) -> SkillVersion: - """Register a skill version. Auto-creates the parent Skill if - it does not exist. If content_path is provided, uploads the - local directory to MLflow artifact storage and sets source_type - and source automatically.""" - - -def register_subagent( - name: str, - version: str, - display_name: str | None = None, - description: str | None = None, - source_type: str | None = None, - source: str | None = None, - subpath: str | None = None, - content_path: str | None = None, - content_digest: str | None = None, -) -> SubagentVersion: - """Register a subagent version. Auto-creates the parent - Subagent if it does not exist.""" - - -def register_hook( - name: str, - version: str, - display_name: str | None = None, - description: str | None = None, - source_type: str | None = None, - source: str | None = None, - subpath: str | None = None, - content_path: str | None = None, - content_digest: str | None = None, -) -> HookVersion: - """Register a hook version. Auto-creates the parent Hook if - it does not exist.""" - - -def pull( - name: str | None = None, - bundle: str | None = None, - version: str | None = None, - alias: str | None = None, - destination: str = ".", -) -> str: - """Pull skill, subagent, hook, or bundle content from registered - sources to a local directory. Specify name for a single - capability or bundle for a skill bundle.""" -``` - -### 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}` | 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}/tags` | Set a version-level tag | -| `DELETE` | `/{name}/versions/{version}/tags/{key}` | Delete a version tag | -| `POST` | `/{name}/aliases` | Set an alias | -| `GET` | `/{name}/aliases/{alias}` | Resolve alias to `SkillVersion` | -| `DELETE` | `/{name}/aliases/{alias}` | Delete an alias | -#### Subagent endpoints - -All paths relative to `/ajax-api/3.0/mlflow/subagents`. Same -structure as skill endpoints: CRUD on subagents and subagent versions, -plus tags and aliases. - -#### Hook endpoints - -All paths relative to `/ajax-api/3.0/mlflow/hooks`. Same structure as -skill endpoints: CRUD on hooks and hook versions, plus tags and -aliases. - -#### Skill bundle endpoints - -All paths relative to `/ajax-api/3.0/mlflow/skill-bundles`. - -| Method | Path | Description | -|---|---|---| -| `POST` | `/` | Create a skill bundle | -| `GET` | `/` | Search skill bundles | -| `GET` | `/{name}` | Get bundle by name | -| `PATCH` | `/{name}` | Update bundle fields | -| `DELETE` | `/{name}` | Delete bundle (cascades versions) | -| `POST` | `/{name}/versions` | Create a bundle version with members | -| `GET` | `/{name}/versions` | Search bundle versions | -| `GET` | `/{name}/versions/{version}` | Get a specific bundle version | -| `PATCH` | `/{name}/versions/{version}` | Update bundle version status | -| `DELETE` | `/{name}/versions/{version}` | Delete a bundle version | -| `POST` | `/{name}/tags` | Set a bundle-level tag | -| `DELETE` | `/{name}/tags/{key}` | Delete a bundle-level tag | -| `POST` | `/{name}/versions/{version}/tags` | Set a bundle version tag | -| `DELETE` | `/{name}/versions/{version}/tags/{key}` | Delete a bundle version tag | -| `POST` | `/{name}/aliases` | Set a bundle alias | -| `GET` | `/{name}/aliases/{alias}` | Resolve bundle alias to version | -| `DELETE` | `/{name}/aliases/{alias}` | Delete a bundle alias | -#### Pagination and filtering - -Search endpoints use page-token-based pagination and `filter_string` -expressions following existing MLflow conventions. - -**Skills, subagents, hooks, and bundles:** `name LIKE '%review%'`, -`status = 'active'`, `tags.team = 'platform'` - -**Versions (all entity types):** `status = 'active'`, -`source_type = 'git'` - -**Skill bundle versions:** `status = 'active'`, -`tags.approved = 'true'` - -### Python SDK and CLI - -The `mlflow.genai.skills` module exposes top-level functions delegating to -`MlflowClient`, with a 1:1 mapping to the store mixin methods above. -CLI command groups (`mlflow skills`, `mlflow subagents`, -`mlflow hooks`, and `mlflow skill-bundles`) 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 the corresponding subagent/hook -method, 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. +Database schema (table definitions), store interface (method +signatures), SDK convenience functions, REST API endpoints, +pagination/filtering, and Python SDK/CLI mapping are in +[implementation-details.md](implementation-details.md). ### Pull semantics diff --git a/rfcs/0005-skill-registry/implementation-details.md b/rfcs/0005-skill-registry/implementation-details.md new file mode 100644 index 0000000..6deaaa8 --- /dev/null +++ b/rfcs/0005-skill-registry/implementation-details.md @@ -0,0 +1,689 @@ +# RFC-0005: Skill Registry Implementation Details + +This document contains implementation-level specifications for +RFC-0005 (Skill Registry). It covers database schema, store interface +method signatures, SDK convenience functions, REST API endpoints, +pagination/filtering, and the Python SDK/CLI mapping. These details +support implementers; the main RFC covers the design rationale. + +## Database schema + +Tables are created via a single Alembic migration. All tables are +workspace-scoped. + +### `skills` + +| Column | Type | Notes | +|--------|------|-------| +| `workspace` | `String(63)` | PK, default `'default'` | +| `name` | `String(256)` | PK | +| `display_name` | `String(256)` | mutable human-readable label | +| `description` | `String(5000)` | | +| `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 | +| `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 | +| `display_name` | `String(256)` | mutable human-readable label | +| `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'` | +| `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 | +| `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 | + +### Subagent tables + +The `subagents`, `subagent_versions`, `subagent_tags`, +`subagent_version_tags`, and `subagent_aliases` tables follow the +same structure as the corresponding skill tables above. FK +relationships mirror the skill tables: `subagent_versions` references +`subagents` with CASCADE delete, etc. + +### Hook tables + +The `hooks`, `hook_versions`, `hook_tags`, `hook_version_tags`, +and `hook_aliases` +tables follow the same structure as the corresponding skill tables. +FK relationships mirror the skill tables. + +### `skill_bundles` + +| Column | Type | Notes | +|--------|------|-------| +| `workspace` | `String(63)` | PK, default `'default'` | +| `name` | `String(256)` | PK | +| `display_name` | `String(256)` | mutable human-readable label | +| `description` | `String(5000)` | | +| `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 | +| `last_updated_timestamp` | `BigInteger` | millis since epoch | + +### `skill_bundle_versions` + +| Column | Type | Notes | +|--------|------|-------| +| `workspace` | `String(63)` | PK, FK | +| `name` | `String(256)` | PK, FK | +| `version` | `String(256)` | PK, publisher-supplied | +| `display_name` | `String(256)` | mutable human-readable label | +| `source_type` | `String(20)` | optional; `git`, `oci`, `zip`, etc. | +| `source` | `String(2048)` | optional pointer to bundle artifact | +| `subpath` | `String(2048)` | nullable; path within the artifact | +| `content_digest` | `String(512)` | optional integrity digest | +| `status` | `String(20)` | default `'draft'` | +| `created_by` | `String(256)` | | +| `last_updated_by` | `String(256)` | | +| `creation_timestamp` | `BigInteger` | millis since epoch | +| `last_updated_timestamp` | `BigInteger` | millis since epoch | + +FK: `(workspace, name)` references `skill_bundles`, CASCADE delete. + +### `skill_bundle_version_members` + +| Column | Type | Notes | +|--------|------|-------| +| `workspace` | `String(63)` | PK | +| `bundle_name` | `String(256)` | PK, FK to `skill_bundle_versions` | +| `bundle_version` | `String(256)` | PK, FK to `skill_bundle_versions` | +| `member_type` | `String(20)` | PK; `skill`, `subagent`, `hook`, or `mcp` | +| `member_name` | `String(256)` | PK | +| `member_version` | `String(256)` | PK | + +FK: `(workspace, bundle_name, bundle_version)` references `skill_bundle_versions`, CASCADE delete. + +The `member_type` column distinguishes member categories. When +`member_type` is `skill`, a FK to `skill_versions` enforces +referential integrity with RESTRICT delete. Similarly for `subagent` +(FK to `subagent_versions`) and `hook` (FK to `hook_versions`). + +**Cross-registry references (`member_type='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 bundle 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_bundle_tags` + +| Column | Type | Notes | +|--------|------|-------| +| `workspace` | `String(63)` | PK, FK | +| `name` | `String(256)` | PK, FK | +| `key` | `String(256)` | PK | +| `value` | `Text` | | + +### `skill_bundle_version_tags` + +| Column | Type | Notes | +|--------|------|-------| +| `workspace` | `String(63)` | PK, FK | +| `name` | `String(256)` | PK, FK | +| `version` | `String(256)` | PK, FK | +| `key` | `String(256)` | PK | +| `value` | `Text` | | + +### `skill_bundle_aliases` + +| Column | Type | Notes | +|--------|------|-------| +| `workspace` | `String(63)` | PK, FK | +| `name` | `String(256)` | PK, FK | +| `alias` | `String(256)` | PK | +| `version` | `String(256)` | target bundle version string | + +**Workspace handling.** All tables use `(workspace, ...)` as the leading +primary key components. Single-tenant deployments use `'default'`. + +**Timestamps.** Set at the application layer via +`get_current_time_millis()`, not via DDL defaults. + +## Store interface + +The store interface follows the mixin pattern established by the MCP +Server Registry (RFC-0004). Methods raise `NotImplementedError` rather +than using `@abstractmethod`, allowing stores that don't support skills +(e.g., `FileStore`) to work without stubbing every method. + +```python +class SkillRegistryMixin: + # --- Skill operations --- + + def create_skill( + self, name: str, + display_name: str | None = None, + description: str | None = None, + ) -> Skill: + raise NotImplementedError + + def get_skill(self, name: str) -> Skill: + raise NotImplementedError + + def search_skills( + self, + filter_string: str | None = None, + max_results: int = 100, + order_by: list[str] | None = None, + page_token: str | None = None, + ) -> PagedList[Skill]: + raise NotImplementedError + + def update_skill( + self, + name: str, + display_name: str | None = None, + description: str | None = None, + latest_version: str | None = None, + ) -> Skill: + raise NotImplementedError + + def delete_skill(self, name: str) -> None: + raise NotImplementedError + + # --- SkillVersion operations --- + + def create_skill_version( + self, + name: str, + version: str, + display_name: str | None = None, + source_type: str | None = None, + source: str | None = None, + subpath: str | None = None, + content_digest: str | None = None, + ) -> SkillVersion: + raise NotImplementedError + + def get_skill_version( + self, name: str, version: str, + ) -> SkillVersion: + raise NotImplementedError + + def get_skill_version_by_alias( + self, name: str, alias: str, + ) -> SkillVersion: + raise NotImplementedError + + def get_latest_skill_version(self, name: str) -> SkillVersion: + raise NotImplementedError + + def search_skill_versions( + self, + name: str, + filter_string: str | None = None, + max_results: int = 100, + order_by: list[str] | None = None, + page_token: str | None = None, + ) -> PagedList[SkillVersion]: + raise NotImplementedError + + def update_skill_version( + self, + name: str, + version: str, + status: SkillStatus | None = None, + ) -> SkillVersion: + raise NotImplementedError + + def delete_skill_version( + self, name: str, version: str, + ) -> None: + raise NotImplementedError + + # --- Skill tag operations --- + + def set_skill_tag( + self, name: str, key: str, value: str, + ) -> None: + raise NotImplementedError + + def delete_skill_tag(self, name: str, key: str) -> None: + raise NotImplementedError + + def set_skill_version_tag( + self, name: str, version: str, + key: str, value: str, + ) -> None: + raise NotImplementedError + + def delete_skill_version_tag( + self, name: str, version: str, key: str, + ) -> None: + raise NotImplementedError + + # --- Skill alias operations --- + + def set_skill_alias( + self, name: str, alias: str, version: str, + ) -> None: + raise NotImplementedError + + def delete_skill_alias( + self, name: str, alias: str, + ) -> None: + raise NotImplementedError + + # --- Subagent operations --- + # Same shape as Skill: create, get, search, update, delete, + # plus version, tag, and alias operations. + + def create_subagent( + self, name: str, + display_name: str | None = None, + description: str | None = None, + ) -> Subagent: + raise NotImplementedError + + def get_subagent(self, name: str) -> Subagent: + raise NotImplementedError + + def search_subagents( + self, + filter_string: str | None = None, + max_results: int = 100, + order_by: list[str] | None = None, + page_token: str | None = None, + ) -> PagedList[Subagent]: + raise NotImplementedError + + def update_subagent( + self, name: str, + display_name: str | None = None, + description: str | None = None, + latest_version: str | None = None, + ) -> Subagent: + raise NotImplementedError + + def delete_subagent(self, name: str) -> None: + raise NotImplementedError + + def create_subagent_version( + self, name: str, version: str, + display_name: str | None = None, + source_type: str | None = None, + source: str | None = None, + subpath: str | None = None, + content_digest: str | None = None, + ) -> SubagentVersion: + raise NotImplementedError + + # Remaining subagent version, tag, and alias operations + # follow the same pattern as skill operations above. + + # --- Hook operations --- + # Same shape as Skill: create, get, search, update, delete, + # plus version, tag, and alias operations. + + def create_hook( + self, name: str, + display_name: str | None = None, + description: str | None = None, + ) -> Hook: + raise NotImplementedError + + def get_hook(self, name: str) -> Hook: + raise NotImplementedError + + def search_hooks( + self, + filter_string: str | None = None, + max_results: int = 100, + order_by: list[str] | None = None, + page_token: str | None = None, + ) -> PagedList[Hook]: + raise NotImplementedError + + def update_hook( + self, name: str, + display_name: str | None = None, + description: str | None = None, + latest_version: str | None = None, + ) -> Hook: + raise NotImplementedError + + def delete_hook(self, name: str) -> None: + raise NotImplementedError + + def create_hook_version( + self, name: str, version: str, + display_name: str | None = None, + source_type: str | None = None, + source: str | None = None, + subpath: str | None = None, + content_digest: str | None = None, + ) -> HookVersion: + raise NotImplementedError + + # Remaining hook version, tag, and alias operations + # follow the same pattern as skill operations above. + + # --- SkillBundle operations --- + + def create_skill_bundle( + self, name: str, + display_name: str | None = None, + description: str | None = None, + ) -> SkillBundle: + raise NotImplementedError + + def get_skill_bundle(self, name: str) -> SkillBundle: + raise NotImplementedError + + def search_skill_bundles( + self, + filter_string: str | None = None, + max_results: int = 100, + order_by: list[str] | None = None, + page_token: str | None = None, + ) -> PagedList[SkillBundle]: + raise NotImplementedError + + def update_skill_bundle( + self, + name: str, + display_name: str | None = None, + description: str | None = None, + latest_version: str | None = None, + ) -> SkillBundle: + raise NotImplementedError + + def delete_skill_bundle(self, name: str) -> None: + raise NotImplementedError + + # --- SkillBundleVersion operations --- + + def create_skill_bundle_version( + self, + name: str, + version: str, + display_name: str | None = None, + skills: list[tuple[str, str]] | None = None, + subagents: list[tuple[str, str]] | None = None, + hooks: list[tuple[str, str]] | None = None, + mcp_servers: list[tuple[str, str]] | None = None, + source_type: str | None = None, + source: str | None = None, + subpath: str | None = None, + content_digest: str | None = None, + ) -> SkillBundleVersion: + raise NotImplementedError + + def get_skill_bundle_version( + self, name: str, version: str, + ) -> SkillBundleVersion: + raise NotImplementedError + + def get_skill_bundle_version_by_alias( + self, name: str, alias: str, + ) -> SkillBundleVersion: + raise NotImplementedError + + def get_latest_skill_bundle_version( + self, name: str, + ) -> SkillBundleVersion: + raise NotImplementedError + + def search_skill_bundle_versions( + self, + name: str, + filter_string: str | None = None, + max_results: int = 100, + order_by: list[str] | None = None, + page_token: str | None = None, + ) -> PagedList[SkillBundleVersion]: + raise NotImplementedError + + def update_skill_bundle_version( + self, + name: str, + version: str, + status: SkillStatus | None = None, + ) -> SkillBundleVersion: + raise NotImplementedError + + def delete_skill_bundle_version( + self, name: str, version: str, + ) -> None: + raise NotImplementedError + + # --- SkillBundle tag operations --- + + def set_skill_bundle_tag( + self, name: str, key: str, value: str, + ) -> None: + raise NotImplementedError + + def delete_skill_bundle_tag( + self, name: str, key: str, + ) -> None: + raise NotImplementedError + + def set_skill_bundle_version_tag( + self, name: str, version: str, + key: str, value: str, + ) -> None: + raise NotImplementedError + + def delete_skill_bundle_version_tag( + self, name: str, version: str, key: str, + ) -> None: + raise NotImplementedError + + # --- SkillBundle alias operations --- + + def set_skill_bundle_alias( + self, name: str, alias: str, version: str, + ) -> None: + raise NotImplementedError + + def delete_skill_bundle_alias( + self, name: str, alias: str, + ) -> None: + 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, + display_name: str | None = None, + description: str | None = None, + source_type: str | None = None, + source: str | None = None, + subpath: str | None = None, + content_path: str | None = None, + content_digest: str | None = None, +) -> SkillVersion: + """Register a skill version. Auto-creates the parent Skill if + it does not exist. If content_path is provided, uploads the + local directory to MLflow artifact storage and sets source_type + and source automatically.""" + + +def register_subagent( + name: str, + version: str, + display_name: str | None = None, + description: str | None = None, + source_type: str | None = None, + source: str | None = None, + subpath: str | None = None, + content_path: str | None = None, + content_digest: str | None = None, +) -> SubagentVersion: + """Register a subagent version. Auto-creates the parent + Subagent if it does not exist.""" + + +def register_hook( + name: str, + version: str, + display_name: str | None = None, + description: str | None = None, + source_type: str | None = None, + source: str | None = None, + subpath: str | None = None, + content_path: str | None = None, + content_digest: str | None = None, +) -> HookVersion: + """Register a hook version. Auto-creates the parent Hook if + it does not exist.""" + + +def pull( + name: str | None = None, + bundle: str | None = None, + version: str | None = None, + alias: str | None = None, + destination: str = ".", +) -> str: + """Pull skill, subagent, hook, or bundle content from registered + sources to a local directory. Specify name for a single + capability or bundle for a skill bundle.""" +``` + +## 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}` | 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}/tags` | Set a version-level tag | +| `DELETE` | `/{name}/versions/{version}/tags/{key}` | Delete a version tag | +| `POST` | `/{name}/aliases` | Set an alias | +| `GET` | `/{name}/aliases/{alias}` | Resolve alias to `SkillVersion` | +| `DELETE` | `/{name}/aliases/{alias}` | Delete an alias | + +### Subagent endpoints + +All paths relative to `/ajax-api/3.0/mlflow/subagents`. Same +structure as skill endpoints: CRUD on subagents and subagent versions, +plus tags and aliases. + +### Hook endpoints + +All paths relative to `/ajax-api/3.0/mlflow/hooks`. Same structure as +skill endpoints: CRUD on hooks and hook versions, plus tags and +aliases. + +### Skill bundle endpoints + +All paths relative to `/ajax-api/3.0/mlflow/skill-bundles`. + +| Method | Path | Description | +|---|---|---| +| `POST` | `/` | Create a skill bundle | +| `GET` | `/` | Search skill bundles | +| `GET` | `/{name}` | Get bundle by name | +| `PATCH` | `/{name}` | Update bundle fields | +| `DELETE` | `/{name}` | Delete bundle (cascades versions) | +| `POST` | `/{name}/versions` | Create a bundle version with members | +| `GET` | `/{name}/versions` | Search bundle versions | +| `GET` | `/{name}/versions/{version}` | Get a specific bundle version | +| `PATCH` | `/{name}/versions/{version}` | Update bundle version status | +| `DELETE` | `/{name}/versions/{version}` | Delete a bundle version | +| `POST` | `/{name}/tags` | Set a bundle-level tag | +| `DELETE` | `/{name}/tags/{key}` | Delete a bundle-level tag | +| `POST` | `/{name}/versions/{version}/tags` | Set a bundle version tag | +| `DELETE` | `/{name}/versions/{version}/tags/{key}` | Delete a bundle version tag | +| `POST` | `/{name}/aliases` | Set a bundle alias | +| `GET` | `/{name}/aliases/{alias}` | Resolve bundle alias to version | +| `DELETE` | `/{name}/aliases/{alias}` | Delete a bundle alias | + +### Pagination and filtering + +Search endpoints use page-token-based pagination and `filter_string` +expressions following existing MLflow conventions. + +**Skills, subagents, hooks, and bundles:** `name LIKE '%review%'`, +`status = 'active'`, `tags.team = 'platform'` + +**Versions (all entity types):** `status = 'active'`, +`source_type = 'git'` + +**Skill bundle versions:** `status = 'active'`, +`tags.approved = 'true'` + +## Python SDK and CLI + +The `mlflow.genai.skills` module exposes top-level functions delegating to +`MlflowClient`, with a 1:1 mapping to the store mixin methods above. +CLI command groups (`mlflow skills`, `mlflow subagents`, +`mlflow hooks`, and `mlflow skill-bundles`) provide the same +operations from the command line. See the basic examples in the main +RFC for usage. + +`pull` is implemented in the SDK/CLI layer, not the store mixin. The +client calls `get_skill_version` (or the corresponding subagent/hook +method, 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. 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 7907b81..ae04cd2 100644 --- a/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md +++ b/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md @@ -218,96 +218,24 @@ class HarnessAdapter: def supported_member_types(self) -> set[str]: ... ``` -### Claude Code / Codex CLI adapter +### Adapter summaries -These two harnesses share nearly identical plugin formats. The adapter -generates: +**Claude Code / Codex CLI:** generates a plugin directory under +`.claude/plugins/` (or `.codex/plugins/`) with `plugin.json`, skill +files, subagent files, merged `.mcp.json` from MCP registry metadata, +and hook entries. MCP server credentials are the user's +responsibility. Hooks require explicit user opt-in. -**`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/{bundle-name}/ - .claude-plugin/plugin.json - skills/{skill-name}/SKILL.md # skill members - agents/{agent-name}.md # subagent members - .mcp.json # mcp_server members, merged -``` - -For Codex CLI, the path uses `.codex/plugins/` instead. - -**MCP server config generation.** When a bundle references MCP servers -in its `mcp_servers` member list, the adapter generates `.mcp.json` -entries from MCP registry metadata. For each referenced server, the -adapter resolves the `MCPServerVersion` from the MCP registry -(RFC-0004) and looks for an `MCPAccessBinding` targeting that version -or alias. If a binding exists, the adapter uses its `endpoint_url` and -`transport_type` as the connection target. If multiple bindings exist -for the same server, the adapter uses the first binding targeting the -referenced version or alias. If no binding exists, the adapter falls -back to the connection details in `server_json` (e.g., `remotes[]`). - -Entries are merged into a single `.mcp.json` using server name as key: - -```json -{ - "mcpServers": { - "github-mcp": { ... }, - "jira-mcp": { ... } - } -} -``` - -**Embedded MCP configs.** When a bundle has a bundle-level source and -the artifact already contains a `.mcp.json`, those embedded configs -are used as-is for any MCP servers not in the `mcp_servers` member -list. If the same server name appears in both the embedded config and -the `mcp_servers` list, the registry-generated entry takes precedence -(the registry is the governed source of truth). - -**MCP server credentials.** The adapter generates connection config -but does not configure credentials, certificates, or authorization -headers. These are the user's responsibility. The adapter logs a -warning when it generates an entry for a server that uses -authenticated transport, so users know to complete the setup -manually. +**Cursor:** places skills and subagents in `.cursor/skills/` and +`.cursor/agents/`. Merges MCP entries into `.cursor/mcp.json`. Hooks +are skipped (unsupported). -**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 # skill members -{destination}/.cursor/agents/{agent-name}.md # subagent members -``` +**Antigravity:** places skills in `.agent/skills/`. Subagents, MCP +servers, and hooks are skipped. -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 # skill members -``` - -Subagents, MCP servers, and hooks are skipped with a warning. +Detailed directory layouts, MCP config generation rules, and +hook handling behavior are in +[implementation-details.md](implementation-details.md). ### Other harness adapters @@ -524,66 +452,11 @@ marketplace infrastructure (currently Claude Code and Codex CLI). Harnesses without marketplace support (Cursor, Antigravity, OpenClaw) use the adapter-based `mlflow skills install` command instead. -### SDK interface - -Installation is a client-side operation: the SDK resolves the skill or -bundle 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. - -```python -def install( - name: str | None = None, - bundle: str | None = None, - harness: str = "claude-code", - destination: str = ".", - version: str | None = None, - alias: str | None = None, -) -> str: - """Install a skill or skill bundle for a specific harness. - Resolves from the registry, pulls content, generates - harness-specific manifests, and places files in the correct - directories.""" - - -def import_bundle( - source: str, - harness: str | None = None, - bundle_name: str | None = None, - version: str | None = None, -) -> SkillBundle: - """Import an existing harness-specific artifact into the registry. - Introspects the artifact, registers individual capabilities, - and creates a skill bundle referencing them all.""" -``` - -### REST API +### Implementation details -The only server-side endpoint is the marketplace catalog, which -harnesses query to discover available plugins. - -| Method | Path | Description | -|---|---|---| -| `GET` | `/ajax-api/3.0/mlflow/skill-bundles/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 bundle -mlflow skills install --bundle pr-workflow --alias production \ - --harness claude-code --destination . - -# Import an existing plugin into the registry -mlflow skills import --source ./my-claude-plugin \ - --harness claude-code --bundle-name my-plugin --version 1.0.0 - -# List supported harnesses -mlflow skills harnesses -``` +SDK function signatures (`install`, `import_bundle`), REST API +endpoints, and CLI commands are in +[implementation-details.md](implementation-details.md). ### Lock file @@ -652,153 +525,24 @@ replay still contacts the registry to verify version status, so governance actions (deprecation, deletion) take effect even for existing lock files. -#### SDK - -```python -mlflow.genai.skills.install( - bundle="pr-workflow", - alias="production", - harness="claude-code", - lock=True, -) - -# Install from lock file -mlflow.genai.skills.install() -``` +Lock file SDK functions are in +[implementation-details.md](implementation-details.md). ### Trace integration RFC-0005 defines `mlflow.skill_context()`, a context manager that creates SKILL spans in MLflow traces (see RFC-0005, Trace -integration). Agent developers using the Python SDK can call -`skill_context()` directly in their code. This section describes how -harness-specific installation can automate that instrumentation so -users get skill-annotated traces without writing any tracing code. - -#### Install-time manifest - -When `mlflow skills install` places files for a harness, it also -writes a manifest that maps installed skill names to their registry -coordinates: - -**`mlflow-skills-manifest.json`:** -```json -{ - "manifest_version": "1.0", - "skills": { - "code-review": { - "name": "code-review", - "version": "1.0.0", - "registry": "default" - }, - "security-auditor": { - "name": "security-auditor", - "version": "1.0.0", - "registry": "default" - } - } -} -``` - -The manifest is keyed by the skill's local name (the name the harness -uses to invoke it). The value provides the `{registry, name, version}` -coordinates that link back to the skill registry. This file is used -by trace hooks to annotate spans with registry coordinates without -requiring a registry lookup at runtime. - -#### Claude Code: hook-based instrumentation - -Claude Code invokes skills via a built-in `Skill` tool, which fires -`PreToolUse` and `PostToolUse` hook events. The `mlflow skills install` -command can configure hooks that create SKILL spans automatically -when a registered skill is invoked. - -**Hook configuration** (added to `.claude/settings.json`): - -```json -{ - "hooks": { - "PreToolUse": [ - { - "matcher": "Skill", - "hooks": [ - { - "type": "command", - "command": "mlflow skills trace-start --manifest .mlflow-skills-manifest.json" - } - ] - } - ], - "PostToolUse": [ - { - "matcher": "Skill", - "hooks": [ - { - "type": "command", - "command": "mlflow skills trace-end --manifest .mlflow-skills-manifest.json" - } - ] - } - ] - } -} -``` - -The `PreToolUse` hook receives the skill name in its input, looks it -up in the manifest to get registry coordinates, and opens a SKILL -span via the `mlflow autolog claude` trace pipeline. The `PostToolUse` -hook closes the span. Because these hooks integrate with the same -tracing mechanism that `mlflow autolog claude` already uses, SKILL -spans appear as part of the existing trace tree alongside LLM and -tool call spans. - -The hook commands shown above are illustrative. The exact CLI -subcommands and their integration with the `mlflow autolog claude` -trace pipeline are implementation details. - -**Hook installation behavior.** `mlflow skills install` writes the -manifest automatically. It does not modify `settings.json` by -default. Instead, it prints instructions showing the hook -configuration to add. Users can opt in with `--install-hooks` to -have the installer merge hook entries into `settings.json`. This -follows the same security principle as hook handling for plugin -members: users must explicitly enable hooks. - -#### Agent SDK: direct instrumentation - -For developers building agents with the Claude Agent SDK or other -Python frameworks, the recommended approach is to use -`mlflow.skill_context()` directly (see RFC-0005). The Agent SDK's -hook system also supports Python callbacks, so a similar automatic -approach is possible: - -```python -from claude_code_sdk import ClaudeAgentOptions - -async def on_skill_start(input_data, tool_use_id, context): - skill_name = input_data["tool_input"].get("skill") - # Look up registry coordinates from manifest - # Open mlflow.skill_context() span - return {} - -options = ClaudeAgentOptions( - hooks={"PreToolUse": [{"matcher": "Skill", "hook": on_skill_start}]} -) -``` - -Because Agent SDK hooks run in-process, they can call -`mlflow.skill_context()` directly, creating SKILL spans in the -same trace tree as the autologger spans. - -#### Other harnesses - -Trace integration depends on each harness exposing a hook or event -mechanism for skill invocation. Harnesses that support pre/post tool -use hooks (Codex CLI, GitHub Copilot) can follow the same pattern as -Claude Code. Harnesses without hook support cannot be automatically -instrumented; users of those harnesses can still use +integration). `mlflow skills install` can automate this: it writes a +manifest mapping installed skill names to registry coordinates, and +harnesses with hook support (Claude Code, Codex CLI) can use +pre/post tool-use hooks to create SKILL spans automatically. Users +of harnesses without hook support can still use `mlflow.skill_context()` manually in SDK-based agent code. +Manifest format, hook configuration examples, and per-harness +instrumentation details are in +[implementation-details.md](implementation-details.md). + ## Drawbacks - **Adapter maintenance.** Each harness adapter must be maintained as diff --git a/rfcs/0006-skill-harness-integration/implementation-details.md b/rfcs/0006-skill-harness-integration/implementation-details.md new file mode 100644 index 0000000..09d8d6d --- /dev/null +++ b/rfcs/0006-skill-harness-integration/implementation-details.md @@ -0,0 +1,300 @@ +# RFC-0006: Harness Integration Implementation Details + +This document contains implementation-level specifications for +RFC-0006 (Skill Registry Harness Integration). It covers detailed +adapter directory layouts and manifest generation, MCP server config +generation, SDK interface and function signatures, REST API endpoints, +CLI commands, lock file SDK functions, and trace instrumentation +details. The main RFC covers the design rationale. + +## Claude Code / Codex CLI adapter details + +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/{bundle-name}/ + .claude-plugin/plugin.json + skills/{skill-name}/SKILL.md # skill members + agents/{agent-name}.md # subagent members + .mcp.json # mcp_server members, merged +``` + +For Codex CLI, the path uses `.codex/plugins/` instead. + +**MCP server config generation.** When a bundle references MCP servers +in its `mcp_servers` member list, the adapter generates `.mcp.json` +entries from MCP registry metadata. For each referenced server, the +adapter resolves the `MCPServerVersion` from the MCP registry +(RFC-0004) and looks for an `MCPAccessBinding` targeting that version +or alias. If a binding exists, the adapter uses its `endpoint_url` and +`transport_type` as the connection target. If multiple bindings exist +for the same server, the adapter uses the first binding targeting the +referenced version or alias. If no binding exists, the adapter falls +back to the connection details in `server_json` (e.g., `remotes[]`). + +Entries are merged into a single `.mcp.json` using server name as key: + +```json +{ + "mcpServers": { + "github-mcp": { ... }, + "jira-mcp": { ... } + } +} +``` + +**Embedded MCP configs.** When a bundle has a bundle-level source and +the artifact already contains a `.mcp.json`, those embedded configs +are used as-is for any MCP servers not in the `mcp_servers` member +list. If the same server name appears in both the embedded config and +the `mcp_servers` list, the registry-generated entry takes precedence +(the registry is the governed source of truth). + +**MCP server credentials.** The adapter generates connection config +but does not configure credentials, certificates, or authorization +headers. These are the user's responsibility. The adapter logs a +warning when it generates an entry for a server that uses +authenticated transport, so users know to complete the setup +manually. + +**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 details + +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 # skill members +{destination}/.cursor/agents/{agent-name}.md # subagent members +``` + +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 details + +``` +{destination}/.agent/skills/{skill-name}/SKILL.md # skill members +``` + +Subagents, MCP servers, and hooks are skipped with a warning. + +## SDK interface + +Installation is a client-side operation: the SDK resolves the skill or +bundle 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. + +```python +def install( + name: str | None = None, + bundle: str | None = None, + harness: str = "claude-code", + destination: str = ".", + version: str | None = None, + alias: str | None = None, +) -> str: + """Install a skill or skill bundle for a specific harness. + Resolves from the registry, pulls content, generates + harness-specific manifests, and places files in the correct + directories.""" + + +def import_bundle( + source: str, + harness: str | None = None, + bundle_name: str | None = None, + version: str | None = None, +) -> SkillBundle: + """Import an existing harness-specific artifact into the registry. + Introspects the artifact, registers individual capabilities, + and creates a skill bundle referencing them all.""" +``` + +## REST API + +The only server-side endpoint is the marketplace catalog, which +harnesses query to discover available plugins. + +| Method | Path | Description | +|---|---|---| +| `GET` | `/ajax-api/3.0/mlflow/skill-bundles/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 bundle +mlflow skills install --bundle pr-workflow --alias production \ + --harness claude-code --destination . + +# Import an existing plugin into the registry +mlflow skills import --source ./my-claude-plugin \ + --harness claude-code --bundle-name my-plugin --version 1.0.0 + +# List supported harnesses +mlflow skills harnesses +``` + +## Lock file SDK + +```python +mlflow.genai.skills.install( + bundle="pr-workflow", + alias="production", + harness="claude-code", + lock=True, +) + +# Install from lock file +mlflow.genai.skills.install() +``` + +## Trace instrumentation details + +### Install-time manifest + +When `mlflow skills install` places files for a harness, it also +writes a manifest that maps installed skill names to their registry +coordinates: + +**`mlflow-skills-manifest.json`:** +```json +{ + "manifest_version": "1.0", + "skills": { + "code-review": { + "name": "code-review", + "version": "1.0.0", + "registry": "default" + }, + "security-auditor": { + "name": "security-auditor", + "version": "1.0.0", + "registry": "default" + } + } +} +``` + +The manifest is keyed by the skill's local name (the name the harness +uses to invoke it). The value provides the `{registry, name, version}` +coordinates that link back to the skill registry. This file is used +by trace hooks to annotate spans with registry coordinates without +requiring a registry lookup at runtime. + +### Claude Code: hook-based instrumentation + +Claude Code invokes skills via a built-in `Skill` tool, which fires +`PreToolUse` and `PostToolUse` hook events. The `mlflow skills install` +command can configure hooks that create SKILL spans automatically +when a registered skill is invoked. + +**Hook configuration** (added to `.claude/settings.json`): + +```json +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Skill", + "hooks": [ + { + "type": "command", + "command": "mlflow skills trace-start --manifest .mlflow-skills-manifest.json" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Skill", + "hooks": [ + { + "type": "command", + "command": "mlflow skills trace-end --manifest .mlflow-skills-manifest.json" + } + ] + } + ] + } +} +``` + +The `PreToolUse` hook receives the skill name in its input, looks it +up in the manifest to get registry coordinates, and opens a SKILL +span via the `mlflow autolog claude` trace pipeline. The `PostToolUse` +hook closes the span. Because these hooks integrate with the same +tracing mechanism that `mlflow autolog claude` already uses, SKILL +spans appear as part of the existing trace tree alongside LLM and +tool call spans. + +The hook commands shown above are illustrative. The exact CLI +subcommands and their integration with the `mlflow autolog claude` +trace pipeline are implementation details. + +**Hook installation behavior.** `mlflow skills install` writes the +manifest automatically. It does not modify `settings.json` by +default. Instead, it prints instructions showing the hook +configuration to add. Users can opt in with `--install-hooks` to +have the installer merge hook entries into `settings.json`. This +follows the same security principle as hook handling for plugin +members: users must explicitly enable hooks. + +### Agent SDK: direct instrumentation + +For developers building agents with the Claude Agent SDK or other +Python frameworks, the recommended approach is to use +`mlflow.skill_context()` directly (see RFC-0005). The Agent SDK's +hook system also supports Python callbacks, so a similar automatic +approach is possible: + +```python +from claude_code_sdk import ClaudeAgentOptions + +async def on_skill_start(input_data, tool_use_id, context): + skill_name = input_data["tool_input"].get("skill") + # Look up registry coordinates from manifest + # Open mlflow.skill_context() span + return {} + +options = ClaudeAgentOptions( + hooks={"PreToolUse": [{"matcher": "Skill", "hook": on_skill_start}]} +) +``` + +Because Agent SDK hooks run in-process, they can call +`mlflow.skill_context()` directly, creating SKILL spans in the +same trace tree as the autologger spans. + +### Other harnesses + +Trace integration depends on each harness exposing a hook or event +mechanism for skill invocation. Harnesses that support pre/post tool +use hooks (Codex CLI, GitHub Copilot) can follow the same pattern as +Claude Code. Harnesses without hook support cannot be automatically +instrumented; users of those harnesses can still use +`mlflow.skill_context()` manually in SDK-based agent code. From c2bd8837377d6490553b234735093dea6cf3809c Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Mon, 1 Jun 2026 15:52:52 -0400 Subject: [PATCH 40/52] Replace persona use cases with step-by-step user journeys Add five concrete user journeys: register a skill bundle, discover a skill, install/run/trace, evaluate two versions with Agent-as-a-Judge, and CI pipeline for automated regression detection. Each journey is an enumerated step-by-step workflow with CLI examples. Co-Authored-By: Claude Opus 4.6 --- .../0005-skill-registry.md | 153 +++++++++++++++--- 1 file changed, 135 insertions(+), 18 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index d1bff82..e09827a 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -370,24 +370,141 @@ address: source system. Users must manually copy source pointers and run harness-specific install steps. -### Use cases - -**Platform administrator** — A platform admin at Acme Corp registers -their team's code-review skill, pointing to its Git source. They -create a version and bundle it with a security-auditor subagent -and a GitHub MCP server -into a "pr-workflow" skill bundle. They set the bundle'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. They find the `pr-workflow` bundle, -resolve its `production` alias, and run -`mlflow skills pull --bundle 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)). +### User journeys + +#### Register a skill bundle + +1. Register individual capability versions pointing to their sources: + ```bash + mlflow skills register --name code-review --version 1.0.0 \ + --source https://github.com/acme/agent-skills/tree/v1.0.0/code-review + mlflow subagents register --name security-auditor --version 1.0.0 \ + --source https://github.com/acme/agent-skills/tree/v1.0.0/security-auditor + mlflow hooks register --name pre-commit-scan --version 1.0.0 \ + --source https://github.com/acme/agent-skills/tree/v1.0.0/pre-commit-scan + ``` +2. Create a skill bundle version that pins these members: + ```bash + mlflow skill-bundles create-version --name pr-workflow --version 1.0.0 \ + --skill code-review:1.0.0 \ + --subagent security-auditor:1.0.0 \ + --hook pre-commit-scan:1.0.0 + ``` +3. Transition the bundle version from draft to active: + ```bash + mlflow skill-bundles update-version --name pr-workflow \ + --version 1.0.0 --status active + ``` +4. Set an alias for stable downstream resolution: + ```bash + mlflow skill-bundles set-alias --name pr-workflow \ + --alias production --version 1.0.0 + ``` + +#### Discover a skill for a specific purpose + +1. Search the registry by keyword: + ```bash + mlflow skills search --filter "name LIKE '%review%'" --status active + ``` +2. Browse the returned list of matching skills with names, + descriptions, and latest versions. +3. Get details on a promising result: + ```bash + mlflow skills get --name code-review + ``` +4. Inspect a specific version's source and metadata: + ```bash + mlflow skills get-version --name code-review --version 1.0.0 + ``` +5. Pull the skill locally to read the content and decide whether + it fits: + ```bash + mlflow skills pull --name code-review --version 1.0.0 \ + --destination ./review-skill + ``` + +#### Install a skill bundle, run the agent, browse traces + +1. Install the bundle for a harness + ([RFC-0006](../0006-skill-harness-integration/0006-skill-harness-integration.md)): + ```bash + mlflow skills install --bundle pr-workflow --alias production \ + --harness claude-code --destination . --lock + ``` + This pulls all member content, generates harness-specific + manifests, writes a lock file, and writes a trace manifest. +2. Run the agent. The harness loads the installed plugin and invokes + skills during a conversation. +3. Open the MLflow UI and navigate to the Traces page. +4. Find the trace for the agent run. Skill invocations appear as + SKILL spans in the trace tree, annotated with registry coordinates + (skill name, version, registry). +5. Click a SKILL span to see which registered skill version was used + and how long it took. + +#### Evaluate two bundle versions with Agent-as-a-Judge + +MLflow's +[Agent-as-a-Judge](https://mlflow.org/docs/latest/genai/eval-monitor/scorers/llm-judge/agentic-overview/) +evaluation uses LLM judges that autonomously explore execution traces +via MCP tools. Because skill invocations produce traced SKILL spans, +Agent-as-a-Judge can analyze how skills were used during an agent run. + +1. Register a new version of the bundle with updated members: + ```bash + mlflow skills register --name code-review --version 2.0.0 \ + --source https://github.com/acme/agent-skills/tree/v2.0.0/code-review + mlflow skill-bundles create-version --name pr-workflow --version 2.0.0 \ + --skill code-review:2.0.0 \ + --subagent security-auditor:1.0.0 \ + --hook pre-commit-scan:1.0.0 + ``` +2. Install v1.0.0 and run it on a set of test inputs. Traces are + recorded in MLflow under experiment A. +3. Install v2.0.0 and run it on the same test inputs. Traces are + recorded under experiment B. +4. Use `mlflow.genai.evaluate()` with a `make_judge` scorer that + uses the `{{ trace }}` template variable to score both sets of + traces against quality criteria (correctness, helpfulness, safety). +5. Compare the evaluation results side by side in the MLflow UI to + determine whether v2.0.0 is an improvement. +6. If v2.0.0 is better, transition it to active and update the + production alias: + ```bash + mlflow skill-bundles update-version --name pr-workflow \ + --version 2.0.0 --status active + mlflow skill-bundles set-alias --name pr-workflow \ + --alias production --version 2.0.0 + ``` + +#### CI pipeline for automated regression detection + +1. A CI job (e.g., GitHub Actions) triggers on pushes to the skill + source repo. +2. The job registers a new skill bundle version from the updated + source: + ```bash + mlflow skills register --name code-review --version 1.1.0 \ + --source https://github.com/acme/agent-skills/tree/v1.1.0/code-review + mlflow skill-bundles create-version --name pr-workflow --version 1.1.0 \ + --skill code-review:1.1.0 \ + --subagent security-auditor:1.0.0 \ + --hook pre-commit-scan:1.0.0 + ``` +3. The job installs the new bundle version and runs it against a + benchmark dataset, collecting traces in a dedicated MLflow + experiment. +4. The job runs + [Agent-as-a-Judge](https://mlflow.org/docs/latest/genai/eval-monitor/scorers/llm-judge/agentic-overview/) + evaluation on the collected traces, producing scored results. +5. The job fetches the benchmark results from the previous production + version (stored as MLflow metrics or evaluation artifacts). +6. The job compares the new scores against the previous scores. If + any quality metric regresses beyond a configured threshold, it + sends an alert (Slack, email, or fails the CI check). +7. If no regression is detected, the job transitions the new version + to active and optionally updates the production alias. ### Out of scope From e6bf128336355febc7aae8e7880be5d84997cb19 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Mon, 1 Jun 2026 17:10:07 -0400 Subject: [PATCH 41/52] Conciseness pass on both RFCs and fix prose issues Tighten both RFCs: collapse redundant sections, compress examples, move lock file JSON to implementation-details.md. Fix em dashes, duplicate ER diagram lines, and AI-pattern words flagged by prose validation. RFC-0005 drops from ~1380 to 1134 lines, RFC-0006 from 584 to 464 lines. Co-Authored-By: Claude Opus 4.6 --- .../0005-skill-registry.md | 322 ++++-------------- .../0006-skill-harness-integration.md | 209 +++--------- .../implementation-details.md | 38 +++ 3 files changed, 144 insertions(+), 425 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index e09827a..3cbf920 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-05-27 | +| **Date Last Modified** | 2026-06-01 | | **AI Assistant(s)** | Claude Code (Opus 4.6) | # Summary @@ -25,11 +25,11 @@ The registry manages three entity types under the `mlflow.genai.skills` SDK namespace (CLI: `mlflow skills`), each with full lifecycle (versioning, aliases, tags, status): -- **Skills** — a directory containing a SKILL.md entry point plus +- **Skills**: a directory containing a SKILL.md entry point plus supporting files (scripts, templates, reference material) -- **Subagents** — sub-agent definitions that can be invoked by a +- **Subagents**: sub-agent definitions that can be invoked by a parent agent -- **Hooks** — event-triggered actions (harness-specific) +- **Hooks**: event-triggered actions (harness-specific) Skill bundles group related capabilities into versioned, governed units that map to the "plugin" concept in agent harnesses. Bundles @@ -252,66 +252,16 @@ bundle_version = mlflow.genai.skills.get_skill_bundle_version_by_alias( ) ``` -## CLI usage - -```bash -# 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... - -# 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 - -# Register a subagent -mlflow subagents register --name security-auditor --version 1.0.0 \ - --description "Security specialist for auth and payment code" \ - --source-type git \ - --source https://github.com/acme/agent-skills/tree/v1.0.0/security-auditor - -# Register a hook -mlflow hooks register --name pre-commit-scan --version 1.0.0 \ - --description "Runs security scan before tool commits" \ - --source-type git \ - --source https://github.com/acme/agent-skills/tree/v1.0.0/pre-commit-scan - -# Create a bundle and a versioned membership snapshot -mlflow skill-bundles create --name pr-workflow \ - --description "End-to-end PR review workflow" -mlflow skill-bundles create-version --name pr-workflow --version 1.0.0 \ - --skill code-review:1.0.0 \ - --skill test-coverage:2.1.0 \ - --subagent security-auditor:1.0.0 \ - --hook pre-commit-scan:1.0.0 \ - --mcp-server github-mcp:2.0.0 -mlflow skill-bundles set-alias --name pr-workflow --alias production \ - --version 1.0.0 - -# Search active skill versions -mlflow skills search-versions --name code-review \ - --filter "status = 'active'" - -# Search active bundles -mlflow skill-bundles search --filter "status = 'active'" -``` +CLI equivalents for these operations use `mlflow skills`, `mlflow +subagents`, `mlflow hooks`, and `mlflow skill-bundles` command groups. +See the user journeys below for CLI examples. ## Motivation ### The problem -AI agent capabilities — skills, sub-agents, MCP server configurations, -and hooks — are becoming a critical asset class in enterprise AI +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. @@ -319,17 +269,17 @@ 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 +- **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 subagents. -- **MCP server configs** — JSON configuration for Model Context +- **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 +- **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, subagents, +- **Plugin bundles**: harness-specific packaging of skills, subagents, MCP configs, and hooks into a single installable unit. Claude Code and Codex CLI use `plugin.json` manifests; other harnesses use directory conventions. @@ -519,8 +469,8 @@ Agent-as-a-Judge can analyze how skills were used during an agent run. looks like. - **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 + 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.** Status transitions are sufficient for initial governance. @@ -541,8 +491,6 @@ Agent-as-a-Judge can analyze how skills were used during an agent run. erDiagram Skill ||--o{ SkillVersion : "has versions" Skill ||--o{ SkillTag : "has tags" -Skill ||--o{ SkillVersion : "has versions" -Skill ||--o{ SkillTag : "has tags" Skill ||--o{ SkillAlias : "has aliases" Subagent ||--o{ SubagentVersion : "has versions" Subagent ||--o{ SubagentTag : "has tags" @@ -724,65 +672,18 @@ read; verification is the consumer's responsibility. to different content, register a new version. Mutable fields (`display_name`, `status`, `tags`) can be updated independently. -#### Subagent - -A subagent is a sub-agent definition that can be invoked by a parent -agent. The `Subagent` entity follows the same structure as `Skill`: -a top-level governed asset with versions, tags, aliases, and full -lifecycle management. - -```python -@dataclass -class Subagent: - name: str - display_name: str | None = None - description: str | None = None - workspace: str | None = None - status: SkillStatus = SkillStatus.DRAFT - tags: dict[str, str] = field(default_factory=dict) - aliases: list["SubagentAlias"] = field(default_factory=list) - latest_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 -``` - -`SubagentVersion` follows the same structure as `SkillVersion`: -`name`, `version`, `source_type`, `source`, `subpath`, -`content_digest`, `status`, `tags`, and timestamps. -Version uniqueness is `(name, version)` within a workspace, and the -same immutability contract applies. - -#### Hook - -A hook is an event-triggered action (e.g., a shell command that runs -before a commit). The `Hook` entity follows the same structure as -`Skill` and `Subagent`. +#### Subagent and Hook -```python -@dataclass -class Hook: - name: str - display_name: str | None = None - description: str | None = None - workspace: str | None = None - status: SkillStatus = SkillStatus.DRAFT - tags: dict[str, str] = field(default_factory=dict) - aliases: list["HookAlias"] = field(default_factory=list) - latest_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 -``` - -`HookVersion` follows the same structure as `SkillVersion`. +`Subagent` (a sub-agent definition invocable by a parent agent) and +`Hook` (an event-triggered action, e.g., a shell command before a +commit) follow the same structure as `Skill`: top-level governed +assets with the same fields, versions, tags, aliases, and lifecycle. +`SubagentVersion` and `HookVersion` follow the same structure as +`SkillVersion`. -**Shared patterns.** All three entity types (Skill, Subagent, Hook) -share the same version, tag, alias, and lifecycle patterns. The store interface, REST API, and SDK expose parallel -operations for each type. The database uses parallel table sets (see -Database schema). +All three entity types share the same version, tag, alias, and +lifecycle patterns. The store interface, REST API, and SDK expose +parallel operations for each type. #### SkillBundle @@ -811,27 +712,13 @@ class SkillBundle: `SkillBundle.status` is read-only, derived from the latest bundle version's status. `latest_version` works the same as on `Skill`. -**Why bundles instead of tags?** Tags on individual skills could -express "these skills are related" but cannot provide: - -- **Versioned membership snapshots.** A bundle 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 bundle version can reference both - skill registry members and MCP server registry members (RFC-0004). - Tags on individual skills cannot express this cross-registry - relationship. -- **Bundle-level source.** A bundle 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 bundle version has its own status, - aliases, and tags. The bundle 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 bundles - map directly to this concept; tags do not. +**Why bundles instead of tags?** Tags could express "these skills +are related" but cannot provide versioned membership snapshots +(reproducible point-in-time combinations), cross-registry references +(MCP servers from RFC-0004), bundle-level source pointers (a single +OCI image), independent lifecycle (deprecate a bundle without +deprecating its members), or direct mapping to the harness plugin +concept. #### SkillBundleVersion @@ -931,37 +818,13 @@ 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. -#### SkillBundleAlias +#### Aliases and tags -```python -@dataclass(frozen=True) -class SkillBundleAlias: - name: str # parent SkillBundle name - alias: str # e.g., "production", "staging" - version: str # bundle 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 - -@dataclass(frozen=True) -class SkillTag: - key: str - value: str -``` - -Subagent and Hook follow the same alias and tag patterns (e.g., -`SubagentAlias`, `SubagentTag`, `HookAlias`, `HookTag`). - -Tags use the same structure for skill-level, version-level, and -bundle-level tags. The distinction is maintained at the storage and -API layer (separate tables, separate endpoints). +All entity types use the same alias pattern: a frozen `(name, alias, +version)` tuple mapping a stable name (e.g., `production`) to a +specific version string. Tags are `(key, value)` pairs at both the +entity level and version level. Subagent, Hook, and SkillBundle +follow the same patterns. ### Status and lifecycle @@ -1086,7 +949,7 @@ The registry does not store, proxy, or manage source credentials. Pull failures due to authentication errors are surfaced to the caller with the underlying error from the source system. -`pull` is harness-agnostic — it downloads content but does not generate +`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. @@ -1179,84 +1042,27 @@ the registry. #### Skill stacks via nesting -Skills can invoke other skills. Because `skill_context()` creates a -real span, nesting context managers naturally produces a skill stack -in the trace tree. Consider an agent that uses a "code-review" skill, -which internally invokes a "style-check" skill: - -```python -import mlflow - -def run_code_review(diff: str): - with mlflow.skill_context(name="code-review", version="1.0.0"): - # First LLM call: analyze the diff - analysis = llm.chat([ - {"role": "user", "content": f"Review this diff:\n{diff}"} - ]) - - # Invoke a sub-skill for style checking - style_issues = run_style_check(diff) - - # Second LLM call: synthesize final review - review = llm.chat([ - {"role": "user", "content": f"Summarize: {analysis}, {style_issues}"} - ]) - return review - -def run_style_check(code: str): - with mlflow.skill_context(name="style-check", version="2.0.0"): - return llm.chat([ - {"role": "user", "content": f"Check style:\n{code}"} - ]) -``` - -The resulting trace tree: +Skills can invoke other skills. Nesting `skill_context()` calls +produces a skill stack in the trace tree: ``` -Trace: tr-abc123 -| -+-- Span: "code-review" (type: SKILL) -| | mlflow.skill.name = "code-review" -| | mlflow.skill.version = "1.0.0" -| | ++-- Span: "code-review" (type: SKILL, version: 1.0.0) | +-- Span: ChatCompletion (type: LLM) -| | "Review this diff: ..." -| | -| +-- Span: "style-check" (type: SKILL) -| | | mlflow.skill.name = "style-check" -| | | mlflow.skill.version = "2.0.0" -| | | +| +-- Span: "style-check" (type: SKILL, version: 2.0.0) | | +-- Span: ChatCompletion (type: LLM) -| | "Check style: ..." -| | | +-- Span: ChatCompletion (type: LLM) -| "Summarize: ..." ``` -For any span in the tree, walking up the ancestor chain and -collecting SKILL-type spans reconstructs the skill stack. For the -"Check style" LLM call, the stack is -`[code-review@1.0.0, style-check@2.0.0]`. For the "Summarize" LLM -call, the stack is just `[code-review@1.0.0]` because it executes -after the style-check block exits. +Walking up the ancestor chain and collecting SKILL spans reconstructs +the skill stack for any span. #### What this enables -With skill-annotated traces, organizations can answer questions that -are impossible without trace-to-registry linkage: - -- **Adoption tracking.** "Which skill versions are most used across - the organization?" Query for SKILL spans grouped by name and - version. -- **Deprecation impact.** "Show me all traces where the deprecated - code-review v1.0 was loaded." Filter traces by - `mlflow.skill.name` and `mlflow.skill.version`. -- **Per-skill cost attribution.** Each SKILL span contains all child - spans. Aggregate token usage and latency per skill, including or - excluding sub-skills. -- **Regression detection.** "Did error rates change after upgrading - style-check from v1.0 to v2.0?" Compare trace outcomes across - skill versions. +Skill-annotated traces enable adoption tracking (which versions are +most used), deprecation impact analysis (which traces used a +deprecated version), per-skill cost attribution (aggregate token +usage and latency per SKILL span), and regression detection (compare +trace outcomes across skill versions). #### Autologger compatibility @@ -1281,26 +1087,20 @@ the coordinates do not resolve. #### Relationship to MCP trace linking -The MCP Registry (RFC-0004) provides `link_mcp_server_versions_to_trace()` -for after-the-fact, trace-level association between traces and MCP -server versions. Skill trace integration takes a different approach: -span-level, inline annotation via context managers. The span-based -approach is a better fit for skills because skills are ambient (active -during inference rather than handling discrete requests) and can nest -(a skill invoking a sub-skill). MCP servers have clearer -request/response boundaries that make after-the-fact linking more -natural. Both approaches produce trace metadata that the MLflow UI -can display together. +The MCP Registry (RFC-0004) uses after-the-fact, trace-level +association (`link_mcp_server_versions_to_trace()`). Skills use +span-level, inline annotation because skills are ambient (active +during inference) and can nest. Both approaches produce trace +metadata that the MLflow UI can display together. ## Drawbacks -- **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. +- **Source pointer validity.** For external sources (git, oci, zip), + the registry cannot guarantee pointers remain valid. The optional + `content_digest` field mitigates content tampering but does not + prevent link rot. Users who need self-contained storage can use + `source_type="mlflow"` to store content directly in MLflow artifact + storage. # Alternatives 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 ae04cd2..5a7ea04 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-05-27 | +| **Date Last Modified** | 2026-06-01 | | **AI Assistant(s)** | Claude Code (Opus 4.6) | # Summary @@ -48,17 +48,9 @@ capabilities from their registered sources, and generates: ## Install for other harnesses ```bash -# Codex CLI (nearly identical to Claude Code) -mlflow skills install --bundle pr-workflow --alias production \ - --harness codex-cli - -# Cursor +# Same command, different --harness: codex-cli, cursor, antigravity mlflow skills install --bundle pr-workflow --alias production \ --harness cursor - -# Antigravity -mlflow skills install --bundle pr-workflow --alias production \ - --harness antigravity ``` ## Import an existing plugin as a skill bundle @@ -103,28 +95,12 @@ mlflow.genai.skills.import_bundle( ### 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. +RFC-0005 provides `pull` for fetching content to a local directory, +but each harness has its own directory layout, manifest format, and +discovery mechanism (see table below). Without harness-specific +installation, users must manually create manifests, place files in +the right directories, and configure discovery. This is error-prone +and discourages adoption. ### The cross-harness landscape @@ -147,18 +123,14 @@ conventions across major agent harnesses: | 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. +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. +- Registry operations (covered in RFC-0005). +- Extending harness functionality (e.g., adding hook support). +- Automatic harness detection (follow-up). ## Detailed design @@ -254,17 +226,9 @@ additional harnesses are community-contributed. Installation takes registry metadata and produces a harness-specific artifact. Bundle import is the reverse: it takes an existing -harness-specific artifact and registers all its elements in the -registry, creating individual capability entries and a skill bundle -that ties them together. - -#### Motivation - -Teams that already have skills, subagents, and hooks organized as -harness-specific artifacts (e.g., a Claude Code plugin directory, a -Cursor project) should not have to manually decompose and register -each element. Bundle import lets them point at an existing artifact -and register everything in one operation. +harness-specific artifact (e.g., a Claude Code plugin), introspects +it to discover individual capabilities, and registers them all in +one operation along with a skill bundle that ties them together. #### Contract @@ -323,22 +287,13 @@ mlflow.genai.skills.import_bundle( #### CLI ```bash -# Import from a local directory +# Import from local, git, OCI, or ZIP sources mlflow skills import --source ./my-claude-plugin \ --harness claude-code --bundle-name my-plugin --version 1.0.0 -# Import from a git repo mlflow skills import --source https://github.com/acme/plugins/tree/v1.0.0/pr-workflow \ --harness claude-code -# Import from an OCI registry -mlflow skills import --source oci://registry.example.com/plugins/review:latest \ - --harness claude-code - -# Import from a ZIP URL -mlflow skills import --source https://example.com/plugins/review-v1.zip \ - --harness claude-code - # Auto-detect harness format mlflow skills import --source ./my-plugin ``` @@ -384,40 +339,17 @@ 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-bundles/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 bundle version and its -members. The `source` field points to a registry endpoint that serves -the installable plugin bundle. - -**Install endpoint behavior.** When a harness fetches the `source` URL, -the registry server resolves the bundle version, pulls member content -from their registered sources, generates harness-specific manifests, -and serves the assembled plugin as a downloadable archive. This is a -server-side equivalent of `mlflow skills install`. Because the server -performs the content fetch, this flow only works for bundles whose -sources are publicly accessible or stored in MLflow artifact storage -(`source_type="mlflow"`). Bundles with private external sources (private -Git repos, authenticated OCI registries) are not included in the -marketplace catalog; users install those via the CLI +The response follows the harness's native marketplace schema (e.g., +a `plugins` array for Claude Code / Codex CLI). Each entry is derived +from a published skill bundle version and its members. The `source` +field points to a registry endpoint that serves the assembled plugin +as a downloadable archive (server-side equivalent of +`mlflow skills install`). + +Because the server performs the content fetch, marketplace entries are +limited to bundles whose sources are publicly accessible or stored in +MLflow artifact storage (`source_type="mlflow"`). Bundles with private +external sources require CLI installation (`mlflow skills install`), which uses the caller's local credentials. #### Configuration @@ -442,8 +374,8 @@ natively: ``` 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. +CLI users. It keeps the harness as the single point of plugin +management. #### Limitations @@ -461,50 +393,9 @@ endpoints, and CLI commands are in ### 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": "bundle", - "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 +harness, exact resolved versions, source URIs, and content digests so +that `mlflow skills install` with no arguments reproduces the same +local setup (analogous to `package-lock.json` or `poetry.lock`). ```bash # First install: resolves from registry and writes lock file @@ -514,18 +405,16 @@ mlflow skills install --bundle pr-workflow --alias production \ # Subsequent installs: reads lock file, no registry resolution needed mlflow skills install -# Update: re-resolves from registry and updates lock file +# Update: re-resolves and updates lock file mlflow skills install --bundle pr-workflow --alias production \ --harness claude-code --lock --update ``` -The lock file records source URIs, exact versions, and content -digests for reproducible installs across team members. Lock file -replay still contacts the registry to verify version status, so -governance actions (deprecation, deletion) take effect even for -existing lock files. +Lock file replay still contacts the registry to verify version +status, so governance actions (deprecation, deletion) take effect +even for existing lock files. -Lock file SDK functions are in +Lock file format and SDK functions are in [implementation-details.md](implementation-details.md). ### Trace integration @@ -567,18 +456,10 @@ critical for driving adoption. # 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. -- Install-time manifest (`mlflow-skills-manifest.json`) for trace - integration. -- Claude Code trace hooks for automatic SKILL span creation via - `PreToolUse`/`PostToolUse` on the `Skill` tool. - -**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. +**Initial release:** Claude Code, Codex CLI, and Cursor adapters. +Marketplace catalog generation for Claude Code / Codex CLI. +Install-time trace manifest and Claude Code trace hooks. + +**Follow-up:** additional adapters based on demand, automatic harness +detection, bi-directional sync (detect local plugins and register +them). diff --git a/rfcs/0006-skill-harness-integration/implementation-details.md b/rfcs/0006-skill-harness-integration/implementation-details.md index 09d8d6d..a96b2e7 100644 --- a/rfcs/0006-skill-harness-integration/implementation-details.md +++ b/rfcs/0006-skill-harness-integration/implementation-details.md @@ -159,6 +159,44 @@ mlflow skills import --source ./my-claude-plugin \ mlflow skills harnesses ``` +## Lock file format + +```json +{ + "harness": "claude-code", + "locked_at": "2026-05-17T21:00:00Z", + "entries": [ + { + "type": "bundle", + "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" + } + ] + } + ] +} +``` + ## Lock file SDK ```python From 850837f38b68ed9153be78e50e93a94612c82940 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Thu, 4 Jun 2026 15:23:08 -0400 Subject: [PATCH 42/52] Acknowledge cross-harness bundle formats and external skill managers Add harness-agnostic bundle format support as a design principle in RFC-0006, with Lola as an illustrative example. Add Lola to the cross-harness landscape table. Add Alternatives section explaining why we implement installation ourselves rather than delegating to skills.sh, Lola, or SkillHub. Update RFC-0005 plugin bundles description to acknowledge the spectrum from harness-specific to cross-harness formats. Co-Authored-By: Claude Opus 4.6 --- .../0005-skill-registry.md | 16 +++--- .../0006-skill-harness-integration.md | 56 +++++++++++++++++-- 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index 3cbf920..4fae8cd 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -279,10 +279,12 @@ the conventions gaining adoption across agent 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, subagents, - MCP configs, and hooks into a single installable unit. Claude Code - and Codex CLI use `plugin.json` manifests; other harnesses use - directory conventions. +- **Plugin bundles**: packaging of skills, subagents, MCP configs, and + hooks into a single installable unit. Formats range from + harness-specific (Claude Code and Codex CLI `plugin.json` manifests) + to cross-harness (e.g., Lola's "AI Context Modules," which use + directory auto-discovery to target multiple harnesses from a single + package). Today, these capabilities are managed as ad-hoc files in Git repositories. This works well for individual developers and small @@ -771,9 +773,9 @@ artifact. The bundle artifact is a generic package of content (skill files, agent definitions, hook scripts). It may or may not be harness-ready; the adapter does not assume either way. Harness -adapters (RFC-0006) generate manifests (`plugin.json`, -`.mcp.json`) from registry metadata at install time, since the -registry is the governed source of truth. If the artifact +adapters (RFC-0006) generate harness-specific manifests from +registry metadata at install time, since the registry is the +governed source of truth. If the artifact contents disagree with the declared members (e.g., a `subpath` points to a missing directory), pull succeeds but install fails when the adapter cannot find the expected content. Correctness of 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 5a7ea04..95c3abf 100644 --- a/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md +++ b/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md @@ -113,6 +113,7 @@ conventions across major agent harnesses: | 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 | +| Lola | SKILL.md | agents/*.md | mcps.json | lola.yaml | (auto-discovered) | per-harness | | OpenClaw | SKILL.md | -- | -- | plugin hooks | openclaw.plugin.json | `skills/` | | Kilo Code | SKILL.md | custom modes | mcp.json | -- | -- | project | | Antigravity | SKILL.md | -- | -- | -- | -- | `.agent/skills/` | @@ -205,6 +206,18 @@ are skipped (unsupported). **Antigravity:** places skills in `.agent/skills/`. Subagents, MCP servers, and hooks are skipped. +**Harness-agnostic bundle formats.** The adapter interface is not +limited to harness-specific formats. Cross-harness bundle formats +that package skills, subagents, hooks, and MCP servers together are +also valid adapter targets. For example, Lola +([LobsterTrap/lola](https://github.com/LobsterTrap/lola)) defines +an "AI Context Module" format that bundles these capability types +using directory auto-discovery and targets multiple harnesses from +a single module. An adapter for a format like this would support +both directions: `install` generates the cross-harness format from +registry metadata, and `import` introspects an existing module to +register its elements. + Detailed directory layouts, MCP config generation rules, and hook handling behavior are in [implementation-details.md](implementation-details.md). @@ -226,9 +239,10 @@ additional harnesses are community-contributed. Installation takes registry metadata and produces a harness-specific artifact. Bundle import is the reverse: it takes an existing -harness-specific artifact (e.g., a Claude Code plugin), introspects -it to discover individual capabilities, and registers them all in -one operation along with a skill bundle that ties them together. +artifact in any supported format (e.g., a Claude Code plugin or a +cross-harness module), introspects it to discover individual +capabilities, and registers them all in one operation along with a +skill bundle that ties them together. #### Contract @@ -454,12 +468,42 @@ 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. +## Delegate installation to an existing skill package manager + +Several open-source projects already handle skill installation: + +- **skills.sh** ([vercel-labs/skills](https://github.com/vercel-labs/skills)): + CLI for installing individual SKILL.md files. Supports 70+ harnesses. +- **Lola** ([LobsterTrap/lola](https://github.com/LobsterTrap/lola)): + Cross-harness package manager. Its "AI Context Module" format bundles + skills, subagents, commands, hooks, and MCP servers. +- **SkillHub** ([iflytek/skillhub](https://github.com/iflytek/skillhub)): + Self-hosted skill registry with CLI installation. Individual skills + only, 14 harnesses. + +We considered delegating installation to one of these tools rather +than implementing our own adapters. skills.sh and SkillHub operate on +individual skills in isolation and have no bundle concept, so they +cannot handle the general case of installing a skill bundle with +skills, subagents, hooks, and MCP server configurations together. +Lola is closer: its AI Context Module format supports all the member +types we need. However, delegating installation to the Lola CLI +would introduce a third-party runtime dependency for a relatively +narrow special case (Lola-format bundles targeting Lola-supported +harnesses) while still requiring our own implementation for the +general problem (any bundle format, any harness, with registry +governance and trace integration). Instead, we implement installation +ourselves via the adapter interface. The adapter interface is +extensible to harness-agnostic bundle formats (see "Harness-agnostic +bundle formats" above), so support for formats like Lola's can be +added as demand warrants without architectural changes. + # Adoption strategy **Initial release:** Claude Code, Codex CLI, and Cursor adapters. Marketplace catalog generation for Claude Code / Codex CLI. Install-time trace manifest and Claude Code trace hooks. -**Follow-up:** additional adapters based on demand, automatic harness -detection, bi-directional sync (detect local plugins and register -them). +**Follow-up:** additional adapters based on demand (including +harness-agnostic bundle formats), automatic harness detection, +bi-directional sync (detect local plugins and register them). From 4f054329c3f6b27dd4fe8c8829b297c82eab1411 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Thu, 4 Jun 2026 17:57:07 -0400 Subject: [PATCH 43/52] Add install scope (project/user) and cross-harness bundle formats Add scope parameter to install interface: "project" (default) or "user" for global install. Each adapter declares both project-level and user-level paths. Acknowledge cross-harness bundle formats as a design principle, with Lola as an illustrative example. Add Alternatives section covering skills.sh, Lola, and SkillHub. Co-Authored-By: Claude Opus 4.6 --- .../0006-skill-harness-integration.md | 22 +++++++++++++------ .../implementation-details.md | 13 ++++++----- 2 files changed, 22 insertions(+), 13 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 95c3abf..6be4332 100644 --- a/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md +++ b/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md @@ -51,6 +51,10 @@ capabilities from their registered sources, and generates: # Same command, different --harness: codex-cli, cursor, antigravity mlflow skills install --bundle pr-workflow --alias production \ --harness cursor + +# Install globally (user scope) instead of project scope +mlflow skills install --bundle pr-workflow --alias production \ + --harness claude-code --scope user ``` ## Import an existing plugin as a skill bundle @@ -79,7 +83,7 @@ mlflow.genai.skills.install( bundle="pr-workflow", alias="production", harness="claude-code", - destination=".", # project root + scope="project", # or "user" for global install ) # Import an existing harness-specific plugin into the registry @@ -140,13 +144,17 @@ Only the directory placement and manifest format differ. Each supported harness has an adapter that knows how to: 1. **Map member types to harness paths.** Given the bundle's member - types (skill, subagent, hook, mcp_server), determine where each - member's content should be placed. -2. **Generate manifests.** Create harness-specific manifest files + types (skill, subagent, hook, mcp_server) and the install scope, + determine where each member's content should be placed. +2. **Declare install paths per scope.** Each adapter knows both the + project-level path (e.g., `.claude/plugins/`) and the user-level + global path (e.g., `~/.claude/plugins/`). The `scope` parameter + selects which one to use. +3. **Generate manifests.** Create harness-specific manifest files (e.g., `plugin.json`, `.mcp.json`) from registry metadata. -3. **Handle unsupported types.** Skip member types the harness does +4. **Handle unsupported types.** Skip member types the harness does not support, with a warning. -4. **Introspect existing bundles.** Given a harness-specific artifact +5. **Introspect existing bundles.** Given a harness-specific artifact (e.g., a Claude Code plugin directory), identify the individual capabilities it contains and their types. @@ -179,7 +187,7 @@ class HarnessAdapter: subagents: list[tuple[Subagent, SubagentVersion]], hooks: list[tuple[Hook, HookVersion]], mcp_servers: list[tuple[str, str]], - destination: str, + scope: str = "project", # "project" or "user" ) -> str: ... @abstractmethod diff --git a/rfcs/0006-skill-harness-integration/implementation-details.md b/rfcs/0006-skill-harness-integration/implementation-details.md index a96b2e7..87d0702 100644 --- a/rfcs/0006-skill-harness-integration/implementation-details.md +++ b/rfcs/0006-skill-harness-integration/implementation-details.md @@ -110,14 +110,15 @@ def install( name: str | None = None, bundle: str | None = None, harness: str = "claude-code", - destination: str = ".", + scope: str = "project", version: str | None = None, alias: str | None = None, ) -> str: """Install a skill or skill bundle for a specific harness. Resolves from the registry, pulls content, generates harness-specific manifests, and places files in the correct - directories.""" + directories. Scope is 'project' (e.g., .claude/plugins/) or + 'user' (e.g., ~/.claude/plugins/) for global install.""" def import_bundle( @@ -143,13 +144,13 @@ harnesses query to discover available plugins. ## CLI ```bash -# Install a single capability +# Install a single capability (project scope, the default) mlflow skills install --name code-review --alias production \ - --harness claude-code --destination . + --harness claude-code -# Install a skill bundle +# Install a skill bundle globally (user scope) mlflow skills install --bundle pr-workflow --alias production \ - --harness claude-code --destination . + --harness claude-code --scope user # Import an existing plugin into the registry mlflow skills import --source ./my-claude-plugin \ From b36870ae207a8fb9cebb52dd5003393abfd4315f Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Mon, 8 Jun 2026 14:24:12 -0400 Subject: [PATCH 44/52] Restructure RFCs based on meeting feedback - Move dataclass definitions, field tables, auth tables, and SDK examples from main RFCs to implementation-details.md - Move user journeys to immediately after Summary in RFC-0005 - Add UI paths (alongside CLI) to user journeys - Expand UI section from 17 to ~80 lines with list view, detail views, and trace integration display (modeled on MCP RFC) - Add skill_context() scope explanation (skills only, not subagents/hooks/bundles) addressing Humair's review - Add workspace resolution explanation (manifest lookup, tracking URI default, explicit override) addressing Humair's review - Expand RFC-0006 import flow UI with multi-step wizard - Add marketplace browsing UI section to RFC-0006 - Add hyperlinks from every main RFC section to implementation details - RFC-0005 main: 1136 -> 808 lines (29% reduction) Co-Authored-By: Claude Opus 4.6 --- .../0005-skill-registry.md | 908 ++++++------------ .../implementation-details.md | 458 +++++++++ .../0006-skill-harness-integration.md | 88 +- .../implementation-details.md | 27 + 4 files changed, 828 insertions(+), 653 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index 4fae8cd..9e0b3f1 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-06-01 | +| **Date Last Modified** | 2026-06-08 | | **AI Assistant(s)** | Claude Code (Opus 4.6) | # Summary @@ -41,290 +41,12 @@ registered content from its source. Harness-specific installation (manifest generation, directory placement) is covered in a companion RFC (RFC-0006). -# Basic example +# User journeys -## Register a skill +These journeys illustrate the end-to-end workflows that the Skill +Registry enables. Each shows both CLI and UI paths. -```python -import mlflow - -# 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...", -) -# version.status == "draft" - -# 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 bundle with a versioned membership snapshot - -```python -# Create a bundle for related capabilities -bundle = mlflow.genai.skills.create_skill_bundle( - name="pr-workflow", - description="End-to-end pull request review workflow", -) - -# Create a bundle version that pins specific versions -bundle_version = mlflow.genai.skills.create_skill_bundle_version( - name="pr-workflow", - version="1.0.0", - skills=[ - ("code-review", "1.0.0"), - ("test-coverage", "2.1.0"), - ], - subagents=[ - ("security-auditor", "1.0.0"), - ], -) - -# Set an alias for stable resolution -mlflow.genai.skills.set_skill_bundle_alias( - name="pr-workflow", - alias="production", - version="1.0.0", -) -``` - -## Register other capability types - -```python -# Register a subagent -mlflow.genai.skills.register_subagent( - name="security-auditor", - version="1.0.0", - description="Security specialist for auth and payment code", - source_type="git", - source="https://github.com/acme/agent-skills/tree/v1.0.0/security-auditor", -) - -# Register a hook -mlflow.genai.skills.register_hook( - name="pre-commit-scan", - version="1.0.0", - description="Runs security scan before tool commits", - source_type="git", - source="https://github.com/acme/agent-skills/tree/v1.0.0/pre-commit-scan", -) -``` - -## Create a skill bundle with cross-registry references - -```python -# A bundle version can include skills, subagents, hooks, and MCP servers -bundle_version = mlflow.genai.skills.create_skill_bundle_version( - name="pr-workflow", - version="1.0.0", - skills=[ - ("code-review", "1.0.0"), - ], - subagents=[ - ("security-auditor", "1.0.0"), - ], - # Reference MCP servers from the MCP registry (RFC-0004) - mcp_servers=[ - ("github-mcp", "2.0.0"), - ], -) -``` - -## 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 bundle with a bundle-level OCI source -bundle_version = mlflow.genai.skills.create_skill_bundle_version( - name="pr-workflow", - version="1.0.0", - source_type="oci", - source="ghcr.io/acme/agent-plugin:v1.0.0", - skills=[ - ("code-review", "1.0.0"), - ("test-coverage", "2.1.0"), - ], -) -``` - -## Pull skills to a local directory - -```python -# Pull a single skill version -mlflow.genai.skills.pull( - name="code-review", - alias="production", - destination="./skills/code-review", -) - -# Pull an entire skill bundle (all members) -mlflow.genai.skills.pull( - bundle="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 --bundle pr-workflow --alias production \ - --destination ./plugins/pr-workflow -``` - -## Discover and consume skills - -```python -# Search for active skill versions -versions = mlflow.genai.skills.search_skill_versions( - name="code-review", - filter_string="status = 'active'", -) - -# Search for active skill bundles -bundles = mlflow.genai.skills.search_skill_bundles( - filter_string="status = 'active'", -) - -# Get a specific version -version = mlflow.genai.skills.get_skill_version( - name="code-review", - version="1.0.0", -) -# version.source_type == "git" -# version.source == "https://github.com/acme/agent-skills/tree/v1.0.0/code-review" - -# Resolve by alias -version = mlflow.genai.skills.get_skill_version_by_alias( - name="code-review", - alias="production", -) - -# Get a bundle version and its pinned members -bundle_version = mlflow.genai.skills.get_skill_bundle_version( - name="pr-workflow", - version="1.0.0", -) -# bundle_version.skills == [("code-review", "1.0.0"), ...] -# bundle_version.subagents == [("security-auditor", "1.0.0"), ...] -# bundle_version.mcp_servers == [("github-mcp", "2.0.0"), ...] - -# Resolve a bundle alias -bundle_version = mlflow.genai.skills.get_skill_bundle_version_by_alias( - name="pr-workflow", - alias="production", -) -``` - -CLI equivalents for these operations use `mlflow skills`, `mlflow -subagents`, `mlflow hooks`, and `mlflow skill-bundles` command groups. -See the user journeys below for CLI examples. - -## Motivation - -### The problem - -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 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 subagents. -- **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**: packaging of skills, subagents, MCP configs, and - hooks into a single installable unit. Formats range from - harness-specific (Claude Code and Codex CLI `plugin.json` manifests) - to cross-harness (e.g., Lola's "AI Context Modules," which use - directory auto-discovery to target multiple harnesses from a single - package). - -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 status lifecycle.** Git has no concept of "this version is - approved for production use" vs. "this is deprecated." Teams resort - to branch naming conventions or external tracking to manage - promotion. - -2. **Fragmented discovery.** 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 cross-type bundling.** Agent harnesses like Claude Code and - Codex CLI support plugins that bundle skills, subagents, MCP - servers, and hooks together. But there is no agent-neutral way to - represent these bundles for governance and discovery. - -5. **No trace-to-skill linkage.** MLflow already traces agent - conversations (Claude Code via `mlflow autolog claude`, SDK - applications via framework autologgers such as - `mlflow.langchain.autolog()` and `mlflow.anthropic.autolog()`). These traces capture LLM calls, - tool use, and token consumption, but there is no way to know which - governed, versioned skill was active during any part of a trace. - Without a registry, organizations cannot answer questions like - "which skill versions are most used?" or "show me all traces where - the deprecated code-review v1.0 was loaded." - -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. - -### User journeys - -#### Register a skill bundle +## Register a skill bundle 1. Register individual capability versions pointing to their sources: ```bash @@ -335,6 +57,21 @@ address: mlflow hooks register --name pre-commit-scan --version 1.0.0 \ --source https://github.com/acme/agent-skills/tree/v1.0.0/pre-commit-scan ``` + **SDK equivalent:** + ```python + import mlflow + + mlflow.genai.skills.register_skill( + name="code-review", + version="1.0.0", + description="Reviews pull requests for correctness, style, and security", + source_type="git", + source="https://github.com/acme/agent-skills/tree/v1.0.0/code-review", + ) + ``` + **UI path:** Navigate to the Skills page, click "Register Skill," + fill in name, version, source type, and source URL, then submit. + Repeat for subagents and hooks using the type selector. 2. Create a skill bundle version that pins these members: ```bash mlflow skill-bundles create-version --name pr-workflow --version 1.0.0 \ @@ -342,29 +79,42 @@ address: --subagent security-auditor:1.0.0 \ --hook pre-commit-scan:1.0.0 ``` + **UI path:** Navigate to the Bundles tab, click "Create Bundle," + add members by searching and selecting from registered capabilities. 3. Transition the bundle version from draft to active: ```bash mlflow skill-bundles update-version --name pr-workflow \ --version 1.0.0 --status active ``` + **UI path:** Open the bundle version detail page, use the status + dropdown to change from "draft" to "active." 4. Set an alias for stable downstream resolution: ```bash mlflow skill-bundles set-alias --name pr-workflow \ --alias production --version 1.0.0 ``` + **UI path:** In the bundle detail page, click "Add Alias" and map + `production` to version `1.0.0`. -#### Discover a skill for a specific purpose +## Discover a skill for a specific purpose 1. Search the registry by keyword: ```bash mlflow skills search --filter "name LIKE '%review%'" --status active ``` + **UI path:** Navigate to the Skills page, type "review" in the + search bar, and filter by status "active" using the dropdown. 2. Browse the returned list of matching skills with names, descriptions, and latest versions. + **UI path:** Scan the card-based list view. Each card shows the + skill name, description, latest version badge, status badge, and + tags. 3. Get details on a promising result: ```bash mlflow skills get --name code-review ``` + **UI path:** Click a card to open the detail view with metadata, + version history, aliases, tags, and bundle memberships. 4. Inspect a specific version's source and metadata: ```bash mlflow skills get-version --name code-review --version 1.0.0 @@ -376,26 +126,28 @@ address: --destination ./review-skill ``` -#### Install a skill bundle, run the agent, browse traces +## Install a skill bundle, run the agent, browse traces 1. Install the bundle for a harness ([RFC-0006](../0006-skill-harness-integration/0006-skill-harness-integration.md)): ```bash mlflow skills install --bundle pr-workflow --alias production \ - --harness claude-code --destination . --lock + --harness claude-code --lock ``` This pulls all member content, generates harness-specific manifests, writes a lock file, and writes a trace manifest. 2. Run the agent. The harness loads the installed plugin and invokes skills during a conversation. -3. Open the MLflow UI and navigate to the Traces page. +3. Open the MLflow UI and navigate to the Traces page. Click the + "Skills" tab to filter for traces with SKILL spans. 4. Find the trace for the agent run. Skill invocations appear as SKILL spans in the trace tree, annotated with registry coordinates (skill name, version, registry). 5. Click a SKILL span to see which registered skill version was used - and how long it took. + and how long it took. Click the skill name link to navigate to the + skill's registry detail page. -#### Evaluate two bundle versions with Agent-as-a-Judge +## Evaluate two bundle versions with Agent-as-a-Judge MLflow's [Agent-as-a-Judge](https://mlflow.org/docs/latest/genai/eval-monitor/scorers/llm-judge/agentic-overview/) @@ -430,7 +182,7 @@ Agent-as-a-Judge can analyze how skills were used during an agent run. --alias production --version 2.0.0 ``` -#### CI pipeline for automated regression detection +## CI pipeline for automated regression detection 1. A CI job (e.g., GitHub Actions) triggers on pushes to the skill source repo. @@ -458,6 +210,77 @@ Agent-as-a-Judge can analyze how skills were used during an agent run. 7. If no regression is detected, the job transitions the new version to active and optionally updates the production alias. +See [implementation-details.md: SDK and CLI code +examples](implementation-details.md#sdk-and-cli-code-examples) for +additional SDK examples including cross-registry bundles, OCI subpath +registration, and discovery/search operations. + +## Motivation + +### The problem + +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 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 subagents. +- **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**: packaging of skills, subagents, MCP configs, and + hooks into a single installable unit. Formats range from + harness-specific (Claude Code and Codex CLI `plugin.json` manifests) + to cross-harness (e.g., Lola's "AI Context Modules," which use + directory auto-discovery to target multiple harnesses from a single + package). + +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 status lifecycle.** Git has no concept of "this version is + approved for production use" vs. "this is deprecated." Teams resort + to branch naming conventions or external tracking to manage + promotion. + +2. **Fragmented discovery.** 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 cross-type bundling.** Agent harnesses like Claude Code and + Codex CLI support plugins that bundle skills, subagents, MCP + servers, and hooks together. But there is no agent-neutral way to + represent these bundles for governance and discovery. + +5. **No trace-to-skill linkage.** MLflow already traces agent + conversations (Claude Code via `mlflow autolog claude`, SDK + applications via framework autologgers such as + `mlflow.langchain.autolog()` and `mlflow.anthropic.autolog()`). These traces capture LLM calls, + tool use, and token consumption, but there is no way to know which + governed, versioned skill was active during any part of a trace. + Without a registry, organizations cannot answer questions like + "which skill versions are most used?" or "show me all traces where + the deprecated code-review v1.0 was loaded." + +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. + ### Out of scope - **Artifact storage as the only path.** The registry supports both @@ -467,21 +290,21 @@ Agent-as-a-Judge can analyze how skills were used during an agent run. - **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, subagent, MCP config, or hook - looks like. -- **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.** 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. -- **Prompts.** MLflow's prompt registry manages template strings with - variable placeholders, loaded at runtime by custom code via - `mlflow.genai.load_prompt()`. Skills are structurally different - (sets of files installed by a harness) and serve a different + not define what a skill must contain or how it must be structured. + The SKILL.md convention is an ecosystem convention, not a registry + requirement. +- **Agent routing or orchestration.** The registry is a metadata and + governance layer. It does not decide which skills to invoke at + runtime or how agents compose capabilities. +- **MCP server hosting.** MCP server deployment and runtime management + are covered by the MCP Server Registry (RFC-0004) and the MCP + Gateway. +- **Prompts.** MLflow already has a Prompt Registry for versioned + prompt template management. Skills and prompts serve different + purposes: a skill provides instructions and tools for agent + autonomy, while a prompt provides templated text for structured + generation. Skills may reference prompts, but they belong in + separate registries because they have different lifecycles, different audience (harness-based agents vs. custom agentic code). The two registries are complementary but separate. @@ -514,43 +337,13 @@ SkillBundleVersion }o--o{ MCPServerVersion : "mcp_servers" A skill is a directory containing a SKILL.md entry point plus supporting files (scripts, templates, reference material). The `Skill` entity is the logical governed asset, scoped to a workspace. +Key fields include `name` (unique within workspace), `display_name`, +`status` (derived from latest version), `aliases`, and +`latest_version`. -```python -from dataclasses import dataclass, field -from enum import StrEnum - - -class SkillStatus(StrEnum): - DRAFT = "draft" - ACTIVE = "active" - DEPRECATED = "deprecated" - DELETED = "deleted" - - -@dataclass -class Skill: - name: str - display_name: str | None = None - description: str | None = None - workspace: str | None = None - status: SkillStatus = SkillStatus.DRAFT - tags: dict[str, str] = field(default_factory=dict) - aliases: list[SkillAlias] = field(default_factory=list) - latest_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 | -| `display_name` | `str` | Mutable human-readable label for UI display | -| `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`) | -| `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 | +See [implementation-details.md: Skill +entity](implementation-details.md#skill-entity) for the dataclass +definition and field table. **MCP servers.** MCP servers are registered in the MCP Server Registry (RFC-0004), not in this registry. Skill bundles can reference MCP @@ -561,118 +354,22 @@ installation (RFC-0006), not as separately registered entities. #### SkillVersion -A versioned record containing a typed source pointer, status, and -tags. - -```python -class SkillSourceType(StrEnum): - GIT = "git" - OCI = "oci" - ZIP = "zip" - MLFLOW = "mlflow" - - -@dataclass -class SkillVersion: - name: str - version: str - display_name: str | None = None - source_type: SkillSourceType | None = None - source: str | None = None - subpath: str | None = None - status: SkillStatus = SkillStatus.DRAFT - content_digest: str | None = None - tags: dict[str, str] = field(default_factory=dict) - aliases: list[str] = field(default_factory=list) - workspace: str | None = None - created_by: str | None = None - last_updated_by: str | None = None - creation_timestamp: int | None = None - last_updated_timestamp: int | None = None -``` - -| Field | Type | Description | -|---|---|---| -| `version` | `str` | Publisher-supplied version string. Semver recommended but not enforced | -| `display_name` | `str` | Mutable human-readable label for UI display | -| `source_type` | `SkillSourceType` | Optional distribution mechanism: `git`, `oci`, `zip`, `mlflow` | -| `source` | `str` | Pointer to the content in the source system. Required for standalone pull. May be omitted only when the version's content lives within a bundle-level artifact (identified by `subpath` on the member) | -| `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) | - -**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. - -**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 -have external Git/OCI infrastructure, who want agent capabilities -stored alongside their models, or who operate in airgapped -environments where external sources are not reachable. - -Content is stored as a directory tree of individual files under an -artifact path, consistent with how MLflow stores model artifacts. For -example, a skill with a SKILL.md, scripts, and reference material is -stored as separate artifacts under a version-specific prefix: - -``` -skills/code-review/1.0.0/ - SKILL.md - scripts/analyze.sh - scripts/lint-config.json - reference/style-guide.md -``` - -The `source` field contains the artifact URI as resolved by MLflow's -artifact storage (e.g., `mlflow-artifacts:/skills/code-review/1.0.0/` -when using the artifact proxy, or a direct artifact-store URI -otherwise). `source_type="mlflow"` means "stored in MLflow-managed -artifact storage," not a specific URI scheme. Pull downloads the -directory tree from the artifact store. The MLflow UI can browse -individual files within a stored skill version when artifact proxying -is enabled. - -The upload API accepts a local directory path and stores each file as -a separate artifact. The `content_digest` is computed over the full -directory contents at upload time. - -**Version uniqueness.** The combination of `(name, version)` is unique -within a workspace. A skill version represents a single logical -version of a capability; `source_type` and `source` describe where to -find it but are not part of its identity. - -**Content integrity.** The optional `content_digest` field stores a -digest of the skill content at registration time (e.g., -`sha256:abc123...`). For `source_type="mlflow"`, the server computes -the digest at upload time and stores it on the version; on pull, the -client recomputes the digest over the downloaded content and rejects -the result if it does not match, detecting out-of-band modification -of the underlying artifact store. For external source types (git, oci, -zip), `content_digest` is client-supplied: for OCI sources, this is -the native image digest; for Git sources, a digest of the file -contents at the pinned commit; for ZIP sources, a digest of the -archive. The registry stores the digest but does not verify it on -read; verification is the consumer's responsibility. - -**Immutability contract.** `source_type`, `source`, `subpath`, -`content_digest`, and `version` are immutable after creation. To point -to different content, register a new version. Mutable fields (`display_name`, -`status`, `tags`) can be updated independently. +A versioned record containing a typed source pointer (`git`, `oci`, +`zip`, or `mlflow`), status, and tags. The `(name, version)` pair is +unique within a workspace. Source pointers and version strings are +immutable after creation; to point to different content, register a +new version. The optional `subpath` field identifies content within a +shared artifact (used with OCI and ZIP). The optional `content_digest` +field enables integrity verification. + +See [implementation-details.md: SkillVersion +entity](implementation-details.md#skillversion-entity) for the +dataclass definition and field table. See +[implementation-details.md: SkillVersion field +details](implementation-details.md#skillversion-field-details) for +source type extensibility, subpath usage by source type, MLflow +artifact storage, version uniqueness, content integrity, and +immutability contract. #### Subagent and Hook @@ -692,27 +389,11 @@ parallel operations for each type. A skill bundle groups related capabilities (skills, subagents, hooks, and MCP servers) into a governed unit that maps to the "plugin" concept in agent harnesses. Follows the same top-level pattern as -Skill: versions, tags, and aliases. +Skill: versions, tags, aliases, and derived status. -```python -@dataclass -class SkillBundle: - name: str - display_name: str | None = None - description: str | None = None - workspace: str | None = None - status: SkillStatus = SkillStatus.DRAFT - tags: dict[str, str] = field(default_factory=dict) - aliases: list["SkillBundleAlias"] = field(default_factory=list) - latest_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 -``` - -`SkillBundle.status` is read-only, derived from the latest bundle -version's status. `latest_version` works the same as on `Skill`. +See [implementation-details.md: SkillBundle +entity](implementation-details.md#skillbundle-entity) for the +dataclass definition. **Why bundles instead of tags?** Tags could express "these skills are related" but cannot provide versioned membership snapshots @@ -724,101 +405,20 @@ concept. #### SkillBundleVersion -A versioned snapshot of a skill bundle's membership. Each version -captures a specific set of capabilities that work together, organized -by type. - -```python -@dataclass -class SkillBundleVersion: - name: str - version: str - display_name: str | None = None - source_type: SkillSourceType | None = None - source: str | None = None - subpath: str | None = None - content_digest: str | None = None - status: SkillStatus = SkillStatus.DRAFT - tags: dict[str, str] = field(default_factory=dict) - skills: list[tuple[str, str]] = field(default_factory=list) - subagents: list[tuple[str, str]] = field(default_factory=list) - hooks: list[tuple[str, str]] = field(default_factory=list) - mcp_servers: list[tuple[str, str]] = field(default_factory=list) - aliases: list[str] = field(default_factory=list) - workspace: str | None = None - created_by: str | None = None - last_updated_by: str | None = None - creation_timestamp: int | None = None - last_updated_timestamp: int | None = None -``` - -Each member list contains `(name, version)` tuples. The `skills`, -`subagents`, and `hooks` lists reference entities in this registry. -The `mcp_servers` list references entries in the MCP Server Registry -(RFC-0004). - -**Version uniqueness.** The combination of `(name, version)` is unique -within a workspace. - -**Bundle-level source.** A bundle version can optionally have its own -`source_type`, `source`, `subpath`, and `content_digest`, pointing to -a single artifact (e.g., an OCI image or Git repo) that contains the -complete plugin. When present, `pull` fetches the bundle 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 bundle-level artifact use `subpath` -on their version entities to identify their location within the -artifact. - -The bundle artifact is a generic package of content (skill files, -agent definitions, hook scripts). It may or may not be -harness-ready; the adapter does not assume either way. Harness -adapters (RFC-0006) generate harness-specific manifests from -registry metadata at install time, since the registry is the -governed source of truth. If the artifact -contents disagree with the declared members (e.g., a `subpath` -points to a missing directory), pull succeeds but install fails -when the adapter cannot find the expected content. Correctness of -the artifact layout is the publisher's responsibility; the registry -does not validate artifact contents at registration time. - -**Source resolution for pull.** When pulling a bundle, if the bundle -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 member lists and source fields of a -bundle version are immutable after creation. To change the set of -members or source pointer, register a new bundle version. Mutable -fields (`display_name`, `status`, `tags`) can be updated independently. - -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 bundles 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 bundles reference them rather than carrying - standalone copies. - -A member can appear in multiple bundles and multiple bundle versions. -Membership is at the version level, so a bundle version is a -reproducible snapshot of "these specific asset versions work together." - -**Bundle-level source and embedded MCP configs.** When a bundle -version has a bundle-level source (e.g., a single OCI image -containing a complete plugin), the artifact may include MCP configs -alongside skills and subagents. 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. +A versioned snapshot of a bundle's membership. Each version captures +`(name, version)` tuples for skills, subagents, hooks, and MCP +servers. A bundle version can optionally have its own source pointer +(e.g., a single OCI image containing a complete plugin), in which +case `pull` fetches the bundle artifact as a unit rather than pulling +members individually. + +See [implementation-details.md: SkillBundleVersion +entity](implementation-details.md#skillbundleversion-entity) for the +dataclass definition. See [implementation-details.md: +SkillBundleVersion field +details](implementation-details.md#skillbundleversion-field-details) +for cross-registry reference details, embedded MCP config handling, +source resolution, and immutability contract. #### Aliases and tags @@ -926,35 +526,15 @@ different sources). If `content_digest` is set, `pull` verifies the fetched content matches the digest and returns an error on mismatch. -**Source availability.** The registry stores source pointers but does -not cache or proxy content. If a source is unreachable or the content -has been deleted, pull fails with an error that surfaces the -underlying failure from the source system (e.g., Git clone failure, -OCI pull 404, HTTP download error). Source availability is the -publisher's responsibility. For bundle pulls, if one member's source -is unavailable, the entire pull fails rather than producing a partial -result. - -**Source authentication.** The registry server stores source pointers -but does not validate source accessibility at registration time and is -not involved in content transfer at pull time. Authentication to -external sources is handled entirely by the client environment: - -| Source type | Authentication mechanism | -|---|---| -| `git` | Standard Git credential resolution: SSH keys (`~/.ssh/`), Git credential helpers (`git-credential-manager`, `git-credential-store`), `.netrc`, and `GIT_SSH_COMMAND`. Private repos work if the caller's Git is configured to access them. | -| `oci` | OCI registry credential resolution: Docker config (`~/.docker/config.json`), registry-specific credential helpers, and container runtime auth. Private registries work if the caller has a valid login session. | -| `zip` | No authentication support. ZIP sources must be publicly accessible URLs. For private content, use `git` or `oci` source types instead. | -| `mlflow` | MLflow artifact storage authentication, using the same credentials as other MLflow API calls. | - -The registry does not store, proxy, or manage source credentials. -Pull failures due to authentication errors are surfaced to the caller -with the underlying error from the source system. - `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. +See [implementation-details.md: Pull semantics +details](implementation-details.md#pull-semantics-details) for source +authentication mechanisms per source type, source availability error +handling, and credential management. + ### Workspace scoping All skill registry operations are workspace-scoped, following MLflow's @@ -991,20 +571,81 @@ Key design choices: ### 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, subagents, hooks, and bundles together, -with name, description, latest version, status, and tags. Users can -filter by type (skill, subagent, hook, bundle), status, source type, -and search by name or description. - -The detail view for a skill, subagent, or hook shows metadata, version -list, aliases, tags, and bundle memberships. +> **Note:** The descriptions below are for illustrative purposes only +> and do not fully align with the MLflow design system. The final +> implementation will follow MLflow's established design system and +> component library, consistent with the MCP Servers page (RFC-0004). -The detail view for a skill bundle shows its description, status, -version list, aliases, and tags. Each bundle version shows its status -and the pinned member versions it contains. +The Skills page lives under the GenAI workflow in the MLflow sidebar, +alongside Experiments, Prompts, MCP Servers, and AI Gateway. + +#### List view + +The list view shows skills, subagents, hooks, and bundles together +using a card-based layout consistent with other MLflow pages. Each +card displays: + +- Entity type badge (skill, subagent, hook, or bundle) +- Name and optional display name +- Description (truncated to 2-3 lines) +- Latest version badge (e.g., "v1.0.0") +- Status badge with color coding: draft (gray), active (green), + deprecated (amber) +- Source type indicator (Git, OCI, ZIP, MLflow) +- Tag chips + +The filter bar provides: + +- **Type dropdown**: skill, subagent, hook, bundle (multi-select) +- **Status dropdown**: draft, active, deprecated +- **Source type dropdown**: git, oci, zip, mlflow +- **Search**: by name or description + +A grid/list toggle allows switching between card and table views. A +"Register Skill" button (with a dropdown for subagent, hook, or +bundle) initiates registration. + +#### Detail view: skills, subagents, hooks + +The detail view for an individual capability shows: + +- **Metadata section**: name, display name, description, status, + workspace, source type, created by, created at, last updated +- **Version table**: Version, Registered at, Status, Source type, + Created by, Description. Clicking a version row navigates to the + version detail page showing source, subpath, content digest, and + tags. +- **Aliases**: alias name to version mapping (e.g., + `production -> 1.0.0`) +- **Tags**: key-value list with edit controls +- **Bundle memberships**: list of bundles that include this capability, + with links to each bundle's detail page +- **Related traces**: link to the GenAI Traces page filtered by this + skill's name, showing recent SKILL spans that reference this + capability + +#### Detail view: bundles + +The bundle detail view shows: + +- **Metadata section** (as above) +- **Members table** for the selected bundle version, grouped by type: + Type (skill/subagent/hook/mcp_server), Name, Pinned version, Source + type, Status. Each row links to the member's detail page. + Cross-registry members (MCP servers) link to the MCP Server Registry + detail page. +- **Version history table**: Version, Registered at, Status, Created + by, Member count +- **Aliases and tags** (as above) + +#### Trace integration display + +The GenAI Traces page includes a "Skills" tab alongside the existing +"Prompts" tab, showing SKILL spans for each trace. The trace detail +view displays SKILL spans with registry coordinates (skill name, +version, workspace) and links to the skill's registry detail page. +Skill version detail pages surface related traces using the same +association data. ### Trace integration @@ -1024,23 +665,54 @@ span of type `SKILL` and attaches registry coordinates as span attributes: ```python -with mlflow.skill_context(name="code-review", version="1.0.0") as span: +with mlflow.skill_context( + name="code-review", version="1.0.0", workspace=None +) as span: # All spans created inside this block (including those from # autologgers) become children of this SKILL span. result = llm.chat([{"role": "user", "content": "Review this code..."}]) ``` -The context manager creates a span with the following attributes: - -| Attribute | Value | Description | -|---|---|---| -| `mlflow.skill.name` | Skill name | Registry name of the active skill | -| `mlflow.skill.version` | Version string | Registered version | -| `mlflow.skill.registry` | Workspace name | MLflow workspace (defaults to `"default"`) | - -These three attributes form the `{workspace, name, version}` -coordinates that link the span back to a specific skill version in -the registry. +The context manager creates a span with `mlflow.skill.name`, +`mlflow.skill.version`, and `mlflow.skill.registry` (workspace) +attributes that link the span back to a specific skill version in +the registry. See [implementation-details.md: skill_context() span +attributes](implementation-details.md#skill_context-span-attributes) +for the full attribute table. + +#### Scope of skill_context() + +`skill_context()` is for skills only, not subagents, hooks, or +bundles: + +- **Subagents** produce their own spans via existing harness tracing. + For example, Claude Code traces sub-agent invocations as tool use + spans via `mlflow autolog claude`. A separate `subagent_context()` + would duplicate existing span creation. +- **Hooks** are infrastructure-level actions that execute outside the + agent's reasoning loop. They are visible in harness logs but are not + traced at the registry level. +- **Skill bundles** are governance and install-time concepts, not + runtime concepts. A bundle is never "invoked" during a conversation. + Bundle-level analytics are derived by aggregating over traces of + individual member skills. + +#### Workspace resolution + +When `skill_context()` is called, the `workspace` parameter +determines which registry instance the span links to: + +1. **Manifest lookup.** If `mlflow-skills-manifest.json` exists in the + project (written by `mlflow skills install`, defined in + [RFC-0006](../0006-skill-harness-integration/0006-skill-harness-integration.md)), + the `registry` field for the matching skill name provides the + workspace. This is the default path for installed skills. +2. **Tracking URI default.** If no manifest is found (SDK users + calling `skill_context()` directly), the workspace defaults to the + current MLflow tracking URI's workspace context, consistent with + other MLflow operations. +3. **Explicit override.** Callers can pass + `workspace="my-workspace"` for full control. #### Skill stacks via nesting diff --git a/rfcs/0005-skill-registry/implementation-details.md b/rfcs/0005-skill-registry/implementation-details.md index 6deaaa8..280b347 100644 --- a/rfcs/0005-skill-registry/implementation-details.md +++ b/rfcs/0005-skill-registry/implementation-details.md @@ -687,3 +687,461 @@ method, 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. + +## Skill entity + +A skill is a directory containing a SKILL.md entry point plus +supporting files (scripts, templates, reference material). The +`Skill` entity is the logical governed asset, scoped to a workspace. + +```python +from dataclasses import dataclass, field +from enum import StrEnum + + +class SkillStatus(StrEnum): + DRAFT = "draft" + ACTIVE = "active" + DEPRECATED = "deprecated" + DELETED = "deleted" + + +@dataclass +class Skill: + name: str + display_name: str | None = None + description: str | None = None + workspace: str | None = None + status: SkillStatus = SkillStatus.DRAFT + tags: dict[str, str] = field(default_factory=dict) + aliases: list[SkillAlias] = field(default_factory=list) + latest_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 | +| `display_name` | `str` | Mutable human-readable label for UI display | +| `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`) | +| `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 | + +## SkillVersion entity + +A versioned record containing a typed source pointer, status, and +tags. + +```python +class SkillSourceType(StrEnum): + GIT = "git" + OCI = "oci" + ZIP = "zip" + MLFLOW = "mlflow" + + +@dataclass +class SkillVersion: + name: str + version: str + display_name: str | None = None + source_type: SkillSourceType | None = None + source: str | None = None + subpath: str | None = None + status: SkillStatus = SkillStatus.DRAFT + content_digest: str | None = None + tags: dict[str, str] = field(default_factory=dict) + aliases: list[str] = field(default_factory=list) + workspace: str | None = None + created_by: str | None = None + last_updated_by: str | None = None + creation_timestamp: int | None = None + last_updated_timestamp: int | None = None +``` + +| Field | Type | Description | +|---|---|---| +| `version` | `str` | Publisher-supplied version string. Semver recommended but not enforced | +| `display_name` | `str` | Mutable human-readable label for UI display | +| `source_type` | `SkillSourceType` | Optional distribution mechanism: `git`, `oci`, `zip`, `mlflow` | +| `source` | `str` | Pointer to the content in the source system. Required for standalone pull. May be omitted only when the version's content lives within a bundle-level artifact (identified by `subpath` on the member) | +| `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) | + +## SkillVersion field details + +**Source type extensibility.** The `source_type` enum is intentionally +small for the initial implementation. New source types (e.g., `s3`, +`azure-blob`) 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 +have external Git/OCI infrastructure, who want agent capabilities +stored alongside their models, or who operate in airgapped +environments where external sources are not reachable. + +Content is stored as a directory tree of individual files under an +artifact path, consistent with how MLflow stores model artifacts. For +example, a skill with a SKILL.md, scripts, and reference material is +stored as separate artifacts under a version-specific prefix: + +``` +skills/code-review/1.0.0/ + SKILL.md + scripts/analyze.sh + scripts/lint-config.json + reference/style-guide.md +``` + +The `source` field contains the artifact URI as resolved by MLflow's +artifact storage (e.g., `mlflow-artifacts:/skills/code-review/1.0.0/` +when using the artifact proxy, or a direct artifact-store URI +otherwise). `source_type="mlflow"` means "stored in MLflow-managed +artifact storage," not a specific URI scheme. Pull downloads the +directory tree from the artifact store. The MLflow UI can browse +individual files within a stored skill version when artifact proxying +is enabled. + +The upload API accepts a local directory path and stores each file as +a separate artifact. The `content_digest` is computed over the full +directory contents at upload time. + +**Version uniqueness.** The combination of `(name, version)` is unique +within a workspace. A skill version represents a single logical +version of a capability; `source_type` and `source` describe where to +find it but are not part of its identity. + +**Content integrity.** The optional `content_digest` field stores a +digest of the skill content at registration time (e.g., +`sha256:abc123...`). For `source_type="mlflow"`, the server computes +the digest at upload time and stores it on the version; on pull, the +client recomputes the digest over the downloaded content and rejects +the result if it does not match, detecting out-of-band modification +of the underlying artifact store. For external source types (git, oci, +zip), `content_digest` is client-supplied: for OCI sources, this is +the native image digest; for Git sources, a digest of the file +contents at the pinned commit; for ZIP sources, a digest of the +archive. The registry stores the digest but does not verify it on +read; verification is the consumer's responsibility. + +**Immutability contract.** `source_type`, `source`, `subpath`, +`content_digest`, and `version` are immutable after creation. To point +to different content, register a new version. Mutable fields (`display_name`, +`status`, `tags`) can be updated independently. + +## SkillBundle entity + +A skill bundle groups related capabilities (skills, subagents, hooks, +and MCP servers) into a governed unit that maps to the "plugin" +concept in agent harnesses. Follows the same top-level pattern as +Skill: versions, tags, and aliases. + +```python +@dataclass +class SkillBundle: + name: str + display_name: str | None = None + description: str | None = None + workspace: str | None = None + status: SkillStatus = SkillStatus.DRAFT + tags: dict[str, str] = field(default_factory=dict) + aliases: list["SkillBundleAlias"] = field(default_factory=list) + latest_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 +``` + +`SkillBundle.status` is read-only, derived from the latest bundle +version's status. `latest_version` works the same as on `Skill`. + +## SkillBundleVersion entity + +A versioned snapshot of a skill bundle's membership. Each version +captures a specific set of capabilities that work together, organized +by type. + +```python +@dataclass +class SkillBundleVersion: + name: str + version: str + display_name: str | None = None + source_type: SkillSourceType | None = None + source: str | None = None + subpath: str | None = None + content_digest: str | None = None + status: SkillStatus = SkillStatus.DRAFT + tags: dict[str, str] = field(default_factory=dict) + skills: list[tuple[str, str]] = field(default_factory=list) + subagents: list[tuple[str, str]] = field(default_factory=list) + hooks: list[tuple[str, str]] = field(default_factory=list) + mcp_servers: list[tuple[str, str]] = field(default_factory=list) + aliases: list[str] = field(default_factory=list) + workspace: str | None = None + created_by: str | None = None + last_updated_by: str | None = None + creation_timestamp: int | None = None + last_updated_timestamp: int | None = None +``` + +Each member list contains `(name, version)` tuples. The `skills`, +`subagents`, and `hooks` lists reference entities in this registry. +The `mcp_servers` list references entries in the MCP Server Registry +(RFC-0004). + +## SkillBundleVersion field details + +**Version uniqueness.** The combination of `(name, version)` is unique +within a workspace. + +**Bundle-level source.** A bundle version can optionally have its own +`source_type`, `source`, `subpath`, and `content_digest`, pointing to +a single artifact (e.g., an OCI image or Git repo) that contains the +complete plugin. When present, `pull` fetches the bundle 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 bundle-level artifact use `subpath` +on their version entities to identify their location within the +artifact. + +The bundle artifact is a generic package of content (skill files, +agent definitions, hook scripts). It may or may not be +harness-ready; the adapter does not assume either way. Harness +adapters (RFC-0006) generate harness-specific manifests from +registry metadata at install time, since the registry is the +governed source of truth. If the artifact +contents disagree with the declared members (e.g., a `subpath` +points to a missing directory), pull succeeds but install fails +when the adapter cannot find the expected content. Correctness of +the artifact layout is the publisher's responsibility; the registry +does not validate artifact contents at registration time. + +**Source resolution for pull.** When pulling a bundle, if the bundle +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 member lists and source fields of a +bundle version are immutable after creation. To change the set of +members or source pointer, register a new bundle version. Mutable +fields (`display_name`, `status`, `tags`) can be updated independently. + +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 bundles 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 bundles reference them rather than carrying + standalone copies. + +A member can appear in multiple bundles and multiple bundle versions. +Membership is at the version level, so a bundle version is a +reproducible snapshot of "these specific asset versions work together." + +**Bundle-level source and embedded MCP configs.** When a bundle +version has a bundle-level source (e.g., a single OCI image +containing a complete plugin), the artifact may include MCP configs +alongside skills and subagents. 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. + +## Pull semantics details + +**Source availability.** The registry stores source pointers but does +not cache or proxy content. If a source is unreachable or the content +has been deleted, pull fails with an error that surfaces the +underlying failure from the source system (e.g., Git clone failure, +OCI pull 404, HTTP download error). Source availability is the +publisher's responsibility. For bundle pulls, if one member's source +is unavailable, the entire pull fails rather than producing a partial +result. + +**Source authentication.** The registry server stores source pointers +but does not validate source accessibility at registration time and is +not involved in content transfer at pull time. Authentication to +external sources is handled entirely by the client environment: + +| Source type | Authentication mechanism | +|---|---| +| `git` | Standard Git credential resolution: SSH keys (`~/.ssh/`), Git credential helpers (`git-credential-manager`, `git-credential-store`), `.netrc`, and `GIT_SSH_COMMAND`. Private repos work if the caller's Git is configured to access them. | +| `oci` | OCI registry credential resolution: Docker config (`~/.docker/config.json`), registry-specific credential helpers, and container runtime auth. Private registries work if the caller has a valid login session. | +| `zip` | No authentication support. ZIP sources must be publicly accessible URLs. For private content, use `git` or `oci` source types instead. | +| `mlflow` | MLflow artifact storage authentication, using the same credentials as other MLflow API calls. | + +The registry does not store, proxy, or manage source credentials. +Pull failures due to authentication errors are surfaced to the caller +with the underlying error from the source system. + +`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. + +## skill_context() span attributes + +The `skill_context()` context manager creates a span with the +following attributes: + +| Attribute | Value | Description | +|---|---|---| +| `mlflow.skill.name` | Skill name | Registry name of the active skill | +| `mlflow.skill.version` | Version string | Registered version | +| `mlflow.skill.registry` | Workspace name | MLflow workspace (defaults to `"default"`) | + +These three attributes form the `{workspace, name, version}` +coordinates that link the span back to a specific skill version in +the registry. + +## SDK and CLI code examples + +### Register other capability types + +```python +# Register a subagent +mlflow.genai.skills.register_subagent( + name="security-auditor", + version="1.0.0", + description="Security specialist for auth and payment code", + source_type="git", + source="https://github.com/acme/agent-skills/tree/v1.0.0/security-auditor", +) + +# Register a hook +mlflow.genai.skills.register_hook( + name="pre-commit-scan", + version="1.0.0", + description="Runs security scan before tool commits", + source_type="git", + source="https://github.com/acme/agent-skills/tree/v1.0.0/pre-commit-scan", +) +``` + +### Create a skill bundle with cross-registry references + +```python +# A bundle version can include skills, subagents, hooks, and MCP servers +bundle_version = mlflow.genai.skills.create_skill_bundle_version( + name="pr-workflow", + version="1.0.0", + skills=[ + ("code-review", "1.0.0"), + ], + subagents=[ + ("security-auditor", "1.0.0"), + ], + # Reference MCP servers from the MCP registry (RFC-0004) + mcp_servers=[ + ("github-mcp", "2.0.0"), + ], +) +``` + +### 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 bundle with a bundle-level OCI source +bundle_version = mlflow.genai.skills.create_skill_bundle_version( + name="pr-workflow", + version="1.0.0", + source_type="oci", + source="ghcr.io/acme/agent-plugin:v1.0.0", + skills=[ + ("code-review", "1.0.0"), + ("test-coverage", "2.1.0"), + ], +) +``` + +### Discover and consume skills + +```python +# Search for active skill versions +versions = mlflow.genai.skills.search_skill_versions( + name="code-review", + filter_string="status = 'active'", +) + +# Search for active skill bundles +bundles = mlflow.genai.skills.search_skill_bundles( + filter_string="status = 'active'", +) + +# Get a specific version +version = mlflow.genai.skills.get_skill_version( + name="code-review", + version="1.0.0", +) +# version.source_type == "git" +# version.source == "https://github.com/acme/agent-skills/tree/v1.0.0/code-review" + +# Resolve by alias +version = mlflow.genai.skills.get_skill_version_by_alias( + name="code-review", + alias="production", +) + +# Get a bundle version and its pinned members +bundle_version = mlflow.genai.skills.get_skill_bundle_version( + name="pr-workflow", + version="1.0.0", +) +# bundle_version.skills == [("code-review", "1.0.0"), ...] +# bundle_version.subagents == [("security-auditor", "1.0.0"), ...] +# bundle_version.mcp_servers == [("github-mcp", "2.0.0"), ...] + +# Resolve a bundle alias +bundle_version = mlflow.genai.skills.get_skill_bundle_version_by_alias( + name="pr-workflow", + alias="production", +) +``` + +CLI equivalents for these operations use `mlflow skills`, `mlflow +subagents`, `mlflow hooks`, and `mlflow skill-bundles` command groups. 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 6be4332..7a6cf73 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-06-01 | +| **Date Last Modified** | 2026-06-08 | | **AI Assistant(s)** | Claude Code (Opus 4.6) | # Summary @@ -201,30 +201,12 @@ class HarnessAdapter: ### Adapter summaries -**Claude Code / Codex CLI:** generates a plugin directory under -`.claude/plugins/` (or `.codex/plugins/`) with `plugin.json`, skill -files, subagent files, merged `.mcp.json` from MCP registry metadata, -and hook entries. MCP server credentials are the user's -responsibility. Hooks require explicit user opt-in. - -**Cursor:** places skills and subagents in `.cursor/skills/` and -`.cursor/agents/`. Merges MCP entries into `.cursor/mcp.json`. Hooks -are skipped (unsupported). - -**Antigravity:** places skills in `.agent/skills/`. Subagents, MCP -servers, and hooks are skipped. - -**Harness-agnostic bundle formats.** The adapter interface is not -limited to harness-specific formats. Cross-harness bundle formats -that package skills, subagents, hooks, and MCP servers together are -also valid adapter targets. For example, Lola -([LobsterTrap/lola](https://github.com/LobsterTrap/lola)) defines -an "AI Context Module" format that bundles these capability types -using directory auto-discovery and targets multiple harnesses from -a single module. An adapter for a format like this would support -both directions: `install` generates the cross-harness format from -registry metadata, and `import` introspects an existing module to -register its elements. +Each builtin adapter maps member types to harness-specific paths, +generates manifests, and skips unsupported types with warnings. See +[implementation-details.md: Adapter +summaries](implementation-details.md#adapter-summaries) for +per-adapter behavior (Claude Code / Codex CLI, Cursor, Antigravity, +and harness-agnostic bundle formats). Detailed directory layouts, MCP config generation rules, and hook handling behavior are in @@ -322,16 +304,28 @@ mlflow skills import --source ./my-plugin #### UI -The UI provides an import flow where users can: - -1. Specify a source (local path or URL) and optionally select a - harness format. -2. Preview the introspected elements before confirming registration. -3. Resolve any naming conflicts with existing registry entries. -4. Confirm the import, which creates all entries and the bundle. - -The preview step calls `introspect_bundle` without writing to the -registry, so users can see what will be registered before committing. +The MLflow UI provides a multi-step import wizard: + +1. **Source selection.** The user enters a source (local path or URL) + and optionally selects a harness format from a dropdown. If no + harness is selected, the system attempts auto-detection by probing + registered adapters. An optional bundle name field allows overriding + the name derived from the artifact. +2. **Preview.** The system calls `introspect_bundle` (read-only, no + registry writes) and displays a table of discovered elements with + columns: Type (skill/subagent/hook/mcp_server), Name, Description, + Source path. Elements that match existing registry entries are + flagged with an "Exists" badge. Users can deselect elements they do + not want to import. +3. **Conflict resolution.** If an element name conflicts with an + existing registry entry that has different content, the UI shows + the conflict with options: skip the element, rename it, or create + a new version of the existing entry. +4. **Confirmation.** A summary page shows counts of new vs. reused + entries, the bundle name, and the version string. The user clicks + "Confirm Import" to execute. +5. **Result.** A success page displays the created bundle with links + to each registered element's detail page in the Skills registry. ### Marketplace integration @@ -406,6 +400,30 @@ marketplace infrastructure (currently Claude Code and Codex CLI). Harnesses without marketplace support (Cursor, Antigravity, OpenClaw) use the adapter-based `mlflow skills install` command instead. +#### Marketplace browsing and the MLflow UI + +For harnesses with marketplace support (Claude Code, Codex CLI), +users browse and install plugins natively from within the harness. +The harness queries the `marketplace.json` endpoint and presents +available bundles with descriptions, member counts, and version info. +Installation is a single command (e.g., `/plugin install +pr-workflow@mlflow`), and the harness handles directory placement and +manifest creation. + +For harnesses without marketplace support, the MLflow Skills page +(RFC-0005) serves as the browsing interface. Users search and filter +registered bundles in the MLflow UI, then copy the install command +from the bundle detail page: + +``` +mlflow skills install --bundle pr-workflow --alias production \ + --harness cursor +``` + +The bundle detail page in the MLflow UI displays a ready-to-copy +install command for each supported harness, reducing the manual steps +required. + ### Implementation details SDK function signatures (`install`, `import_bundle`), REST API diff --git a/rfcs/0006-skill-harness-integration/implementation-details.md b/rfcs/0006-skill-harness-integration/implementation-details.md index 87d0702..5cfee74 100644 --- a/rfcs/0006-skill-harness-integration/implementation-details.md +++ b/rfcs/0006-skill-harness-integration/implementation-details.md @@ -337,3 +337,30 @@ use hooks (Codex CLI, GitHub Copilot) can follow the same pattern as Claude Code. Harnesses without hook support cannot be automatically instrumented; users of those harnesses can still use `mlflow.skill_context()` manually in SDK-based agent code. + +## Adapter summaries + +**Claude Code / Codex CLI:** generates a plugin directory under +`.claude/plugins/` (or `.codex/plugins/`) with `plugin.json`, skill +files, subagent files, merged `.mcp.json` from MCP registry metadata, +and hook entries. MCP server credentials are the user's +responsibility. Hooks require explicit user opt-in. + +**Cursor:** places skills and subagents in `.cursor/skills/` and +`.cursor/agents/`. Merges MCP entries into `.cursor/mcp.json`. Hooks +are skipped (unsupported). + +**Antigravity:** places skills in `.agent/skills/`. Subagents, MCP +servers, and hooks are skipped. + +**Harness-agnostic bundle formats.** The adapter interface is not +limited to harness-specific formats. Cross-harness bundle formats +that package skills, subagents, hooks, and MCP servers together are +also valid adapter targets. For example, Lola +([LobsterTrap/lola](https://github.com/LobsterTrap/lola)) defines +an "AI Context Module" format that bundles these capability types +using directory auto-discovery and targets multiple harnesses from +a single module. An adapter for a format like this would support +both directions: `install` generates the cross-harness format from +registry metadata, and `import` introspects an existing module to +register its elements. From f16e3c32418aba945d8b441eec46de2959c3faf8 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Mon, 8 Jun 2026 14:35:15 -0400 Subject: [PATCH 45/52] Replace Agent-as-a-Judge with LLM judges and update link Co-Authored-By: Claude Opus 4.6 --- rfcs/0005-skill-registry/0005-skill-registry.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index 9e0b3f1..98d1e88 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -147,13 +147,13 @@ Registry enables. Each shows both CLI and UI paths. and how long it took. Click the skill name link to navigate to the skill's registry detail page. -## Evaluate two bundle versions with Agent-as-a-Judge +## Evaluate two bundle versions with LLM judges MLflow's -[Agent-as-a-Judge](https://mlflow.org/docs/latest/genai/eval-monitor/scorers/llm-judge/agentic-overview/) -evaluation uses LLM judges that autonomously explore execution traces -via MCP tools. Because skill invocations produce traced SKILL spans, -Agent-as-a-Judge can analyze how skills were used during an agent run. +[LLM judges](https://mlflow.org/docs/latest/genai/eval-monitor/scorers/) +can autonomously explore execution traces via MCP tools. Because +skill invocations produce traced SKILL spans, LLM judges can +analyze how skills were used during an agent run. 1. Register a new version of the bundle with updated members: ```bash @@ -200,7 +200,7 @@ Agent-as-a-Judge can analyze how skills were used during an agent run. benchmark dataset, collecting traces in a dedicated MLflow experiment. 4. The job runs - [Agent-as-a-Judge](https://mlflow.org/docs/latest/genai/eval-monitor/scorers/llm-judge/agentic-overview/) + [LLM judge](https://mlflow.org/docs/latest/genai/eval-monitor/scorers/) evaluation on the collected traces, producing scored results. 5. The job fetches the benchmark results from the previous production version (stored as MLflow metrics or evaluation artifacts). From 6eb2a4a4f36fd2b17164d6dbd92599faa9c89d9a Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Mon, 8 Jun 2026 14:41:17 -0400 Subject: [PATCH 46/52] Consolidate entity section hyperlinks into single data model link Co-Authored-By: Claude Opus 4.6 --- .../0005-skill-registry.md | 27 +++---------------- 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index 98d1e88..e190eca 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -341,10 +341,6 @@ Key fields include `name` (unique within workspace), `display_name`, `status` (derived from latest version), `aliases`, and `latest_version`. -See [implementation-details.md: Skill -entity](implementation-details.md#skill-entity) for the dataclass -definition and field table. - **MCP servers.** MCP servers are registered in the MCP Server Registry (RFC-0004), not in this registry. Skill bundles can reference MCP registry entries in their `mcp_servers` list. MCP configs embedded in @@ -362,15 +358,6 @@ new version. The optional `subpath` field identifies content within a shared artifact (used with OCI and ZIP). The optional `content_digest` field enables integrity verification. -See [implementation-details.md: SkillVersion -entity](implementation-details.md#skillversion-entity) for the -dataclass definition and field table. See -[implementation-details.md: SkillVersion field -details](implementation-details.md#skillversion-field-details) for -source type extensibility, subpath usage by source type, MLflow -artifact storage, version uniqueness, content integrity, and -immutability contract. - #### Subagent and Hook `Subagent` (a sub-agent definition invocable by a parent agent) and @@ -391,10 +378,6 @@ and MCP servers) into a governed unit that maps to the "plugin" concept in agent harnesses. Follows the same top-level pattern as Skill: versions, tags, aliases, and derived status. -See [implementation-details.md: SkillBundle -entity](implementation-details.md#skillbundle-entity) for the -dataclass definition. - **Why bundles instead of tags?** Tags could express "these skills are related" but cannot provide versioned membership snapshots (reproducible point-in-time combinations), cross-registry references @@ -412,13 +395,9 @@ servers. A bundle version can optionally have its own source pointer case `pull` fetches the bundle artifact as a unit rather than pulling members individually. -See [implementation-details.md: SkillBundleVersion -entity](implementation-details.md#skillbundleversion-entity) for the -dataclass definition. See [implementation-details.md: -SkillBundleVersion field -details](implementation-details.md#skillbundleversion-field-details) -for cross-registry reference details, embedded MCP config handling, -source resolution, and immutability contract. +Dataclass definitions, field tables, source type details, and +cross-registry reference handling for all entity types are in +[implementation-details.md](implementation-details.md#skill-entity). #### Aliases and tags From 500cbd051a763a0590ae59c6360740c6576be467 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Mon, 8 Jun 2026 15:32:24 -0400 Subject: [PATCH 47/52] Remove mockup disclaimer from UI section (no mockups present) Co-Authored-By: Claude Opus 4.6 --- rfcs/0005-skill-registry/0005-skill-registry.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index e190eca..672f1f0 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -550,11 +550,6 @@ Key design choices: ### UI -> **Note:** The descriptions below are for illustrative purposes only -> and do not fully align with the MLflow design system. The final -> implementation will follow MLflow's established design system and -> component library, consistent with the MCP Servers page (RFC-0004). - The Skills page lives under the GenAI workflow in the MLflow sidebar, alongside Experiments, Prompts, MCP Servers, and AI Gateway. From 9a3f64e0b8de0233056b12e3cf0d7d90102e75e5 Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Mon, 8 Jun 2026 15:55:09 -0400 Subject: [PATCH 48/52] Rename mlflow.skill.registry to mlflow.skill.workspace, defer marketplace - Rename span attribute to mlflow.skill.workspace to match RFC-0004's {workspace, name, version} convention - Move marketplace catalog generation from initial release to follow-up in RFC-0006 adoption strategy - Bundle import stays in initial release Co-Authored-By: Claude Opus 4.6 --- rfcs/0005-skill-registry/0005-skill-registry.md | 2 +- rfcs/0005-skill-registry/implementation-details.md | 2 +- .../0006-skill-harness-integration.md | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index 672f1f0..6cbc698 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -648,7 +648,7 @@ with mlflow.skill_context( ``` The context manager creates a span with `mlflow.skill.name`, -`mlflow.skill.version`, and `mlflow.skill.registry` (workspace) +`mlflow.skill.version`, and `mlflow.skill.workspace` attributes that link the span back to a specific skill version in the registry. See [implementation-details.md: skill_context() span attributes](implementation-details.md#skill_context-span-attributes) diff --git a/rfcs/0005-skill-registry/implementation-details.md b/rfcs/0005-skill-registry/implementation-details.md index 280b347..4707b42 100644 --- a/rfcs/0005-skill-registry/implementation-details.md +++ b/rfcs/0005-skill-registry/implementation-details.md @@ -1015,7 +1015,7 @@ following attributes: |---|---|---| | `mlflow.skill.name` | Skill name | Registry name of the active skill | | `mlflow.skill.version` | Version string | Registered version | -| `mlflow.skill.registry` | Workspace name | MLflow workspace (defaults to `"default"`) | +| `mlflow.skill.workspace` | Workspace name | MLflow workspace (defaults to `"default"`) | These three attributes form the `{workspace, name, version}` coordinates that link the span back to a specific skill version in 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 7a6cf73..5b0021f 100644 --- a/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md +++ b/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md @@ -527,9 +527,10 @@ added as demand warrants without architectural changes. # Adoption strategy **Initial release:** Claude Code, Codex CLI, and Cursor adapters. -Marketplace catalog generation for Claude Code / Codex CLI. -Install-time trace manifest and Claude Code trace hooks. +Bundle import. Install-time trace manifest and Claude Code trace +hooks. -**Follow-up:** additional adapters based on demand (including +**Follow-up:** Marketplace catalog generation for Claude Code / +Codex CLI. Additional adapters based on demand (including harness-agnostic bundle formats), automatic harness detection, bi-directional sync (detect local plugins and register them). From db75cc7d04bb3f27c5cd314ff0e5769cefd12e7b Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Mon, 8 Jun 2026 15:58:29 -0400 Subject: [PATCH 49/52] Add soft warning for bundle/member source misalignment When a bundle version has its own source and a member also has a source that does not obviously correspond, pull logs a warning. Sources may legitimately differ, so this is a warning, not an error. Co-Authored-By: Claude Opus 4.6 --- rfcs/0005-skill-registry/0005-skill-registry.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0005-skill-registry/0005-skill-registry.md index 6cbc698..5403e57 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0005-skill-registry/0005-skill-registry.md @@ -502,6 +502,12 @@ This supports both distribution patterns: a monolithic plugin artifact (single OCI image or Git repo) and an assembled plugin (members from different sources). +When a bundle version has its own `source` and a member also has a +`source` that does not obviously correspond to the bundle source, +`pull` logs a warning. The sources may be legitimately different +(e.g., a bundle OCI image that aggregates content from multiple Git +repos), so this is a soft warning, not an error. + If `content_digest` is set, `pull` verifies the fetched content matches the digest and returns an error on mismatch. From 339ad272749b5fc838810c973b49837633cbc60c Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Mon, 8 Jun 2026 16:05:51 -0400 Subject: [PATCH 50/52] Renumber RFC-0005 to RFC-0008 and RFC-0006 to RFC-0009 RFC-0005 was taken by RBAC migration, RFC-0006 by pluggable authn/authz, RFC-0007 by Scorer Presets. Renumber skill registry to 0008 and harness integration to 0009. Updated all cross-references and PR title/description. Co-Authored-By: Claude Opus 4.6 --- .../0008-skill-registry.md} | 14 +++++++------- .../implementation-details.md | 10 +++++----- .../0009-skill-harness-integration.md} | 14 +++++++------- .../implementation-details.md | 6 +++--- 4 files changed, 22 insertions(+), 22 deletions(-) rename rfcs/{0005-skill-registry/0005-skill-registry.md => 0008-skill-registry/0008-skill-registry.md} (98%) rename rfcs/{0005-skill-registry => 0008-skill-registry}/implementation-details.md (99%) rename rfcs/{0006-skill-harness-integration/0006-skill-harness-integration.md => 0009-skill-harness-integration/0009-skill-harness-integration.md} (97%) rename rfcs/{0006-skill-harness-integration => 0009-skill-harness-integration}/implementation-details.md (98%) diff --git a/rfcs/0005-skill-registry/0005-skill-registry.md b/rfcs/0008-skill-registry/0008-skill-registry.md similarity index 98% rename from rfcs/0005-skill-registry/0005-skill-registry.md rename to rfcs/0008-skill-registry/0008-skill-registry.md index 5403e57..de675aa 100644 --- a/rfcs/0005-skill-registry/0005-skill-registry.md +++ b/rfcs/0008-skill-registry/0008-skill-registry.md @@ -39,7 +39,7 @@ can also reference MCP servers from the MCP Server Registry `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). +RFC (RFC-0009). # User journeys @@ -129,7 +129,7 @@ Registry enables. Each shows both CLI and UI paths. ## Install a skill bundle, run the agent, browse traces 1. Install the bundle for a harness - ([RFC-0006](../0006-skill-harness-integration/0006-skill-harness-integration.md)): + ([RFC-0009](../0009-skill-harness-integration/0009-skill-harness-integration.md)): ```bash mlflow skills install --bundle pr-workflow --alias production \ --harness claude-code --lock @@ -346,7 +346,7 @@ Key fields include `name` (unique within workspace), `display_name`, registry entries in their `mcp_servers` list. MCP configs embedded in bundle-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. +installation (RFC-0009), not as separately registered entities. #### SkillVersion @@ -513,7 +513,7 @@ 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. +directories. Harness-specific installation is covered in RFC-0009. See [implementation-details.md: Pull semantics details](implementation-details.md#pull-semantics-details) for source @@ -684,7 +684,7 @@ determines which registry instance the span links to: 1. **Manifest lookup.** If `mlflow-skills-manifest.json` exists in the project (written by `mlflow skills install`, defined in - [RFC-0006](../0006-skill-harness-integration/0006-skill-harness-integration.md)), + [RFC-0009](../0009-skill-harness-integration/0009-skill-harness-integration.md)), the `registry` field for the matching skill name provides the workspace. This is the default path for installed skills. 2. **Tracking URI default.** If no manifest is found (SDK users @@ -727,7 +727,7 @@ with existing autologgers without modification. When an autologger the SKILL span. No changes to the autologgers are needed. For harness-specific integration (e.g., Claude Code automatically -wrapping skill loads in `skill_context()` spans), see RFC-0006. +wrapping skill loads in `skill_context()` spans), see RFC-0009. #### Registry validation @@ -784,5 +784,5 @@ The two approaches are complementary. New feature, not a breaking change. Phased rollout: - **Phase 1 (this RFC):** Registry entities (Skill, Subagent, Hook, SkillBundle), store, REST API, SDK, CLI, UI, `mlflow skills pull`, and `mlflow.skill_context()` for trace integration. -- **Phase 2 (RFC-0006):** Harness-specific `mlflow skills install` for Claude Code, Codex CLI, and Cursor. Automatic `skill_context()` wrapping in harness-specific autologgers. +- **Phase 2 (RFC-0009):** Harness-specific `mlflow skills install` for Claude Code, Codex CLI, and Cursor. Automatic `skill_context()` wrapping in harness-specific autologgers. - **Phase 3 (follow-up):** Usage analytics dashboards, install count tracking, cross-workspace export/import (following cross-registry patterns), and shared base extraction with the MCP registry. diff --git a/rfcs/0005-skill-registry/implementation-details.md b/rfcs/0008-skill-registry/implementation-details.md similarity index 99% rename from rfcs/0005-skill-registry/implementation-details.md rename to rfcs/0008-skill-registry/implementation-details.md index 4707b42..a32a37f 100644 --- a/rfcs/0005-skill-registry/implementation-details.md +++ b/rfcs/0008-skill-registry/implementation-details.md @@ -1,7 +1,7 @@ -# RFC-0005: Skill Registry Implementation Details +# RFC-0008: Skill Registry Implementation Details This document contains implementation-level specifications for -RFC-0005 (Skill Registry). It covers database schema, store interface +RFC-0008 (Skill Registry). It covers database schema, store interface method signatures, SDK convenience functions, REST API endpoints, pagination/filtering, and the Python SDK/CLI mapping. These details support implementers; the main RFC covers the design rationale. @@ -145,7 +145,7 @@ 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 bundle version and returns `RESOURCE_DOES_NOT_EXIST` if it does not. This avoids -deployment-ordering dependencies between RFC-0004 and RFC-0005 +deployment-ordering dependencies between RFC-0004 and RFC-0008 migrations and allows either registry to be deployed independently. ### `skill_bundle_tags` @@ -928,7 +928,7 @@ artifact. The bundle artifact is a generic package of content (skill files, agent definitions, hook scripts). It may or may not be harness-ready; the adapter does not assume either way. Harness -adapters (RFC-0006) generate harness-specific manifests from +adapters (RFC-0009) generate harness-specific manifests from registry metadata at install time, since the registry is the governed source of truth. If the artifact contents disagree with the declared members (e.g., a `subpath` @@ -1004,7 +1004,7 @@ with the underlying error from the source system. `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. +directories. Harness-specific installation is covered in RFC-0009. ## skill_context() span attributes diff --git a/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md b/rfcs/0009-skill-harness-integration/0009-skill-harness-integration.md similarity index 97% rename from rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md rename to rfcs/0009-skill-harness-integration/0009-skill-harness-integration.md index 5b0021f..cf603ab 100644 --- a/rfcs/0006-skill-harness-integration/0006-skill-harness-integration.md +++ b/rfcs/0009-skill-harness-integration/0009-skill-harness-integration.md @@ -14,7 +14,7 @@ rfc_pr: https://github.com/mlflow/rfcs/pull/10 # Summary Add harness-specific installation to the MLflow Skill Registry -(RFC-0005). Where RFC-0005 provides `mlflow skills pull` to fetch +(RFC-0008). Where RFC-0008 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 @@ -99,7 +99,7 @@ mlflow.genai.skills.import_bundle( ### The problem -RFC-0005 provides `pull` for fetching content to a local directory, +RFC-0008 provides `pull` for fetching content to a local directory, but each harness has its own directory layout, manifest format, and discovery mechanism (see table below). Without harness-specific installation, users must manually create manifests, place files in @@ -133,7 +133,7 @@ Only the directory placement and manifest format differ. ### Out of scope -- Registry operations (covered in RFC-0005). +- Registry operations (covered in RFC-0008). - Extending harness functionality (e.g., adding hook support). - Automatic harness detection (follow-up). @@ -411,7 +411,7 @@ pr-workflow@mlflow`), and the harness handles directory placement and manifest creation. For harnesses without marketplace support, the MLflow Skills page -(RFC-0005) serves as the browsing interface. Users search and filter +(RFC-0008) serves as the browsing interface. Users search and filter registered bundles in the MLflow UI, then copy the install command from the bundle detail page: @@ -459,8 +459,8 @@ Lock file format and SDK functions are in ### Trace integration -RFC-0005 defines `mlflow.skill_context()`, a context manager that -creates SKILL spans in MLflow traces (see RFC-0005, Trace +RFC-0008 defines `mlflow.skill_context()`, a context manager that +creates SKILL spans in MLflow traces (see RFC-0008, Trace integration). `mlflow skills install` can automate this: it writes a manifest mapping installed skill names to registry coordinates, and harnesses with hook support (Claude Code, Codex CLI) can use @@ -487,7 +487,7 @@ instrumentation details are in ## Let users write their own install scripts -Provide only `pull` (RFC-0005) and let users or third parties build +Provide only `pull` (RFC-0008) and let users or third parties build harness-specific tooling. Rejected because the gap between "pull" and "working in my harness" diff --git a/rfcs/0006-skill-harness-integration/implementation-details.md b/rfcs/0009-skill-harness-integration/implementation-details.md similarity index 98% rename from rfcs/0006-skill-harness-integration/implementation-details.md rename to rfcs/0009-skill-harness-integration/implementation-details.md index 5cfee74..5119796 100644 --- a/rfcs/0006-skill-harness-integration/implementation-details.md +++ b/rfcs/0009-skill-harness-integration/implementation-details.md @@ -1,7 +1,7 @@ -# RFC-0006: Harness Integration Implementation Details +# RFC-0009: Harness Integration Implementation Details This document contains implementation-level specifications for -RFC-0006 (Skill Registry Harness Integration). It covers detailed +RFC-0009 (Skill Registry Harness Integration). It covers detailed adapter directory layouts and manifest generation, MCP server config generation, SDK interface and function signatures, REST API endpoints, CLI commands, lock file SDK functions, and trace instrumentation @@ -307,7 +307,7 @@ members: users must explicitly enable hooks. For developers building agents with the Claude Agent SDK or other Python frameworks, the recommended approach is to use -`mlflow.skill_context()` directly (see RFC-0005). The Agent SDK's +`mlflow.skill_context()` directly (see RFC-0008). The Agent SDK's hook system also supports Python callbacks, so a similar automatic approach is possible: From 0f11318dbda9cc1ab3da6809439d08261d8160af Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Wed, 10 Jun 2026 16:55:08 -0400 Subject: [PATCH 51/52] Mention OpenSharing as a future source type The OpenSharing protocol (Linux Foundation) defines AgentSkill as a first-class asset type using the same SKILL.md directory structure. Note it as a candidate source type in the extensibility section. Co-Authored-By: Claude Opus 4.6 --- rfcs/0008-skill-registry/implementation-details.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/rfcs/0008-skill-registry/implementation-details.md b/rfcs/0008-skill-registry/implementation-details.md index a32a37f..857668a 100644 --- a/rfcs/0008-skill-registry/implementation-details.md +++ b/rfcs/0008-skill-registry/implementation-details.md @@ -778,8 +778,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`, `opensharing`) can be added without schema changes +since the column stores a string value. In particular, the +[OpenSharing](https://github.com/OpenSharing-IO/OpenSharing) protocol +(Linux Foundation) defines AgentSkill as a first-class asset type +using the same SKILL.md directory structure. An `opensharing` source +type would let the registry govern and track skills whose content is +shared via OpenSharing's credential-vending protocol. **Subpath usage by source type.** The `subpath` field separates "what to download" from "where inside the downloaded content the relevant From fc1b838b6b705ed21c2ba9cd858c04edfc8b056c Mon Sep 17 00:00:00 2001 From: Bill Murdock Date: Wed, 10 Jun 2026 17:57:50 -0400 Subject: [PATCH 52/52] Address Humair's review: shorten skill_context scope, fix workspace naming, remove REST API - Shorten skill_context() scope section to a single sentence - Rename manifest field from "registry" to "workspace" to match mlflow.skill.workspace span attribute - Simplify workspace resolution: manifest lookup + tracking URI default, drop explicit override parameter - Remove REST API section from RFC-0009 impl details (marketplace deferred to follow-up) Co-Authored-By: Claude Opus 4.6 --- .../0008-skill-registry.md | 38 ++++++------------- .../0009-skill-harness-integration.md | 4 +- .../implementation-details.md | 17 ++------- 3 files changed, 17 insertions(+), 42 deletions(-) diff --git a/rfcs/0008-skill-registry/0008-skill-registry.md b/rfcs/0008-skill-registry/0008-skill-registry.md index de675aa..aeb1bc2 100644 --- a/rfcs/0008-skill-registry/0008-skill-registry.md +++ b/rfcs/0008-skill-registry/0008-skill-registry.md @@ -646,7 +646,7 @@ attributes: ```python with mlflow.skill_context( - name="code-review", version="1.0.0", workspace=None + name="code-review", version="1.0.0" ) as span: # All spans created inside this block (including those from # autologgers) become children of this SKILL span. @@ -662,37 +662,21 @@ for the full attribute table. #### Scope of skill_context() -`skill_context()` is for skills only, not subagents, hooks, or -bundles: - -- **Subagents** produce their own spans via existing harness tracing. - For example, Claude Code traces sub-agent invocations as tool use - spans via `mlflow autolog claude`. A separate `subagent_context()` - would duplicate existing span creation. -- **Hooks** are infrastructure-level actions that execute outside the - agent's reasoning loop. They are visible in harness logs but are not - traced at the registry level. -- **Skill bundles** are governance and install-time concepts, not - runtime concepts. A bundle is never "invoked" during a conversation. +`skill_context()` is for skills only, and it is not applicable to +subagents, hooks, or bundles. Bundle-level analytics are derived by aggregating over traces of individual member skills. #### Workspace resolution -When `skill_context()` is called, the `workspace` parameter -determines which registry instance the span links to: - -1. **Manifest lookup.** If `mlflow-skills-manifest.json` exists in the - project (written by `mlflow skills install`, defined in - [RFC-0009](../0009-skill-harness-integration/0009-skill-harness-integration.md)), - the `registry` field for the matching skill name provides the - workspace. This is the default path for installed skills. -2. **Tracking URI default.** If no manifest is found (SDK users - calling `skill_context()` directly), the workspace defaults to the - current MLflow tracking URI's workspace context, consistent with - other MLflow operations. -3. **Explicit override.** Callers can pass - `workspace="my-workspace"` for full control. +When `skill_context()` is called, the workspace is resolved from +the `mlflow-skills-manifest.json` written by `mlflow skills install` +(defined in +[RFC-0009](../0009-skill-harness-integration/0009-skill-harness-integration.md)). +The manifest always contains the workspace for each installed skill. +For SDK users calling `skill_context()` directly without a manifest, +the workspace defaults to the current MLflow tracking URI's workspace +context, consistent with other MLflow operations. #### Skill stacks via nesting diff --git a/rfcs/0009-skill-harness-integration/0009-skill-harness-integration.md b/rfcs/0009-skill-harness-integration/0009-skill-harness-integration.md index cf603ab..cfa5764 100644 --- a/rfcs/0009-skill-harness-integration/0009-skill-harness-integration.md +++ b/rfcs/0009-skill-harness-integration/0009-skill-harness-integration.md @@ -426,8 +426,8 @@ required. ### Implementation details -SDK function signatures (`install`, `import_bundle`), REST API -endpoints, and CLI commands are in +SDK function signatures (`install`, `import_bundle`) and CLI +commands are in [implementation-details.md](implementation-details.md). ### Lock file diff --git a/rfcs/0009-skill-harness-integration/implementation-details.md b/rfcs/0009-skill-harness-integration/implementation-details.md index 5119796..6c2e163 100644 --- a/rfcs/0009-skill-harness-integration/implementation-details.md +++ b/rfcs/0009-skill-harness-integration/implementation-details.md @@ -3,7 +3,7 @@ This document contains implementation-level specifications for RFC-0009 (Skill Registry Harness Integration). It covers detailed adapter directory layouts and manifest generation, MCP server config -generation, SDK interface and function signatures, REST API endpoints, +generation, SDK interface and function signatures, CLI commands, lock file SDK functions, and trace instrumentation details. The main RFC covers the design rationale. @@ -132,15 +132,6 @@ def import_bundle( and creates a skill bundle referencing them all.""" ``` -## REST API - -The only server-side endpoint is the marketplace catalog, which -harnesses query to discover available plugins. - -| Method | Path | Description | -|---|---|---| -| `GET` | `/ajax-api/3.0/mlflow/skill-bundles/marketplace.json` | Generate marketplace catalog for a harness | - ## CLI ```bash @@ -228,19 +219,19 @@ coordinates: "code-review": { "name": "code-review", "version": "1.0.0", - "registry": "default" + "workspace": "default" }, "security-auditor": { "name": "security-auditor", "version": "1.0.0", - "registry": "default" + "workspace": "default" } } } ``` The manifest is keyed by the skill's local name (the name the harness -uses to invoke it). The value provides the `{registry, name, version}` +uses to invoke it). The value provides the `{workspace, name, version}` coordinates that link back to the skill registry. This file is used by trace hooks to annotate spans with registry coordinates without requiring a registry lookup at runtime.