-
Notifications
You must be signed in to change notification settings - Fork 172
Split skill unpack into a dedicated function #1165
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -45,10 +45,13 @@ import ( | |||||||||||
|
|
||||||||||||
| // UnpackModelKit performs the core unpacking logic for a ModelKit. | ||||||||||||
| func UnpackModelKit(ctx context.Context, opts *UnpackOptions) error { | ||||||||||||
| if opts == nil { | ||||||||||||
| return fmt.Errorf("unpack options must not be nil") | ||||||||||||
| } | ||||||||||||
| // If an unpack directory is provided, temporarily change the working directory | ||||||||||||
| // so that unpack operations that are relative to CWD behave as expected. | ||||||||||||
| // This centralizes tar -C semantics inside the unpack library. | ||||||||||||
| if opts != nil && opts.UnpackDir != "" { | ||||||||||||
| if opts.UnpackDir != "" { | ||||||||||||
| // Ensure the directory exists | ||||||||||||
| if err := os.MkdirAll(opts.UnpackDir, 0o755); err != nil { | ||||||||||||
| return fmt.Errorf("failed to create unpack directory %s: %w", opts.UnpackDir, err) | ||||||||||||
|
|
@@ -66,6 +69,9 @@ func UnpackModelKit(ctx context.Context, opts *UnpackOptions) error { | |||||||||||
| } | ||||||||||||
| }() | ||||||||||||
| } | ||||||||||||
| if opts.SkillOptions != nil { | ||||||||||||
| return unpackSkill(ctx, opts) | ||||||||||||
| } | ||||||||||||
| return unpackRecursive(ctx, opts, []string{}) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
|
|
@@ -121,10 +127,6 @@ func unpackRecursive(ctx context.Context, opts *UnpackOptions, visitedRefs []str | |||||||||||
| // through the config's relevant field to get the correct path for unpacking | ||||||||||||
| // We need to support older ModelKits (that were packed without diffIDs and digest | ||||||||||||
| // in the config) for now, so we need to continue using the old structure. | ||||||||||||
| // Skill-mode tracking | ||||||||||||
| var skillErrors []error | ||||||||||||
| var promptsFiltered, skillsFound int | ||||||||||||
|
|
||||||||||||
| var modelPartIdx, codeIdx, datasetIdx, docsIdx, promptIdx int | ||||||||||||
| for _, layerDesc := range manifest.Layers { | ||||||||||||
| // This variable supports older-format tar layers (that don't include the | ||||||||||||
|
|
@@ -144,9 +146,6 @@ func unpackRecursive(ctx context.Context, opts *UnpackOptions, visitedRefs []str | |||||||||||
| var layerInfo *artifact.LayerInfo | ||||||||||||
| switch mediaType.Base() { | ||||||||||||
| case mediatype.ModelBaseType: | ||||||||||||
| if opts.SkillOptions != nil { | ||||||||||||
| continue | ||||||||||||
| } | ||||||||||||
| entry := config.Model | ||||||||||||
| if !kitfile.LayerMatchesAnyFilter(entry, opts.FilterConfs) { | ||||||||||||
| continue | ||||||||||||
|
|
@@ -155,10 +154,6 @@ func unpackRecursive(ctx context.Context, opts *UnpackOptions, visitedRefs []str | |||||||||||
| output.Infof("Unpacking model %s to %s", config.Model.Name, config.Model.Path) | ||||||||||||
|
|
||||||||||||
| case mediatype.ModelPartBaseType: | ||||||||||||
| if opts.SkillOptions != nil { | ||||||||||||
| modelPartIdx += 1 | ||||||||||||
| continue | ||||||||||||
| } | ||||||||||||
| entry := config.Model.Parts[modelPartIdx] | ||||||||||||
| modelPartIdx += 1 | ||||||||||||
| if !kitfile.LayerMatchesAnyFilter(entry, opts.FilterConfs) { | ||||||||||||
|
|
@@ -175,37 +170,9 @@ func unpackRecursive(ctx context.Context, opts *UnpackOptions, visitedRefs []str | |||||||||||
| if !kitfile.LayerMatchesAnyFilter(entry, opts.FilterConfs) { | ||||||||||||
| continue | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Skill mode: install as agent skill | ||||||||||||
| if opts.SkillOptions != nil { | ||||||||||||
| promptsFiltered++ | ||||||||||||
| result, sErr := installPromptAsSkill(ctx, store, layerDesc, entry, mediaType.Compression(), opts) | ||||||||||||
| if sErr != nil { | ||||||||||||
| output.Logf(output.LogLevelWarn, "Error reading prompt %q: %s", entry.Path, sErr) | ||||||||||||
| skillErrors = append(skillErrors, fmt.Errorf("prompt %q: %s", entry.Path, sErr)) | ||||||||||||
| } else if result != nil { | ||||||||||||
| skillsFound++ | ||||||||||||
| for _, ar := range result.Agents { | ||||||||||||
| if ar.Err != nil { | ||||||||||||
| output.Infof("Failed to install skill '%s' for %s: %s", result.SkillName, ar.Agent, ar.Err) | ||||||||||||
| skillErrors = append(skillErrors, fmt.Errorf("skill '%s' for %s: %s", result.SkillName, ar.Agent, ar.Err)) | ||||||||||||
| } else if ar.Skipped { | ||||||||||||
| output.Infof("Skipped skill '%s' for %s: already exists (use -o to overwrite)", result.SkillName, ar.Agent) | ||||||||||||
| } else { | ||||||||||||
| output.Infof("Installed skill '%s' for %s → %s", result.SkillName, ar.Agent, ar.Path) | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| continue | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| layerInfo, layerPath = entry.LayerInfo, entry.Path | ||||||||||||
| output.Infof("Unpacking prompt to %s", entry.Path) | ||||||||||||
| } else { | ||||||||||||
| if opts.SkillOptions != nil { | ||||||||||||
| codeIdx += 1 | ||||||||||||
| continue | ||||||||||||
| } | ||||||||||||
| entry := config.Code[codeIdx] | ||||||||||||
| codeIdx += 1 | ||||||||||||
| if !kitfile.LayerMatchesAnyFilter(entry, opts.FilterConfs) { | ||||||||||||
|
|
@@ -216,17 +183,6 @@ func unpackRecursive(ctx context.Context, opts *UnpackOptions, visitedRefs []str | |||||||||||
| } | ||||||||||||
|
|
||||||||||||
| case mediatype.DatasetBaseType: | ||||||||||||
| if opts.SkillOptions != nil { | ||||||||||||
| // Skip datasets but still advance the index past non-remote datasets | ||||||||||||
| for idx := datasetIdx; idx < len(config.DataSets); idx++ { | ||||||||||||
| if config.DataSets[idx].RemotePath != "" { | ||||||||||||
| continue | ||||||||||||
| } | ||||||||||||
| datasetIdx = idx + 1 | ||||||||||||
| break | ||||||||||||
| } | ||||||||||||
| continue | ||||||||||||
| } | ||||||||||||
| // Since some datasets may be remote, we need to search the Kitfile for the next non-remote dataset | ||||||||||||
| var entry *artifact.DataSet | ||||||||||||
| for idx := datasetIdx; idx < len(config.DataSets); idx++ { | ||||||||||||
|
|
@@ -248,10 +204,6 @@ func unpackRecursive(ctx context.Context, opts *UnpackOptions, visitedRefs []str | |||||||||||
| output.Infof("Unpacking dataset %s to %s", entry.Name, entry.Path) | ||||||||||||
|
|
||||||||||||
| case mediatype.DocsBaseType: | ||||||||||||
| if opts.SkillOptions != nil { | ||||||||||||
| docsIdx += 1 | ||||||||||||
| continue | ||||||||||||
| } | ||||||||||||
| entry := config.Docs[docsIdx] | ||||||||||||
| docsIdx += 1 | ||||||||||||
| if !kitfile.LayerMatchesAnyFilter(entry, opts.FilterConfs) { | ||||||||||||
|
|
@@ -269,11 +221,6 @@ func unpackRecursive(ctx context.Context, opts *UnpackOptions, visitedRefs []str | |||||||||||
| output.Logf(output.LogLevelWarn, "Unknown media type %s: skipping unpack", layerDesc.MediaType) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // In skill mode, prompts are handled above; nothing else to unpack | ||||||||||||
| if opts.SkillOptions != nil { | ||||||||||||
| continue | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| if layerInfo != nil { | ||||||||||||
| if layerInfo.Digest != layerDesc.Digest.String() { | ||||||||||||
| return fmt.Errorf("digest in config and manifest do not match in %s", mediaType.UserString()) | ||||||||||||
|
|
@@ -292,22 +239,6 @@ func unpackRecursive(ctx context.Context, opts *UnpackOptions, visitedRefs []str | |||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Skill mode: end-of-loop summary | ||||||||||||
| if opts.SkillOptions != nil { | ||||||||||||
| if promptsFiltered == 0 { | ||||||||||||
| output.Infof("No prompt layers matched the specified filters") | ||||||||||||
| return nil | ||||||||||||
| } | ||||||||||||
| if skillsFound == 0 && len(skillErrors) == 0 { | ||||||||||||
| output.Infof("No agent skills found in modelkit. Prompt layers must contain a SKILL.md file to be installed as skills.") | ||||||||||||
| return nil | ||||||||||||
| } | ||||||||||||
| if len(skillErrors) > 0 { | ||||||||||||
| return fmt.Errorf("failed to install %d skill(s)", len(skillErrors)) | ||||||||||||
| } | ||||||||||||
| return nil | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Handle remotely stored files: first build a list so we can show a warning if remote files are skipped | ||||||||||||
| remoteFiles := map[string]s3api.S3ObjectReference{} | ||||||||||||
| for _, dataset := range config.DataSets { | ||||||||||||
|
|
@@ -369,6 +300,92 @@ func unpackRecursive(ctx context.Context, opts *UnpackOptions, visitedRefs []str | |||||||||||
| return nil | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // unpackSkill handles the --as-skill flow: prompt layers containing SKILL.md | ||||||||||||
| // are installed as agent skills and all other layer types are ignored. | ||||||||||||
| // Unlike unpackRecursive, it does not unpack the Kitfile, traverse parent | ||||||||||||
| // modelkits, or touch the filesystem outside the resolved skills directories. | ||||||||||||
| func unpackSkill(ctx context.Context, opts *UnpackOptions) error { | ||||||||||||
| ref := opts.ModelRef | ||||||||||||
| store, err := getStoreForRef(ctx, opts) | ||||||||||||
| if err != nil { | ||||||||||||
| ref := artifact.FormatRepositoryForDisplay(opts.ModelRef.String()) | ||||||||||||
| return fmt.Errorf("failed to find reference %s: %s", ref, err) | ||||||||||||
| } | ||||||||||||
| manifestDesc, err := store.Resolve(ctx, ref.Reference) | ||||||||||||
| if err != nil { | ||||||||||||
| return fmt.Errorf("failed to resolve reference: %w", err) | ||||||||||||
| } | ||||||||||||
| manifest, err := util.GetManifest(ctx, store, manifestDesc) | ||||||||||||
| if err != nil { | ||||||||||||
| return fmt.Errorf("failed to read manifest: %s", err) | ||||||||||||
| } | ||||||||||||
| config, err := util.GetKitfileForManifest(ctx, store, manifest) | ||||||||||||
| if err != nil { | ||||||||||||
| if errors.Is(err, util.ErrNoKitfile) { | ||||||||||||
| output.Infof("No Kitfile found in modelkit; no prompt layers to install as skills") | ||||||||||||
| return nil | ||||||||||||
| } | ||||||||||||
| return err | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| var promptsFiltered, skillsFound, skillErrorCount, promptIdx int | ||||||||||||
|
|
||||||||||||
| for _, layerDesc := range manifest.Layers { | ||||||||||||
| mediaType, err := mediatype.ParseMediaType(layerDesc.MediaType) | ||||||||||||
| if err != nil { | ||||||||||||
| output.Logf(output.LogLevelWarn, "Unknown media type %s: skipping", layerDesc.MediaType) | ||||||||||||
| continue | ||||||||||||
| } | ||||||||||||
| if mediaType.Base() != mediatype.CodeBaseType { | ||||||||||||
| continue | ||||||||||||
| } | ||||||||||||
| if layerDesc.Annotations[constants.LayerSubtypeAnnotation] != constants.LayerSubtypePrompt { | ||||||||||||
| continue | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
|
||||||||||||
| if promptIdx >= len(config.Prompts) { | |
| return fmt.Errorf("manifest contains more prompt layers than kitfile entries: prompt index %d out of range (prompts=%d)", promptIdx, len(config.Prompts)) | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The manifest's prompt layers and config.Prompts are produced 1:1 by the pack step from the same source, so promptIdx can't exceed len(config.Prompts) unless the modelkit is corrupt.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UnpackModelKit guards opts != nil above, but then unconditionally dereferences opts.SkillOptions. If opts is nil (which this function currently appears to allow), this will panic. Add an early opts == nil check (return a clear error) or change the condition to
if opts != nil && opts.SkillOptions != nilbefore calling unpackSkill/unpackRecursive.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added a check