examples/plugin/simple/README.md
This is the full mixed-capability skeleton. For single-capability examples, see ../README.md.
This directory is the reference skeleton for the current standard dynamic library plugin ABI. The ABI is language-neutral: the host loads a native dynamic library, calls cliproxy_plugin_init, and then exchanges JSON envelopes through a stable C function table.
This directory contains complete Go, C, and Rust implementations of the same mixed-capability sample. The Go sample uses -buildmode=c-shared; the C sample uses CMake; the Rust sample uses a cdylib crate.
Every plugin must export:
int cliproxy_plugin_init(const cliproxy_host_api* host, cliproxy_plugin_api* plugin);
The plugin fills cliproxy_plugin_api with:
int call(char* method, uint8_t* request, size_t request_len, cliproxy_buffer* response);
void free_buffer(void* ptr, size_t len);
void shutdown(void);
The host provides cliproxy_host_api with:
int call(void* host_ctx, char* method, uint8_t* request, size_t request_len, cliproxy_buffer* response);
void free_buffer(void* ptr, size_t len);
The C ABI never passes Go interfaces, Go slices, Go maps, Go channels, context.Context, or Go errors.
Successful responses use:
{
"ok": true,
"result": {}
}
Errors use:
{
"ok": false,
"error": {
"code": "invalid_request",
"message": "request is invalid"
}
}
Raw byte fields are encoded as base64 by JSON.
plugin.register and plugin.reconfigure return metadata and capability flags. This sample declares the full provider-native surface:
Executor plugins must declare executor_input_formats and executor_output_formats in their capability block. The host passes requests through directly when the client protocol is declared by the executor. Otherwise, the host translates the inbound request into one declared input format and translates the executor response back to the client protocol. This example declares chat-completions for both lists, so non-chat-completions protocols are translated by the host. The host also accepts the existing internal aliases openai, openai-response, and claude for Chat Completions, Responses, and Anthropic protocols.
The host keeps the existing precedence rules: native logic wins, plugins fill gaps, and higher-priority plugins run before lower-priority plugins.
go/: full mixed-capability Go implementation.c/: full mixed-capability C implementation with no external dependencies.rust/: full mixed-capability Rust implementation with no external dependencies.All three implementations parse incoming JSON requests for the methods where request content matters. Auth methods persist the raw request payload as StorageJSON; request and response transforms echo the inbound Body; Thinking decodes Body and appends plugin_example_thinking; executor methods use request fields such as Model, Format, and Payload; Usage keeps an in-process count.
Build from the repository root.
Build all plugin examples, including all three simple variants:
make -C examples/plugin build
Artifacts are written to examples/plugin/bin as simple-go, simple-c, and simple-rust with the current platform dynamic-library extension.
Manual Go build on macOS:
mkdir -p plugins/darwin/$(go env GOARCH)
go build -buildmode=c-shared -o plugins/darwin/$(go env GOARCH)/simple-go.dylib ./examples/plugin/simple/go
rm -f plugins/darwin/$(go env GOARCH)/simple-go.h
Manual C build on macOS:
mkdir -p plugins/darwin/$(go env GOARCH)
cmake -S examples/plugin/simple/c -B /tmp/cliproxy-simple-c-build -DCMAKE_LIBRARY_OUTPUT_DIRECTORY=$PWD/plugins/darwin/$(go env GOARCH)
cmake --build /tmp/cliproxy-simple-c-build
Manual Rust build on macOS:
mkdir -p plugins/darwin/$(go env GOARCH)
cd examples/plugin/simple/rust
CARGO_TARGET_DIR=/tmp/cliproxy-simple-rust-target cargo build --release --locked
cp /tmp/cliproxy-simple-rust-target/release/libcliproxy_simple_rust.dylib ../../../../plugins/darwin/$(go env GOARCH)/simple-rust.dylib
For Linux, FreeBSD, or Windows, keep the same source directory and use the platform extension selected by examples/plugin/Makefile.
The plugin ID is the dynamic library basename without the platform extension. Makefile-built artifacts map to plugins.configs.simple-go, plugins.configs.simple-c, and plugins.configs.simple-rust.
The host searches:
plugins/<GOOS>/<GOARCH>-<variant>
plugins/<GOOS>/<GOARCH>
plugins
Accepted extensions are:
.so on Linux and FreeBSD.dylib on macOS.dll on WindowsPlugin IDs must match:
[A-Za-z0-9][A-Za-z0-9._-]{0,127}
Dynamic plugins are disabled by default.
plugins:
enabled: true
dir: "plugins"
configs:
simple-go:
enabled: true
priority: 1
config1: true
config2: "string"
config3: 3
mode: "safe"
plugins.configs.<pluginID> is passed to plugin.register or plugin.reconfigure as normalized YAML bytes inside the JSON request.
Plugins can call host functionality through host.call. The HTTP bridge method is:
host.http.do
The host still performs the real HTTP request, so proxy handling, transport policy, auth context, and request logging stay under host control.
The native plugin management endpoints remain:
GET /v0/management/plugins
PATCH /v0/management/plugins/{pluginID}/enabled
PUT /v0/management/plugins/{pluginID}/config
PATCH /v0/management/plugins/{pluginID}/config
Plugin-owned Management API routes are registered through management.register and handled through management.handle.
Standard dynamic library plugins are trusted in-process code. Panic recovery can protect host-managed calls, but it cannot prevent a plugin from exiting the process, corrupting memory, mutating global process state, or leaking secrets. Install only plugins you trust as much as the service binary.
Current platform sample builds:
make -C examples/plugin list
make -C examples/plugin build
find examples/plugin/bin -maxdepth 1 -type f | wc -l
make -C examples/plugin clean
After changing Go code in this repository, also run:
go build -o test-output ./cmd/server && rm test-output