molecule-core/docs/plugins/sources.md
Hongming Wang 24fec62d7f initial commit — Molecule AI platform
Forked clean from public hackathon repo (Starfire-AgentTeam, BSL 1.1)
with full rebrand to Molecule AI under github.com/Molecule-AI/molecule-monorepo.

Brand: Starfire → Molecule AI.
Slug: starfire / agent-molecule → molecule.
Env vars: STARFIRE_* → MOLECULE_*.
Go module: github.com/agent-molecule/platform → github.com/Molecule-AI/molecule-monorepo/platform.
Python packages: starfire_plugin → molecule_plugin, starfire_agent → molecule_agent.
DB: agentmolecule → molecule.

History truncated; see public repo for prior commits and contributor
attribution. Verified green: go test -race ./... (platform), pytest
(workspace-template 1129 + sdk 132), vitest (canvas 352), build (mcp).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:55:37 -07:00

6.9 KiB

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/<name>/
                     ▼
┌──────────────────────────────────────────────┐
│ 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:

{"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:

curl $PLATFORM/plugins/sources
{"schemes":["github","local"]}

Registering a new source

// platform/internal/router/router.go
plgh := handlers.NewPluginsHandler(pluginsDir, dockerCli, wh.RestartByID).
    WithSourceResolver(NewClawhubResolver(clawhubToken))

A SourceResolver must satisfy:

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://<name>
<name>              # 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://<owner>/<repo>
github://<owner>/<repo>#<ref>

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://<tarball-url> — direct HTTP tarball install. Planned.
  • clawhub://<name>@<version> — ClawHub registry install. Planned behind a third-party package dep on the ClawHub client.
  • oci://<registry>/<image>:<tag> — 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 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.