Help us shape the future of Go! Take the 2026 Go Developer Survey

plugin

package
v1.4.15 Latest Latest
Warning

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

Go to latest
Published: Oct 19, 2026 License: MIT Imports: 44 Imported by: 0

Documentation

Overview

Package plugin — lifecycle audit helpers.

Wires plugin install/remove/update events to nself_ops.audit_log. Each event is recorded as: plugin name, operation type, actor (CLI user or system), timestamp, and optional metadata (version, source, error).

Writes are best-effort: failure to write the audit row never blocks the underlying operation. Errors are logged via stderr fallback (slog when available in callers).

Initial wiring and completion verified via integration test suite.

reverse_deps.go — which installed plugins depend on a plugin (D-0325).

Purpose: let `nself bundle remove` keep a member that a plugin outside the bundle still needs, using the same scan `plugin remove` uses. Inputs: the plugin directory and a plugin name. Outputs: the manifest names of the dependents. Constraints: read only; the name is validated before the scan.

Index

Constants

View Source
const (
	// DefaultRegistryURL is the primary registry endpoint (Cloudflare Worker).
	DefaultRegistryURL = "https://plugins.nself.org/registry.json"

	// FallbackRegistryURL is the GitHub raw fallback when the primary is down.
	FallbackRegistryURL = "https://raw.githubusercontent.com/nself-org/plugins/main/registry.json"

	// DefaultCacheTTL is the registry cache lifetime in seconds.
	DefaultCacheTTL = 300
)
View Source
const (
	TierReasonInstalled   = "installed"    // an installed plugin keeps its tier
	TierReasonOverride    = "override"     // --tier chose it
	TierReasonEntitlement = "entitlement"  // the licence entitles the Licensed tier
	TierReasonOnlyTier    = "only-tier"    // the registry serves one Licensed entry and nothing was checked
	TierReasonDefaultFree = "default-free" // nothing else decided, so free
	TierReasonUnresolved  = "unresolved"   // no single tier could be decided; see the error
)

Tier reasons reported with a resolution (and by `plugin info --json`).

View Source
const (
	WireTierFree    = "free" // wire: registry tier and counts key
	WireTierPro     = "pro"  // wire: registry tier and licence API product
	LicenseFree     = "free"
	LicenseLicensed = "licensed"
)

Purpose: keep registry wire tiers separate from the licence labels shown by the CLI. Inputs: registry tier strings. Outputs: display labels and public licence values. Constraints: registry and licence API wire values remain unchanged.

View Source
const ArtifactKindSource = "source"

ArtifactKindSource is the artifact kind downloadPluginPackageForTier reports when it fetched the source tarball. Any other value it returns is one of PlatformArch()'s platform strings (darwin-arm64, darwin-amd64, linux-amd64, linux-arm64, windows-amd64), identifying a per-platform binary tarball instead. Callers use this to pick the checksum that actually matches the bytes on disk — see installer_locked.go's Step 5.

View Source
const (
	// EnvCLIVersion carries the cli's own version (semver, no leading "v").
	EnvCLIVersion = "NSELF_CLI_VERSION"
)

Environment variable names of contract:cli.plugin-exec-env v1.

Variables

This section is empty.

Functions

func BundleLabel added in v1.4.13

func BundleLabel(bundle string) string

BundleLabel names the bundle that includes a licensed plugin.

func CacheLicense

func CacheLicense(key string, valid bool, cacheDir string) error

CacheLicense writes the validation result for a license key to a cache file inside cacheDir. The cache format is: {data}|{hmac_hex} where data is {key_prefix}|{status}|{timestamp} and hmac_hex is the HMAC-SHA256 of data keyed by the random 32-byte key persisted at ~/.nself/license/.hmac-key (SIEGE V03-F02 fix — replaces observable-derived machineID key).

Before writing, it prunes any cache entries older than 30 days.

func CaptureOutput added in v1.4.15

func CaptureOutput(ctx context.Context, binPath string, args []string) (stdout []byte, exit int, err error)

CaptureOutput runs an installed plugin binary with args and returns what it wrote to stdout (P7-CI-53; Constitution §2.2, the plugin exec funnel for callers that need the output rather than an inherited terminal).

Purpose: let the composition root run a plugin subcommand without its own exec.Command, with the same environment execPluginBinary gives (ExecEnv).

Inputs: ctx bounds the run (the process is killed when it ends), binPath is an already validated plugin binary path, args is its full argv tail.

Outputs: stdout bytes (at most 4 MiB), the process exit code, and an error. A process that ran and exited non-zero is NOT an error: exit carries the code and err is nil. err is set when the process could not start, was killed by ctx (wraps ctx.Err()), or printed more than the cap; exit is -1 then.

Constraints: stdin is empty, stderr is discarded, the environment is ExecEnv(os.Environ(), compat.V15).

func CheckCLICompat added in v1.0.6

func CheckCLICompat(compat *CompatBlock, cliVersion string) error

CheckCLICompat checks whether cliVersion satisfies the plugin's compat.nself range. Returns nil if no compat block is set or if the version is in range.

func CheckComplianceJSON added in v1.0.6

func CheckComplianceJSON(ctx context.Context, name string, port int) ([]byte, error)

CheckComplianceJSON returns the compliance result as JSON bytes.

func CheckEOLBlock added in v1.0.11

func CheckEOLBlock(ctx context.Context, name string, allowEOL bool) error

CheckEOLBlock fetches the registry entry for name and returns an error if the plugin's status is "eol" and allowEOL is false. (S58-T03) If the registry cannot be reached or the plugin is not found, this function returns nil (install will fail downstream with a more specific error).

func CheckServiceCompat added in v1.0.6

func CheckServiceCompat(compat *CompatBlock, actual map[string]string) []string

CheckServiceCompat checks whether each service version in actual satisfies the corresponding constraint in compat.requires. Returns a list of services that fail the check.

func CompareSemver added in v1.4.14

func CompareSemver(a, b string) (int, error)

CompareSemver returns -1 if a < b, 0 if a == b and 1 if a > b. It returns an error (and 0) when either version cannot be parsed.

CompareSemver("1.10.0", "1.9.9") → 1, nil
CompareSemver("1.2.3-beta", "1.2.3") → -1, nil

func CompareVersions added in v1.0.11

func CompareVersions(a, b string) int

CompareVersions compares two semver strings a and b. Returns -1 if a < b, 0 if a == b, +1 if a > b. (S58-T06)

func DefaultCacheDir added in v1.0.11

func DefaultCacheDir() string

DefaultCacheDir is the exported alias of defaultCacheDir for use by commands that need to call FetchRegistry directly. (S58-T06)

func DefaultEntitlement added in v1.4.14

func DefaultEntitlement(ctx context.Context, bundles []string) (bool, error)

DefaultEntitlement is the production EntitlementFunc (the licence check ResolvePluginTier uses when given nil), exported so a caller can wrap it and see a lookup error that resolution itself fails closed on.

func DeriveCanonicalPermissions added in v1.3.2

func DeriveCanonicalPermissions(grouped map[string][]string) []string

DeriveCanonicalPermissions reduces the descriptive permission object to the canonical vocabulary the allowlist validates.

The result is sorted and de-duplicated so a manifest produces the same set every time regardless of map iteration order.

func DisablePlugin added in v1.0.2

func DisablePlugin(name, pluginDir string) error

DisablePlugin creates a .disabled marker file in the plugin's directory, causing it to be excluded from compose files on the next build.

func DormantBanner added in v1.1.1

func DormantBanner(rec *PluginLifecycleRecord, now time.Time) string

DormantBanner returns the warning text for a dormant plugin showing days remaining.

func EffectiveStatus added in v1.3.6

func EffectiveStatus(status string) string

EffectiveStatus normalises a plugin's raw registry `status` value for every status-driven decision: an absent (empty) status defaults to "stable" — see pluginEntry.PublishStatus / PluginManifest.PublishStatus's doc comment ("Missing status defaults to stable for backwards compatibility"). Before FIX-CLI-6 every status gate compared the raw field directly against the literal string "stable", so "" and "stable" were treated inconsistently: the lifecycle switch in installLocked already special-cased both the same way, but verifyChecksum/verifyPluginSignature's "missing value" branch did not, leaving all 130 (of 177, as of 2026-09-04) registry entries without an explicit status effectively unable to ever hit the "stable" hard-fail path. Every status-driven gate must compare against EffectiveStatus(status), not the raw field, so "" and an explicit "stable" are never inconsistent again.

func EnablePlugin added in v1.0.2

func EnablePlugin(name, pluginDir string) error

EnablePlugin removes the .disabled marker file from the plugin's directory, allowing it to be included in compose files on the next build.

func ExecEnv added in v1.4.15

func ExecEnv(base []string, v15 func() bool) []string

ExecEnv returns the environment for a plugin process the cli starts (contract:cli.plugin-exec-env v1, P7-CI-59).

Purpose: a plugin follows the cli's resolved compat mode and can check the cli version at startup, so the P7-SHIP-09 default flip needs no flip in the plugins.

Inputs: base is the environment the plugin would otherwise get (a copy is returned; base is never modified). v15 resolves the compat mode; nil means compat.V15. It is called once per ExecEnv call and its result is not cached.

Outputs: base plus NSELF_V15=1|0 and NSELF_CLI_VERSION=<semver without v>.

Constraints: a NSELF_V15 already in base (the user's choice) passes through unchanged. Any NSELF_CLI_VERSION in base is replaced: it can only be a parent cli's value and must not outlive the cli that starts the plugin. Nothing else cli-internal is added.

func IncompatiblePlugins added in v1.0.6

func IncompatiblePlugins(pluginDir, newCLIVersion string) []string

IncompatiblePlugins checks all installed plugins and returns a list of those whose compat.nself range will not be satisfied by newCLIVersion. This is used by `nself upgrade` to warn the user before upgrading.

func Install

func Install(ctx context.Context, cfg *config.Config, name string, pluginDir string) error

Install downloads, extracts, and configures a plugin. For paid plugins it checks the license first. Dependencies are resolved and recursively installed before the target plugin. If any step after extraction fails, the extracted directory and database schema are rolled back.

A file lock on {pluginDir}/.install.lock is held for the duration of the operation to prevent concurrent installs from corrupting plugin state.

func InstallFromURL added in v1.3.0

func InstallFromURL(ctx context.Context, cfg *config.Config, sourceURL string, pluginDir string, expectedChecksum string) error

InstallFromURL downloads a plugin tarball from an arbitrary HTTPS URL, extracts it, validates its bundled plugin.json, and installs it exactly like a registry plugin (schema creation, route/table conflict checks) EXCEPT for the registry-specific trust steps that don't apply to a source with no registry entry: license check (there is no license tier for a URL) and Ed25519 signature verification (there is no registry-pinned author key to verify against). Checksum verification still runs against expectedChecksum when the caller supplies one (see the package doc comment above for why that must come from outside the tarball, not from the extracted plugin.json).

Callers MUST obtain user confirmation (or an explicit non-interactive opt-in such as --yes) BEFORE calling this — it downloads and executes arbitrary third-party code with no vetting beyond what's described above.

func InstalledTier added in v1.4.14

func InstalledTier(pluginDir, name string) (tier, reason string, ok bool)

InstalledTier reports the tier an installed plugin keeps (licence vocabulary: free or licensed) and why, for `plugin info --json`. ok is false when the plugin is not installed or its manifest names no tier.

func IsCommandInstalled added in v1.3.0

func IsCommandInstalled(cmdName string) bool

IsCommandInstalled reports whether a plugin providing the named command is present, i.e. whether ProxyCommand would find a binary for it.

Used by the CLI to decide whether a relocated command still needs its "moved to a plugin" notice. Once the plugin is installed the old spelling is the supported spelling, and repeating the install hint is noise.

func IsDisabled added in v1.0.2

func IsDisabled(name, pluginDir string) bool

IsDisabled returns true if the named plugin has a .disabled marker file.

func IsFreeKey added in v1.0.12

func IsFreeKey(key string) bool

IsFreeKey returns true if the key is a nself_free_ key.

func IsPaidPlugin added in v1.0.12

func IsPaidPlugin(name string) bool

IsPaidPlugin is the exported wrapper around the package-private isPaidPlugin. Returns true if the named plugin requires a paid license key.

func IsRateLimited added in v1.1.0

func IsRateLimited(err error) (retryAfter string, ok bool)

IsRateLimited reports whether err is (or wraps) an ErrRateLimited.

func IsThirdPartyInstallSource added in v1.3.0

func IsThirdPartyInstallSource(ref string) bool

IsThirdPartyInstallSource reports whether ref looks like a URL rather than a plain registry plugin name. Plugin names are validated elsewhere against namePattern (lowercase-with-hyphens, no scheme), so any ref containing a "://" is unambiguously a URL, not a name.

func Label added in v1.4.13

func Label(tier string) string

Label returns the human licence label for a registry tier.

func LicenseCacheDir

func LicenseCacheDir() string

LicenseCacheDir returns the directory used for license validation caching. This is ~/.nself/license/ (or /tmp/.nself/license/ if the home directory cannot be determined).

func LicenseTier added in v1.4.14

func LicenseTier(t string) string

LicenseTier is the public licence value (free or licensed) of a registry tier: every licensed wire tier (pro, max, ...) is "licensed".

func LicenseValue added in v1.4.13

func LicenseValue(tier string) string

LicenseValue returns the public licence value for a registry tier.

func LogPluginError added in v1.0.16

func LogPluginError(ctx context.Context, cfg *config.Config, op AuditOp, name string, err error)

LogPluginError records a failed lifecycle event for traceability.

func LogPluginInstall added in v1.0.16

func LogPluginInstall(ctx context.Context, cfg *config.Config, name, version, source string)

LogPluginInstall records a plugin install event. Best-effort: never blocks the install path on failure.

func LogPluginRemove added in v1.0.16

func LogPluginRemove(ctx context.Context, cfg *config.Config, name string, keepData bool)

LogPluginRemove records a plugin remove event.

func LogPluginUpdate added in v1.0.16

func LogPluginUpdate(ctx context.Context, cfg *config.Config, name, fromVersion, toVersion string)

LogPluginUpdate records a plugin update event.

func NeedsRevalidation

func NeedsRevalidation(key string, cacheDir string) bool

NeedsRevalidation returns true if the license cache in cacheDir is missing, tampered with, or if the most recent validation timestamp is older than revalidationInterval (7 days). This is used during nself start to trigger a periodic heartbeat check against the license server. Owner keys never need revalidation.

func PlatformArch added in v1.1.10

func PlatformArch() (string, error)

PlatformArch returns the platform string for binary plugin archive selection. Supported values: darwin-arm64, darwin-amd64, linux-amd64, linux-arm64, windows-amd64. Any other GOOS/GOARCH pair returns an error.

func PluginBinDir added in v1.3.0

func PluginBinDir() string

PluginBinDir returns the directory the command proxy searches for plugin binaries. Exported so the installer and the router cannot drift apart on where that is.

func PluginEnv added in v1.3.0

func PluginEnv(projectDir string, m *PluginManifest) []string

PluginEnv returns the environment entries to append for a plugin process.

Returns nil when the plugin declares nothing, when there is no project here, or when a variable is not set anywhere — an absent value is passed as absent, not as empty, so the plugin's own default applies.

func ProxyBinary added in v1.4.13

func ProxyBinary(binName, slug string, args []string) error

ProxyBinary execs the named plugin binary with args verbatim. It is the exec path of the plugin-command mount (contract:cli.plugin-command-mount v1): the binary is resolved ONLY inside the plugin bin directory (S-002), argv is passed straight through with no shell, stdio is inherited and a non-zero plugin exit status comes back as ExitCodeError. slug names the plugin whose manifest (if present) declares project settings for the child environment, exactly like the unknown-command proxy.

The mount validates the binary at discovery time (mount.Discover, E406); a binary that vanishes between discovery and exec fails with E406 here.

func ProxyBinaryAt added in v1.4.13

func ProxyBinaryAt(candidate, rootDir, slug string, args []string) error

ProxyBinaryAt executes the discovered path after a fresh containment check. The plugins directory owner can replace the file between check and exec; this accepted window does not cross a different trust boundary.

func ProxyCommand

func ProxyCommand(cmdName string, args []string) error

ProxyCommand checks if a plugin binary exists and executes it. If not, it instructs the user to install it.

For security, the binary is looked up ONLY in the plugin bin directory, never via the full system PATH. This prevents PATH hijacking attacks.

func ProxyCommandWithHint added in v1.3.0

func ProxyCommandWithHint(cmdName string, args []string, installHint string) error

ProxyCommandWithHint is ProxyCommand with control over the install hint in the not-found error.

The generic hint assumes the plugin is named after the command, which is not always true: `claw` lives in a plugin called `claw-cli`, because a paid `claw` service plugin already owns that name. When the deprecation registry has already told the user the right thing to install, passing an empty hint here stops the proxy contradicting it — the user was being shown "use 'nself install claw-cli'" and "nself install claw" one line apart.

func PublishedBinaryPath added in v1.3.0

func PublishedBinaryPath(binName string) string

PublishedBinaryPath returns where a plugin's command binary is published, including the platform's executable suffix.

Exported so tests assert against the same path the installer writes. Having the test compute the name itself is how the Windows job went red: the installer appended .exe and the test did not.

func RegisterFreeAccount added in v1.0.12

func RegisterFreeAccount(ctx context.Context, pingURL string) (string, error)

RegisterFreeAccount calls ping_api to create a new nself_free_ key. Returns the raw key string on success.

func RegistryFetchedAt added in v1.3.0

func RegistryFetchedAt() string

RegistryFetchedAt returns the top-level "fetchedAt" snapshot timestamp from the most recently cached registry response, or "" when no cache exists or the cached registry didn't carry one (CLI-R16). It reads the on-disk cache only — no network call — so it is safe to call right after a List/Fetch that already populated the cache in the same command invocation.

func Remove

func Remove(ctx context.Context, cfg *config.Config, name string, pluginDir string, keepData bool, force bool) error

Remove stops a plugin (if running), optionally drops its database schema, and removes its directory from disk. When force is false and other installed plugins depend on the target, Remove returns an error listing them.

A file lock on {pluginDir}/.install.lock is held for the duration of the operation so that concurrent install and remove calls serialize correctly.

func ReverseDependencies added in v1.4.14

func ReverseDependencies(pluginDir, name string) ([]string, error)

ReverseDependencies returns the manifest names of installed plugins whose plugin.json lists name as a dependency (compared without regard to case). Directories without a valid manifest are skipped. A missing plugin directory has no dependents. An invalid name is an error.

func SatisfiesRange added in v1.0.6

func SatisfiesRange(version, rangeStr string) (bool, error)

SatisfiesRange checks whether version satisfies a space-separated list of semver constraints. All constraints must be satisfied (AND logic). Supported operators: >=, <=, >, <, =, != Examples: ">=1.0.0 <2.0.0", ">=14", ">=2.30"

func SendFreeInstallTelemetry added in v1.0.12

func SendFreeInstallTelemetry(pingURL string, licenseKey string, pluginName string)

SendFreeInstallTelemetry fires a non-blocking install event to ping_api. Errors are silently discarded — telemetry must never block installs.

func SignCanonicalObject added in v1.1.2

func SignCanonicalObject(bundle, name, sha256, visibility, version string, size int64, privateKeyHex string) (string, error)

SignCanonicalObject signs the canonical plugin object with the provided Ed25519 private key and returns the hex-encoded signature.

privateKeyHex is a hex-encoded 64-byte Ed25519 private key (seed || public key). This function is intended for use in the ping_api worker and in tests; the CLI itself only verifies — it does not sign.

func Start

func Start(ctx context.Context, pluginDir string, name string) error

Start launches a plugin process in the background. It locates the plugin's entry point inside pluginDir, starts it, writes the PID to ~/.nself/runtime/pids/{name}.pid, and records the state in ~/.nself/runtime/states/{name}.state.

func Stop

func Stop(ctx context.Context, name string) error

func UndoInstall added in v1.4.15

func UndoInstall(_ context.Context, _ *config.Config, name, pluginDir string) error

UndoInstall reverses a fresh install that the caller's reconcile refused: it unpublishes the plugin's command binary and deletes its directory. The schema is never dropped: an install reuses a schema kept by an earlier `remove --keep-data` or bundle remove, so dropping here could destroy kept data; a schema this install created stays empty and `nself remove` drops it. Unlike Remove it does not stop containers (the refused reconcile never started any), check reverse dependencies, touch a .prev backup, or print the build hint. It takes the install lock, so it must not run inside Install.

func Update

func Update(ctx context.Context, cfg *config.Config, name string, pluginDir string) error

Update backs up the current installation, then reinstalls from the registry. If the new install fails, the previous version is restored automatically. The installed tier is kept: installLocked reads it from the backup (v1.5, see ResolvePluginTier). A caller that wants to switch tier on purpose sets NSELF_PLUGIN_INSTALL_TIER for the call, as `plugin install --tier` does (the cmd layer does this for `plugin update --tier`).

func ValidateBinaryName added in v1.4.14

func ValidateBinaryName(bin string) error

ValidateBinaryName reports whether bin is a single plain path element that is safe to join to the plugin bin directory. A manifest's binaryName and cliCommands names come from the plugin author, so they get the same checks as a plugin name (no separator, no dot reference, no control character, Base unchanged) with the binary name alphabet.

func ValidateCommandName added in v1.4.14

func ValidateCommandName(cmd string) error

ValidateCommandName reports whether cmd is a plain command name: the part after "nself-" in a published binary, and the first argument of the command proxy. Same shape as a plugin slug, but reserved plugin names are allowed.

func ValidateLicenseRemote

func ValidateLicenseRemote(ctx context.Context, key string, pingURL string) (bool, error)

ValidateLicenseRemote performs a remote license check by POSTing to the given pingURL. It returns (true, nil) on HTTP 200, (false, nil) on 401/403/404, and (false, error) on network or unexpected failures.

func ValidateName added in v1.4.14

func ValidateName(name string) error

ValidateName reports whether name is a plain plugin slug that is safe to join to the plugin directory and is not a reserved infrastructure name. It rejects the empty string, ".", "..", any path separator (slash or backslash, and the OS separator), absolute paths, a leading '-', NUL and other control characters, whitespace, and anything filepath.Base would change.

func ValidateNetworkAccess

func ValidateNetworkAccess(ctx context.Context, registryURL string) error

ValidateNetworkAccess does a HEAD request to the registry URL. Returns an error with a clear message if the registry is unreachable. Call before any operation that requires network access to the plugin registry.

func ValidateThirdPartyURL added in v1.3.0

func ValidateThirdPartyURL(ref string) (*url.URL, error)

ValidateThirdPartyURL parses ref and enforces the transport-security policy for third-party plugin sources: HTTPS only, except for localhost/127.0.0.1 (dev and integration-test use, matching enforceRegistryHTTPS's convention for the official registry URL).

func VerifyCanonicalSignature added in v1.1.2

func VerifyCanonicalSignature(bundle, name, sha256, visibility, version string, size int64, publicKeyHex, signatureHex string) error

VerifyCanonicalSignature checks an Ed25519 signature over the canonical plugin object constructed from the provided fields.

publicKeyHex is a hex-encoded 32-byte Ed25519 public key. signatureHex is a hex-encoded 64-byte Ed25519 signature.

Returns nil on success, an error describing the failure otherwise.

func WithCallerReconciles added in v1.4.15

func WithCallerReconciles(ctx context.Context) context.Context

WithCallerReconciles marks ctx as belonging to a caller that reconciles generated state right after the install, so the build hint is redundant in v1.5. Callers that do not reconcile must not use it.

func WithSignatureBypassAcknowledged added in v1.4.15

func WithSignatureBypassAcknowledged(ctx context.Context, acknowledged bool) context.Context

WithSignatureBypassAcknowledged records --force for this install and its dependencies. Consent alone never requests a bypass: SKIP_VERIFY must be set.

func WriteRegistryCache added in v1.4.14

func WriteRegistryCache(cacheDir string, reg *Registry) error

WriteRegistryCache stores reg as the shared registry cache file (cacheDir/registry.json, mode 0600), in the same normalised form a normal fetch writes, so the next Install within the cache TTL resolves versions from reg. `nself update --yes` uses it to pin the plugin steps to the registry the plan was computed from.

Types

type APIEndpointList added in v1.3.0

type APIEndpointList []string

APIEndpointList is the set of HTTP endpoints a plugin exposes.

Published manifests use two shapes: an array of "METHOD /path" strings, and an array of {method, path, description} objects. The field was typed for the first, so the one plugin using the second was unparseable and therefore invisible.

func (*APIEndpointList) UnmarshalJSON added in v1.3.0

func (a *APIEndpointList) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts an array of strings, an array of objects, or null.

type AuditEvent added in v1.0.16

type AuditEvent struct {
	Op         AuditOp                `json:"op"`
	PluginName string                 `json:"plugin"`
	Actor      string                 `json:"actor"`
	Version    string                 `json:"version,omitempty"`
	Source     string                 `json:"source,omitempty"` // "free" | "pro" | "local"
	Error      string                 `json:"error,omitempty"`
	Metadata   map[string]interface{} `json:"metadata,omitempty"`
	Timestamp  time.Time              `json:"timestamp"`
}

AuditEvent captures one plugin lifecycle event for the audit log.

type AuditOp added in v1.0.16

type AuditOp string

AuditOp identifies a plugin lifecycle operation type.

const (
	AuditOpInstall AuditOp = "plugin.install"
	AuditOpRemove  AuditOp = "plugin.remove"
	AuditOpUpdate  AuditOp = "plugin.update"
	AuditOpDisable AuditOp = "plugin.disable"
)

type CLICommand

type CLICommand struct {
	Name        string `json:"name"`
	Description string `json:"description"`
}

CLICommand describes a CLI command provided by a plugin.

type CompatBlock added in v1.0.6

type CompatBlock struct {
	Nself    string            `json:"nself,omitempty" yaml:"nself,omitempty"`
	Requires map[string]string `json:"requires,omitempty" yaml:"requires,omitempty"`
}

CompatBlock describes the compatibility requirements declared by a plugin manifest. The Nself field is a semver range constraint for the CLI version (e.g. ">=1.0.0 <2.0.0"). Requires maps service names to their own semver range constraints (e.g. "postgres": ">=14", "hasura": ">=2.30").

type ComplianceResult added in v1.0.6

type ComplianceResult struct {
	PluginName string
	Compliant  bool
	Endpoints  []EndpointCheck
}

ComplianceResult holds the outcome of a plugin compliance check.

func CheckCompliance added in v1.0.6

func CheckCompliance(ctx context.Context, name string, port int) *ComplianceResult

CheckCompliance verifies that a running plugin implements the standard interface endpoints (/health, /version, /capabilities). Non-compliant plugins log a warning but are not stopped.

type DeprecationBlock added in v1.0.11

type DeprecationBlock struct {
	AnnouncedDate   string `json:"announcedDate"`
	EOLDate         string `json:"eolDate"`
	ReplacedBy      string `json:"replacedBy,omitempty"`
	MigrationGuide  string `json:"migrationGuide"`
	MigrationScript string `json:"migrationScript,omitempty"`
}

DeprecationBlock carries the required deprecation metadata when a plugin's status is "deprecated". All five fields must be present (S58-T02). announcedDate and eolDate are ISO 8601 date strings (YYYY-MM-DD). replacedBy is the name of the replacement plugin (optional but recommended). migrationGuide is a URL that must resolve with HTTP 200 at CI time. migrationScript is a repo-relative path to an automated migration script (optional).

type EndpointCheck added in v1.0.6

type EndpointCheck struct {
	Path      string
	Available bool
	Status    int
}

EndpointCheck records whether a specific endpoint responded.

type EntitlementFunc added in v1.3.6

type EntitlementFunc func(ctx context.Context, bundles []string) (bool, error)

EntitlementFunc reports whether the operator's current license entitles at least one of the given bundles.nself.org slugs. Production callers use defaultEntitlement; tests inject a fixture so resolution logic is verifiable without a live ping_api call.

type EnvVar

type EnvVar struct {
	Name        string `json:"name"`
	Required    bool   `json:"required"`
	Description string `json:"description"`
	Default     string `json:"default,omitempty"`
}

EnvVar describes an environment variable required or used by a plugin.

type EnvVarList added in v1.3.0

type EnvVarList []EnvVar

EnvVarList is the set of environment variables a plugin declares.

Purpose: accept every shape real plugin.json files use. The field was typed `[]EnvVar`, but the manifest template every CLI-R11 extraction was copied from writes an object instead:

"envVars": { "required": [], "optional": ["HCLOUD_TOKEN"] }

Unmarshalling that into a slice fails, and a manifest that fails to parse is discarded — readPluginManifest returns nil, ListInstalled skips the plugin. So every extracted plugin was absent from `nself plugin list` and from anything else that enumerates what is installed, silently. This is the same failure the `webhooks` field had, for the same reason: the type described one of the shapes in use rather than all of them.

Inputs: the `envVars` value from plugin.json, in any of three shapes.

Outputs: the declared variables, with Required set from whichever shape said so.

Constraints: parsing must never fail on a shape a real manifest uses. A manifest this code cannot read makes a plugin invisible rather than loud, which is the worst of both.

func (EnvVarList) MarshalJSON added in v1.3.0

func (e EnvVarList) MarshalJSON() ([]byte, error)

MarshalJSON always writes the canonical array-of-objects form.

func (EnvVarList) Names added in v1.3.0

func (e EnvVarList) Names() []string

Names returns every declared variable name, in declaration order.

func (*EnvVarList) UnmarshalJSON added in v1.3.0

func (e *EnvVarList) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts an array of objects, an array of names, an object with required/optional name lists, or null.

type ErrRateLimited added in v1.1.0

type ErrRateLimited struct {
	RetryAfter string
}

ErrRateLimited is returned when the license validation server responds with HTTP 429 Too Many Requests. RetryAfter holds the value of the Retry-After header (in seconds) as a string, defaulting to "60" when the header is absent.

func (*ErrRateLimited) Error added in v1.1.0

func (e *ErrRateLimited) Error() string

type ExitCodeError added in v1.0.2

type ExitCodeError struct {
	Code int
}

ExitCodeError is returned when a plugin process exits with a non-zero code. main() reads ExitCode() and mirrors the plugin's status; the message is not printed, because the plugin already wrote its own output to the terminal.

func (*ExitCodeError) Error added in v1.0.2

func (e *ExitCodeError) Error() string

func (*ExitCodeError) ExitCode added in v1.3.0

func (e *ExitCodeError) ExitCode() int

ExitCode satisfies the interface main() and errs.ExitCodeFor look for.

func (*ExitCodeError) Silent added in v1.3.0

func (e *ExitCodeError) Silent() bool

Silent reports that main() must not print this error: the plugin subprocess inherited stdout/stderr and has already reported the failure itself.

type InstalledPluginInfo

type InstalledPluginInfo struct {
	Name        string
	Version     string
	Tier        string
	Status      string
	Description string
}

InstalledPluginInfo is a richer view of an installed plugin used by the inventory subcommand. It includes Description and Tier from the manifest.

func ListInstalled

func ListInstalled(pluginDir string) ([]InstalledPluginInfo, error)

ListInstalled scans pluginDir and returns detailed information for every installed plugin. Each entry's Status is "running", "installed", or "unknown" depending on the plugin's current runtime state.

type LicenseClient

type LicenseClient interface {
	Validate(ctx context.Context, key string) (bool, error)
}

LicenseClient abstracts license validation for testability.

type LicenseValidateResponse added in v1.0.4

type LicenseValidateResponse struct {
	Valid   bool     `json:"valid"`
	Reason  string   `json:"reason,omitempty"`
	Tier    string   `json:"tier"`
	Plugins []string `json:"plugins"`
	Expires string   `json:"expires,omitempty"`
}

LicenseValidateResponse is the JSON body returned by the license validation endpoint on HTTP 200. The Tier and Plugins fields are used to populate the local entitlement cache.

func ValidateLicenseRemoteWithDetails added in v1.0.4

func ValidateLicenseRemoteWithDetails(ctx context.Context, key string, pingURL string) (bool, *LicenseValidateResponse, error)

ValidateLicenseRemoteWithDetails performs a remote license check and returns the full response including tier, plugins, and expiry information.

type LifecycleState added in v1.1.1

type LifecycleState string

LifecycleState represents the current state of a plugin's license lifecycle.

const (
	// StateActive means the plugin license is valid.
	StateActive LifecycleState = "active"
	// StateDormant means the license expired but the grace period is still active.
	StateDormant LifecycleState = "dormant"
	// StateExpired means the grace period exhausted — plugin is an auto-remove candidate.
	StateExpired LifecycleState = "expired"

	// DefaultGracePeriod is the time a plugin stays dormant before auto-removal.
	DefaultGracePeriod = 7 * 24 * time.Hour
)

type LifecycleStore added in v1.1.1

type LifecycleStore struct {
	Version int                               `json:"version"`
	Records map[string]*PluginLifecycleRecord `json:"records"`
}

LifecycleStore is the persistent state file for plugin lifecycle tracking.

func LoadLifecycleStore added in v1.1.1

func LoadLifecycleStore() (*LifecycleStore, error)

LoadLifecycleStore reads the persistent store, creating an empty one if missing.

func (*LifecycleStore) CheckExpiry added in v1.1.1

func (s *LifecycleStore) CheckExpiry(now time.Time) (dormant, autoRemove []string)

CheckExpiry transitions plugins to DORMANT or EXPIRED based on the given time. It returns two slices: plugins newly transitioned to DORMANT, and plugins now EXPIRED (auto-remove candidates). The caller must call Save() to persist transitions.

Renewal check: MarkRenewal must be called before CheckExpiry to clear dormant state for any plugin that was renewed; this prevents a renewed plugin from immediately cycling back to EXPIRED.

func (*LifecycleStore) MarkRenewal added in v1.1.1

func (s *LifecycleStore) MarkRenewal(name string, newExpiry time.Time)

MarkRenewal transitions a plugin back to active state with a new expiry. If the record does not exist yet, it is created.

func (*LifecycleStore) Save added in v1.1.1

func (s *LifecycleStore) Save() error

Save writes the store atomically via a temp file + rename to prevent corruption on concurrent CLI invocations (TOCTOU protection).

type MultiApp

type MultiApp struct {
	Supported       bool   `json:"supported"`
	IsolationColumn string `json:"isolationColumn,omitempty"`
	PKStrategy      string `json:"pkStrategy,omitempty"`
	DefaultValue    string `json:"defaultValue,omitempty"`
}

MultiApp describes multi-tenancy configuration for a plugin.

type PermissionSet added in v1.3.0

type PermissionSet struct {
	// Canonical holds array-form entries, validated against the allowlist.
	Canonical []string
	// Grouped holds object-form entries verbatim, in their own vocabulary.
	Grouped map[string][]string
}

PermissionSet is what a plugin declares it needs.

Two vocabularies are in use and they do not describe the same things:

  • The canonical one this CLI validates: "db:read", "fs:write", "network:internet", "system:exec" — an array of strings, checked fail-closed against an allowlist (S71-T01).
  • A descriptive one used by 121 shipped manifests: an object such as {"database": ["create","read"], "filesystem": ["logs"], "network": ["*"]}. None of its tokens appear in the allowlist.

Flattening the second into the first would produce "database:create" and be rejected, turning an invisible plugin into one that refuses to install. Mapping between them — deciding that "database:create" means "db:write" — would be inventing security policy, which is not a parser's job.

So both are kept, separately. Canonical is validated. Grouped is recorded and displayed, and the divergence is a real finding for someone to settle.

func (PermissionSet) Effective added in v1.3.2

func (p PermissionSet) Effective() []string

Effective returns the permissions to validate: the canonical entries as written, plus the canonical reduction of any descriptive ones.

Both are checked against the same allowlist. The descriptive form used to be recorded and skipped, which left 121 plugins outside a fail-closed check; see DeriveCanonicalPermissions for why reducing it is sound and which direction each mapping moves in.

func (PermissionSet) Len added in v1.3.0

func (p PermissionSet) Len() int

Len reports how many permissions were declared, in either vocabulary.

func (PermissionSet) MarshalJSON added in v1.3.0

func (p PermissionSet) MarshalJSON() ([]byte, error)

MarshalJSON writes back whichever form was read, preferring the canonical array when both are somehow present.

func (PermissionSet) Strings added in v1.3.0

func (p PermissionSet) Strings() []string

Strings renders every declared permission for display. Grouped entries are shown as "category:action" so a human can read them, which is NOT the same as treating them as canonical — nothing validates these.

func (*PermissionSet) UnmarshalJSON added in v1.3.0

func (p *PermissionSet) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts the array form, the object form, or null.

func (PermissionSet) UsesLegacyVocabulary added in v1.3.0

func (p PermissionSet) UsesLegacyVocabulary() bool

UsesLegacyVocabulary reports whether this plugin declared permissions in the descriptive form.

type PluginGraphQLBlock added in v1.0.12

type PluginGraphQLBlock struct {
	// Enabled must be true for this plugin to register as a subgraph.
	Enabled bool `json:"enabled" yaml:"enabled"`
	// SubgraphName is the unique lowercase subgraph identifier.
	// Must not be "hasura" (reserved). Format: [a-z][a-z0-9_]*.
	SubgraphName string `json:"subgraph_name" yaml:"subgraph_name"`
	// SubgraphURL is the HTTP endpoint serving the subgraph SDL.
	// ${PORT} is substituted with the plugin's declared Port field.
	SubgraphURL string `json:"subgraph_url" yaml:"subgraph_url"`
	// SchemaPath is a repo-relative path to the static SDL file.
	// Used by rover supergraph compose. Optional when SubgraphURL is live.
	SchemaPath string `json:"schema_path,omitempty" yaml:"schema_path,omitempty"`
	// Entities lists Apollo Federation entity types this subgraph owns.
	Entities []PluginGraphQLEntityKey `json:"entities,omitempty" yaml:"entities,omitempty"`
}

PluginGraphQLBlock is the graphql: block in a plugin manifest. When Enabled is true and NSELF_FEDERATION=true, nself build registers this plugin as a subgraph in the Apollo Router supergraph (G05).

type PluginGraphQLEntityKey added in v1.0.12

type PluginGraphQLEntityKey struct {
	// Type is the GraphQL type name (e.g. "User").
	Type string `json:"type" yaml:"type"`
	// Key is the @key field used for entity resolution (e.g. "id").
	Key string `json:"key" yaml:"key"`
}

PluginGraphQLEntityKey describes a single Apollo Federation entity type that a plugin subgraph owns. Used for cross-subgraph entity resolution.

type PluginInfo

type PluginInfo struct {
	Name          string
	Version       string
	Category      string
	Installed     bool
	Running       bool
	PublishStatus string // S58-T01: "experimental"|"planned"|"beta"|"stable"|"deprecated"|"eol" — from registry
	UpdatedAt     string // CLI-R16: per-plugin freshness, empty when the source (registry entry or local plugin.json) doesn't carry one
}

PluginInfo describes a plugin's identity and current state.

func List

func List(pluginDir string, installed bool) ([]PluginInfo, error)

List returns plugin information. When installed is true it scans pluginDir for locally installed plugins. When false it returns all plugins known to the registry.

type PluginLifecycleRecord added in v1.1.1

type PluginLifecycleRecord struct {
	Name          string         `json:"name"`
	State         LifecycleState `json:"state"`
	LicenseExpiry time.Time      `json:"license_expiry"`
	DormantSince  *time.Time     `json:"dormant_since,omitempty"`
	// GracePeriod is stored per-record so that future config changes cannot
	// retroactively shorten grace periods that are already in progress.
	GracePeriod time.Duration `json:"grace_period"`
}

PluginLifecycleRecord tracks a single plugin's license lifecycle state.

type PluginManifest

type PluginManifest struct {
	// Required fields
	Name        string `json:"name"`
	Version     string `json:"version"`
	Description string `json:"description"`
	Category    string `json:"category"`
	License     string `json:"license"`

	// Optional metadata
	Author               string   `json:"author,omitempty"`
	Homepage             string   `json:"homepage,omitempty"`
	Repository           string   `json:"repository,omitempty"`
	Tags                 []string `json:"tags,omitempty"`
	IsCommercial         bool     `json:"isCommercial,omitempty"`
	LicenseType          string   `json:"licenseType,omitempty"`
	RequiredEntitlements []string `json:"requiredEntitlements,omitempty"`
	RequiresLicense      bool     `json:"requires_license,omitempty"`
	MinNselfVersion      string   `json:"minNselfVersion,omitempty"`
	MinNodeVersion       string   `json:"minNodeVersion,omitempty"`
	ArchSupport          []string `json:"arch_support,omitempty"`

	// Implementation
	Language   string `json:"language,omitempty"`
	Runtime    string `json:"runtime,omitempty"`
	Port       int    `json:"port,omitempty"`
	EntryPoint string `json:"entryPoint,omitempty"`
	CLI        string `json:"cli,omitempty"`
	// PluginType distinguishes a plugin that adds a CLI command ("cli") from
	// one that runs as a service. Only a "cli" plugin gets its binary published
	// into the directory ProxyCommand searches — see cli_binary.go.
	PluginType string `json:"pluginType,omitempty"`
	// BinaryName overrides the nself-<name> convention for the published
	// binary. ProxyCommand resolves nself-<command>, so this should only differ
	// when the command name differs from the plugin name.
	BinaryName     string `json:"binaryName,omitempty"`
	HealthEndpoint string `json:"health_endpoint,omitempty"`
	PackageManager string `json:"packageManager,omitempty"`
	Framework      string `json:"framework,omitempty"`

	// Database
	Tables []string `json:"tables,omitempty"`
	Views  []string `json:"views,omitempty"`

	// API
	APIEndpoints APIEndpointList `json:"apiEndpoints,omitempty"`
	// WebhookNames, not []string: real manifests use both an array of event
	// names and an object keyed by event name. See webhooks.go.
	Webhooks    WebhookNames `json:"webhooks,omitempty"`
	CLICommands []CLICommand `json:"cliCommands,omitempty"`

	// Environment
	EnvVars EnvVarList `json:"envVars,omitempty"`

	// Dependencies
	Dependencies         []string           `json:"dependencies,omitempty"`
	OptionalDependencies []string           `json:"optionalDependencies,omitempty"`
	SystemDependencies   SystemDependencies `json:"systemDependencies,omitempty"`

	// Inter-plugin communication contract (S43-T17).
	// Consumes lists the plugin names whose HTTP APIs this plugin may call via
	// X-Source-Plugin. Every entry must be installed for this plugin to work
	// correctly; install-time validation enforces this.
	// Provides lists the API services this plugin exposes for other plugins to
	// consume. Both fields use plugin name format (lowercase-with-hyphens).
	Consumes []string `json:"consumes,omitempty"`
	Provides []string `json:"provides,omitempty"`

	// Multi-tenancy
	MultiApp MultiApp `json:"multiApp,omitempty"`

	// Permissions
	Permissions PermissionSet `json:"permissions,omitempty"`

	// Compatibility
	Compat *CompatBlock `json:"compat,omitempty"`

	// PostgresExtensions is manifest v2 requires.postgres_extensions: Postgres
	// extension names (e.g. "vector") the install checks before download
	// (P7-ADOPT-06). Read from a registry entry's catalog.requires.
	PostgresExtensions []string `json:"postgres_extensions,omitempty"`

	// Registry-specific fields (not in plugin.json, populated by registry)
	Tier     string `json:"tier,omitempty"`
	Checksum string `json:"checksum,omitempty"`

	// TierPair marks a genuine free/pro pair of the SAME product sharing one
	// slug (e.g. cron, notify) — set true in BOTH the free and pro registry
	// entries by the plugins/plugins-pro registry authors. Install-time
	// resolution (tier_resolve.go) uses this to tell a real tier pair from an
	// unrelated registry-data collision, which is a hard error instead
	// (OWNER-ACTIONS.md item 15).
	TierPair bool `json:"tier_pair,omitempty"`

	// Bundles lists the bundles.json slugs this specific registry entry
	// belongs to (e.g. cron's pro entry: ["claw"]). Used by tier_resolve.go
	// to decide entitlement for a TierPair's pro entry via
	// license.BundleEntitled. Empty for free entries and for pro plugins sold
	// standalone rather than through a bundle.
	Bundles []string `json:"bundles,omitempty"`

	// PublishStatus is the plugin's lifecycle status from the registry (S58-T01).
	// Valid values: "experimental" | "planned" | "beta" | "stable" | "deprecated" | "eol"
	// Missing status defaults to "stable" for backwards compatibility (CR-A).
	// "planned"     — not yet installable; rejected at install time.
	// "experimental"— early-access; installs with a prominent warning.
	// "beta"        — pre-stable; installs with a warning.
	// "stable"      — default production-ready state; no warning.
	// "deprecated"  — install warns and offers replacedBy alternative if set.
	//                 Requires a Deprecation block in the manifest.
	// "eol"         — install blocked by default; --allow-eol override required.
	PublishStatus string `json:"status,omitempty"`

	// Deprecation carries the required metadata when status == "deprecated". (S58-T02)
	// All five fields are required when the block is present.
	Deprecation *DeprecationBlock `json:"deprecation,omitempty"`

	// GraphQL carries the Apollo Federation subgraph registration for this
	// plugin. When graphql.enabled is true and NSELF_FEDERATION=true, the
	// build pipeline includes this plugin as a subgraph in the Apollo Router
	// supergraph. The field is ignored in non-federation deployments (G05).
	GraphQL *PluginGraphQLBlock `json:"graphql,omitempty"`

	// MaxNselfVersion is the last CLI version this plugin is compatible with. (S58-T06)
	// Empty means "no upper bound". compat-check reads this field.
	MaxNselfVersion string `json:"maxNselfVersion,omitempty"`

	// AuthorPublicKey is the hex-encoded Ed25519 public key of the plugin
	// publisher, pinned in the registry. Used to verify Signature.
	AuthorPublicKey string `json:"author_public_key,omitempty"`

	// Signature is the hex-encoded Ed25519 signature of the tarball checksum.
	// Format: Ed25519Sign(privateKey, sha256(tarball)).
	// Pinned in registry alongside AuthorPublicKey.
	Signature string `json:"signature,omitempty"`

	// ReleaseSignature is the registry's release_signature object (hq PLUG D7,
	// contract plugins.release-signature v1), verified against the keys
	// compiled into the CLI. It is the only signature the installer trusts;
	// AuthorPublicKey and Signature above are decoded for compatibility and
	// never used for trust. A malformed registry value decodes to a non-nil
	// empty Release so the install refuses (E121) instead of seeing "absent".
	ReleaseSignature *signature.Release `json:"release_signature,omitempty"`

	// UpdatedAt is a per-plugin freshness timestamp (CLI-R16). The live
	// plugins.nself.org registry does not send this field as of 2026-08-23 —
	// only a registry-wide "fetchedAt" (see Registry.FetchedAt /
	// RegistryFetchedAt). This field is plumbing: `nself plugin list
	// --detailed` displays it when present (registry entry or local
	// plugin.json) and shows nothing invented when it is not.
	UpdatedAt string `json:"updated_at,omitempty"`

	// PlatformChecksums holds one SHA-256 checksum (lowercase hex, no
	// "sha256:" prefix) per per-platform binary tarball, keyed by the exact
	// platform string PlatformArch() returns (darwin-arm64, darwin-amd64,
	// linux-amd64, linux-arm64, windows-amd64). Populated from the registry's
	// nested `checksums.platforms` object.
	//
	// Checksum above is ALWAYS the source tarball's checksum, never a
	// platform one — the two packages have different bytes, so one flat
	// field can never validate both (FIX-CLI-plugins-83: a platform tarball
	// downloaded via binaryPluginDownloadURL was being checked against
	// Checksum, which could never match). downloadPluginPackageForTier
	// reports which artifact it fetched; installLocked picks the matching
	// entry from this map, or Checksum for the source artifact — see
	// verifyChecksum's caller in installer_locked.go.
	//
	// Only present for a plugin with a binaryName. A plugin whose release
	// predates this field (or a platform the release never built a checksum
	// for) has no entry here — see downloadPluginPackageForTier's doc
	// comment for what happens then.
	PlatformChecksums map[string]string `json:"platform_checksums,omitempty"`
}

PluginManifest describes a single plugin, parsed from plugin.json.

func FindPluginByName added in v1.0.11

func FindPluginByName(reg *Registry, name string) (*PluginManifest, bool)

FindPluginByName is the exported wrapper around findPlugin for use by commands outside the plugin package. (S58-T06)

func LoadManifest added in v1.4.13

func LoadManifest(path string) (*PluginManifest, error)

LoadManifest reads and validates the plugin.json at path, v1 or v2.

func LoadManifestsFromDir added in v1.0.12

func LoadManifestsFromDir(pluginDir string) ([]*PluginManifest, error)

LoadManifestsFromDir scans pluginDir and returns the full PluginManifest for every installed plugin. It is the full-manifest counterpart to ListInstalled, used by features that need manifest fields beyond name/version/tier/status (e.g., federation's GraphQL block). Directories without a valid plugin.json are silently skipped. If pluginDir does not exist, nil is returned.

func ResolvePlugin added in v1.3.6

func ResolvePlugin(ctx context.Context, reg *Registry, name, tierOverride string, entitled EntitlementFunc) (*PluginManifest, error)

ResolvePlugin picks the single registry entry `nself plugin install <name>` (or a bundle's own install loop) should install, by --tier and entitlement alone (a fresh install); installLocked calls ResolvePluginTier with the installed tier of an existing copy. See ResolvePluginTier.

func ResolvePluginTier added in v1.4.14

func ResolvePluginTier(ctx context.Context, reg *Registry, name, tierOverride, installedTier string, entitled EntitlementFunc) (*PluginManifest, string, error)

ResolvePluginTier resolves like ResolvePlugin and also returns the reason. Zero matches: ErrPluginNotFound. One match: returned as-is (a --tier that disagrees with it is rejected). A declared tier pair: --tier picks the entry ("pro" still needs the entitlement); otherwise entitlement decides and falls back to free. Any other duplicate: duplicateSlugError, never a first-match pick.

installedTier is the tier of the copy being updated ("" for a fresh install). In v1.5 mode it wins over entitlement unless tierOverride is set, and a plugin that cannot keep it fails with E131 instead of switching.

func (PluginManifest) MarshalJSON added in v1.3.0

func (m PluginManifest) MarshalJSON() ([]byte, error)

MarshalJSON writes the canonical shapes, so anything this package re-emits round-trips through its own parser.

func (*PluginManifest) RequiresPostgresExtensions added in v1.4.13

func (m *PluginManifest) RequiresPostgresExtensions() []string

RequiresPostgresExtensions returns the Postgres extensions the plugin needs, from manifest v2 requires.postgres_extensions (a v2 plugin.json) or the registry entry's catalog.requires. Empty when none are declared.

func (*PluginManifest) UnmarshalJSON added in v1.3.0

func (m *PluginManifest) UnmarshalJSON(data []byte) error

UnmarshalJSON reads a manifest, normalising the field shapes that vary between published plugins.

Dependencies needs handling here rather than on the field, because the grouped form carries optional entries that belong in a DIFFERENT field (OptionalDependencies), and a field-level unmarshaller cannot write to a sibling. 31 shipped plugins have non-empty optional dependencies, so flattening them into one list or dropping them both change behaviour.

type PluginStatus

type PluginStatus struct {
	Name  string
	State string // starting, running, stopping, stopped, failed
	PID   int
}

PluginStatus describes the current state of a plugin process.

func Status

func Status(name string) (*PluginStatus, error)

Status returns the current status of a plugin by reading its PID and state files. If the PID file indicates a process that is no longer running, the state is corrected to "stopped".

type Registry

type Registry struct {
	Plugins []PluginManifest
	// FetchedAt is the registry-wide snapshot timestamp reported by the live
	// endpoint's top-level "fetchedAt" field (CLI-R16). Empty for registries
	// that don't send it (e.g. the GitHub raw fallback).
	FetchedAt string
	// Origin is the registry URL this snapshot was fetched from (D-0849). The
	// cache records it so an entry cached from one registry never answers for
	// another. Empty for a registry read from a file that predates the field.
	Origin string
}

Registry represents the full plugin registry response.

func FetchRegistry

func FetchRegistry(ctx context.Context, registryURL string, cacheDir string) (*Registry, error)

FetchRegistry is the standalone entry point for fetching a registry. It creates an ephemeral client and delegates to Fetch.

Fallback chain:

  1. registryURL (default: https://plugins.nself.org/registry.json)
  2. GitHub raw fallback
  3. Stale cache at cacheDir/registry.json

A cached file answers only for the registry it was fetched from (or the fallback host, or a file with no recorded origin); see FetchRegistryExclusive for the mode that accepts nothing else.

func FetchRegistryExclusive added in v1.4.15

func FetchRegistryExclusive(ctx context.Context, registryURL string, cacheDir string) (*Registry, error)

FetchRegistryExclusive fetches from the configured registry only (registryURL, else NSELF_PLUGIN_REGISTRY, else the default). It never uses the GitHub fallback or a stale cache, and it reads a cache file only if that file records the same registry as its origin. A failure is an error. The returned Registry carries Origin, so WriteRegistryCache pins it as scoped to that registry.

func ParseRegistry added in v1.4.14

func ParseRegistry(body []byte) (*Registry, error)

ParseRegistry parses a registry response body (the object or the array plugin format) without touching the network, a cache or the fallback host. A body that does not parse is an error. Callers that must not fall back (the strict reads of `nself update`, P7-LIVE-10) use it instead of FetchRegistry.

func (Registry) MarshalJSON

func (r Registry) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler for Registry so the cache round-trips through the array format consistently.

type RegistryClient

type RegistryClient interface {
	Fetch(ctx context.Context) (*Registry, error)
	GetPlugin(ctx context.Context, name string) (*PluginManifest, error)
}

RegistryClient abstracts plugin registry HTTP operations for testability.

type StandardEndpoint added in v1.0.6

type StandardEndpoint struct {
	Path     string
	Required bool // if false, missing endpoint is a warning not an error
}

StandardEndpoint describes one of the standard plugin interface endpoints.

type SystemDependencies

type SystemDependencies struct {
	Required    []SystemDependency `json:"required,omitempty"`
	Recommended []SystemDependency `json:"recommended,omitempty"`
}

SystemDependencies groups required and recommended system deps.

func (*SystemDependencies) UnmarshalJSON added in v1.3.0

func (s *SystemDependencies) UnmarshalJSON(data []byte) error

UnmarshalJSON lets SystemDependencies accept the empty-array form.

Four shipped manifests write `"systemDependencies": []` where this package expects an object. An empty array means "none", which is the zero value.

type SystemDependency

type SystemDependency struct {
	Name          string `json:"name"`
	Verify        string `json:"verify"`
	MinVersion    string `json:"minVersion,omitempty"`
	Apt           string `json:"apt,omitempty"`
	Brew          string `json:"brew,omitempty"`
	CustomInstall string `json:"custom_install,omitempty"`
}

SystemDependency describes a system-level dependency for a plugin.

type TieredPluginInfo added in v1.3.6

type TieredPluginInfo struct {
	Name      string
	Tier      string
	Version   string
	Category  string
	TierPair  bool
	IsDefault bool // true if ResolvePlugin(ctx, reg, Name, "", entitled) picks this exact entry
}

TieredPluginInfo describes one registry entry as shown by `nself plugin list --available`.

func ListAvailableWithTiers added in v1.3.6

func ListAvailableWithTiers(ctx context.Context, entitled EntitlementFunc) ([]TieredPluginInfo, error)

ListAvailableWithTiers fetches the registry and returns every entry, grouped by case-insensitive slug in first-seen order, with IsDefault set on whichever entry a tier-override-free install would currently resolve to.

type WebhookNames added in v1.3.0

type WebhookNames []string

WebhookNames is the list of webhook event names a plugin declares.

Purpose: accept both shapes real plugin.json files use. The field was typed `[]string`, but manifests in the wild use an object as often as an array — `{"progress.updated": "Update playback position"}` alongside `["acquisition.subscribed"]`. Unmarshalling an object into []string fails, and ListInstalled skips any plugin whose manifest fails to parse, so those plugins vanished silently from `nself plugin list`, `nself costs`, and everything else that enumerates installed plugins. Nothing reported an error; the plugin was simply absent.

Inputs: the `webhooks` value from plugin.json, in either shape.

Outputs: the event names. For the object form the keys are the event names and the values are human descriptions, so the keys are what this carries.

Constraints: unmarshalling must never fail on a shape a real manifest uses — a manifest this code cannot read makes a plugin invisible rather than loud.

func (WebhookNames) MarshalJSON added in v1.3.0

func (w WebhookNames) MarshalJSON() ([]byte, error)

MarshalJSON always writes the array form, which is the documented shape.

func (*WebhookNames) UnmarshalJSON added in v1.3.0

func (w *WebhookNames) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts an array of names, an object keyed by name, or null.

Directories

Path Synopsis
Package catalog is the CLI side of contract:plugins.catalog v1 (P7-PLUG-09): the one JSON document at https://plugins.nself.org/catalog.json that lists every plugin of both tiers, its commands and its bundles.
Package catalog is the CLI side of contract:plugins.catalog v1 (P7-PLUG-09): the one JSON document at https://plugins.nself.org/catalog.json that lists every plugin of both tiers, its commands and its bundles.
Package count is the CLI's single source for "how many plugins are there".
Package count is the CLI's single source for "how many plugins are there".
Package lifecycle provides the dormant-plugin state machine for the nSelf plugin license lifecycle.
Package lifecycle provides the dormant-plugin state machine for the nSelf plugin license lifecycle.
Package manifestv2 owns the plugin manifest v2 contract (contract:plugin.manifest).
Package manifestv2 owns the plugin manifest v2 contract (contract:plugin.manifest).
Package mount discovers the CLI command surface an installed plugin declares (contract:cli.plugin-command-mount v1).
Package mount discovers the CLI command surface an installed plugin declares (contract:cli.plugin-command-mount v1).
Package readiness verifies that a plugin has applied its own boot-time migrations (contract:plugin.boot-migrations v1, ADR 0010).
Package readiness verifies that a plugin has applied its own boot-time migrations (contract:plugin.boot-migrations v1, ADR 0010).
Package requires checks a plugin's requires.postgres_extensions against the project's Postgres before the plugin is downloaded (P7-ADOPT-06).
Package requires checks a plugin's requires.postgres_extensions against the project's Postgres before the plugin is downloaded (P7-ADOPT-06).
Package scaffold provides the canonical plugin scaffolding logic shared between the nself CLI (plugin new command) and the standalone new-plugin binary in plugin-sdk-go/devkit.
Package scaffold provides the canonical plugin scaffolding logic shared between the nself CLI (plugin new command) and the standalone new-plugin binary in plugin-sdk-go/devkit.
signature/signaturetest
Package signaturetest gives tests a throwaway release key and swaps the compiled-in key table and floor, restoring both on t.Cleanup.
Package signaturetest gives tests a throwaway release key and swaps the compiled-in key table and floor, restoring both on t.Cleanup.

Jump to

Keyboard shortcuts

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