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>
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.