Python SDK Concepts
This page explains the core concepts behind the Python SDK's object model. Understanding these will help you choose the right function for each use case.
Resolve vs Pull
The SDK offers two ways to interact with a bundle:
resolve()— fetches manifest metadata only. No asset content is downloaded. Returns aResolveResult.pull()— fetches metadata and downloads all asset content, verifying each asset's SHA-256 checksum. Returns aBundle.
Use resolve() when you only need to check which version a reference points to, list what
assets exist, or validate a bundle reference — without paying the cost of downloading files.
import musher
# Metadata only — no downloads
result = musher.resolve("acme/code-review-kit:^1.0")
if result.manifest:
for layer in result.manifest.layers:
print(f"{layer.logical_path} ({layer.asset_type})")
# Full download — assets verified and cached locally
bundle = musher.pull("acme/code-review-kit:^1.0")
for fh in bundle.files():
print(fh.text())Manifest Layers vs Typed Handles
These two concepts represent different views of the same bundle content, depending on whether you
used resolve() or pull().
Manifest layers
A ResolveResult contains a manifest whose layers list describes
each asset as a raw entry: its logical path, asset type, digest, and size. Layers are metadata — they
tell you what's in the bundle without giving you the content.
Typed handles
A Bundle provides typed accessor methods that return handle objects for each asset category:
bundle.files()→list[FileHandle]bundle.skills()→list[SkillHandle]bundle.prompts()→list[PromptHandle]bundle.toolsets()→list[ToolsetHandle]bundle.agent_specs()→list[AgentSpecHandle]
Each handle gives you the actual content via methods like .text(), .bytes(), or .parse_json(). Handles are the primary way you interact
with bundle assets after pulling.
bundle = musher.pull("acme/code-review-kit")
# Typed handle access
for skill in bundle.skills():
print(f"{skill.name}: {skill.description}")
print(skill.skill_md().text())
# Single asset lookup (raises KeyError if not found)
toolset = bundle.toolset("lint-rules")
config = toolset.parse_json()Client Architecture
The SDK provides three ways to make calls:
Module-level functions
musher.pull() and musher.resolve() use a shared global client internally.
This is the simplest path and works for most applications.
Synchronous client
Client is a synchronous wrapper that uses a background event loop internally. This design
makes it safe to use in Jupyter notebooks, scripts, and other environments that may already have a running
event loop. Use it as a context manager to ensure proper cleanup:
from musher import Client
with Client() as client:
bundle = client.pull("acme/code-review-kit")Asynchronous client
AsyncClient is a native asyncio client for applications that are already async.
All methods return awaitables:
from musher import AsyncClient
async with AsyncClient() as client:
bundle = await client.pull("acme/code-review-kit")Credential Resolution
The SDK discovers credentials automatically by checking four sources in order, using the first one found:
- Programmatic — pass a token directly via
configure()or the client constructor (takes precedence over all other sources) MUSHER_API_KEYenvironment variable — simplest option for CI and local development- OS keyring (
musher/<host>) — set by the Musher CLI aftermusher auth login - Credential file (
~/.local/share/musher/credentials/<host>/api-key) — file-based fallback