Files
overlord/plugins/docs/server-side-plugins.md

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.