Help us shape the future of Go! Take the 2026 Go Developer Survey

agentskill

package module
v0.0.12 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 10, 2026 License: MIT Imports: 22 Imported by: 3

README

agentskill

The Agent Skills format for Go agents over Open Responses: skills loaded, validated and rendered into the text a product puts in its instructions, plus the skill tool through which the model reads a skill's body and files. The AGENTS.md convention, loaded up front rather than on use, is the separate agentsmd module.

A skill is read through fs.FS, so it can come from a directory, an embedded bundle, an archive or an adapter over a remote store, and the model reaches every level of it through one tool rather than through a local read tool that may not exist.

catalog, err := agentskill.DiscoverDirs(".dax/skills", filepath.Join(home, ".dax", "skills"))
if err != nil {
	return err
}
for location, problems := range catalog.Problems {
	log.Printf("%s: %v", location, problems) // broken skills are listed, not hidden
}

cfg := agentturn.Config{
	Instructions: catalog.Prompt() + "\n\n" + catalog.Usage(),
	Tools:        []agenttool.Tool{catalog.Tool()},
}

Prompt renders the <available_skills> block byte for byte as the reference skills-ref to-prompt does for the same directories. It renders the skills the model can use: a skill with no name to call it by, or no description to choose it from, is loaded and reported in Problems but is neither listed nor served, since an entry the model cannot follow costs tokens on every turn. The tool serves the other two levels of progressive disclosure: called with a name it returns the body and the list of files; called with a name and a path it returns the file, text as text and an image as an image part. Each read sets a Read as the result's Details, which a recorder writes under agentskill.RecordNS: the name, the SKILL.md behind it, the path served and a sha256 of the bytes, so a session can say which skill that was and a replay serving other bytes is detectable. Skill.Instructions returns the text a read of the instructions serves, so a product can compute that digest itself, and Skill.FrontmatterSHA256 the digest of the frontmatter its allowed-tools came from, recorded on each read too: an approval bound to both covers what the model reads and what it is granted. The tool reads the skill file at each call, so an edited body is served as it now is. Edited frontmatter takes effect only when the product discovers again; until then the read says the catalogue is stale.

The tool reads any file in a skill's tree, and a skill's tree is wherever its source points: a project's skills directory that links into the workspace makes the workspace's .env a skill file. A product that holds such reads to its own rules builds the tool with WithFileClaim(fn). The tool then makes agenttool's facts claim (agenttool.Factual), and a policy can decide the read before it happens. The claim for a call is the call itself, followed by what fn returns for the file the call would serve. That file is the skill file as loaded for a read of the instructions, and the path otherwise. The claim and the call resolve the skill and the path in one place, so the file claimed is the file served. A call the tool refuses before reading anything, an unknown skill or an invalid path, claims the call itself alone. agenttool.Wrap forwards the claim.

Skills

  • Load(fsys, location) reads the SKILL.md at the root of any fs.FS; LoadDir does it for a directory; Parse reads bytes.
  • Validate reports every rule of the specification as a Problem, with the reference validator's verdicts and wording, and warns on a body over 500 lines. A malformed file fails to load, a repeated frontmatter key included; a valid file with a bad name loads and says what is wrong.
  • Discover(sources...) walks each source's direct children; the first skill with a given name wins, as PATH resolves a command. A source with a Qualifier lists a skill of a taken name as <qualifier>:<name>, as Claude Code lists a nested or plugin skill, rather than shadowing it. A skill with no name or description claims no name, and two sources may not share a Location. Catalog.Listed is the subset the prompt renders and the tool serves, and Lookup and Names agree with it.
  • Dir(path) is the source for a local directory, with one guard os.DirFS lacks: a symlink resolving outside the directory is refused. docs/source.md states the source, its path rules and that guard for an implementation in any language, beside the parts of the specification they rest on.
  • Files, Open and Rules reach a skill's resources and its parsed allowed-tools; enforcement is the host's. A token that opens a specifier and supplies none, Bash(), is refused rather than parsed as the bare Bash, which grants every call of the tool.
  • Encode writes a skill back; Parse(Encode(s)) yields s.

Conformance

The claim is bounded. Over the frontmatter both readers parse, and on the rules of the specification, Validate gives skills-ref 0.1.1's verdicts in its wording and Prompt is its to-prompt byte for byte. Three classes of input fall outside that, and they part in different directions.

  • The YAML dialect. The reference parses frontmatter with strictyaml, which forbids flow style, anchors and aliases and infers no types. This module parses with go.yaml.in/yaml/v3, which allows them, so metadata: {author: example-org} loads here and is refused there. The one member of that class where the two readers disagreed about what a skill says rather than whether it loads, a repeated key, is refused on both sides.
  • Unicode normalisation. The reference normalises the name and the directory name to NFKC before comparing them and before checking the character rules; this module compares them as written, so a name whose ligature folds to the directory name is valid there and rejected here. Closing it needs golang.org/x/text, which the module's dependency rule excludes.
  • An empty allowed-tools specifier. Bash() and Bash(!) are refused here and accepted there, deliberately: an empty specifier is indistinguishable from the bare Bash, which grants every call of the tool.

testdata/ref holds the fixture tree with what skills-ref 0.1.1 printed over it; the tests compare both sides, so a divergence that is not one of these fails the build. make interop runs the reference CLI live over testdata/skills.

CLI

go run ./cmd/agentskill mirrors the reference CLI: validate, read-properties and to-prompt. make interop diffs its output against skills-ref over the fixtures.

Development

make check    # gofmt, tidy, vet, deps, staticcheck, govulncheck, race tests
make interop  # needs uvx and the network

Golden outputs live under testdata/golden; regenerate with go test . -update and review the diff.

License

MIT; see LICENSE. NOTICE credits the Agent Skills project, whose specification (CC BY 4.0) docs/source.md quotes and whose reference library (Apache 2.0) Validate and Prompt match.

Documentation

Overview

Package agentskill loads, validates and renders the Agent Skills format for Go agents over Open Responses, read through fs.FS so a skill can live in a directory, an embedded bundle, an archive or an adapter over a remote store, with the skill tool through which the model reads a skill's body and files.

A Skill is one skill directory, loaded byte for byte: the parsed frontmatter, the Markdown body and the tree of resource files. Load reads one from any fs.FS and [Validate] reports the specification's rules with the same verdicts as the reference skills-ref validator. Discover loads every skill under an ordered list of Source values into a Catalog, whose Catalog.Prompt renders the available_skills block a product puts in its instructions and whose Catalog.Tool serves the body and the files to the model.

The package depends on openresponses, agenttool, one YAML parser and the standard library. The agent loop is never imported; a product wires the rendered prompt and the tool into its configuration. The AGENTS.md convention, loaded up front rather than on use, is the separate agentsmd module.

Index

Constants

View Source
const (
	MaxNameLength          = 64
	MaxDescriptionLength   = 1024
	MaxCompatibilityLength = 500
	// MaxBodyLines is the length the specification advises a SKILL.md
	// to stay under. A longer body is a warning.
	MaxBodyLines = 500
)

The specification's limits.

View Source
const DefaultMaxBytes = 1 << 20

DefaultMaxBytes is the size above which the tool refuses a file.

View Source
const RecordNS = "agentskill:read"

RecordNS is the namespace of the session entry a recorder writes for a skill read, the value Read.RecordNS returns.

View Source
const ToolName = "skill"

ToolName is the name of the tool Catalog.Tool returns.

Variables

View Source
var (
	// ErrNoFrontmatter is returned when the file does not begin with
	// the opening fence.
	ErrNoFrontmatter = errors.New("SKILL.md must start with YAML frontmatter (---)")
	// ErrUnclosedFrontmatter is returned when the closing fence is
	// missing.
	ErrUnclosedFrontmatter = errors.New("SKILL.md frontmatter not properly closed with ---")
	// ErrNotMapping is returned when the frontmatter is valid YAML but
	// not a mapping.
	ErrNotMapping = errors.New("SKILL.md frontmatter must be a YAML mapping")
)

Errors from Parse. They are about the frontmatter fence and the YAML alone; a value of the wrong shape loads and is reported by Skill.Validate.

View Source
var ErrNoSkillFile = errors.New("Missing required file: SKILL.md")

ErrNoSkillFile is returned by Load when the root of the file system holds no SKILL.md.

View Source
var ErrOutside = errors.New("path resolves outside the directory")

ErrOutside is the error of a Dir file whose path resolves, through a symlink, to a place outside the directory.

View Source
var ErrSkillChanged = errors.New("the skill file changed since the skills were discovered")

ErrSkillChanged is returned by Skill.Instructions, and so by the skill tool, when the skill file is no longer one the skill can be served from: it is gone, it is now spelled otherwise, or its frontmatter no longer parses, so there is no body to separate from it. The product discovers the skills again. Changed frontmatter that parses is not this error; see Skill.Instructions.

Functions

func Encode

func Encode(s *Skill) ([]byte, error)

Encode writes the skill back as a SKILL.md: the frontmatter with the specification's keys in specification order, Extra keys after them in sorted order, then the body verbatim. Parse(Encode(s)) yields s. Optional fields that are empty are omitted, as is a value that failed to decode into its field, since the Skill never held it.

func HasErrors

func HasErrors(problems []Problem) bool

HasErrors reports whether any problem is an Error.

Types

type Catalog

type Catalog struct {
	// Skills holds the skills the prompt lists, in source order and
	// then directory-name order. A skill with problems is still here;
	// a product that wants only valid skills filters on Problems.
	Skills []*Skill
	// Shadowed holds skills whose name an earlier source had already
	// claimed, kept so a product can report them. Each one's
	// [Skill.ShadowedBy] names the skill that holds the name.
	Shadowed []*Skill
	// Problems maps a skill's location to what [Skill.Validate]
	// reported, or to the one error that stopped it loading. A
	// directory skipped for a skill file misnamed in case alone has its
	// [Warning] under that file's location.
	Problems map[string][]Problem
}

Catalog is the skills found by Discover: the winners in source order, the duplicates they shadowed, and every problem found on the way.

func Discover

func Discover(sources ...Source) (*Catalog, error)

Discover loads every skill under the sources: each direct child directory holding a SKILL.md is one skill. Sources are searched in order and the first skill with a given name wins, as PATH resolves a command, so a caller lists the most specific source first. A later skill of a taken name is shadowed, unless its source has a Source.Qualifier, in which case it is listed under the qualified name, and shadowed only if that is taken too. A child without a SKILL.md is not a skill and is skipped; one holding a file named SKILL.md in another case, such as Skill.md, is skipped on every host with a Warning in Problems under that file's location.

A skill Catalog.Listed will not offer, for want of a name or a description or for a colon in its name, is kept in Skills but claims no name, so it never shadows a skill that could be offered.

A skill that fails to load is not fatal: the error goes into Problems under its location and discovery continues. A source whose FS cannot be read at its root is an error, because a missing local directory should be skipped by the caller on Dir's error, not silently here. So is a qualifier holding whitespace or a control character, and so are two sources with one Location, a trailing slash aside, whose skills of one directory name would share a location in the prompt and a key in Problems.

func DiscoverDirs

func DiscoverDirs(dirs ...string) (*Catalog, error)

DiscoverDirs is Discover over Dir for each path. A path that is not a directory is an error.

func (*Catalog) Listed added in v0.0.3

func (c *Catalog) Listed() []*Skill

Listed returns the skills the model is offered, in catalog order: the winners that have both a name to call them by and a description to choose them from. A skill missing either cannot be used, since the name is the only handle the tool takes and the description is the only thing the model chooses on, so it is left out of Catalog.Prompt, Catalog.Names, Catalog.Lookup and the tool, as the client implementation guide says to skip it and as the reference's to_prompt refuses to render it.

A skill whose name holds a colon is left out too, where the reference would render it: the specification never allows one, and listed it would claim the "<qualifier>:<name>" a Source.Qualifier gives another skill, so that the model reached the wrong one.

It stays in Skills and its problems stay in Problems, so a product can still list a broken skill and say what is wrong with it, which is why they are loaded at all.

func (*Catalog) Lookup

func (c *Catalog) Lookup(name string) (*Skill, bool)

Lookup returns the skill listed as name, its Skill.ListedName, among the skills Catalog.Listed offers, so what the model can be served is what the prompt lists.

func (*Catalog) Names

func (c *Catalog) Names() []string

Names returns the listed names of the skills Catalog.Listed offers, in catalog order.

func (*Catalog) Prompt

func (c *Catalog) Prompt() string

Prompt renders the available_skills block in the exact shape of the reference library's to_prompt: one skill entry per skill in catalog order, its listed name, description and location each on their own line between their tags, with the name and description HTML-escaped. For local sources without a Source.Qualifier the output is byte for byte what skills-ref to-prompt prints for the same directories; the reference has no qualified names. An empty catalog renders the empty block.

Only the skills Catalog.Listed offers are rendered: an entry the model cannot call, because the skill has no name, or choose, because it has no description, would cost tokens on every turn and offer a pointer that goes nowhere. The reference refuses to render the whole block over such a set; this renders the rest.

func (*Catalog) Tool

func (c *Catalog) Tool(opts ...ToolOption) agenttool.Tool

Tool returns an agenttool.Tool named "skill". Called with a name alone it returns the skill's body followed by the list of its files, so the model learns both what to do and what there is to read. Called with a name and a path it returns that file: text as text, an image as an image part, anything else refused with its size and detected type. An unknown skill is an error listing the names the model could have used; an unknown path is an error listing the files. A path naming the skill file itself, "SKILL.md" in any case, is served as the instructions, not as a file: models know a skill's instructions as SKILL.md and ask for it by path, and a host that grants allowed-tools on a read of the instructions must see that read as one, while the frontmatter stays out of the reply.

Everything the tool returns is bytes from the source, framed, never transformed, so the transcript records exactly what the model read. The instructions are read from the skill file at the call, as the files are, so a reply never puts a body from discovery beside a file list from now. A skill whose frontmatter changed since discovery is served its new body under the frontmatter as loaded, and the Read says so; one whose skill file is gone, renamed or unparseable is refused with ErrSkillChanged.

The tool serves the skills Catalog.Listed offers, which are the ones Catalog.Prompt renders, so the model can fetch everything it is shown and nothing it is not.

Every successful call sets a Read as the result's Details, naming the skill, the SKILL.md behind the name and a digest of the bytes served. It implements agenttool.Recordable, so a recorder writes it under RecordNS; the model never sees it.

Built with WithFileClaim, the tool claims the file each call would serve, so a product's policy can decide the read before it happens.

func (*Catalog) Usage

func (c *Catalog) Usage() string

Usage is one paragraph telling the model how to use the skills the prompt lists through the skill tool. A product appends it to Catalog.Prompt when it offers Catalog.Tool.

type Problem

type Problem struct {
	Severity Severity
	// Field is the frontmatter key the problem is about, "body" for
	// the Markdown, or "" for the skill as a whole.
	Field string
	// Message is the reference validator's wording where it has one.
	Message string
}

Problem is one thing Skill.Validate found.

func (Problem) String

func (p Problem) String() string

String returns the severity and the message, "error: ...".

type Read added in v0.0.3

type Read struct {
	// Name is the skill's name, as the model wrote it.
	Name string `json:"name"`
	// Location is the skill's Location: the SKILL.md that was read.
	Location string `json:"location"`
	// Path is the file inside the skill that was served, "" for a read
	// of the skill's own instructions. A call whose path names the
	// skill file, "SKILL.md" in any case, is a read of the
	// instructions and is recorded with "".
	Path string `json:"path,omitempty"`
	// Bytes is the number of bytes served.
	Bytes int `json:"bytes"`
	// SHA256 is the hex digest of the bytes served: for a read of the
	// instructions the text of the reply, which [Skill.Instructions]
	// returns, and for a file its own bytes, before an image is base64
	// encoded into its part.
	SHA256 string `json:"sha256"`
	// FrontmatterSHA256 is [Skill.FrontmatterSHA256]: the digest of the
	// frontmatter the catalogue was built from, whose allowed-tools a
	// host grants on a read of the instructions. SHA256 says what the
	// model read and this says what it was granted, so an approval
	// bound to both catches a rewrite of either.
	FrontmatterSHA256 string `json:"frontmatter_sha256,omitempty"`
	// FrontmatterChanged is set when the skill file on disk holds other
	// frontmatter than the catalogue was built from. The body served is
	// the one on disk, under the frontmatter as loaded: the listing and
	// any grant stay the old ones until the product discovers again.
	FrontmatterChanged bool `json:"frontmatter_changed,omitempty"`
}

Read is what the skill tool did, set as the Result.Details of every successful call. It implements agenttool.Recordable, so a recorder that knows nothing about skills writes it beside the call as a namespaced JSON entry, and the session can then say which SKILL.md was served rather than only which name the model wrote.

A name is not an identity: it is what a PATH-style search across several sources resolved to at discovery time. Location says which skill won, and SHA256 says what its bytes were, so a replay that serves different bytes for the same name is detectable.

func (Read) RecordNS added in v0.0.3

func (Read) RecordNS() string

RecordNS returns RecordNS, the namespace a recorder writes the read under.

type Severity

type Severity int

Severity says whether a Problem fails validation.

const (
	// Error is a rule of the specification the skill breaks. Any Error
	// problem fails validation.
	Error Severity = iota
	// Warning is advice the specification gives that the skill does
	// not follow. Warnings never fail validation.
	Warning
)

func (Severity) String

func (s Severity) String() string

String returns "error" or "warning".

type Skill

type Skill struct {
	// FS is the skill's own tree, rooted at its directory: a fs.Sub of
	// the source. SKILL.md is at its root. It is nil for a skill built
	// by [Parse].
	FS fs.FS
	// Location is what the prompt shows: the source location joined
	// with the directory name and SKILL.md.
	Location string
	// DirName is the directory's base name, which Name must match. It
	// is empty when the skill was not loaded from a directory, and the
	// match is then not checked.
	DirName string

	// Name is the frontmatter name.
	Name string
	// Description is the frontmatter description.
	Description string
	// License is the frontmatter license, if any.
	License string
	// Compatibility is the frontmatter compatibility, if any.
	Compatibility string
	// Metadata is the frontmatter metadata map, if any.
	Metadata map[string]string
	// AllowedTools is the frontmatter allowed-tools string, if any.
	// [Skill.Rules] parses it.
	AllowedTools string
	// Extra holds frontmatter keys the specification does not define,
	// decoded but not interpreted, so a newer skill loads in an older
	// reader and re-encodes without loss. Each is a validation error,
	// as the reference validator rejects them.
	Extra map[string]any

	// Body is the Markdown after the frontmatter, verbatim, as it was
	// when the skill was loaded. [Skill.Instructions], which the skill
	// tool serves, reads the skill file again.
	Body string

	// Qualifier is the source's [Source.Qualifier] when [Discover]
	// listed the skill under a qualified name because an earlier source
	// held its own, and empty otherwise. [Skill.ListedName] joins it
	// with Name. It is the product's, not the author's, so validation
	// never reads it.
	Qualifier string
	// ShadowedBy is the Location of the skill that holds the name this
	// one wanted, which may be a qualified name when Qualifier was
	// cleared. [Discover] sets it on each skill in [Catalog.Shadowed]
	// and leaves it empty otherwise.
	ShadowedBy string
	// contains filtered or unexported fields
}

Skill is one skill directory, loaded. The frontmatter fields carry the specification's keys; Name and Description are trimmed of surrounding space as the reference reader trims them, every other value is kept as written.

func Load

func Load(fsys fs.FS, location string) (*Skill, error)

Load reads the skill at the root of fsys: its SKILL.md, parsed with Parse, attached to fsys so Skill.Files and Skill.Open reach the resource files. location names the skill directory for the prompt; the skill's Location is it joined with the name of the skill file, and its DirName is its last element, so a name that does not match the directory is reported. An empty location leaves DirName empty and the match unchecked.

A malformed SKILL.md fails to load. A well-formed one with a bad name loads, and Skill.Validate says what is wrong, so a product can list a broken skill and say why rather than make it vanish.

func LoadDir

func LoadDir(dir string) (*Skill, error)

LoadDir is Load over the directory, with the absolute, symlink resolved path as the location and the Dir guard on its files.

func Parse

func Parse(src []byte) (*Skill, error)

Parse reads the frontmatter and the body of a SKILL.md from src. The result has no FS, Location or DirName; Load fills those in. Parse fails only on the fence and the YAML: a value of the wrong shape, such as a name that is a list, loads with an empty field and is reported by Skill.Validate.

A key written twice is one of the YAML failures, as it is for the reference reader's strictyaml, rather than a problem to report: the file says two things with equal authority and there is no correct value to load. A reviewer's eye stops at the first line, and the model would be told the second.

The fence is line based: the first line must be "---" and the frontmatter runs to the next line that is "---". The body is everything after that line, verbatim. Scalars are read as their text, so a name written as a number is the number's text, as the reference reader does.

func (*Skill) Files

func (s *Skill) Files() ([]string, error)

Files lists every file in the skill's tree except the skill file itself, as fs paths relative to the skill's root, sorted. A skill with no FS has no files.

A symlink is listed only when it stats as a file, so the list holds only what Skill.Open can serve. A link the source refuses, as Dir refuses one leading outside, a dangling link and a link to a directory, which the walk does not enter, are left out.

func (*Skill) FrontmatterSHA256 added in v0.0.10

func (s *Skill) FrontmatterSHA256() string

FrontmatterSHA256 returns the hex SHA-256 of the skill file's frontmatter as Load read it, the bytes between the fences: what the skill's fields, its allowed-tools among them, were parsed from. It is "" for a skill Load did not build. It does not change when the file on disk does; a product that approves a skill's grant binds the approval to it, and a rewrite of allowed-tools reaches a host only through a new discovery, under a new digest.

func (*Skill) Instructions added in v0.0.10

func (s *Skill) Instructions() (string, error)

Instructions returns the text the skill tool serves for a read of the skill's instructions: the Markdown body followed by the list of the skill's files with their sizes. Read.SHA256 for such a read is the hex SHA-256 of this text, so a product that binds an approval to what the model reads computes the digest the session will record:

text, err := s.Instructions()
sum := sha256.Sum256([]byte(text))
digest := hex.EncodeToString(sum[:])

The text does not cover the frontmatter, so it says nothing of what allowed-tools grants; bind an approval to Skill.FrontmatterSHA256 as well.

The text is read now, not at discovery: a skill Load read has its skill file read again, and the body is the body on disk. When the frontmatter on disk differs from the one loaded, the body is still served: the skill's fields, its listing and any grant made from its allowed-tools stay those loaded, which is what was approved, until the product discovers again. When the file is gone, renamed or no longer parses, Instructions returns an error wrapping ErrSkillChanged. A skill Load did not build serves Skill.Body.

The file list carries sizes, so the digest changes when any of the skill's files is added, removed or resized: the model reads those too.

func (*Skill) ListedName added in v0.0.6

func (s *Skill) ListedName() string

ListedName is the name the catalog lists the skill under, which the model calls the skill tool with: Name, or "<Qualifier>:<Name>" when Discover qualified it.

func (*Skill) Open

func (s *Skill) Open(name string) (fs.File, error)

Open opens a file in the skill's tree. name must satisfy fs.ValidPath, which refuses "..", absolute paths and empty elements, so nothing outside the tree can be named.

func (*Skill) Rules

func (s *Skill) Rules() ([]ToolRule, error)

Rules parses AllowedTools into one ToolRule per token. Tokens are separated by ASCII whitespace (space, tab, line feed, vertical tab, form feed, carriage return) outside parentheses, so a specifier may hold spaces; unbalanced parentheses are an error, because the token cannot then be delimited. An empty field yields no rules and no error.

A token that opens a specifier and supplies none, "Bash()", or whose specifier is the bare carve-out "Bash(!)", is an error rather than a rule, as agentpolicy's parser of the same grammar refuses both. An empty specifier would otherwise be indistinguishable from the bare token "Bash", which grants every call of the tool, so the narrowest thing a skill can write would arrive as the widest grant there is.

func (*Skill) Validate

func (s *Skill) Validate() []Problem

Validate checks the skill against the specification and returns one Problem per rule broken, in the reference validator's order and with its wording: unknown frontmatter keys, then name, description and compatibility. Rules the reference validator does not check follow: metadata values that are not strings, allowed-tools tokens that do not parse, and a body over MaxBodyLines lines, which is a Warning. A nil result means the skill is valid.

The verdicts and their wording are the reference validator's over the frontmatter both readers parse. Three classes of input fall outside that: Unicode normalisation, the YAML dialect, and an empty allowed-tools specifier, which Skill.Rules refuses and the reference accepts. The README names all three.

Name length is counted in characters, not bytes. The directory match is a plain comparison; the reference validator normalises both sides to NFKC first, which the standard library cannot do, so a name and directory that differ only in Unicode normalisation pass there and fail here.

type Source

type Source struct {
	// FS holds skill directories as its direct children, each with a
	// SKILL.md. os.DirFS, embed.FS, a zip, or an adapter over a remote
	// store.
	FS fs.FS
	// Location names the source for the model, such as an absolute
	// directory, a URL or "mcp://docs". A skill's location is this
	// joined with the skill's directory name and SKILL.md. Two sources
	// given to one [Discover] must not share it.
	Location string
	// Qualifier, when set, names the source for the model the way a
	// nested directory or a plugin is named, such as "apps/web" or
	// "my-plugin". A skill of this source whose name an earlier source
	// already claimed is listed as "<Qualifier>:<name>" rather than
	// shadowed; a skill whose name is free is listed under its name,
	// and a second skill of one name within this source is shadowed.
	// Without it a taken name is shadowed. It may not hold whitespace
	// or control characters.
	Qualifier string
}

Source is where skills come from: a file system and the name the prompt shows for it.

func Dir

func Dir(dir string) (Source, error)

Dir is the Source for a local directory: os.DirFS over the directory, with its absolute, symlink resolved path as the location, so the prompt shows what skills-ref shows. The directory must exist.

Dir adds the one guard os.DirFS lacks: a path that resolves through a symlink to somewhere outside the directory is refused with ErrOutside, because a local skill tree may be user-installed and a link to /etc inside it must not be readable through the tool. That includes a skill directory that is itself a link elsewhere; add the link's target as its own Dir source instead.

type ToolOption

type ToolOption func(*toolOptions)

ToolOption configures Catalog.Tool.

func WithFileClaim added in v0.0.12

func WithFileClaim(fn func(ctx context.Context, s *Skill, file string) ([]agenttool.FactCall, error)) ToolOption

WithFileClaim has the skill tool make the facts claim (agenttool.Factual) for each call, naming the file the call would serve, so a product's policy can decide the read before it happens: hold it to the rules of the product's own read tool, say, where a skills directory is a link into the workspace and a skill's file is the workspace's .env.

The claim is the call itself followed by the calls fn returns for that file. file is a path inside s.FS: for a read of the skill's instructions, by no path or by "SKILL.md" in any case, the skill file as Load read it, and otherwise the path the call names. The claim and the call resolve the skill and the path in one place, so they cannot name different files. The list of files that follows the instructions is not claimed: it names files and serves none.

A call the tool will refuse without reading anything, an unknown skill or a path that is not a relative path inside the skill, claims the call itself alone, and fn is not called. So does a read of the instructions of a skill Load did not build, which serves the body it holds and reads no file. An error from fn is the claim's error, which a policy reads as a call it cannot decide.

Without this option, or with a nil fn, the tool makes no claim.

func WithMaxBytes

func WithMaxBytes(n int64) ToolOption

WithMaxBytes sets the largest file the tool returns; a larger one is refused with its size. There is no partial read, because a half file is a worse instruction than none. The default is DefaultMaxBytes.

type ToolRule

type ToolRule struct {
	Tool string
	// Spec is the text inside the parentheses, "" when the token has
	// no parentheses at all. A token whose parentheses are empty does
	// not parse; see [Skill.Rules].
	Spec string
}

ToolRule is one token of allowed-tools: a tool name, or a name with a specifier in parentheses, "Bash(git:*)". The specifier's syntax belongs to the product; the rule keeps it as a string, spaces and nested parentheses included, as in "Bash(git add *)" and "Edit(./Finance (2024)/**)".

func (ToolRule) Matches

func (r ToolRule) Matches(toolName string) bool

Matches reports whether the rule names the tool. The specifier is not consulted; a host that interprets it checks Spec itself.

func (ToolRule) String

func (r ToolRule) String() string

String returns the token as it was written.

Directories

Path Synopsis
cmd
agentskill command
Command agentskill mirrors the skills-ref CLI: validate a skill directory, print its frontmatter as JSON, or render the available_skills block for one or more skill directories.
Command agentskill mirrors the skills-ref CLI: validate a skill directory, print its frontmatter as JSON, or render the available_skills block for one or more skill directories.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL