From aef44daccedbc445a3d575d6040f62f248e9a4c0 Mon Sep 17 00:00:00 2001 From: "molecule-ai[bot]" <276602405+molecule-ai[bot]@users.noreply.github.com> Date: Tue, 21 Apr 2026 08:02:29 +0000 Subject: [PATCH] docs: add docs/plugins/sources.md --- content/docs/plugins/sources.md | 167 ++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 content/docs/plugins/sources.md diff --git a/content/docs/plugins/sources.md b/content/docs/plugins/sources.md new file mode 100644 index 0000000..820e47b --- /dev/null +++ b/content/docs/plugins/sources.md @@ -0,0 +1,167 @@ +# 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.