Renames: - platform/ → workspace-server/ (Go module path stays as "platform" for external dep compat — will update after plugin module republish) - workspace-template/ → workspace/ Removed (moved to separate repos or deleted): - PLAN.md — internal roadmap (move to private project board) - HANDOFF.md, AGENTS.md — one-time internal session docs - .claude/ — gitignored entirely (local agent config) - infra/cloudflare-worker/ → Molecule-AI/molecule-tenant-proxy - org-templates/molecule-dev/ → standalone template repo - .mcp-eval/ → molecule-mcp-server repo - test-results/ — ephemeral, gitignored Security scrubbing: - Cloudflare account/zone/KV IDs → placeholders - Real EC2 IPs → <EC2_IP> in all docs - CF token prefix, Neon project ID, Fly app names → redacted - Langfuse dev credentials → parameterized - Personal runner username/machine name → generic Community files: - CONTRIBUTING.md — build, test, branch conventions - CODE_OF_CONDUCT.md — Contributor Covenant 2.1 All Dockerfiles, CI workflows, docker-compose, railway.toml, render.yaml, README, CLAUDE.md updated for new directory names. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
168 lines
6.9 KiB
Markdown
168 lines
6.9 KiB
Markdown
# 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:
|
|
|
|
```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://<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](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.
|