Backplane Plugin Protocol
Standalone Agent Plugins 1.0.0 package inspection and Backplane distribution client. No Repo, Phoenix, application supervisor, process execution, filesystem installation, credential storage or network adapter is required.
{:ok, package} = Backplane.PluginProtocol.load(%{
"plugin.json" => JSON.encode!(%{
"$schema" => "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name" => "example"
})
})
{:ok, package} = Backplane.PluginProtocol.load_directory("/path/to/example")
{:ok, artifact} = Backplane.PluginProtocol.Archive.pack(package.files)
digest = Backplane.PluginProtocol.Archive.digest(artifact)
load/2 returns {:ok, package} or {:error, diagnostics}. Package fields:
manifest: original JSON object, including unknown fields and extensions. Unknown fields are reported and ignored for interpretation; original bytes stay infiles. Missingversionand arbitrary version strings are supported.skills: immediateskills/*/SKILL.mdentries withpath,root,name,description, rawcontent, Markdownbody, YAMLmetadata, and resourcefileskeyed by their full package-relative paths.mcp_servers: server-name map of validated raw string-key configurations. Types arestdio,streamable-http, and optional legacysse.diagnostics:%{severity: :error | :warning, code: atom, path: string, message: string}. Component errors can appear in a successful package.files: the complete original relative-path-to-binary snapshot.
Manifest errors reject the package, except unknown top-level fields and non-object
extensions. Unimplemented extension values are not validated. Invalid individual
skills or MCP servers are skipped. Invalid mcp.json disables only MCP. Schemas
are bundled as references; loading selects locally implemented rules and never
downloads a schema. transports: ["stdio", "streamable-http"] restricts discovery
to supported transports, with diagnostics for skipped transports.
MCP values remain unexpanded. Runtime hosts must expand only ${PLUGIN_ROOT} and
${PLUGIN_DATA}, once, in args/env values/cwd; provide both reserved variables;
recheck resolved command/cwd containment; enforce HTTP origin and header rules;
and handle connection failures per server. The loader validates reserved env
names, token commands, cwd containment, HTTPS/loopback URLs and HTTP headers.
Arguments and environment values remain opaque, including unknown placeholders.
Snapshots and safety
Archive.pack/2, unpack/2, digest/1, and read_directory/2 are shared by
publishers and installers. Archives use sorted deterministic USTAR in gzip,
fixed uid/gid/mtime, and regular files with 0644 default permissions.
pack(files, modes: %{relative_path => 0o755}) preserves executable resources.
unpack_with_metadata/2 and read_directory_with_metadata/2 return
{:ok, %{files: files, modes: modes}}, preserving low permission bits only;
privileged setuid/setgid/sticky bits are never retained. File maps alone carry
bytes, so publishers and installers should use the metadata APIs. Paths that cannot fit
USTAR name/prefix fields fail packing. Unpacking accepts USTAR regular files and
directories, ignores empty directories, and rejects links, special files, PAX/GNU
extensions, duplicates, invalid checksums, absolute paths, traversal, Windows
separators/drive prefixes, file/directory conflicts and trailing nonzero data.
Decoding happens in memory and never extracts filesystem paths.
Defaults: 32 MiB compressed, 80 MB decompressed tar, 64 MiB total file bytes,
16 MiB per file, 10,000 files, 20,000 entries and 64 filesystem levels. Override
with max_archive_bytes, max_uncompressed_bytes, max_total_bytes,
max_file_bytes, max_files, max_entries, max_depth. Strict Archive directory
readers reject all links and special files. PluginProtocol.load_directory/2
instead resolves safe links within the filesystem-resolved plugin root, retaining
their target bytes under the original package-relative alias paths. An escaping
manifest rejects the plugin; an escaping fixed component location disables that
component, an escaping skill document skips that skill, and other escaping
resources are omitted with diagnostics. Link and directory cycles are denied.
The file-map and archive snapshot APIs remain unchanged. Callers must keep the
directory stable during reads; these APIs are not an OS sandbox against concurrent
hostile filesystem mutation.
Distribution client
client = Backplane.PluginProtocol.Client.new(
base_url: "https://backplane.example",
auth: access_token,
transport: fn request -> MyHTTP.request(request) end
)
{:ok, catalog} = Backplane.PluginProtocol.Client.catalog(client)
{:ok, release} = Backplane.PluginProtocol.Client.resolve(client,
plugin_id: plugin_id, channel: "stable")
{:ok, download} = Backplane.PluginProtocol.Client.download(client,
release["artifact_digest"])
Transport accepts %{method: method, url: string, headers: pairs, body: nil | binary} and
returns {:ok, %{status: integer, body: binary | map}} | {:error, reason}. It must
enforce TLS, timeouts, response limits, cancellation and safe redirect policy.
auth also accepts a zero-arity header provider; headers supplies extra pairs.
Catalog and resolve return the decoded API object. Download checks the exact
SHA-256 before unpacking/loading, then returns %{artifact, artifact_digest, files, modes, package}. HTTP failures return {:error, {:http_status, status}} without
exposing raw provider bodies. Endpoints are /plugin-protocol/v1/catalog,
/resolve (GET query: plugin_id/release_id/channel), and /artifacts/:digest.
This is Backplane's API, not an official Agent Plugins registry protocol.
Client.request(client, method, path, body \\ nil) makes a JSON request under
the same prefix for installation listing/reporting. path starts with / and
cannot contain traversal, a query or an alternate origin. A supplied body is
JSON-encoded with content-type: application/json; responses are decoded objects.
Specification references, checked 2026-10-23: