# Plugin install sources > **TL;DR** — plugin **sources** (where a plugin comes from) and plugin > **shapes** (what's inside it) are independent axes. Both are pluggable. > Today we ship two sources (`local`, `github`) and one shape adapter > (`AgentskillsAdaptor`). Both layers scale the same way: write one new > class, register it, done. ## The two axes ``` ┌──────────────────────────────────────────────┐ │ SOURCE — where we fetch the plugin from │ │ │ │ local://my-plugin │ │ github://owner/repo#v1.0 │ │ clawhub://name@1.2 │ │ https://example.com/plugin.tgz │ │ │ │ registered via plugins.Registry │ └──────────────────────────────────────────────┘ │ │ 1. resolver.Fetch() → platform-local staging dir │ 2. tar + copy → workspace container /configs/plugins// ▼ ┌──────────────────────────────────────────────┐ │ SHAPE — what the plugin's files mean │ │ │ │ agentskills.io format (SKILL.md + scripts) │ │ MCP server │ │ DeepAgents sub-agent │ │ LangGraph sub-graph │ │ │ │ registered via plugins_registry resolver │ │ inside the workspace runtime │ └──────────────────────────────────────────────┘ ``` Neither layer mandates the other. A plugin installed from `github://…` might be agentskills-format. A plugin installed from `local://…` might be an MCP server. A plugin installed from `clawhub://…` in the future might be whatever shape ClawHub packs happen to be. ## Source API `POST /workspaces/:id/plugins` takes a single `source` field: ```json {"source": "local://my-plugin"} // platform registry {"source": "github://org/repo"} // GitHub default branch (public repos only) {"source": "github://org/repo#v1.2.0"} // pinned tag/branch/sha // Future: clawhub://, https://, oci:// — not registered by default. // Call GET /plugins/sources to see what's actually wired. ``` `GET /plugins/sources` lists the currently registered schemes: ```bash curl $PLATFORM/plugins/sources {"schemes":["github","local"]} ``` ## Registering a new source ```go // workspace-server/internal/router/router.go plgh := handlers.NewPluginsHandler(pluginsDir, dockerCli, wh.RestartByID). WithSourceResolver(NewClawhubResolver(clawhubToken)) ``` A `SourceResolver` must satisfy: ```go type SourceResolver interface { Scheme() string // unique scheme name Fetch(ctx context.Context, spec, dst string) (string, error) // copy into dst, return plugin name } ``` Implementations must honour `ctx` cancellation, clean up temp state on error, and validate their spec format before hitting the network. ## Built-in sources ### `local` — filesystem ``` local:// # bare name, same as above ``` Reads from the directory configured as `pluginsDir` at startup (defaults to the repo's `plugins/` directory). Name must match `^[a-z0-9][a-z0-9._-]*$`; path-traversal attempts are rejected pre-fetch. ### `github` — GitHub repository ``` github:/// github:///# ``` **Public repositories only.** The resolver performs an anonymous shallow clone via the system `git` binary (the platform Dockerfile installs `git`); it does not authenticate. Private-repo support is deliberately out of scope for now — doing it safely requires per-tenant credential storage, scope-limited tokens, and an audit trail, none of which have been designed yet. Until that lands, private-repo installs fail at clone time with a 404 (mapped from git's "repository not found" output). Owner + repo names are length-bounded; refs cannot start with `-` to prevent ref-as-flag injection. The resolver also passes `--` before the URL when invoking git, as belt-and-braces defense. ### Future resolvers (not yet implemented) - **`https://`** — direct HTTP tarball install. Planned. - **`clawhub://@`** — ClawHub registry install. Planned behind a third-party package dep on the ClawHub client. - **`oci:///:`** — OCI artifact install. Planned for enterprise registries. Adding a resolver is a single Go file — see "Registering a new source" above. The set of built-in resolvers is intentionally small; anything beyond `local` + `github` is extension territory. ## Security model **In scope (enforced):** - Every resolver validates its spec format before any network or filesystem operation (regex + length caps). - The handler re-validates the plugin name returned by the resolver before using it as a path component, so a hostile resolver can't smuggle a traversal name into `/configs/plugins/`. - Request body is size-capped (`PLUGIN_INSTALL_BODY_MAX_BYTES`, default 64 KiB) to bound JSON-parser work. - Fetch is timed out (`PLUGIN_INSTALL_FETCH_TIMEOUT`, default 5 min) so a slow/malicious source can't tie up a handler indefinitely. - Staged tree is size-capped (`PLUGIN_INSTALL_MAX_DIR_BYTES`, default 100 MiB) before copy into the container. - Concurrent registry writes protected by RWMutex. **Out of scope (not enforced):** - **Plugin file contents are trusted.** Installing a plugin is a code-execution grant for the workspace's runtime. Audit plugin sources as you would any dependency. - Network egress from resolvers isn't sandboxed (no netns/cgroup isolation around `git clone`). If you self-host Molecule AI in a multi-tenant or untrusted-tenant context, restrict egress at the network layer — an egress firewall, VPC NAT with allowlist, or equivalent. The platform itself does not isolate resolver traffic. - No signature or checksum verification on fetched content — planned alongside an OCI-based resolver where content addressability is native. - No per-workspace rate limit on installs (platform-global rate limit applies via the standard middleware). ## Shapes — the other axis See [agentskills-compat.md](agentskills-compat.md) for how the shape layer works. The two are wired together but independent: the source layer's job ends when plugin files are staged on disk; the shape layer (per-runtime adapter inside the workspace) decides what to do with them on workspace startup.