14 KiB
Server-Side Plugins
Server-side plugins run inside the Overlord server process environment. They are for global plugin pages, persistent server data, RPC-backed tools, build pipeline customization, uploads, artifact replacement, signing, or internal release workflows.
They are trusted code. A server-side plugin has normal server filesystem and network access, so treat it like part of the program itself. Use plugin signing and only install code you trust.
Server-Only Plugins
Set runtime: "server" in config.json:
{
"name": "Internal Builder",
"version": "1.0.0",
"apiVersion": 2,
"runtime": "server",
"description": "Adds custom build buttons and upload hooks."
}
Server-only plugins:
| Behavior | Notes |
|---|---|
| Plugin Manager look | Shown as server extensions, with worker/build-plugin state where available. |
| Client loading | Never sent to agents. |
| Auto-load | Hidden and unavailable because there is no per-client agent module. |
| Build access | Can hook the builder when server.js or src/server.ts exports build handlers. |
Lifecycle
If a bundle includes server.js or compiled src/server.ts, the server starts one worker for the plugin when the plugin is enabled.
export default {
async setup(ctx) {
ctx.log.info("server plugin started");
},
async onEvent(ctx, clientId, event, payload) {
ctx.log.info(`${event} from ${clientId}: ${JSON.stringify(payload)}`);
},
rpc: {
async list(ctx, params) {
return ctx.db.query("select * from items").all();
},
},
async teardown(ctx) {
ctx.log.info("server plugin stopping");
},
};
onEvent receives events emitted by that plugin's client module.
The ctx Object
| Field | Description |
|---|---|
ctx.pluginId |
Sanitized plugin ID. |
ctx.db |
bun:sqlite database backed by plugins/<id>/data/plugin.db; WAL mode is enabled. |
ctx.dataDir |
Absolute path to the plugin's persistent data/ directory. |
ctx.log |
{ debug, info, warn, error } forwarded to the server logger as [plugin:<id>] .... |
ctx.broadcast(channel, data) |
Sends JSON-serializable data to every UI subscribed to this plugin's SSE stream. |
Persistent Data
Each plugin owns:
Overlord-Server/plugins/<id>/data/
The directory is created automatically and is not removed when a plugin is reinstalled or deleted. Use it for SQLite state, cached files, generated artifacts, helper binaries, or plugin-local configuration.
The server also exposes plugin data APIs for listing, reading, writing, deleting, and running files in that directory. Execution endpoints are restricted to admin/operator roles.
UI RPC And Broadcasts
Browser UI code can call plugin RPC methods:
const res = await fetch(`/api/plugins/my-plugin/rpc`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ method: "list", params: {} }),
});
const { ok, result, error } = await res.json();
Subscribe to server broadcasts:
const stream = new EventSource(`/api/plugins/my-plugin/stream`);
stream.addEventListener("changed", (event) => {
const payload = JSON.parse(event.data);
console.log(payload);
});
RPC calls require clients:control, time out after 30 seconds, and must target a method exported in the plugin's rpc map. SSE streams also require clients:control.
Trusted Requests
Server-side plugin code, including build hooks, can make trusted outbound requests with ctx.fetch(url, options). These requests run from the Overlord server process and are not subject to browser CORS.
Browser plugin UI is still normal browser JavaScript. If a plugin page needs to call an API that does not allow browser CORS, call the same-origin proxy instead:
const res = await fetch("/api/plugins/my-plugin/proxy", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
url: "https://example.internal/api/releases",
method: "POST",
headers: { "Content-Type": "application/json" },
body: { buildId: "abc123" },
}),
});
The proxy requires the signed-in user to have access to that plugin. It only proxies http: and https: URLs.
Build Hooks
Server-side plugins can hook the agent builder without shipping agent code. Browser UI is never called from the build process; only server.js or compiled src/server.ts participates.
export default {
onBuildPrepare(ctx, payload) {
return { config: { outputName: "custom-agent" } };
},
onBuildTarget(ctx, payload) {
if (payload.platform === "linux-amd64") {
return {
env: { CGO_ENABLED: "0" },
addTags: ["my_feature"],
ldflagsAppend: "-X overlord-client/cmd/agent/config.PluginChannel=stable",
message: "linux-amd64 template patched",
};
}
},
buildHooks: {
before_donut(ctx, payload) {
ctx.log.info(`Donut input: ${payload.file.filename}`);
},
after_donut(ctx, payload) {
ctx.log.info(`Donut output: ${payload.file.filename}`);
},
before_sgn(ctx, payload) {
ctx.log.info(`SGN iterations: ${payload.iterations}`);
},
after_sgn(ctx, payload) {
ctx.log.info(`SGN output: ${payload.file.filename}`);
},
},
async onBuildArtifact(ctx, payload) {
ctx.log.info(`artifact ready: ${payload.file.path}`);
return { message: `saw ${payload.file.filename}` };
},
async onBuildComplete(ctx, payload) {
ctx.log.info(`build ${payload.buildId} produced ${payload.files.length} files`);
},
onBuildFailed(ctx, payload) {
ctx.log.warn(`build failed: ${payload.error}`);
},
};
onBuildTarget can return:
| Field | Effect |
|---|---|
env |
Merges string values into the Go build environment. |
ldflags |
Replaces the full -ldflags string. |
ldflagsAppend |
Appends to the existing -ldflags string. |
tags |
Replaces the full build tag list. |
addTags / removeTags |
Adds or removes build tags. |
outputName |
Renames the target output file after server sanitization. |
skip: true |
Skips that target. |
message / messages |
Writes informational lines into the build stream. |
Transform Hook Stages
Implement transform hooks either in buildHooks with the snake-case name, or as a named method such as onBuildBeforeDonut.
| Hook | Named method | When it runs |
|---|---|---|
post_build |
onBuildPostBuild |
After the raw Go build output exists. |
before_upx / after_upx |
onBuildBeforeUpx / onBuildAfterUpx |
Around UPX compression. |
before_script_wrapper / after_script_wrapper |
onBuildBeforeScriptWrapper / onBuildAfterScriptWrapper |
Around .bat, .cmd, and .ps1 wrapping. |
before_ipa / after_ipa |
onBuildBeforeIpa / onBuildAfterIpa |
Around iOS IPA packaging. |
before_donut / after_donut |
onBuildBeforeDonut / onBuildAfterDonut |
Around Windows PE to shellcode conversion with Donut. |
before_linux_shellcode / after_linux_shellcode |
onBuildBeforeLinuxShellcode / onBuildAfterLinuxShellcode |
Around Linux ELF shellcode wrapping. |
before_sgn / after_sgn |
onBuildBeforeSgn / onBuildAfterSgn |
Around SGN shellcode encoding. |
before_sgn_txt / after_sgn_txt |
onBuildBeforeSgnTxt / onBuildAfterSgnTxt |
Around SGN TXT artifact conversion. |
Transform hook payloads include:
| Field | Description |
|---|---|
buildId |
Current build ID. |
platform, os, arch, targetKey |
Target identity. |
outDir, clientDir |
Build output and client source directories. |
config |
Full build config, including selected build plugins. |
file |
{ filename, path, platform, size } for the current artifact. |
Some stages include extra fields such as outputFilename, outputPath, donutArch, shellcodeArch, or iterations.
Artifact Replacement And Uploads
onBuildArtifact receives payload.file with:
{
name,
filename,
path,
platform,
version,
size,
}
Return file: { filename } to replace the downloadable build artifact with another file already inside dist-clients, or return files: [...] to add extra downloadable artifacts from dist-clients.
Paths outside the build output directory are ignored for downloadable metadata, but the hook can still upload, copy, sign, or publish files itself because server-side plugins run with normal server privileges.
Build Plugin Settings, Buttons, And Requirements
Declare builder UI in config.json:
{
"runtime": "server",
"build": {
"label": "Uploader",
"description": "Uploads completed builds to an internal bucket.",
"enabledByDefault": true,
"settings": [
{
"key": "bucket",
"label": "Bucket",
"type": "string",
"required": true,
"placeholder": "release-builds"
},
{
"key": "publish",
"label": "Publish after build",
"type": "boolean",
"default": false
}
],
"actions": [
{
"id": "shellcode-release",
"label": "Shellcode Release",
"icon": "fa-solid fa-fire",
"setBuild": { "useDonut": true, "useSgn": true },
"setSettings": { "publish": true }
}
],
"requires": [
{
"field": "useSgn",
"truthy": true,
"message": "Uploader release mode requires SGN to be enabled."
}
]
}
}
Supported setting types are string, textarea, number, boolean, and select.
Build profiles save these values and send them to hooks as:
payload.config.buildPlugins[ctx.pluginId] // { enabled, settings }
Buttons in build.actions can set core builder fields through setBuild and plugin fields through setSettings. Requirements can check:
| Requirement field | Meaning |
|---|---|
field |
Core build field such as useSgn or useDonut. |
pluginSetting |
Setting from this plugin's build settings. |
platforms |
Selected build platforms. |
See plugins/sample-build-hooks for a minimal server-only build plugin with its own build button and artifact replacement.
Custom Build Providers
A server-only plugin can replace the built-in Go compilation step while keeping the normal Build page, permissions, audit log, progress stream, artifact hooks, file-share upload, build history, and expiration handling. Declare the provider and export onBuild:
{
"runtime": "server",
"build": {
"provider": {
"label": "My Lite Agent",
"description": "Builds the reduced agent implementation.",
"icon": "fa-solid fa-feather",
"platforms": ["windows-amd64", "linux-amd64"]
}
}
}
export default {
async onBuild(ctx, payload) {
ctx.build.status("Compiling lite agent...");
ctx.build.output("Compiler started", "info");
// Write only inside payload.outputDir.
const filename = `lite-${payload.platforms[0]}`;
await compileAgent({ output: `${payload.outputDir}/${filename}` });
return {
artifacts: [{
name: "Lite Agent",
filename,
platform: payload.platforms[0],
version: "1.0.0"
}]
};
}
};
The provider payload contains buildId, providerId, runtimeRoot, a temporary outputDir, selected platforms, serverUrl, agentToken, signed buildTag, tlsSpkiPins, initialClientTag, outputName, provider settings, and the sanitized full build config.
An artifact entry must contain a selected platform and either filename or a relative path beneath outputDir. Optional fields are name and version. Provider output is limited to 32 regular, non-symlink files of at most 512 MB each. The server validates containment, copies approved files into dist-clients, adds the build ID suffix, then invokes normal artifact and completion hooks.
ctx.build.output(text, level) accepts debug, info, success, warn, or error. ctx.build.status(text, progress?, etaSeconds?) changes the Build page status line and can display a progress bar and estimated time remaining. Progress is a number from 0 through 100. These methods exist only during onBuild.
The selected provider must be enabled, include server code, support every requested target, and be accessible to the building user. A provider plugin is forced on for its own build, even if a modified request marks its hook settings as disabled.
See plugins/rust-lite-builder for a complete provider that invokes Cargo and embeds authenticated connection defaults into Overlord Lite.
RBAC And Build Plugin Access
Build-hook plugins use the normal per-user plugin access policy. Admins can open a user in the Users page, choose Plugin Access, and set one of these modes:
| Mode | Effect |
|---|---|
| No Access | The user cannot open plugin pages or use build plugins. |
| Selected Plugins | The user can only open and use checked plugins. |
| All Plugins | The user can use every enabled plugin. |
For build plugins this means:
| Path | Enforcement |
|---|---|
| Build page plugin list | /api/build/plugins only returns build plugins the user can access. |
| Build start | Inaccessible build plugin settings are ignored server-side, so a user cannot invoke a hidden build hook by editing the request. |
| Plugin RPC/UI/assets | Plugin pages, assets, RPC, and SSE streams require access to that plugin. |
Users still need clients:build to build at all. Plugin access is the second gate that decides which build-hook plugins they can see and use.
Caveats
| Caveat | Detail |
|---|---|
| Worker trust | Workers are not a security sandbox. They can access server files/network with normal server permissions. |
| One worker per plugin | RPC calls into the same plugin serialize; long calls block other RPCs for that plugin. |
| SQLite | Each plugin gets a separate DB. Avoid opening the same DB from another process. |
| Worker crash | Crashes are logged and pluginState.lastError is set; disable and re-enable the plugin to restart it. |
| Bundled builds | The production build emits both the main server bundle and src/server/plugin-runtime/worker-host.ts. |