skills

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Aug 16, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Package skills validates skill uploads and normalizes them into the canonical archive form the registry stores: a zip whose single top-level entry is the skill directory, with SKILL.md at its root. Both the /v1/skills upload forms (loose path-qualified files, or one zip archive) and the slice-3 operator import funnel through here, so the validation rules — the skills-guide's published constraints on name/description/size — cannot drift between entry points. Every error returned by this package is caused by the upload's content and is safe to echo to the client as a 400.

Index

Constants

View Source
const (
	// MaxTotalBytes caps a skill's total uncompressed content (the
	// skills-guide's published 30 MB bundle limit).
	MaxTotalBytes = 30 << 20
	// MaxMembers caps the file count, matching the reference worker's own
	// extraction guard (anthropic-sdk-go tools/agenttoolset).
	MaxMembers = 10000
)
View Source
const ArchiveDigestHeader = "x-skill-archive-sha256"

ArchiveDigestHeader carries Digest(archive) on the /content download response so the BYOC worker — which never reads the database — can verify what it downloaded. It lives beside BlobKey for the same reason: one definition shared by the API that sends it and the worker that reads it. Additive and ignored by reference clients (the SDK treats the body as opaque bytes).

View Source
const ExtractMaxBytes = 1 << 30

ExtractMaxBytes caps a skill archive's total decompressed content at extraction time, matching the reference worker's guard (anthropic-sdk-go tools/agenttoolset/skillarchive.go: 1 GiB). Far above the 30 MB upload cap on purpose — extraction guards protect the sandbox even if a stored object did not come through this platform's upload validation.

View Source
const MaxArchiveBytes = 64 << 20

MaxArchiveBytes caps how many *compressed* bytes a materializer reads from an object-store stream before handing the archive to Extract. It is set well above the platform's upload limit (MaxTotalBytes, 30 MB) but far below ExtractMaxBytes: a canonical zip is built from at most MaxTotalBytes of validated content, so its compressed form fits comfortably here, while a stored object larger than this is malformed or hostile and is refused before it can consume memory — Extract's own decompressed cap then guards what a valid archive expands to. Capping the *read* at a realistic archive size (not the gigabyte decompressed ceiling) is what keeps refusing a hostile object from ever needing a gigabyte-scale allocation.

View Source
const SentinelName = ".materialized"

SentinelName is the marker file written under {workdir}/skills/ after a successful materialization pass, recording the resolved {skill_id: version} set so re-entrant sandbox provisioning skips rewriting unchanged skills.

View Source
const SentinelVersion = 2

SentinelVersion is the marker's integrity generation — what a successful materialization the marker records was actually guaranteed to have done. Version 2 means "every recorded archive was verified against the digest the registry holds for it, where one was recorded" (#155); version 1 was the bare, unversioned JSON array written before any digest existed, and is no longer accepted. Bumping this is how a marker written under a weaker guarantee is stopped from satisfying a stronger one: it costs exactly one re-materialization pass per live sandbox at upgrade — which is where the stronger guarantee gets applied — and nothing at steady state. Recording the digests themselves would be the alternative, and is not viable: the BYOC worker learns a digest only from the download response, i.e. after the skip decision, so it would have to spend a wire round trip per skill per pass to answer a question this constant answers for free.

Variables

View Source
var ErrDigestMismatch = errors.New("skill archive does not match its recorded sha256")

ErrDigestMismatch reports an archive whose bytes do not hash to the digest recorded for that skill version at upload — storage bit-rot, truncation, or a substituted object between upload and materialization.

Functions

func BlobKey

func BlobKey(skillID, version string) string

BlobKey is the object-store key for a skill version's archive — the one layout the API's upload/download, the importer, and the executor's materialization all share.

func Digest

func Digest(data []byte) string

Digest is the archive digest this platform records and verifies: the lowercase-hex sha256 of the stored bytes.

func IsZip

func IsZip(data []byte) bool

IsZip reports whether data begins with the zip local-file-header magic. The API uses it to pick the upload form when exactly one file part arrives; magic-byte detection is an inference recorded in docs/DIVERGENCES.md.

func ParseSentinel

func ParseSentinel(data []byte) ([]markerEntry, bool)

ParseSentinel decodes a marker file; ok is false for bytes that are not a marker of the current integrity generation — unreadable, or written under an older one (including the unversioned array form) — which a caller treats as "materialize", so the current generation's guarantees are applied on that pass. A marker from a *newer* generation is likewise not accepted: a downgraded binary must re-materialize rather than trust a claim it cannot evaluate.

func ReadArchive

func ReadArchive(r io.Reader, wantSHA256 string) ([]byte, error)

ReadArchive reads a skill archive from an object-store stream, refusing more than MaxArchiveBytes so a hostile or corrupt object cannot exhaust memory, then verifies it against the digest recorded for that version. The store's reported length is deliberately NOT used to pre-size the buffer: it is untrusted, and a large hint would let a tiny hostile stream provoke a huge eager allocation. The cap is enforced on bytes actually read.

Verification lives here, in the one function both materialization halves call between fetching an archive and extracting it, so a caller cannot read an archive and forget to check it. wantSHA256 empty means no digest was recorded for this version — a row predating the sha256 column, or (on the wire half) a control plane that sends no digest header — and the archive is read unverified; callers log that. Anything else must match, case-insensitively: our own digests are lowercase by construction, but rejecting another implementation's uppercase hex would be a gratuitous failure. A malformed expectation needs no format check of its own — it can never equal a real digest, so it fails closed.

func Sentinel

func Sentinel(rs []Resolved) []byte

Sentinel canonically encodes the materialized set for the marker file: the integrity generation plus sorted {skill_id, version} entries, so equal sets always produce equal bytes.

func SentinelMatches

func SentinelMatches(ctx context.Context, read func(context.Context, string) ([]byte, error),
	workdir string, data []byte, rs []Resolved) bool

SentinelMatches reports whether the marker proves the resolved set rs is already fully materialized. Against an agent-writable marker it holds these:

  • the probe directory is rs[i].Dir (trusted metadata), never the marker, so a rewritten marker cannot redirect the probe at a decoy directory;
  • the marker's {id, version} set must be an EXACT bijection with rs (same length, each rs id present exactly once with its version), so a forged, duplicated, or zero-value entry cannot mask a skill that is absent from its directory;
  • every resolved directory must still hold its SKILL.md (canonical archives place one at the root), so a deleted tree self-heals next pass.

It is NOT a soundness proof against a fully hostile agent: the SKILL.md probe tests presence, not content, so an agent that both forges the marker's version and leaves an older version's files in place (the landing directory is the skill name, shared across a skill's versions) can suppress an upgrade — the same tampering class as an in-place content edit, and equally beyond a presence probe. Both residuals are recorded in docs/DIVERGENCES.md; closing them would mean abandoning the skip and re-extracting every pass, as the reference does. read is the sandbox's ReadFile, passed as a function so this package needs no sandbox dependency. rs must be deduplicated by id.

func TargetDir

func TargetDir(name, skillID string) string

TargetDir mirrors the reference worker's materialization-directory choice: the version object's name, falling back to the skill id when the name is empty or unusable as a single path segment.

Types

type Bundle

type Bundle struct {
	Name        string
	Description string
	Directory   string
	Zip         []byte
	SHA256      string
}

Bundle is a validated upload: the SKILL.md frontmatter extraction plus the canonical archive the registry stores and later materializes into sandboxes. SHA256 is Digest(Zip), recorded beside the metadata so materialization can prove the object it reads back is the archive that was validated here.

func FromFiles

func FromFiles(files []File) (*Bundle, error)

FromFiles validates the loose-files upload form and builds the canonical zip. Part order does not affect the archive bytes.

func FromZip

func FromZip(data []byte) (*Bundle, error)

FromZip validates the zip upload form. The original bytes are kept verbatim as the stored archive — the download endpoint streams them unmodified.

type File

type File struct {
	Path string
	Data []byte
}

File is one uploaded file: a slash-separated path as sent by the client ("financial-skill/SKILL.md") and its content.

func Extract

func Extract(data []byte) ([]File, error)

Extract opens a stored skill archive (the canonical zip the registry serves) and returns its files with slash-relative paths, the single top-level wrapper directory stripped — the reference worker's extraction semantics with its guards: escape ("slip") refusal and member/byte caps. Only zip is accepted: this platform stores and serves canonical zips, so the reference's tar fallback would be dead code here.

type Resolved

type Resolved struct {
	ID      string
	Version string
	Dir     string
	// SHA256 is the archive digest recorded for this version, from the same
	// trusted metadata that supplies Dir — empty when the source records none.
	// The executor fills it from the version row it already reads; the BYOC
	// worker leaves it empty because the SDK's version object carries no
	// checksum field, and learns the digest from the download response instead.
	SHA256 string
}

Resolved is a skill the caller intends to materialize: its id, the concrete version resolved at use time, and the directory it lands in. Dir is TargetDir of the version's name derived from TRUSTED metadata (the DB row or the version object), never from the marker — so the skip's presence probe cannot be redirected by an agent that rewrites the marker.

Jump to

Keyboard shortcuts

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