docs: add docs/plugins/sources.md
This commit is contained in:
parent
6aece0f6de
commit
aef44dacce
167
content/docs/plugins/sources.md
Normal file
167
content/docs/plugins/sources.md
Normal file
@ -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/<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.
|
||||
Loading…
Reference in New Issue
Block a user