Skip to content

Core (agentskills-core)

Registry, provider contract, validation, and shared exceptions.

pip install agentskills-core
pip install "agentskills-core[verification]"   # Ed25519 signature verification
pip install "agentskills-core[telemetry]"      # OpenTelemetry export

agentskills_core

Core runtime model for the Agent Skills format.

This package provides the foundational abstractions for registering, validating, and accessing Agent Skills <https://agentskills.io>_:

  • :class:SkillProvider -- abstract base class for skill backends.
  • :class:Skill -- lightweight runtime handle to a single skill.
  • :class:SkillRegistry -- unified index with explicit registration and built-in catalog builder.
  • :func:validate_skill -- validates a skill against the specification.
  • :func:validate_version -- validates an optional semver version field.
  • :func:get_logger -- returns a logger in the shared agentskills.* namespace.
  • :func:redact_url -- strips credentials from a URL before it is logged or raised.
  • :func:encode_resource_content -- safely encodes resource bytes as tool output.
  • :class:SkillNotFoundError -- raised when a skill does not exist.
  • :class:ResourceNotFoundError -- raised when a resource within a skill does not exist.
  • :class:ResourceListingNotSupportedError -- raised when a provider cannot enumerate a skill's resources.
  • :class:DiscoveryNotSupportedError -- raised when a provider cannot enumerate the skills it holds.
  • :class:SkillUnavailableError -- raised when a backend is unreachable or fails transiently, as distinct from a skill that does not exist.
  • :class:AgentSkillsError -- base class for all library exceptions.
  • :class:AgentSkillsError -- base class for all library exceptions.

Install::

pip install agentskills-core

AgentSkillsError

Bases: Exception

Base exception for all Agent Skills library errors.

DiscoveryNotSupportedError

Bases: AgentSkillsError, NotImplementedError

The provider cannot enumerate the skills it holds.

Raised by the default :meth:SkillProvider.discover <agentskills_core.SkillProvider.discover> implementation, and by providers whose backend offers no index -- a static HTTP host with no skill manifest, for instance.

Like :class:ResourceListingNotSupportedError, this is deliberately not an empty list. A caller told a backend holds no skills stops looking; a caller told the backend cannot be enumerated falls back to explicit registration. Check :attr:SkillProvider.supports_discovery <agentskills_core.SkillProvider.supports_discovery> to avoid the exception entirely.

Example::

if provider.supports_discovery:
    await registry.register_all(provider)

FastPath dataclass

A resolved single-skill fast path.

Passed to an integration's fast_path= argument. Holding the decision in a value rather than re-deriving it means the prompt an integration injects and the tools it drops cannot disagree about which skill was chosen.

body instance-attribute

body: str

That skill's full body, frontmatter already stripped.

prompt property

prompt: str

The instruction block to inject instead of a catalog.

skill_id instance-attribute

skill_id: str

The one skill whose body is being inlined.

tokens instance-attribute

tokens: int

Estimated cost of the body, by the counter the outline uses.

keeps

keeps(tool_name: str) -> bool

Whether tool_name should still be offered to the agent.

FileAccessNotSupportedError

Bases: AgentSkillsError, NotImplementedError

The provider cannot enumerate and read a skill's original files losslessly.

ProviderUnavailableError

Bases: SkillUnavailableError

A transient provider outage, eligible for an explicit verified-stale policy.

Not for access denial, removal, integrity failures, or concurrent source drift.

ResourceListingNotSupportedError

Bases: AgentSkillsError, NotImplementedError

The provider cannot enumerate a skill's resources.

Raised by the default :meth:SkillProvider.list_resources <agentskills_core.SkillProvider.list_resources> implementation, and by providers whose backend offers no way to enumerate -- a static HTTP host with no manifest, for instance.

This is deliberately not an empty result. "Cannot enumerate" and "enumerated, found nothing" lead a caller to different behaviour, so they must be distinguishable. Check :attr:SkillProvider.supports_resource_listing <agentskills_core.SkillProvider.supports_resource_listing> to avoid the exception entirely.

Example::

if provider.supports_resource_listing:
    resources = await provider.list_resources("incident-response")

ResourceMedia dataclass

What a resource is, and whether a model can be shown it.

is_text instance-attribute

is_text: bool

Whether this should be treated as text needing no special handling.

False for anything whose leading bytes identify a renderable image, whatever those bytes happen to decode to -- a file that starts GIF89a is a GIF, and a small one can decode as UTF-8 by coincidence.

media_type instance-attribute

media_type: str

Detected media type, or application/octet-stream.

name instance-attribute

name: str

The resource file name, as requested.

renderable instance-attribute

renderable: bool

Whether this should be delivered as a native content block.

False for text, for unrecognised binaries, and for images past :data:DEFAULT_MAX_INLINE_IMAGE_BYTES -- in every case the JSON envelope remains correct, so a caller can ignore this field and lose nothing but the improvement.

size_bytes instance-attribute

size_bytes: int

Size of the resource.

ResourceNotFoundError

Bases: AgentSkillsError, LookupError

A requested resource does not exist within a skill.

Resources include scripts, assets, and reference documents bundled with a skill.

Raised by get_script, get_asset, and get_reference on both :class:~agentskills_core.SkillProvider and :class:~agentskills_core.Skill when the named resource cannot be found.

Example::

try:
    data = await skill.get_reference("missing.md")
except ResourceNotFoundError:
    print("Reference not found")

Section dataclass

One heading of a body, with the text charged to it.

Attributes:

Name Type Description
key str

Stable, addressable identifier, unique within one body.

title str

The heading text, or :data:PREAMBLE_TITLE.

level int

Number of leading #, or 0 for the preamble.

text str

The heading line and everything up to the next heading.

SectionNotFoundError

Bases: AgentSkillsError, LookupError

A requested section does not exist within a skill's body.

Raised by :meth:Skill.get_section <agentskills_core.Skill.get_section> and :meth:SkillRegistry.get_skill_section <agentskills_core.SkillRegistry.get_skill_section> when the given key does not match any heading. Section keys are derived from the body, so they change when the body does; the message lists the keys that are currently valid.

Example::

try:
    text = await skill.get_section("rollbck")
except SectionNotFoundError as exc:
    print(exc)   # names the keys that do exist

SectionRef dataclass

One line of an outline: what a section is, and what it costs.

Skill

Runtime handle to a single Agent Skill.

All data access is delegated to the backing :class:~agentskills_core.SkillProvider. The handle itself stores only the skill name and a provider reference -- no content is cached, and no execution logic is included.

Parameters:

Name Type Description Default
skill_id str

The skill name (must match the name field in the skill's YAML frontmatter).

required
provider SkillProvider

The :class:~agentskills_core.SkillProvider that owns this skill.

required

Example::

skill = registry.get_skill("incident-response")
meta = await skill.get_metadata()
print(meta["description"])
body = await skill.get_body()

supports_file_access property

supports_file_access: bool

Whether the provider can enumerate and read all original files.

supports_resource_listing property

supports_resource_listing: bool

Whether the backing provider can enumerate this skill's resources.

get_asset async

get_asset(name: str) -> bytes

Return the raw content of a bundled asset.

Parameters:

Name Type Description Default
name str

Name of the asset to retrieve.

required

Returns:

Type Description
bytes

Raw content bytes.

Raises:

Type Description
ResourceNotFoundError

If the asset does not exist.

get_body async

get_body() -> str

Return the markdown instruction body for this skill.

This represents the full skill instructions that an agent reads upon activation -- the content after the YAML frontmatter in the Agent Skills format.

Returns:

Type Description
str

Markdown text.

get_id

get_id() -> str

Return the unique skill name, matching the frontmatter name.

get_metadata async

get_metadata() -> dict[str, Any]

Return the parsed YAML frontmatter for this skill.

Always contains the required name and description keys. May also include optional keys such as license, compatibility, metadata, and allowed-tools.

Returns:

Type Description
dict[str, Any]

Dictionary of frontmatter key-value pairs.

get_outline async

get_outline() -> SkillOutline

Return the body's sections without returning the body.

Computed on top of :meth:get_body, which providers already cache, so this costs no extra round-trip on a warm provider and needs nothing from the :class:~agentskills_core.SkillProvider interface.

Returns:

Name Type Description
A SkillOutline

class:~agentskills_core.SkillOutline carrying one

SkillOutline

class:~agentskills_core.SectionRef per heading, the

SkillOutline

whole body's estimated size, and whether fetching that

SkillOutline

whole body is the cheaper move.

get_reference async

get_reference(name: str) -> bytes

Return the raw content of a bundled reference document.

Parameters:

Name Type Description Default
name str

Name of the reference to retrieve.

required

Returns:

Type Description
bytes

Raw content bytes.

Raises:

Type Description
ResourceNotFoundError

If the reference does not exist.

get_script async

get_script(name: str) -> bytes

Return the raw content of a bundled script.

Parameters:

Name Type Description Default
name str

Name of the script to retrieve.

required

Returns:

Type Description
bytes

Raw content bytes.

Raises:

Type Description
ResourceNotFoundError

If the script does not exist.

get_section async

get_section(key: str) -> str

Return one section of the body, addressed by key.

Parameters:

Name Type Description Default
key str

A key from :meth:get_outline.

required

Returns:

Type Description
str

The heading line and everything up to the next heading of

str

any level. Sections do not overlap, so a parent does not

str

include its subsections.

Raises:

Type Description
SectionNotFoundError

If no section carries that key.

list_files async

list_files() -> list[str]

Return every file path relative to this skill's root, including SKILL.md.

list_resources async

list_resources() -> dict[str, list[str]]

Return this skill's resource names, grouped by kind.

Check :attr:supports_resource_listing first -- not every backend can enumerate.

Returns:

Type Description
dict[str, list[str]]

Mapping of "references" / "scripts" / "assets" to

dict[str, list[str]]

sorted resource names.

Raises:

Type Description
ResourceListingNotSupportedError

If the provider cannot enumerate resources.

read_file async

read_file(path: str) -> bytes

Return the original bytes of a file relative to this skill's root.

SkillFile dataclass

One captured file with a digest computed from its original bytes.

size property

size: int

Return the raw byte count, before any transport encoding.

SkillNotFoundError

Bases: AgentSkillsError, LookupError

A requested skill does not exist.

Raised by :class:~agentskills_core.SkillProvider methods when the given skill_id cannot be resolved, and by :meth:SkillRegistry.get_skill <agentskills_core.SkillRegistry.get_skill> when the skill is not in the index.

Example::

try:
    skill = await registry.get_skill("nonexistent")
except SkillNotFoundError:
    print("Skill not found")

SkillOutline dataclass

A body's sections, their cost, and whether they are worth fetching.

A section fetch is not automatically cheaper than a body fetch: it costs a tool call, a model turn, and the tokens of the outline that preceded it. The outline therefore carries the whole-body figure and says plainly which is cheaper, because shipping the split without that guidance makes the common case worse to improve the rare one.

whole_body_is_cheaper property

whole_body_is_cheaper: bool

Whether the agent should skip sectioning and read the body.

render

render() -> str

Return the outline as text for an agent to read.

One rendering, shared by every integration, so the three cannot drift into giving an agent three different cost estimates for the same skill.

SkillProvider

Bases: ABC

Abstract base class that every skill backend must implement.

A :class:SkillProvider is a pure content accessor -- it serves skill metadata, body text, and resources by skill ID. Enumerating the skills a backend holds is an optional capability: see :meth:discover. Without it, registration is explicit via :meth:SkillRegistry.register <agentskills_core.SkillRegistry.register>.

Implementations must enforce progressive disclosure: expensive I/O (reading file bodies, fetching resources) should only happen when the corresponding method is called explicitly.

All methods are async to support non-blocking implementations.

Subclass this to back skills with any storage -- filesystem, database, remote API, etc. Register skills with a :class:~agentskills_core.SkillRegistry to create a unified skill catalog.

Example::

class MyProvider(SkillProvider):
    async def get_metadata(self, skill_id: str) -> dict: ...
    async def get_body(self, skill_id: str) -> str: ...
    # ... remaining abstract methods

discover async

discover() -> list[str]

Return the IDs of every skill this provider can serve.

Optional capability. The default implementation refuses. Override it and set :attr:supports_discovery to True in backends that can enumerate.

Implementations return sorted IDs, and return only skills that appear to exist -- a directory without a SKILL.md is not a skill. Discovery does not validate: an ID that comes back here can still fail :func:~agentskills_core.validate_skill, which is what :meth:SkillRegistry.register_all <agentskills_core.SkillRegistry.register_all> reports on.

Returns:

Type Description
list[str]

Sorted skill IDs. An empty list means the backend was

list[str]

enumerated and holds nothing.

Raises:

Type Description
DiscoveryNotSupportedError

If this provider cannot enumerate its skills. Never returns an empty list to signal the same thing -- that would be indistinguishable from an empty backend, and would silently register nothing.

get_asset abstractmethod async

get_asset(skill_id: str, name: str) -> bytes

Return the raw bytes of a single asset.

Parameters:

Name Type Description Default
skill_id str

The skill name containing the asset.

required
name str

Name of the asset to retrieve.

required

Returns:

Type Description
bytes

Raw content as bytes.

Raises:

Type Description
ResourceNotFoundError

If the asset does not exist.

get_body abstractmethod async

get_body(skill_id: str) -> str

Return the markdown body (instructions) for a skill.

The body contains the skill's full instructions in Markdown format, corresponding to the content after the YAML frontmatter in the Agent Skills format. It should only be loaded when the agent decides to activate the skill (progressive disclosure).

Parameters:

Name Type Description Default
skill_id str

The skill name to look up.

required

Returns:

Type Description
str

The markdown instruction text.

Raises:

Type Description
SkillNotFoundError

If the skill does not exist.

get_metadata abstractmethod async

get_metadata(skill_id: str) -> dict[str, Any]

Return the parsed YAML frontmatter for a skill.

Per the specification the returned dict will always contain at least the required name and description keys. Optional keys include license, compatibility, metadata, and allowed-tools.

This method should return only the frontmatter metadata -- never the full instruction body -- to keep context usage low during the discovery phase.

The returned dict must not be shared between calls: a caller that mutates it must not affect the next caller. Implementations that cache a parsed dict should return a copy.

Parameters:

Name Type Description Default
skill_id str

The skill name to look up.

required

Returns:

Type Description
dict[str, Any]

Dictionary of frontmatter key-value pairs.

Raises:

Type Description
SkillNotFoundError

If the skill does not exist.

get_reference abstractmethod async

get_reference(skill_id: str, name: str) -> bytes

Return the raw bytes of a single reference document.

Parameters:

Name Type Description Default
skill_id str

The skill name containing the reference.

required
name str

Name of the reference to retrieve.

required

Returns:

Type Description
bytes

Raw content as bytes.

Raises:

Type Description
ResourceNotFoundError

If the reference does not exist.

get_script abstractmethod async

get_script(skill_id: str, name: str) -> bytes

Return the raw bytes of a single script.

Parameters:

Name Type Description Default
skill_id str

The skill name containing the script.

required
name str

Name of the script to retrieve.

required

Returns:

Type Description
bytes

Raw content as bytes.

Raises:

Type Description
ResourceNotFoundError

If the script does not exist.

list_files async

list_files(skill_id: str) -> list[str]

List every original file, including SKILL.md, relative to the skill root.

Optional capability paired with :meth:read_file. Implementations set supports_file_access and return sorted, unique POSIX relative paths. The listing must include arbitrary supporting directories, not only the standard resource kinds. Missing skills raise SkillNotFoundError. Listing must not read supporting file contents.

Parameters:

Name Type Description Default
skill_id str

The skill whose files should be enumerated.

required

Raises:

Type Description
FileAccessNotSupportedError

If lossless file access is unavailable.

list_resources async

list_resources(skill_id: str) -> dict[str, list[str]]

Return the resource names a skill contains, grouped by kind.

Optional capability. The default implementation refuses. Override it and set :attr:supports_resource_listing to True in backends that can enumerate.

Implementations return every key in :data:RESOURCE_KINDS, using an empty list for a category the skill does not use, so callers never have to guard on key presence. Names are sorted.

Parameters:

Name Type Description Default
skill_id str

The skill name to enumerate.

required

Returns:

Type Description
dict[str, list[str]]

Mapping of "references" / "scripts" / "assets" to

dict[str, list[str]]

sorted resource names.

Raises:

Type Description
ResourceListingNotSupportedError

If this provider cannot enumerate resources. Never returns an empty mapping to signal the same thing -- that would be indistinguishable from a skill with no resources.

SkillNotFoundError

If the skill does not exist.

read_file async

read_file(skill_id: str, path: str) -> bytes

Read one original file without decoding, truncation, or reconstruction.

Parameters:

Name Type Description Default
skill_id str

The skill containing the file.

required
path str

A POSIX relative path returned by :meth:list_files.

required

Returns:

Type Description
bytes

Original bytes, including frontmatter and line endings for SKILL.md.

Raises:

Type Description
FileAccessNotSupportedError

If lossless file access is unavailable.

SkillNotFoundError

If the skill does not exist.

ResourceNotFoundError

If the path is absent or escapes the skill root.

SkillRegistry

Unified index over explicitly registered skills.

Skills are added via :meth:register, either one at a time or as a batch of (skill_id, provider) tuples, or via :meth:register_all for a provider that can enumerate itself. The registry enforces a flat namespace: each skill ID must be unique. A :exc:ValueError is raised if a duplicate is detected.

:meth:register is async because it validates every skill against the Agent Skills specification before storing it. :meth:list_skills and :meth:get_skill are synchronous lookups. :meth:list_skills returns :class:~agentskills_core.Skill instances sorted by ID. :meth:get_skills_catalog is async because it fetches metadata from providers.

__init__

__init__(*, catalog_concurrency: int = DEFAULT_CATALOG_CONCURRENCY) -> None

Create an empty registry.

Parameters:

Name Type Description Default
catalog_concurrency int

Maximum number of provider metadata fetches issued in parallel by :meth:get_skills_catalog.

DEFAULT_CATALOG_CONCURRENCY

Raises:

Type Description
ValueError

If catalog_concurrency is less than 1.

get_skill

get_skill(skill_id: str) -> Skill

Return the :class:~agentskills_core.Skill handle by name.

Parameters:

Name Type Description Default
skill_id str

Skill name to look up.

required

Returns:

Type Description
Skill

The registered :class:~agentskills_core.Skill instance.

Raises:

Type Description
SkillNotFoundError

If no skill with the given name is registered.

get_skill_outline async

get_skill_outline(skill_id: str) -> SkillOutline

Return a registered skill's body outline.

Parameters:

Name Type Description Default
skill_id str

Skill name to look up.

required

Returns:

Type Description
SkillOutline

The skill's :class:~agentskills_core.SkillOutline.

Raises:

Type Description
SkillNotFoundError

If no skill with the given name is registered.

get_skill_section async

get_skill_section(skill_id: str, key: str) -> str

Return one section of a registered skill's body.

Parameters:

Name Type Description Default
skill_id str

Skill name to look up.

required
key str

A key from :meth:get_skill_outline.

required

Returns:

Type Description
str

The section text.

Raises:

Type Description
SkillNotFoundError

If no skill with the given name is registered.

SectionNotFoundError

If the skill has no section with that key.

get_skills_catalog async

get_skills_catalog(*, format: Literal['xml', 'markdown'] = 'xml', tags: Iterable[str] | None = None, include: Iterable[str] | None = None, exclude: Iterable[str] | None = None, max_chars: int | None = None, selection_hints: bool = True, total: int | None = None) -> str

Build a skill-catalog string for system-prompt injection.

Two output formats are supported:

"xml" An <available_skills> XML block. This is the recommended format when using Claude or other Anthropic models.

"markdown" A human-readable Markdown catalog listing every registered skill's name and description.

Only name, description and the selection metadata are extracted from each skill's metadata, keeping token usage low.

The catalog is injected into every system prompt on every turn, so its size is a fixed cost per request. The filters below narrow it, and max_chars caps it::

await registry.get_skills_catalog(
    tags=["incident"],
    exclude=["deprecated-runbook"],
    max_chars=8000,
)

include and exclude match skill IDs and are applied before any metadata is fetched, so narrowing a large registry costs proportionally fewer provider round-trips. tags needs metadata and is applied after.

Parameters:

Name Type Description Default
format Literal['xml', 'markdown']

Output format — "xml" (default) or "markdown".

'xml'
tags Iterable[str] | None

Keep only skills carrying at least one of these tags, read from the spec's free-form metadata mapping under tags. Matching is case-insensitive. A skill with no tags matches no tag filter.

None
include Iterable[str] | None

Allow-list of skill IDs. Only these are considered. An ID that is not registered raises, since an allow-list naming a skill that does not exist silently costs the agent a capability.

None
exclude Iterable[str] | None

Deny-list of skill IDs, applied after include and winning over it — a deny-list that another argument can override is not a deny-list. Unregistered IDs are ignored here, because a deny-list is meant to outlive the thing it denies.

None
max_chars int | None

Hard ceiling on the length of the returned string, including the truncation note. Whole entries are dropped from the end until the result fits, so the output stays valid and the same arguments always produce the same catalog. Roughly four characters per token is the usual estimate.

None
selection_hints bool

Include each skill's when_to_use and when_not_to_use entries. They make selection more accurate and they are charged on every turn, so pass False to trade that accuracy back for tokens. The result is then byte-identical to a catalog built from skills that declare neither field.

True
total int | None

The size of the set these skills were chosen from, for a caller that has already narrowed the catalog itself — a ranked subset, say. The catalog then reports the shortfall the same way it reports truncation, so a skill that was ranked out is not indistinguishable from one that was never registered. Defaults to the number of entries left after this call's own filters, which is to say no shortfall is reported for them: a static tags= filter is a deliberate, unchanging choice, and repeating it in the prompt every turn is noise the caller is charged for.

None

Returns:

Type Description
str

A string ready for insertion into a system prompt. When

str

entries were dropped, the XML root carries truncated,

str

shown and total attributes and the Markdown gains a

str

closing note; nothing is ever dropped silently, because a

str

catalog that varies without saying so makes agent behaviour

str

non-reproducible.

Raises:

Type Description
ValueError

If format is not "xml" or "markdown", or if max_chars is too small to hold even an empty catalog and its note.

SkillNotFoundError

If include names an unregistered skill.

list_skills

list_skills() -> list[Skill]

Return registered skills sorted by ID.

Returns:

Type Description
list[Skill]

Alphabetically sorted list of :class:~agentskills_core.Skill

list[Skill]

instances. Use :meth:Skill.get_id to obtain a skill's name.

register async

register(skill_id: str, provider: SkillProvider) -> None
register(skills: list[tuple[str, SkillProvider]]) -> None
register(skill_id_or_skills: str | list[tuple[str, SkillProvider]], provider: SkillProvider | None = None) -> None

Register one or more skills with their providers.

Validates each skill against the Agent Skills specification using :func:~agentskills_core.validate_skill. This catches misconfiguration (missing SKILL.md, unreachable endpoint, invalid metadata) at registration time rather than at first use.

Single skill::

await registry.register("incident-response", provider)

Batch registration::

await registry.register([
    ("incident-response", fs_provider),
    ("api-style-guide", http_provider),
])

Batch registration is atomic — if any skill fails validation, none of the skills in the batch are registered.

Parameters:

Name Type Description Default
skill_id_or_skills str | list[tuple[str, SkillProvider]]

Either a single skill ID str, or a list of (skill_id, provider) tuples for batch registration.

required
provider SkillProvider | None

The :class:~agentskills_core.SkillProvider for the skill. Required when registering a single skill; must be omitted for batch registration.

None

Raises:

Type Description
ValueError

If a skill_id is already registered, if a skill fails validation, or if the arguments are invalid.

register_all async

register_all(provider: SkillProvider) -> list[str]

Register every skill a provider holds.

Asks the provider to enumerate itself and registers the result, so a folder of thirty skills does not require thirty hard-coded identifiers::

provider = LocalFileSystemSkillProvider(Path("./skills"))
registered = await registry.register_all(provider)

Discovery is an optional capability. Providers that cannot enumerate raise rather than returning an empty list, so a misconfigured backend cannot look like an empty one -- check :attr:SkillProvider.supports_discovery <agentskills_core.SkillProvider.supports_discovery> first if the provider may not support it.

Registration is atomic and reports every validation failure at once. Discovering thirty skills and being told only about the first broken one turns a single fix into thirty rounds.

Parameters:

Name Type Description Default
provider SkillProvider

The backend to enumerate and register.

required

Returns:

Type Description
list[str]

The registered skill IDs, sorted.

Raises:

Type Description
DiscoveryNotSupportedError

If provider cannot enumerate the skills it holds.

ValueError

If any discovered ID is already registered, or if any discovered skill fails validation. Nothing is registered in either case.

SkillSnapshot dataclass

An immutable file set captured from one skill.

The source must be trusted and immutable during capture. Repeated reads detect ordinary concurrent changes, but are not a filesystem transaction or a defense against a malicious provider. Later source changes cannot affect the captured files. Digests establish consistency, not publisher trust.

total_bytes property

total_bytes: int

Return the sum of all captured file sizes.

get_file

get_file(path: str) -> SkillFile

Look up an exact relative path without contacting the provider.

SkillUnavailableError

Bases: AgentSkillsError

A skill backend could not be reached, or failed transiently.

Raised for server-side failures (5xx), rate limiting (429), timeouts, and connection errors -- anything where the skill may well exist and the same request could succeed later.

This is deliberately not :class:SkillNotFoundError. Mapping a 503 onto "not found" tells the caller a skill was deleted when the truth is that a server had a bad minute, which turns a retryable blip into a permanent-looking failure.

Parameters:

Name Type Description Default
message str

Human-readable description. Must not contain credential material -- see the agentskills-http provider, which redacts URL query strings before they reach an exception message.

required
retry_after float | None

Server-advised delay in seconds, taken from a Retry-After header when present. None when the server gave no advice.

None

Example::

try:
    body = await skill.get_body()
except SkillNotFoundError:
    ...        # genuinely gone; stop asking
except SkillUnavailableError as exc:
    ...        # try again later, optionally after exc.retry_after

build_skill_manifest

build_skill_manifest(snapshot: SkillSnapshot, *, skill_path: str | None = None) -> dict[str, Any]

Build a complete JSON manifest without rewriting any captured bytes.

skill_path is an unescaped server-chosen path ending in the frontmatter name. By default an alias becomes a prefix, preserving the declared name as the final segment. This does not establish publisher trust.

Parameters:

Name Type Description Default
snapshot SkillSnapshot

The complete original file capture to describe.

required
skill_path str | None

Optional unescaped canonical path ending in the declared name.

None

Returns:

Type Description
dict[str, Any]

A JSON-compatible skill entry with every author field and file digest.

Raises:

Type Description
ValueError

Frontmatter, file paths, publication limits, or skill_path are invalid.

capture_skill async

capture_skill(skill: Skill, *, max_files: int = DEFAULT_SNAPSHOT_MAX_FILES, max_total_bytes: int = DEFAULT_SNAPSHOT_MAX_BYTES, previous_snapshot: SkillSnapshot | None = None) -> SkillSnapshot

Capture and verify a complete file set, failing without a partial result.

Defaults match the MCP Skills extension's per-skill delivery limits. Callers must also bound the number of retained snapshots. Provider reads must enforce their own per-file bounds, as bytes arrive already allocated.

Raises:

Type Description
FileAccessNotSupportedError

The skill lacks lossless file access.

ValueError

A limit, listing, or returned file is invalid.

SkillUnavailableError

The source changed during capture.

classify_resource

classify_resource(name: str, data: bytes, *, max_inline_image_bytes: int = DEFAULT_MAX_INLINE_IMAGE_BYTES) -> ResourceMedia

Report what data is and whether it can be shown to a model.

Parameters:

Name Type Description Default
name str

Resource file name. Used only when the bytes are not self-identifying, since a name is a claim and bytes are evidence.

required
data bytes

Raw resource bytes as returned by a provider.

required
max_inline_image_bytes int

Ceiling above which an image is described rather than shown. Use 0 to never render.

DEFAULT_MAX_INLINE_IMAGE_BYTES

Returns:

Name Type Description
A ResourceMedia

class:ResourceMedia.

Raises:

Type Description
ValueError

If max_inline_image_bytes is negative.

encode_resource_content

encode_resource_content(name: str, data: bytes, *, max_inline_binary_bytes: int = DEFAULT_MAX_INLINE_BINARY_BYTES) -> str

Encode raw resource bytes as text an LLM can consume.

Valid UTF-8 is returned verbatim. Everything else is returned as a JSON envelope::

{
  "name": "architecture.png",
  "media_type": "image/png",
  "size_bytes": 20481,
  "encoding": "base64",
  "content": "iVBORw0KGgo..."
}

Binaries larger than max_inline_binary_bytes use the same envelope with "encoding": "none" and an explanatory note instead of content, so the agent still learns the resource exists and why it was withheld.

Parameters:

Name Type Description Default
name str

Resource file name, used to infer the media type.

required
data bytes

Raw resource bytes as returned by a provider.

required
max_inline_binary_bytes int

Size ceiling for inlining binary content. Use 0 to never inline binaries.

DEFAULT_MAX_INLINE_BINARY_BYTES

Returns:

Type Description
str

The decoded text, or a JSON envelope string for binary content.

Raises:

Type Description
ValueError

If max_inline_binary_bytes is negative.

Example::

text = encode_resource_content("sev.md", await skill.get_reference("sev.md"))

estimate_tokens

estimate_tokens(text: str) -> int

Estimate the token cost of text at four characters per token.

get_logger

get_logger(module: str) -> logging.Logger

Return the agentskills.* logger for a module.

Pass __name__. The distribution prefix is rewritten so that agentskills_http.static logs as agentskills.http.static, giving every package a common namespace root while keeping the logger name tied to the module it lives in.

Parameters:

Name Type Description Default
module str

The calling module's __name__.

required

Returns:

Type Description
Logger

A configured :class:~logging.Logger.

outline_of

outline_of(skill_id: str, body: str) -> SkillOutline

Build the outline of body.

redact_url

redact_url(url: str, *, relative_to: str | None = None) -> str

Strip credentials from url so it is safe to log or raise.

Query strings, fragments and userinfo are dropped, because that is where credentials actually live: SAS tokens, signed-URL signatures and basic-auth passwords. When relative_to is given the scheme and host are dropped too, leaving only the path beneath that base -- a host is recoverable from configuration, but a full path sitting in a shared log is not something the operator can take back.

Parameters:

Name Type Description Default
url str

The URL to sanitise.

required
relative_to str | None

Base URL to express the result relative to.

None

Returns:

Type Description
str

A string safe to place in a log record or an exception message.

resolve_fast_path async

resolve_fast_path(registry: SkillRegistry, *, include: Iterable[str] | None = None, max_tokens: int = DEFAULT_FAST_PATH_MAX_TOKENS) -> FastPath | None

Decide whether registry qualifies for the single-skill fast path.

Returns None -- meaning "use the normal catalog path" -- unless the effective skill set is exactly one and its body fits under max_tokens.

include narrows the registry to an effective set, so a registry of fifty skills that a selector has narrowed to one takes the same path as a registry that only ever held one. That is the same situation and the len(list_skills()) == 1 test would miss it.

Both refusals are logged, because an integration silently choosing between two prompt shapes is worse than a slightly slower one.

split_frontmatter

split_frontmatter(raw: str) -> tuple[dict[str, Any], str]

Split SKILL.md content into YAML frontmatter and markdown body.

Frontmatter is the YAML block delimited by --- on its own line at the very start of the file. If no valid frontmatter is detected the entire content is returned as the body with an empty dict.

Parameters:

Name Type Description Default
raw str

Full text content of a SKILL.md file.

required

Returns:

Type Description
dict[str, Any]

A (frontmatter_dict, body_str) tuple. frontmatter_dict

str

is {} when no frontmatter is present.

Example::

meta, body = split_frontmatter(Path("SKILL.md").read_text())
print(meta.get("name"))

split_sections

split_sections(body: str) -> list[Section]

Split body at its headings, without overlapping.

A heading owns its own line and everything up to the next heading of any level, so a nested section is not also counted inside its parent. The parts therefore sum to the whole, which is the only property that makes a cost breakdown checkable.

Text before the first heading becomes a level-0 section, and # inside a fenced code block is a shell comment rather than a heading.

validate_skill async

validate_skill(skill: Skill) -> list[str]

Validate a single skill against the Agent Skills specification.

Checks that the skill's definition is well-formed and that its frontmatter satisfies all required and optional field constraints.

Validation rules:

  • Skill body must be non-empty.
  • name (required) -- 1-64 characters, lowercase [a-z0-9-], must not start or end with a hyphen, must not contain consecutive hyphens, and must match the skill ID.
  • description (required) -- 1-1024 characters.
  • version (optional) -- valid semver when present. Not yet part of the upstream specification; see :func:validate_version.
  • when_to_use / when_not_to_use (optional) -- lists of at most five non-empty strings of at most 200 characters each. They are charged on every turn, so they are bounded; see :data:SELECTION_FIELDS.

Parameters:

Name Type Description Default
skill Skill

The :class:~agentskills_core.Skill to validate.

required

Returns:

Type Description
list[str]

A list of human-readable error messages. An empty list means

list[str]

the skill is valid.

validate_version

validate_version(value: object) -> str | None

Check a frontmatter version value, returning an error or None.

version is not part of the upstream Agent Skills specification. It is supported here as an optional extension so that consumers can pin, compare, and detect drift; absence is always valid.

Parameters:

Name Type Description Default
value object

The raw value parsed from YAML frontmatter.

required

Returns:

Type Description
str | None

A human-readable error message, or None when value is a

str | None

valid semver string.

Publisher verification

See Trust and Operability for the contract.

agentskills_core.trust

Detached publisher verification over complete, immutable skill captures.

Sign signature_payload(snapshot, origin=...) outside the SDK and supply the detached proof through trusted deployment configuration. Keys and proofs are not inferred from skill frontmatter or from a server-supplied digest.

ContentIdentity dataclass

Verification evidence, never an assertion that instructions are safe.

DetachedSignature dataclass

An Ed25519 signature and a key identifier resolved only from trusted keys.

TrustPolicy dataclass

Verify configured publisher keys and optional exact content/version pins.

Parameters:

Name Type Description Default
origin str

Trusted stable identity for this source.

required
trusted_keys Mapping[str, bytes]

Key IDs mapped to raw 32-byte Ed25519 public keys.

dict()
require_signature bool

Reject unsigned content by default.

True
revision str | None

Optional exact SHA-256 content revision.

None
version str | None

Optional exact frontmatter version, in addition to byte verification.

None

verify

verify(snapshot: SkillSnapshot, proof: DetachedSignature | None = None) -> ContentIdentity

Verify bytes and identity, rejecting supplied invalid proofs even if unsigned is allowed.

Raises:

Type Description
VerificationError

A signature, publisher, or pin is invalid.

ImportError

Signature verification needs the core verification extra.

VerificationError

Bases: AgentSkillsError

Content, publisher, or immutable pin verification failed closed.

content_revision

content_revision(snapshot: SkillSnapshot) -> str

Return a deterministic SHA-256 revision covering every path and byte.

File enumeration order is insignificant. Invalid or incomplete native snapshots are rejected before a revision can be used for approval.

signature_payload

signature_payload(snapshot: SkillSnapshot, *, origin: str) -> bytes

Return the versioned, domain-separated bytes for detached Ed25519 signing.

Origin is a caller-assigned non-secret stable identity, not a display name supplied by an untrusted server. The payload is an SDK attestation contract, not an extension to SKILL.md or the MCP Skills wire format.

Content policy

agentskills_core.policy

Content policy applied before publication and after source verification.

ContentDecision dataclass

A hook's replacement bytes, rejection, and deployment-owned annotation codes.

data=None preserves bytes. Annotations are not inserted into SKILL.md. Hooks are trusted application code. Injection heuristics are advisory only.

ContentRejectedError

Bases: AgentSkillsError

A configured content policy rejected publication.

Publication dataclass

Delivered bytes and their revision, separate from original publisher evidence.

transformed property

transformed: bool

Whether delivered bytes differ from the publisher's verified bytes.

publish_snapshot

publish_snapshot(snapshot: SkillSnapshot, *, trust: TrustPolicy, proof: DetachedSignature | None = None, hooks: Sequence[Callable[[SkillFile], ContentDecision]] = (), max_tokens: int | None = None, count_tokens: Callable[[str], int] | None = None) -> Publication

Verify, apply hooks, bound text tokens, and validate the final file set.

A token limit requires an explicitly chosen tokenizer, never an implicit heuristic. All UTF-8 files count, including frontmatter and nested skills. Binary content remains subject to snapshot byte limits. Hook failures abort publication. No approval, execution grant, or sandbox is implied.

Refresh and stale serving

agentskills_core.refresh

Atomic, audience-scoped publication with opt-in verified stale fallback.

CatalogGeneration dataclass

One all-or-nothing publication for a single authorization audience.

SnapshotCatalog

Stage a complete catalog and atomically replace its published generation.

The loader must make fresh, bounded reads and apply audience authorization. Policy is rerun on retained source bytes before stale serving, so revocation or changed policy cannot be bypassed by an outage. One instance belongs to one audience. Never share it across principals with different access rights. The host must still bind approvals to delivered origin and revision.

current property

current: CatalogGeneration

Return a coherent generation or fail if no eligible catalog is available.

ready property

ready: bool

Report local publication readiness, not transport or authorization health.

refresh async

refresh() -> CatalogGeneration

Refresh all content or fail closed, optionally retaining verified outage data.

Ordinary SkillUnavailableError (including drift) is not an eligible outage. No automatic retry loop or background task is created.

Telemetry

agentskills_core.telemetry

Opt-in, content-free operational events and OpenTelemetry export.

DisclosureEvent dataclass

A server observation, not evidence of host activation or task success.

Origin is hashed by emit_event before reaching observers. Revisions describe delivered content. No request IDs, file paths, content, or errors are included. Observers are explicitly configured by the application.

OpenTelemetryObserver

Export spans and low-cardinality metrics using application-configured providers.

Install agentskills-core[telemetry]. This adapter never configures an exporter, global SDK provider, endpoint, or credentials. Origins and revisions appear on spans only, not on metric labels.

emit_event

emit_event(observer: Callable[[DisclosureEvent], None] | None, operation: Literal['discovery', 'lookup', 'fetch', 'verification', 'refresh', 'cache'], *, origin: str, revision: str | None = None, status: Literal['ok', 'error', 'stale'] = 'ok', byte_count: int = 0, duration_seconds: float = 0.0, cache_hit: bool = False) -> None

Emit sanitized metadata without allowing telemetry failures to affect delivery.