Documentation
¶
Overview ¶
Package skill implements the Skill tool: an on-demand reader of curated embedded (and optionally untrusted workspace) SKILL.md bodies, scoped to the one agent the tool is bound to. Preparation validates the name once, takes a TOCTOU-safe workspace snapshot when applicable, and emits a typed context.load requirement scoped to the skill identity.
Index ¶
- Constants
- func EmbeddedSkillIdentity(name string) string
- func NewEmbeddedSkillLoader(fsys fs.FS, allow map[identity.AgentName]map[string]struct{}) *embeddedSkillLoader
- func WorkspaceSkillIdentity(name string) string
- type MalformedSkillError
- type Skill
- func (s *Skill) AuditSummary(argsJSON string) string
- func (s *Skill) Info(context.Context) (*tool.ToolInfo, error)
- func (s *Skill) InvokableRun(ctx context.Context, _ string) (*tool.ToolResult, error)
- func (s *Skill) PrepareCall(_ context.Context, executionID uuid.UUID, argsJSON string) (tool.Request, tool.PreparedArtifact, error)
- type SkillContainmentError
- type SkillDescriber
- type SkillLoader
- type SkillMeta
- type SkillNotFoundError
- type SkillOption
- type UnknownSkillError
Constants ¶
const CapabilityContextLoad = "context.load"
CapabilityContextLoad is the normalized capability kind for loading curated context (a skill body) into the conversation. It is a PRODUCT-bound kind: the consumer routes it to its own access source via a gate AccessBinding (the access-profile spec names the product composition root's binding), never to the sandbox profile, and it is never silently mapped to command execution. It has no durable workspace-rule representation, so context.load requirements carry NO reusable candidates.
Variables ¶
This section is empty.
Functions ¶
func EmbeddedSkillIdentity ¶
EmbeddedSkillIdentity is the canonical skill identity (requirement Scope and Match) for a curated, compiled-in skill load.
func NewEmbeddedSkillLoader ¶
func NewEmbeddedSkillLoader(fsys fs.FS, allow map[identity.AgentName]map[string]struct{}) *embeddedSkillLoader
NewEmbeddedSkillLoader wires an embeddedSkillLoader from an injected file system and a per-agent allow-map. fsys is the catalogue root holding skills/<name>/SKILL.md (an embed.FS satisfies fs.FS); allow[agent] is that agent's closed set of permitted skill names. Dependencies are injected here at the composition root so the tools package never imports the embed package product, keeping the dependency arrow product -> tools and cycle-free.
A nil allow-map is treated as "no agent is authorized for anything" — the fail-secure default. The returned concrete type satisfies SkillLoader.
func WorkspaceSkillIdentity ¶
WorkspaceSkillIdentity is the canonical skill identity (requirement Scope and Match) for an untrusted, project-local workspace skill load.
Types ¶
type MalformedSkillError ¶
type MalformedSkillError struct {
Name string // the skill identifier, if known ("" when unknown)
Reason string // non-secret, human-readable reason for the rejection
}
MalformedSkillError is returned when a SKILL.md document cannot be parsed: it is oversize, has no opening frontmatter fence, has an unterminated fence, or carries a duplicate frontmatter key. The parser is fail-secure and never returns a partial or ambiguous parse alongside this error. It is errors.As-recoverable so the caller can surface the offending skill and a non-secret reason. Name is the skill identifier when known (empty when the raw bytes are parsed before a name is established).
func (*MalformedSkillError) Error ¶
func (e *MalformedSkillError) Error() string
type Skill ¶
type Skill struct {
// contains filtered or unexported fields
}
Skill loads a single named SKILL.md body for the ONE agent it is bound to. It depends only on the narrow SkillLoader (DIP) and its own immutable agent identity; it never imports a product package and holds no allow-map of its own.
workspaceRoot is the OPTIONAL untrusted workspace source. When empty (the default), the tool is embedded-only and behaves exactly as in P2 (auto-approve, no Prepare/gate). When set (via WithWorkspaceRoot at the composition root, for an agent the operator grants runtime skills in Phase 3c), a non-embedded name is resolved as a human-gated workspace load. It is immutable after construction.
func NewSkill ¶
func NewSkill(loader SkillLoader, agent identity.AgentName, opts ...SkillOption) *Skill
NewSkill constructs a Skill bound to a loader and the agent identity it serves. The swarm wires one per skilled agent at the composition root; the agent name is fixed at construction so every Load is scoped to that agent's closed allow-set. With no options it is embedded-only; pass WithWorkspaceRoot to additionally allow human-gated workspace skill loads.
func (*Skill) AuditSummary ¶
AuditSummary returns a redacted, body-free one-line summary: the skill NAME only. An unparseable or empty-name args document yields a generic summary; the skill body is never included.
func (*Skill) Info ¶
Info returns Skill's self-description. Name MUST equal "Skill". The catalog of which skills are available is rendered into the agent's SYSTEM PROMPT by the swarm, not here, so the description is a static lead.
func (*Skill) InvokableRun ¶
InvokableRun returns the requested skill body as the tool result, executing ONLY the prepared artifact bound to this call (the raw argsJSON is never reparsed; without an artifact the tool fails closed). For a WORKSPACE load it returns the APPROVED SNAPSHOT body PrepareCall bound to this call — read back from ctx via loop.PreparedCallFromContext, NEVER re-reading the file — so the bytes that run are exactly the bytes the human approved (TOCTOU-safe; a workspace writer cannot swap the body between the prompt and execution). For the embedded path it asks the bound loader for the curated body of the PREPARED name, scoped to this tool's agent. Every failure mode is a tool-result error STRING — never a Go error and never echoing a body on an error path.
func (*Skill) PrepareCall ¶
func (s *Skill) PrepareCall(_ context.Context, executionID uuid.UUID, argsJSON string) (tool.Request, tool.PreparedArtifact, error)
PrepareCall decodes and validates the untrusted {name} ONCE and produces the typed access request plus the per-call artifact executed by InvokableRun — the TOCTOU-safe snapshot seam for a workspace load. Malformed or empty-name args fail here (fail-secure: no gate, no execution) in every configuration.
- Embedded-wins: an embedded name never consults the workspace (so an embedded skill can never be shadowed by an attacker-planted workspace file of the same name). The request carries one context.load requirement scoped to the canonical EMBEDDED skill identity; the artifact binds the validated name (a tool.TokenArtifact) so execution never reparses JSON.
- Workspace-enabled, non-embedded name → loadWorkspaceSkill takes the snapshot ONCE (body + RelPath/Size/SHA256) and the request carries context.load scoped to the WORKSPACE skill identity PLUS the applicable filesystem.read requirement for the canonical snapshot path, so one combined prompt covers both. A load error (containment/malformed/ not-found) fails preparation.
- Embedded-only, unknown name → an EMPTY request (no capability is exercised: the load can only fail) with the name-bound artifact; InvokableRun fails secure at the result with the UnknownSkillError string, which names the valid recovery (pick a listed skill).
type SkillContainmentError ¶
type SkillContainmentError struct {
Name string // the rejected workspace skill name
Reason string // non-secret, human-readable reason for the rejection
}
SkillContainmentError is returned when an UNTRUSTED workspace skill name or path is rejected by the containment rules of the workspace loader (design §7a): a name that is not a bounded ASCII slug (empty, ".", "..", a path separator, a control character, uppercase, or over-length), or a resolved path that escapes the workspace root (an intermediate-dir or final-file symlink leaving the root, a ".." traversal) or whose target is not a regular file (a directory, device, FIFO, or symlink). It is fail-secure — the load is denied, never guessed — and errors.As-recoverable so the caller can surface the offending name and a non-secret reason WITHOUT loading the untrusted body. It is distinct from UnknownSkillError (an unauthorized embedded name) and SkillNotFoundError (an authorized name whose file is absent): this one means the request itself violated containment.
func (*SkillContainmentError) Error ¶
func (e *SkillContainmentError) Error() string
type SkillDescriber ¶
type SkillDescriber interface {
Describe(ctx context.Context, agent identity.AgentName, name string) (SkillMeta, error)
}
SkillDescriber resolves a named skill into its frontmatter METADATA (name+description) WITHOUT the body — the data the swarm renders into an agent's <available_skills> catalog. It authorizes (agent, name) against the SAME closed allow-set as SkillLoader.Load, so a catalog can only ever list a skill the agent is actually allowed to load. It is a separate, focused interface (interface segregation): the Skill tool depends only on SkillLoader; the catalog builder depends only on SkillDescriber.
type SkillLoader ¶
type SkillLoader interface {
Load(ctx context.Context, agent identity.AgentName, name string) (string, error)
Allowed(agent identity.AgentName, name string) bool
}
SkillLoader resolves a named skill into the markdown body to inject into an agent's context. It is the narrow seam between the Skill tool and the on-disk (embedded) skill catalogue: the tool asks for a body by (agent, name) and the loader is responsible for authorizing the request and reading the document.
The agent identity is a parameter — not loader state — so a single loader can serve every agent in a swarm while still scoping each call to that agent's own allowed-skill set. Implementations MUST be fail-secure: an unauthorized or unknown name is denied, never guessed.
Allowed is the read-only membership predicate over the SAME closed allow-set Load authorizes against, WITHOUT touching the filesystem: it answers "is name an embedded skill this agent may load?". The workspace-aware Skill tool uses it as the embedded-wins discriminator (embedded names auto-approve and resolve via Load; only a NON-embedded name is ever considered for an untrusted workspace load). It is fail-secure: an unknown agent, a nil allow-map, or a non-member name all report false. Both methods are used by the single Skill-tool consumer, so combining them does not over-widen the interface (interface segregation).
type SkillMeta ¶
SkillMeta is the flat, typed view of a SKILL.md frontmatter block. Only the fields the loader needs are surfaced; every other frontmatter key is parsed (to detect duplicates / malformed lines) but otherwise ignored. The frontmatter is treated as inert data — values are trimmed but never executed, interpolated, or otherwise interpreted.
func DiscoverWorkspaceSkills ¶
DiscoverWorkspaceSkills returns validated metadata for direct .skills/<name>/SKILL.md candidates beneath root. Discovery is intentionally metadata-only and fail-soft: malformed, unreadable, empty, mismatched, or unsafe candidates are omitted, and filesystem errors are not exposed to the caller. It reads directory entries in batches of 32 and reads at most 257 entries: a directory exceeding 256 entries produces an empty catalog. For a bounded directory, eligible names are sorted before at most 64 documents are inspected and at most 32 records returned. Skill bodies remain available only through the gated Skill tool.
type SkillNotFoundError ¶
type SkillNotFoundError struct {
Name string // the authorized skill whose SKILL.md is missing
Err error // the wrapped fs error (e.g. fs.ErrNotExist)
}
SkillNotFoundError is returned when a skill name has passed the per-agent authorization check (it is a member of the agent's closed allow-set) but its SKILL.md document is absent from the backing file system. This is distinct from UnknownSkillError — which denies an untrusted, unauthorized name — and signals a catalogue/embed integrity problem (an allowed skill whose file was not shipped) rather than a denied request. It is errors.As-recoverable so the caller can surface the missing skill name; the underlying fs error is wrapped and reachable via errors.Unwrap for diagnostics.
func (*SkillNotFoundError) Error ¶
func (e *SkillNotFoundError) Error() string
func (*SkillNotFoundError) Unwrap ¶
func (e *SkillNotFoundError) Unwrap() error
type SkillOption ¶
type SkillOption func(*Skill)
SkillOption configures an optional capability on a Skill at construction. Options are applied at the composition root only; the resulting Skill is immutable.
func WithWorkspaceRoot ¶
func WithWorkspaceRoot(root string) SkillOption
WithWorkspaceRoot enables the untrusted workspace skill source rooted at root: a non-embedded skill name is then resolved from <root>/.skills/<name>/SKILL.md as a human-gated (Ask) load bound to a TOCTOU-safe snapshot. An empty root is a no-op (the Skill stays embedded-only) — the fail-secure default. Only the wiring for an agent the operator grants runtime skills sets this (Phase 3c); embedded-only agents never receive it and so never gain the workspace source.
type UnknownSkillError ¶
type UnknownSkillError struct {
Agent identity.AgentName // the agent that requested the skill
Name string // the requested (unknown/unauthorized) skill name
}
UnknownSkillError is returned when a skill is requested by name that is not known to — or not authorized for — the requesting agent. It is fail-secure: the Skill tool denies the invocation rather than guessing. It is errors.As-recoverable so the caller can report which agent asked for which skill without leaking the (curated) skill catalogue.
func (*UnknownSkillError) Error ¶
func (e *UnknownSkillError) Error() string