Skip to content

Commit d1c91d9

Browse files
authored
Merge pull request #17 from stacklok/oci-skills-mediatypes
Add OCI skills media types, constants, and platform types
2 parents b556d2d + 6ad39be commit d1c91d9

3 files changed

Lines changed: 492 additions & 0 deletions

File tree

oci/skills/doc.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// SPDX-FileCopyrightText: Copyright 2026 Stacklok, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
/*
5+
Package skills provides OCI artifact types, media types, and local storage for
6+
ToolHive skill packages.
7+
8+
A skill is an OCI artifact containing MCP server configuration, prompt files,
9+
and metadata. This package defines the constants, data structures, and storage
10+
layer that the rest of the ToolHive ecosystem uses to package, push, pull, and
11+
cache skills as OCI images.
12+
13+
# Media Types and Constants
14+
15+
Standard OCI media types and ToolHive-specific annotation/label keys:
16+
17+
// Artifact type identifies a skill manifest
18+
skills.ArtifactTypeSkill // "dev.toolhive.skills.v1"
19+
20+
// Annotations carry metadata in manifests
21+
skills.AnnotationSkillName
22+
skills.AnnotationSkillVersion
23+
24+
// Labels carry metadata in OCI image configs
25+
skills.LabelSkillName
26+
skills.LabelSkillFiles
27+
28+
# Stability
29+
30+
This package is Alpha. Breaking changes are possible between minor versions.
31+
*/
32+
package skills

oci/skills/mediatypes.go

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// SPDX-FileCopyrightText: Copyright 2026 Stacklok, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package skills
5+
6+
import (
7+
"encoding/json"
8+
"fmt"
9+
"strings"
10+
)
11+
12+
// Artifact type for skill identification.
13+
const (
14+
// ArtifactTypeSkill identifies skill artifacts in manifests.
15+
ArtifactTypeSkill = "dev.toolhive.skills.v1"
16+
)
17+
18+
// OCI Image Index media type.
19+
const (
20+
// MediaTypeImageIndex is the OCI image index media type.
21+
MediaTypeImageIndex = "application/vnd.oci.image.index.v1+json"
22+
)
23+
24+
// Standard OCI media types for Kubernetes image volume compatibility.
25+
const (
26+
// MediaTypeImageManifest is the OCI image manifest media type.
27+
MediaTypeImageManifest = "application/vnd.oci.image.manifest.v1+json"
28+
29+
// MediaTypeImageConfig is the standard OCI image config media type.
30+
MediaTypeImageConfig = "application/vnd.oci.image.config.v1+json"
31+
32+
// MediaTypeImageLayer is the standard OCI image layer media type.
33+
MediaTypeImageLayer = "application/vnd.oci.image.layer.v1.tar+gzip"
34+
)
35+
36+
// Annotation keys for skill metadata in manifests.
37+
const (
38+
// AnnotationCreated is the OCI standard annotation for creation time.
39+
AnnotationCreated = "org.opencontainers.image.created"
40+
41+
// AnnotationSkillName is the annotation key for skill name.
42+
AnnotationSkillName = "dev.toolhive.skills.name"
43+
44+
// AnnotationSkillDescription is the annotation key for skill description.
45+
AnnotationSkillDescription = "dev.toolhive.skills.description"
46+
47+
// AnnotationSkillVersion is the annotation key for skill version.
48+
AnnotationSkillVersion = "dev.toolhive.skills.version"
49+
50+
// AnnotationSkillRequires is the annotation key for skill external dependencies (JSON array of OCI references).
51+
AnnotationSkillRequires = "dev.toolhive.skills.requires"
52+
)
53+
54+
// Label keys for skill metadata in OCI image config.
55+
const (
56+
// LabelSkillName is the label key for skill name.
57+
LabelSkillName = "dev.toolhive.skills.name"
58+
59+
// LabelSkillDescription is the label key for skill description.
60+
LabelSkillDescription = "dev.toolhive.skills.description"
61+
62+
// LabelSkillVersion is the label key for skill version.
63+
LabelSkillVersion = "dev.toolhive.skills.version"
64+
65+
// LabelSkillAllowedTools is the label key for allowed tools (JSON array).
66+
LabelSkillAllowedTools = "dev.toolhive.skills.allowedTools"
67+
68+
// LabelSkillLicense is the label key for skill license.
69+
LabelSkillLicense = "dev.toolhive.skills.license"
70+
71+
// LabelSkillFiles is the label key for skill files (JSON array).
72+
LabelSkillFiles = "dev.toolhive.skills.files"
73+
)
74+
75+
// SkillConfig represents skill metadata extracted from OCI image config labels.
76+
type SkillConfig struct {
77+
Name string `json:"name"`
78+
Description string `json:"description"`
79+
Version string `json:"version,omitempty"`
80+
AllowedTools []string `json:"allowedTools,omitempty"`
81+
License string `json:"license,omitempty"`
82+
Compatibility string `json:"compatibility,omitempty"`
83+
Metadata map[string]string `json:"metadata,omitempty"`
84+
Files []string `json:"files"`
85+
}
86+
87+
// ImageConfig represents a standard OCI image configuration.
88+
// This structure is required for Kubernetes image volume compatibility.
89+
type ImageConfig struct {
90+
Architecture string `json:"architecture"`
91+
OS string `json:"os"`
92+
Config ImageConfigData `json:"config,omitempty"`
93+
RootFS RootFS `json:"rootfs"`
94+
History []HistoryEntry `json:"history,omitempty"`
95+
}
96+
97+
// ImageConfigData contains container configuration including labels.
98+
type ImageConfigData struct {
99+
Labels map[string]string `json:"Labels,omitempty"`
100+
}
101+
102+
// RootFS describes the rootfs of the image.
103+
type RootFS struct {
104+
Type string `json:"type"`
105+
DiffIDs []string `json:"diff_ids"`
106+
}
107+
108+
// HistoryEntry describes a layer in the image history.
109+
type HistoryEntry struct {
110+
Created string `json:"created,omitempty"`
111+
CreatedBy string `json:"created_by,omitempty"`
112+
}
113+
114+
// SkillConfigFromImageConfig extracts SkillConfig from OCI image config labels.
115+
func SkillConfigFromImageConfig(imgConfig *ImageConfig) (*SkillConfig, error) {
116+
if imgConfig == nil {
117+
return nil, fmt.Errorf("image config is nil")
118+
}
119+
120+
labels := imgConfig.Config.Labels
121+
if labels == nil {
122+
return nil, fmt.Errorf("oci config has no labels")
123+
}
124+
125+
config := &SkillConfig{
126+
Name: labels[LabelSkillName],
127+
Description: labels[LabelSkillDescription],
128+
Version: labels[LabelSkillVersion],
129+
License: labels[LabelSkillLicense],
130+
}
131+
132+
if config.Name == "" {
133+
return nil, fmt.Errorf("skill name is required in labels")
134+
}
135+
136+
// Parse JSON-encoded arrays
137+
if toolsJSON := labels[LabelSkillAllowedTools]; toolsJSON != "" {
138+
if err := json.Unmarshal([]byte(toolsJSON), &config.AllowedTools); err != nil {
139+
return nil, fmt.Errorf("parsing allowed tools: %w", err)
140+
}
141+
}
142+
143+
if filesJSON := labels[LabelSkillFiles]; filesJSON != "" {
144+
if err := json.Unmarshal([]byte(filesJSON), &config.Files); err != nil {
145+
return nil, fmt.Errorf("parsing files: %w", err)
146+
}
147+
}
148+
149+
return config, nil
150+
}
151+
152+
// Platform represents a target platform for OCI artifacts.
153+
type Platform struct {
154+
Architecture string `json:"architecture"`
155+
OS string `json:"os"`
156+
}
157+
158+
// String returns the platform in "os/arch" format.
159+
func (p Platform) String() string {
160+
return p.OS + "/" + p.Architecture
161+
}
162+
163+
// ParsePlatform parses a platform string in "os/arch" format.
164+
func ParsePlatform(s string) (Platform, error) {
165+
parts := strings.Split(s, "/")
166+
if len(parts) != 2 {
167+
return Platform{}, fmt.Errorf("invalid platform format: %q (expected os/arch)", s)
168+
}
169+
osName := strings.TrimSpace(parts[0])
170+
arch := strings.TrimSpace(parts[1])
171+
if osName == "" || arch == "" {
172+
return Platform{}, fmt.Errorf("invalid platform format: %q (os and arch cannot be empty)", s)
173+
}
174+
return Platform{OS: osName, Architecture: arch}, nil
175+
}
176+
177+
// DefaultPlatforms are the default platforms for skill artifacts.
178+
// These cover most Kubernetes clusters.
179+
var DefaultPlatforms = []Platform{
180+
{OS: "linux", Architecture: "amd64"},
181+
{OS: "linux", Architecture: "arm64"},
182+
}
183+
184+
// ImageIndex represents an OCI image index (multi-platform manifest list).
185+
type ImageIndex struct {
186+
SchemaVersion int `json:"schemaVersion"`
187+
MediaType string `json:"mediaType"`
188+
ArtifactType string `json:"artifactType,omitempty"`
189+
Manifests []IndexDescriptor `json:"manifests"`
190+
Annotations map[string]string `json:"annotations,omitempty"`
191+
}
192+
193+
// IndexDescriptor describes a manifest in an image index.
194+
type IndexDescriptor struct {
195+
MediaType string `json:"mediaType"`
196+
Digest string `json:"digest"`
197+
Size int64 `json:"size"`
198+
Platform *Platform `json:"platform,omitempty"`
199+
Annotations map[string]string `json:"annotations,omitempty"`
200+
}
201+
202+
// ParseRequiresAnnotation parses skill dependency references from manifest annotations.
203+
// Returns nil if the annotation is missing or invalid.
204+
func ParseRequiresAnnotation(annotations map[string]string) []string {
205+
requiresJSON := annotations[AnnotationSkillRequires]
206+
if requiresJSON == "" {
207+
return nil
208+
}
209+
210+
var refs []string
211+
if err := json.Unmarshal([]byte(requiresJSON), &refs); err != nil {
212+
// Invalid annotation format - return nil rather than propagating error
213+
// since annotations may come from older versions or external sources
214+
return nil
215+
}
216+
return refs
217+
}

0 commit comments

Comments
 (0)