Plugin Contract
This document is normative for GUM plugin authoring and install-time validation. It is the plugin-authoring entry point; docs/catalog-abi.md owns the runtime catalog state model, and spec.md §13 owns MCP resource wire behavior.
Companion Contracts
Plugin authors must also follow these supporting contracts:
spec.md§8 for manifest field semantics, host services by shape, canary grammar and re-ingestion, crash/quarantine behavior, atomic registry updates, and install/restart semantics.spec.md§13 for MCP inventory resource-template wire behavior.docs/expression-profile-dsl.mdanddocs/expression-profile-dsl.jsonfor output-profile syntax, validation, tests, and MCP-root-based project-local lookup.docs/catalog-abi.mdfor stable IDs,backend_kind, capability atoms, variant lifecycle, andnull_elision_safe_fields.docs/test-matrix.mdfor required proof artifacts.
The author-facing walkthrough — manifest field-by-field, wire ABI with worked JSON-RPC exchanges, packaging layouts, install workflow, and an end-to-end hello plugin — is docs/plugin-author-guide.md. Read this contract for the *what is required*; read the author guide for *how to ship one*.
Plugin Shapes
| Shape | Status | Expansion role |
|---|---|---|
| Shape 1: MCP subprocess | Supported in v2.0.0 | Accepts existing FastMCP/Python plugins. The plugin owns HTTP/TLS/cookies/retry/rate-limit internals and is fully trusted as user-level code. Not policy-complete. |
| Shape 2: gRPC subprocess | Future authoring model; no target release | Host provides HTTP client, cookie jar, retry, rate limiter, credential access, cache, logging, and optional headless browser. This is the future preferred unofficial expansion substrate once the public SDK/proto is frozen. |
In v2.0.0, Shape 1 is the only supported external authoring path. Shape 1 plugins are allowed only when their manifest declares the infrastructure they own, ships schemas/output profiles/canaries, and accepts the trust warning. Shape 2 is the long-term unofficial expansion substrate, but it is not an authoring contract until a release freezes the public plugin SDK and proto.
In v2.0.0, gum plugin install rejects third-party Shape 2 manifests with PLUGIN_SHAPE_UNSUPPORTED. This includes [plugin].shape = "grpc-subprocess" and any [[tools]] record with backend_kind = "grpc-plugin". The Shape 2 gate runs immediately after structural manifest parsing has identified shape and backend_kind, wins over PLUGIN_BINDING_INVALID, and happens before schema copy, executable staging, canary execution, or registry writes. Shape 2 binding examples in the docs are ABI fixtures only, not an external authoring surface for v2.0.0.
Shape 2 notes (future authoring contract)
Shape 2 plugins are Go subprocesses that communicate with the host via GUM's plugin gRPC interface. Until a release publishes a public importable SDK/proto package, plugin authors MUST NOT depend on GUM internal/... packages or treat any local development path as stable. A canonical go.work stub and public module path will be published when the Shape 2 interface is frozen.
Manifest ABI
Every plugin manifest MUST declare:
- top-level
manifest_schema_version(sibling of[plugin], not inside it) [plugin]name, version, description,namespace_owner, shape, command, license, ToS status, and risk.commandis an install-time selector; non-dev runtime execution always uses the normalized executable binding recorded in the selected profile'splugins.lock.[package]source, ref, checksum
Command normalization (normative). Install resolves command once and records the result as argv_normalized in the selected profile's plugins.lock, alongside executable_path, executable_sha256, and install_root. The spawn path launches that argv and never re-reads the selector. For every source except pypi, resolution is exact: command[0] MUST name the manifest's executable (./bin/mcp and bin/mcp both match bin/mcp), and the residual tokens become argv_normalized[1:]. A PATH-only token, a shell interpreter, an absolute path, a traversal, and a wrapper script outside the declared executable each fail with PLUGIN_EXECUTABLE_UNTRUSTED before any file is copied. An omitted command means the executable takes no arguments. Dev profiles accept an unresolvable command[0] and still bind the declared executable. For source = "pypi", install drops an optional leading uvx or pipx selector token, reads the next token as the console-script name, requires executable = "venv/bin/<script>", and records argv_normalized = [<install_root>/venv/bin/<script>] + rest; the manifest example command = ["uvx", "fli", "mcp"] becomes <install_root>/venv/bin/fli mcp and uvx is never spawned. The dev escape hatch does not apply to pypi.
Package sources (normative). [package].source selects the resolver: local (the default when [package] is omitted), bundled, github_release, git, or pypi. checksum is sha256: plus 64 hex digits. Manifest load validates the block: pypi requires ref = "<name>==<exact version>" and a checksum; github_release requires an https URL ending in .tar.gz, .tgz, or .zip and a checksum; git takes an https or file URL, optionally suffixed @<40-hex commit>. Install materializes the declared source into the install root, then copies the manifest tree over it, so curated manifest files and schemas always win over fetched artifact contents. pypi selects the index artifact whose digest equals the declared checksum, re-hashes the download, builds venv/ with the host python3, and runs pip install --no-index --no-deps on the verified artifact; no network resolver ever runs. github_release re-hashes the download (PLUGIN_ARTIFACT_CHECKSUM_MISMATCH on any disagreement, nothing unpacked) and unpacks with pinned modes: directories 0755, the declared executable 0755, every other file 0644; non-regular entries (symlinks, links, devices) and absolute or traversal paths are refused. git clones, checks out the pinned commit, verifies HEAD equals the pin, and strips .git; an unpinned ref fails non-dev installs with PLUGIN_PACKAGE_SOURCE_UNTRUSTED. After the copy the declared executable MUST be a regular file inside the install root, or install fails with PLUGIN_EXECUTABLE_UNTRUSTED before any hash or registry write. plugins.lock rows record source, ref, and checksum; local and unpinned-git installs carry risk = "dev-untrusted".
[requirements]rate policy, cache TTL, canary, credential/env needs (see needs_user_creds denylist and credential descriptors below)- one or more
[[tools]]records withop_id,variant_id, null_elision_safe_fieldswhen the referencedoutput_profileusesstrip_nulls=true
backend_kind, interface_kind, risk_class, capabilities, scopes, schema_ref, output_profile, optional confirmation_policy, and the binding-kind selector fields required by docs/catalog-abi.md (tool_name for mcp-plugin; rpc_service and rpc_method for bundled grpc-plugin ABI fixtures and future Shape 2 manifests). The manifest supplies selector inputs; build/install materializes the resolved variant's nested binding object.
Unsupported manifest_schema_version, missing manifest_schema_version on a third-party manifest, or manifest_schema_version placed inside [plugin] fails before subprocess start with PLUGIN_MANIFEST_SCHEMA_UNSUPPORTED.
Missing or malformed plugin binding selector fields fail before subprocess start with PLUGIN_BINDING_INVALID. For Shape 1 MCP plugins, tool_name is required. For bundled ABI fixtures and future Shape 2 manifests, backend_kind = "grpc-plugin" requires rpc_service and rpc_method. Third-party v2.0.0 manifests that declare Shape 2 are rejected earlier with PLUGIN_SHAPE_UNSUPPORTED, regardless of selector completeness.
confirmation_policy is optional and defaults to none. The only non-default value is high_stakes_write, valid only for risk_class = "write" tools. It makes gum.write and gum call --risk=write require user confirmation before dispatch while preserving MCP destructiveHint=false.
needs_user_creds denylist (normative). The [requirements].needs_user_creds field lists environment variable names that the host MUST pass through to the plugin subprocess from the user's environment. To prevent plugin authors from siphoning GUM's own configuration, credentials, or operational state into a plugin's address space:
- Variable names matching the case-sensitive prefix
GUM_are PROHIBITED inneeds_user_creds. Listing one fails build/install withPLUGIN_ENV_PROHIBITED: needs_user_creds entry '<name>' on plugin '<plugin>' is a prohibited env var name.(single canonical message form, shared with spec.md §8.1; applies to both theGUM_prefix rule and the exact-name denylist). - The denylist is enforced from a single curated in-binary source of truth shared by
cmd/gen-catalog,gum plugin install, and runtime env scrubbing. It may be embedded viago:embedor compiled as a constant slice, but behavior must be identical in all three paths. The list contains, at minimum: theGUM_prefix rule, exact namesGOOGLE_APPLICATION_CREDENTIALS(use catalog-managed ADC instead),OPENAI_API_KEY,ANTHROPIC_API_KEY, and any env var beginning with_GUM. Future additions require a normative spec patch. - The validation runs at both build time (catalog-bundled plugins) and install time (runtime
gum plugin install). Bypassing it via runtime env injection by the host is PROHIBITED; the dispatch layer MUST scrub the plugin subprocess environment of any denylisted variable regardless of manifest declarations. TestPluginEnvProhibited(ininternal/plugin/manifest_test.go) MUST verify rejection on a fixture manifest that listsGUM_PROFILEinneeds_user_creds, assertingPLUGIN_ENV_PROHIBITEDis returned and the subprocess is never started.
Credential descriptors (normative). needs_user_creds is a raw env allowlist for process launch; it is not safe UX copy. Any manifest with non-empty needs_user_creds MUST also declare [requirements].credential_descriptors, one descriptor per env var, with fields alias, env, kind, display_name, and setup_hint as specified in spec.md §8.2. Inventory resources, AUTH_REQUIRED messages, and setup prompts use aliases/display names/hints only. Missing, duplicate, or extra descriptor entries fail build/install with PLUGIN_CREDENTIAL_DESCRIPTOR_INVALID.
Plugins that require product setup beyond a secret value (for example a Google Ads developer token, customer ID, manager login customer ID, billing-enabled account, or user-owned OAuth client) MUST also declare auth prerequisites using the auth_strategy / auth_components[] taxonomy from spec.md §7. Secret components are collected by gum plugin setup <name> and stored in the OS keychain. External components are displayed as checklist items that GUM cannot complete, one line per component, before setup prompts for any secret. The field is [requirements].auth_components; each entry carries kind, optional, secret, external, and setup_hint. A kind outside the spec.md §7 closed enum, and an empty kind, fail build/install with AUTH_COMPONENT_UNKNOWN; x- prefixed kinds are informational and accepted. A plugin like Google Keyword Planner is therefore compound, not ordinary OAuth: setup must collect/store the token-like fields and explicitly tell the user which Ads account/billing/access-level prerequisites remain outside GUM. When a compound plugin needs the user's Google access token, it uses the reserved google_access_token component and receives only the short-lived access token documented in spec.md §7; the host never forwards refresh tokens, service-account material, ADC files, or unrelated GUM credentials. plugin_managed means the plugin owns its upstream auth stack and does not receive GUM's official Google OAuth flow. Setup canaries for compound plugins MUST validate that the selected credential subject can actually access the declared account identifiers; storing syntactically valid secrets is not enough to clear needs_configuration.
Forwarded-token strategy gate (normative). advertised_tools[].auth_strategy is an optional manifest field on each tool record and takes the same closed §7 strategy enum as a catalog variant; an unrecognised value fails install as an invalid manifest. A plugin subprocess gets one environment block, so the host cannot scope GOOGLE_ACCESS_TOKEN to one tool of several. A manifest that declares google_access_token in needs_user_creds therefore MUST advertise at least one tool and MUST set auth_strategy = "compound" on every advertised tool. Mixing strategies, omitting the field, or advertising no tools fails install with PLUGIN_ENV_PROHIBITED. Ship a plugin that needs both a compound tool and a non-compound one as two plugins.
The host resolves the forwarded token from the profile's already-granted scopes, so starting a plugin never opens a consent window. When the profile has granted no Google scopes the spawn fails with AUTH_REQUIRED naming gum login. When there is no active session the host leaves GOOGLE_ACCESS_TOKEN unset: it does not fall back to a gum plugin setup secret stored under that name, and it does not pass through an ambient GOOGLE_ACCESS_TOKEN from the parent environment, even if env_allow lists it. A plugin cannot tell a stale string from the host's live token, so either fallback would be spent upstream and surface as an opaque 401. Each spawn that does forward a token appends one plugin_token_forwarded entry to the profile audit log with the plugin id, the credential subject fingerprint, and the forwarded scopes.
If an output profile strips null or empty values, the manifest's tool record must declare the exact dot paths where that elision is safe, for example null_elision_safe_fields = ["price.currency", "segments[].aircraft"]. Use "*" only for curator-reviewed whole-response elision. Missing or insufficient declarations fail catalog build with PROFILE_STRIP_NULLS_UNSAFE: cmd/gen-catalog resolves every variant's output_profile against the built-in profile set and runs the check with that variant's null_elision_safe_fields. gum plugin install runs no profile validation in v0.1.0. A registry variant row becomes a dispatchable catalog op at process start, when the session merge described in spec.md §4.2 reads plugin-catalog.json, but the merged variant carries no output_profile. Shaping falls back to the default profile, so there is no profile binding to check at install time. A plugin that needs a named profile must wait for a release that binds one, and PROFILE_STRIP_NULLS_UNSAFE stays a build-time gate over the generated catalog.
Schema Refs
schema_ref resolves to schemas/<schema_ref>.json inside the plugin artifact or bundled plugin directory. The document is a JSON Schema 2020-12 bundle and MUST contain object-valued $defs.request and $defs.response. Build/install derives the resolved binding refs as request_ref = "<schema_ref>.request" and response_ref = "<schema_ref>.response"; plugin manifests do not declare these refs directly. The ref strings MUST match the safe served-ref grammar in spec.md §8.2 before any path is constructed; path separators, traversal markers, URI-encoded separators, and control characters fail with PLUGIN_SCHEMA_REF_INVALID. Runtime-installed third-party request/response schemas are copied into the active profile's plugin schema store as plugin-schemas/<request_ref>.<sha256>.json and plugin-schemas/<response_ref>.<sha256>.json; the corresponding schema_hashes are recorded in plugin-catalog.json.
Missing or invalid refs, or bundles missing $defs.request or $defs.response, fail with PLUGIN_SCHEMA_REF_INVALID. A plugin whose schema ref collides with any schema ref already present in the selected profile's full inventory (embedded catalog plus active, pending-restart, needs-configuration, and quarantined plugin schemas) with a different JCS-canonical schema digest fails install with SCHEMA_REF_COLLISION; identical-body reuse is allowed.
Runtime Registry
Installed plugin variants are recorded in ~/.local/share/gum/<profile>/plugin-catalog.json, a versioned JSON object:
{
"plugin_catalog_schema_version": 1,
"updated_at": "2026-05-19T00:00:00Z",
"variants": []
}Updates use the full-state install transaction in spec.md: plugin-catalog.json, plugins.lock, and plugin-state.json are staged and published together under one profile-scoped generation. Runtime visibility, activation timestamps, inventory-vs-active snapshot reads, and quarantine precedence are owned by the runtime catalog state model in docs/catalog-abi.md; this document does not restate that state machine.
Deterministic Discovery
MCP clients enumerate plugins via:
gum://pluginsgum://plugin/{name}
CLI users use gum plugin list. Search may surface plugin operations, but search phrasing is not the inventory contract.
Plugin inventory status is a closed enum: active, installed_pending_restart, needs_configuration, or quarantined. In v2.0.0, a plugin installed while an MCP server is already running is inventory-only in that session with status installed_pending_restart; its operations are not searchable, operation-completable, describable as active, usable from code mode, or invokable until the MCP server restarts and marks it activated. A credentialed plugin installed without required credentials is needs_configuration: install validation succeeded, live canary was skipped, and the user must provide the declared credentials and run gum canary --plugin=<name> --live before activation. The restart affects operation reachability through the existing Tier A/meta-tool surface only; plugin variants never add individual MCP tools in v2.0.0. Standalone CLI commands see install-valid configured plugins on their next process start. If a plugin is both quarantined and another inactive state, quarantined wins in every MCP and CLI surface.
gum://plugin/{name} metadata is assembled from the selected profile's plugin-catalog.json plus plugin-state.json; the same profile's plugins.lock is consulted for package source/ref/checksum fields and the runtime executable binding (executable_path, executable_sha256, argv_normalized, install_root). Lock lookups are keyed by (profile, plugin_name) and MUST NOT cross profile boundaries. If those sources disagree, runtime status from plugin-state.json wins for quarantine/retry state, variant records from plugin-catalog.json win for dispatch metadata, and lockfile package fields are surfaced with metadata_warning: "lock_catalog_mismatch". The resource shape is fixed in spec.md §13 and includes safe credential descriptors for needs_configuration; raw env var names must never appear in this resource. Users configure missing plugin credentials through gum plugin setup <name>, which prompts using descriptor display names/setup hints, stores secrets in the OS keychain for the active profile, and runs a live canary before clearing needs_configuration.
For non-dev profiles, the launched executable must be inside the host-managed install root derived from the verified artifact. Runtime PATH-only lookup, shell wrappers, and fresh uvx/package-manager resolution are prohibited. GUM re-verifies executable_sha256 before each spawn; mismatch quarantines the plugin and refuses execution. Source-specific normalization is fixed: PyPI commands resolve to a console script inside the profile-scoped virtualenv; GitHub release and Git sources must declare one executable artifact path inside the unpacked install root; local paths are dev-only.
Reserved Namespaces
Plugins may not claim Google-owned prefixes (gmail, drive, calendar, etc.) unless they are bundled first-party plugins reviewed with the host. The check runs at build time and install time. A conflict fails with PLUGIN_NAMESPACE_CONFLICT.
Third-party plugins must also declare namespace_owner in the manifest. The owner is a reverse-DNS or package-registry publisher identity displayed at install time and recorded in the selected profile's plugins.lock. A non-dev profile rejects any plugin whose op_id prefix is already owned by a different namespace_owner; local development may bypass only with --dev-allow-namespace-conflict.
Canaries
Every plugin declares a canary. Relative date specifiers are preferred. Live canary failures soft-quarantine the plugin rather than blocking the registry write, but failed-canary plugins are not searchable, invokable, or auto-started until a later canary pass or explicit trusted unquarantine. Missing required user credentials skip the live canary and record needs_configuration instead of quarantine; after credentials are present, gum canary --plugin=<name> --live is the only path that clears that state.
Use gum canary --plugin=<name> --live --canary-args='key=value ...' to rerun or override canary parameters.
Output Profiles and Tests
Each plugin tool must declare output_profile unless the variant explicitly declares raw_result_allowed=true with a token-budget exception. Plugin profile files are validated with gum profile validate; fixture-backed profiles are tested with gum profile test. Lossy plugin profiles must keep recovery enabled and must declare null_elision_safe_fields when strip_nulls=true.
Trust Posture
Shape 1 install is equivalent to running arbitrary user-level code. GUM verifies package and executable checksums, displays ToS/risk, narrows env vars, validates schemas/output, and enforces declared network=false plus fs_write_dir with the OS sandbox backend on supported macOS/Linux hosts. That confinement is a defense-in-depth boundary, not a multi-tenant sandbox or a substitute for trusting the installed package. Unsupported sandbox platforms fail closed for enforced plugin execution. Shape 1 plugins do not implicitly inherit GUM's official Google OAuth credential; the only host-mediated Google-token path is the explicit compound google_access_token forwarding rule in spec.md §7, and that rule forwards a short-lived access token only. plugin_managed plugins own their own auth. Shape 2 narrows ambient authority through host services, but still is not a multi-tenant sandbox.
