Documentation
¶
Overview ¶
Package marketplace models the Claude marketplace.json + plugin.json schemas, the Fetcher interface for resolving plugin sources, and the projection layer that decomposes plugin manifests into canonical source model entries.
Package marketplace models the Claude marketplace plugin format and provides the projection layer that decomposes plugin manifests into canonical source model entries. Skills, commands, and subagents are fully loaded from their on-disk markdown files (frontmatter + body + clean Name) via the injected readFile function so that adapters downstream receive complete, render-ready entries.
Index ¶
- Constants
- func LoadProjected(fs afero.Fs, home, pluginCacheRoot string) (source.Canonical, error)
- func LoadProjectedExcluding(fs afero.Fs, home, pluginCacheRoot string, disabled []string) (source.Canonical, error)
- func LoadProjectedLenient(fs afero.Fs, home, pluginCacheRoot string, disabled []string) (source.Canonical, error)
- func PluginEntryHash(entry PluginEntry) (string, error)
- func PluginTreeHash(fs afero.Fs, cacheDir string) (string, error)
- type Author
- type Bump
- type FetchResult
- type Fetcher
- type GitFetcher
- type Marketplace
- type MarketplaceMetadata
- type NPMFetcher
- type Owner
- type PluginEntry
- type PluginManifest
- type ProjectionResult
- type RelativeFetcher
- type SHAWarning
- type Source
Constants ¶
const MaxMetadataBytes = 16 * 1024 * 1024
MaxMetadataBytes caps the npm registry metadata JSON response. A 10 GB malicious response would otherwise be buffered by json.Decoder.
const MaxTarballBytes = 512 * 1024 * 1024
MaxTarballBytes caps the total decompressed bytes from a single npm tarball. A 10KB zip-bomb expands to many GB; without a ceiling the extraction fills the user's disk and OOMs the process. 512 MiB is well above any legitimate plugin size and small enough to fail quickly on a malicious upload. Override via AGENTSYNC_MAX_TARBALL_MB.
Variables ¶
This section is empty.
Functions ¶
func LoadProjected ¶
LoadProjected loads the canonical model and expands each installed plugin's cached manifest into it, so downstream adapters see plugin components transparently. It is the single projecting load: source.Load stays plugin- unaware, and every command that needs the full canonical (apply, status, diff, reconcile, import, explain, update) calls this.
This is the ONLY plugin projector — it delegates to Project, which honours plugin.json hooks AND marketplace-entry inline overrides. Previously the loader carried a separate, leaner reimplementation that silently dropped both; collapsing to one function is what keeps the two from drifting again.
pluginCacheRoot is <home>/.state/cache/plugins; an empty root skips projection (behaving like source.Load).
func LoadProjectedExcluding ¶
func LoadProjectedExcluding(fs afero.Fs, home, pluginCacheRoot string, disabled []string) (source.Canonical, error)
LoadProjectedExcluding is LoadProjected with an additional set of plugin IDs to skip projecting. At project scope the CLI collects these from the project tree's plugins/<id>.toml entries marked `disabled = true` (the dir-model successor to the M5 marker's `[plugins] disabled`) and passes them when projecting BOTH the user and project homes, so a plugin disabled by the project never renders its components in that repo.
disabled is matched against pl.ID (the plugins/<id>.toml filename stem), so it composes with the per-plugin `disabled = true` flag below: either path skips the same projection.
func LoadProjectedLenient ¶
func LoadProjectedLenient(fs afero.Fs, home, pluginCacheRoot string, disabled []string) (source.Canonical, error)
LoadProjectedLenient is LoadProjected for read-only/diagnostic commands (status, diff, explain): a strict same-name plugin.json/entry conflict is resolved entry-wins with a logged warning instead of a hard error, so those commands still show state rather than refusing to run on a conflict they exist to surface. Mutating commands (apply, reconcile, import, update) use the fatal LoadProjected/LoadProjectedExcluding so they never act on ambiguity.
func PluginEntryHash ¶
func PluginEntryHash(entry PluginEntry) (string, error)
PluginEntryHash pins an entry-only plugin — one with no cached plugin.json or component bodies — over its marketplace entry definition. The entry-prefix tag tells verify to skip recomputation (the entry isn't available there).
func PluginTreeHash ¶
PluginTreeHash computes a deterministic content hash over every file in a plugin's cache dir, EXCLUDING .git/ (which projection never reads and which would otherwise make a git-sourced plugin's pin non-deterministic across clones). It covers the component bodies projection actually ships — convention-discovered skills, command/subagent markdown — not just plugin.json, so a body tamper with an unchanged plugin.json is detected.
The hash is sha256 over the sorted "<slash-relpath>\x00<sha256(content)>" lines, so it captures file additions/removals as well as edits, and is independent of walk order and OS path separators. A symlink is hashed by its TARGET PATH (not followed): the fetcher already guarantees a cached symlink stays in-tree, and hashing the link string — rather than skipping it — keeps a swapped target from hiding from the pin.
Types ¶
type Bump ¶
type Bump struct {
// ID is the plugin's filesystem ID (e.g. "demo").
ID string
// From is the currently pinned version (from plugins/<id>.toml).
From string
// To is the latest version available in the fetched marketplace.
To string
// UpdateMode is the plugin's configured update mode ("pinned", "track", "manual").
UpdateMode string
}
Bump describes a pending plugin version change discovered by the update scan.
func ComputePendingBumps ¶
func ComputePendingBumps( _ *state.Targets, _ []source.Marketplace, plugins []source.Plugin, fetched map[string]map[string]PluginEntry, defaultMode string, ) []Bump
ComputePendingBumps compares each installed plugin's current version against the freshly-fetched marketplace data and returns the list of plugins that have a newer version available and whose update mode allows automatic upgrade.
Only plugins whose Update mode is "" (default → "track") or "track" are included as pending bumps. Plugins with Update = "pinned" or "manual" are skipped (they need explicit user action).
fetched maps marketplace name → fetched Marketplace index (keyed by plugin name).
type FetchResult ¶
FetchResult carries metadata about a completed fetch.
type Fetcher ¶
type Fetcher interface {
// Fetch resolves src into the directory at into, creating it if necessary.
// Returns a FetchResult with available metadata (HeadSHA for git sources,
// Version for npm).
Fetch(src Source, into string) (FetchResult, error)
}
Fetcher fetches one plugin source into a local directory.
type GitFetcher ¶
type GitFetcher struct{}
GitFetcher handles "github", "url", and "git-subdir" source kinds. All three are git repositories; the difference is:
- "github": repo = "owner/repo", clone from https://github.com/<repo>
- "url": repo or url = full git URL
- "git-subdir": same as github/url but also limits to a sub-directory path
func (*GitFetcher) Fetch ¶
func (f *GitFetcher) Fetch(src Source, into string) (FetchResult, error)
Fetch clones (or re-uses an already-cloned) repository into into, optionally checking out a specific ref/sha and, for git-subdir, extracting only the configured subdirectory.
type Marketplace ¶
type Marketplace struct {
Schema string `json:"$schema,omitempty"`
Name string `json:"name"`
Owner Owner `json:"owner"`
Description string `json:"description,omitempty"`
Version string `json:"version,omitempty"`
Metadata *MarketplaceMetadata `json:"metadata,omitempty"`
Plugins []PluginEntry `json:"plugins"`
AllowCrossMarketplaceDependenciesOn []string `json:"allowCrossMarketplaceDependenciesOn,omitempty"`
}
Marketplace is the .claude-plugin/marketplace.json document.
type MarketplaceMetadata ¶
type MarketplaceMetadata struct {
PluginRoot string `json:"pluginRoot,omitempty"`
}
MarketplaceMetadata holds optional extra metadata for a marketplace.
type NPMFetcher ¶
type NPMFetcher struct {
// HTTPClient overrides the default http.DefaultClient. Used in tests to
// inject a fake httptest.Server.
HTTPClient *http.Client
}
NPMFetcher fetches npm packages by downloading the tarball directly from the registry HTTP API — no `npm` CLI required at runtime.
func (*NPMFetcher) Fetch ¶
func (f *NPMFetcher) Fetch(src Source, into string) (FetchResult, error)
Fetch downloads the tarball for src.Package at src.Version into into. If src.Version is empty or "latest", the latest dist-tag is used.
type PluginEntry ¶
type PluginEntry struct {
Name string `json:"name"`
Source Source `json:"source"`
Description string `json:"description,omitempty"`
Version string `json:"version,omitempty"`
Author *Author `json:"author,omitempty"`
Homepage string `json:"homepage,omitempty"`
Repository string `json:"repository,omitempty"`
License string `json:"license,omitempty"`
Keywords []string `json:"keywords,omitempty"`
Category string `json:"category,omitempty"`
Tags []string `json:"tags,omitempty"`
Strict *bool `json:"strict,omitempty"` // conflict policy on the plugin.json+entry union (default true): strict errors on a same-name conflict, non-strict lets the entry override. See marketplace.resolveConflicts.
// Component config can be inlined here (overlaid on plugin.json):
Skills any `json:"skills,omitempty"` // string | []string
Commands any `json:"commands,omitempty"` // string | []string
Agents any `json:"agents,omitempty"` // string | []string
Hooks any `json:"hooks,omitempty"` // string | object
MCPServers map[string]any `json:"mcpServers,omitempty"`
LSPServers map[string]any `json:"lspServers,omitempty"`
}
PluginEntry is one plugin listed in a marketplace.
type PluginManifest ¶
type PluginManifest struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Version string `json:"version,omitempty"`
MCPServers map[string]any `json:"mcpServers,omitempty"`
Skills any `json:"skills,omitempty"`
Commands any `json:"commands,omitempty"`
Agents any `json:"agents,omitempty"`
Hooks any `json:"hooks,omitempty"`
LSPServers map[string]any `json:"lspServers,omitempty"`
}
PluginManifest is .claude-plugin/plugin.json for a strict-mode plugin.
type ProjectionResult ¶
type ProjectionResult struct {
MCPServers []source.MCPServer
Skills []source.Skill
Subagents []source.Subagent
Commands []source.Command
Hooks []source.Hook
LSPServers []source.LSPServer
}
ProjectionResult collects the canonical model entries derived from a plugin's components.
func Project ¶
func Project(entry PluginEntry, cacheDir string) (ProjectionResult, error)
Project loads the plugin's components and returns them as canonical entries:
- Reads cacheDir/.claude-plugin/plugin.json (when present) for the primary component list; a missing plugin.json is fine (a curator-defined plugin may declare everything in the entry).
- If the manifest lists no skills, falls back to convention-based discovery: scans cacheDir/skills/*/ for SKILL.md files.
- Then overlays any component fields on the PluginEntry itself.
This is a UNION: plugin.json PLUS entry additions. The entry.Strict flag governs how a same-name CONFLICT between the two is resolved (see resolveConflicts): strict (the default) errors so a packaging disagreement is never silently guessed; non-strict lets the entry override. Union semantics never silently drop a plugin's declared components — the pre-Strict-policy non-strict path used to IGNORE plugin.json entirely, dropping a plugin's own components after an upstream strict-flip.
${CLAUDE_PLUGIN_ROOT} in command/url strings is replaced with cacheDir so non-Claude adapters can resolve binary paths.
func ProjectWithReader ¶
func ProjectWithReader(entry PluginEntry, cacheDir string, readFile func(string) ([]byte, error)) (ProjectionResult, error)
ProjectWithReader is like Project but uses a caller-supplied readFile function for loading plugin.json and component markdown files. This enables in-memory filesystem use in tests. Convention-based discovery (skills/ directory scan) is disabled when using ProjectWithReader; use Project for full behavior.
type RelativeFetcher ¶
type RelativeFetcher struct{}
RelativeFetcher copies a local directory tree into the destination. The src.Relative field is treated as an absolute path or a path relative to the caller's working directory; callers should resolve it to absolute before invoking Fetch.
If src.RootDir is non-empty, the resolved Relative path is required to be contained within RootDir — this prevents a malicious marketplace entry from setting `"source": "../../../../etc"` and copying arbitrary host files into the plugin cache.
func (*RelativeFetcher) Fetch ¶
func (f *RelativeFetcher) Fetch(src Source, into string) (FetchResult, error)
Fetch copies src.Relative (a local directory) into into.
type SHAWarning ¶
type SHAWarning struct {
// ID is the plugin's filesystem ID.
ID string
// Version is the version that was re-uploaded.
Version string
// RecordedSHA is what was stored in plugins/<id>.toml at install time.
RecordedSHA string
// FetchedSHA is the SHA of the freshly-fetched plugin.json.
FetchedSHA string
}
SHAWarning is emitted when a plugin's recorded manifest_sha differs from the freshly-fetched SHA for the same version — a signal that the marketplace publisher re-uploaded the same version with different content.
func DetectSHADrift ¶
func DetectSHADrift(plugins []source.Plugin, freshSHAs map[string]string) []SHAWarning
DetectSHADrift checks installed plugins against freshly-fetched manifest SHAs (provided by the caller after re-fetching each plugin's cache). For each plugin whose version hasn't changed but whose SHA has, a SHAWarning is returned.
freshSHAs maps plugin ID → sha256 hex string of the freshly-fetched plugin.json.
type Source ¶
type Source struct {
Kind string `json:"source,omitempty"` // "github" | "url" | "git-subdir" | "npm"
Repo string `json:"repo,omitempty"`
URL string `json:"url,omitempty"`
Path string `json:"path,omitempty"`
Ref string `json:"ref,omitempty"`
SHA string `json:"sha,omitempty"`
Package string `json:"package,omitempty"`
Version string `json:"version,omitempty"`
Registry string `json:"registry,omitempty"`
// Relative is the relative-path string when Source was a JSON string.
Relative string `json:"-"`
// RootDir, if non-empty, constrains where the RelativeFetcher will copy
// from — Relative must resolve inside RootDir or the fetch is rejected.
// Callers that resolve a marketplace-supplied relative path against the
// marketplace cache directory should set this to the cache root so a
// hostile marketplace.json entry like `"source": "../../../etc"` cannot
// copy host files into the plugin cache.
RootDir string `json:"-"`
}
Source is the polymorphic plugin source. Tag-based dispatch: - A JSON string → Relative path - A JSON object → Kind-based (github, url, git-subdir, npm)
func (Source) MarshalJSON ¶
MarshalJSON serialises a Source back to either a JSON string (relative) or a JSON object (all other kinds).
func (*Source) UnmarshalJSON ¶
UnmarshalJSON handles the polymorphic shape: string → Relative; object → Kind etc.