# Custom Plugin Guide

## Custom Plugin Integration Guide

This guide covers how to register a custom executable as a managed plugin — a long-running component that the agent starts, monitors, and restarts as part of its own lifecycle. If you have not already read the [Overview](/keeperpam/endpoint-privilege-manager/integrations/overview.md) page, start there: it defines the plugin pattern, explains when it is the right choice over a job task, and describes the concepts this guide builds on.

If you are building a tool that runs on a schedule and exits — a scanner, reporter, or maintenance job — you want the [Custom Job Integration Guide](/keeperpam/endpoint-privilege-manager/integrations/custom-job-guide.md) instead. This guide is specifically for processes that stay running.

{% stepper %}
{% step %}

#### The Plugin JSON File

**Location and Naming**

A plugin is registered by placing a JSON file at:

```
{AgentRoot}/Plugins/{PluginId}.json
```

The filename without `.json` must match the `id` field inside the file. A file named `MyBridge.json` must contain `"id": "MyBridge"`. The plugin ID is also used as the key when calling Plugin Settings — `GET /api/PluginSettings/MyBridge` — so choose a stable, unique identifier and do not change it once deployed.

Binaries are typically placed under:

```
{AgentRoot}/Plugins/bin/{PluginId}/{PluginId}.exe    (Windows)
{AgentRoot}/Plugins/bin/{PluginId}/{PluginId}        (Linux / macOS)
```

Set `executablePath` in the JSON to the path relative to the agent root, or to a full absolute path if your deployment layout requires it.

**Anatomy of a Plugin: JSON**

Here is a complete annotated example:

```json
{
  "id": "MyBridge",
  "name": "My Bridge",
  "description": "Bridges inbound commands to an internal service.",
  "version": "1.0.0",

  "pluginType": "Executable",
  "executablePath": "bin/MyBridge/MyBridge.exe",
  "supportedPlatforms": ["Windows"],

  "Subscription": {
    "Topic": "MyBridge",
    "Qos": 2,
    "CleanSession": true
  },

  "metadata": {
    "mqttRole": ["subscriber", "publisher"],
    "mqttTopics": {
      "publish": ["KeeperLogger", "MyBridge/Responses/+"],
      "subscribe": ["MyBridge/Commands/+"]
    }
  },

  "startupPriority": 60,
  "autoStart": true,
  "executionContext": "Service",
  "requiresMonitoring": true,
  "autoRestart": true
}
```

**Field Reference**

<table data-header-hidden="false" data-header-sticky><thead><tr><th width="200.066650390625">Field</th><th width="109.6668701171875">Required</th><th>Description</th></tr></thead><tbody><tr><td><code>id</code></td><td>Yes</td><td>Stable identifier. Must match the filename. Used as the key in <code>/api/PluginSettings/{id}</code>.</td></tr><tr><td><code>name</code></td><td>No</td><td>Display name shown in logs and admin views.</td></tr><tr><td><code>description</code></td><td>No</td><td>Human-readable description.</td></tr><tr><td><code>version</code></td><td>No</td><td>Semantic version string. Used for diagnostics and change tracking.</td></tr><tr><td><code>pluginType</code></td><td>Yes</td><td><code>Executable</code> for a standalone binary. Other types may be available depending on your agent version — confirm with your administrator.</td></tr><tr><td><code>executablePath</code></td><td>Yes</td><td>Path to the binary. Relative to the agent root or absolute.</td></tr><tr><td><code>arguments</code></td><td>No</td><td>Command-line arguments passed to the binary at launch. Supports some variable substitution — confirm supported tokens with your administrator.</td></tr><tr><td><code>supportedPlatforms</code></td><td>Yes</td><td>Array of <code>"Windows"</code>, <code>"Linux"</code>, and/or <code>"macOS"</code>.</td></tr><tr><td><code>Subscription</code></td><td>Yes</td><td>Primary MQTT subscription for this plugin. See <a href="#mqtt-for-plugins">MQTT for Plugins</a>.</td></tr><tr><td><code>metadata</code></td><td>Yes</td><td>Container for <code>mqttTopics</code> and any plugin-specific keys.</td></tr><tr><td><code>metadata.mqttTopics.publish</code></td><td>Yes</td><td>Topics this plugin may publish to. The broker enforces this list.</td></tr><tr><td><code>metadata.mqttTopics.subscribe</code></td><td>Yes</td><td>Topics this plugin subscribes to beyond the primary <code>Subscription</code> topic.</td></tr><tr><td><code>startupPriority</code></td><td>No</td><td>Controls the order in which plugins start. Lower numbers start earlier. Look at the values used by other plugins in your deployment and choose a value that fits your component's dependencies.</td></tr><tr><td><code>autoStart</code></td><td>No</td><td><code>true</code> to have the agent start the plugin automatically when the agent starts. <code>false</code> for on-demand or job-triggered invocation. Defaults to <code>false</code> if omitted.</td></tr><tr><td><code>executionContext</code></td><td>No</td><td><code>Service</code> to run as the agent service account. Match this to your security requirements.</td></tr><tr><td><code>requiresMonitoring</code></td><td>No</td><td><code>true</code> to have the agent watch the process and detect if it exits unexpectedly.</td></tr><tr><td><code>autoRestart</code></td><td>No</td><td><code>true</code> to have the agent restart the plugin if monitoring detects it has stopped. Only meaningful when <code>requiresMonitoring</code> is also <code>true</code>.</td></tr><tr><td><code>enabled</code></td><td>No</td><td>Set to <code>false</code> to register the plugin without it running. Useful during development.</td></tr></tbody></table>
{% endstep %}

{% step %}
**MQTT for Plugins**

Plugins interact with the agent's MQTT broker differently from job tasks in three important ways:

1. They have a primary `Subscription` .
2. They declare topics under `metadata.mqttTopics` rather than on the job root.
3. They use a different MQTT client ID format.

**The Primary Subscription**

Every plugin declares a primary MQTT subscription:

```json
"Subscription": {
  "Topic": "MyBridge",
  "Qos": 2,
  "CleanSession": true
}
```

This is the topic the agent uses to identify and communicate with the plugin as a named component. It is separate from the additional topics declared in `metadata.mqttTopics`. Use `Qos: 2` for commands that must not be lost or duplicated. Set `CleanSession: true` unless your use case requires a persistent session.

**Declaring Publish and Subscribe Topics**

All topics your plugin publishes to or subscribes from must be declared in `metadata.mqttTopics`. The broker enforces these lists — a publish to an undeclared topic will be denied even if the MQTT connection succeeds.

```json
"metadata": {
  "mqttTopics": {
    "publish": ["KeeperLogger", "MyBridge/Responses/+"],
    "subscribe": ["MyBridge/Commands/+"]
  }
}
```

Declare only what you actually use. Avoid broad wildcards like `#` unless your use case genuinely requires them — tighter topic lists reduce the blast radius of misconfiguration.

If you publish log messages to `KeeperLogger`, include it in `publish`. The message format is the same `RequestMessage` JSON structure described in the [Custom Job Integration Guide](/keeperpam/endpoint-privilege-manager/integrations/custom-job-guide.md).

**MQTT Client ID Format for Plugins**

Plugin MQTT client IDs use a different format from job tasks. Do not use the job triple (`{JobId}_{Token}_{Pid}`) for a plugin — the broker uses the client ID format to route topic permission checks, and using the wrong format will cause those checks to fail.

For a plugin running as a service account (the typical case):

```
{PluginName}_{ProcessId}
```

For a plugin running in a user session:

```
{PluginName}_{UserName}_{ProcessId}
```

Where `{PluginName}` is a short, stable token — typically aligned with your plugin `id`. A complete example:

```python
import os

plugin_name = "MyBridge"
pid = os.getpid()
client_id = f"{plugin_name}_{pid}"
# Example result: "MyBridge_51204"
```

**Connecting to the Broker**

The broker address is not injected as an environment variable. Your plugin fetches it from Plugin Settings at startup, using the same pattern as job tasks — call `GET /api/PluginSettings/KeeperPrivilegeManager` using the `KeeperApiBaseUrl` the agent makes available. See the code example on the [Overview](/keeperpam/endpoint-privilege-manager/integrations/overview.md) page.

Unlike a job task, your plugin does not receive `{KeeperApiBaseUrl}` as an injected argument automatically. You will need to either pass the API base URL explicitly via `arguments` in the plugin JSON, or fall back to the default `https://127.0.0.1:6889` if your deployment guarantees that port.

The broker uses TLS on the loopback interface. Connect with your MQTT client configured for TLS, using your organization's CA bundle if provided, or disabling certificate verification for loopback-only connections per your security policy.
{% endstep %}

{% step %}
**Plugin Settings Over HTTPS**

Your plugin can read its own scoped configuration — separate from the system-wide `KeeperPrivilegeManager` settings — using its own plugin ID:

```
GET https://127.0.0.1:{httpsPort}/api/PluginSettings/MyBridge
```

This returns the effective merged settings for your plugin as a flat JSON object of string key-value pairs. Settings can come from:

* Your plugin JSON file (`Plugins/MyBridge.json`)
* Unified storage (applied by policy)
* System defaults

Always call this endpoint to read effective settings rather than parsing the JSON file on disk directly. The file on disk may not reflect policy overrides.

To update a single setting key from inside a running plugin:

```
PUT https://127.0.0.1:{httpsPort}/api/PluginSettings/MyBridge/{settingName}
```

Both endpoints require **Plugin-tier authorization** — your plugin must be started by the agent to pass process authentication. A manual run of the same binary will receive `403`. See the [HTTP Reference](/keeperpam/endpoint-privilege-manager/integrations/http-reference-guide.md) for the full Plugin Settings API.
{% endstep %}

{% step %}
**Signing and Process Trust**

Binaries placed under `Plugins/` paths are subject to stricter trust rules than binaries under `Jobs/bin/`. When the plugin orchestrator starts your binary, it registers the process in the launched-process registry, which grants MQTT and Plugin-tier HTTPS access — the same mechanism as job tasks. However, the path expectations and certificate checks that apply to plugin executables are tighter.

**Sign all plugin binaries for production.** Windows Authenticode is required for the standard plugin trust path on Windows. On macOS, use Apple Developer ID. On Linux, align with your deployment's signing enforcement (GPG-signed packages or IMA where applicable).

Add your code-signing certificate thumbprint to `Settings:AlternativeSignatures` in `appsettings.json` if your deployment requires explicit trust registration for the thumbprint:

```json
{
  "Settings": {
    "AlternativeSignatures": [
      "A1B2C3D4E5F6789012345678901234567890ABCD"
    ]
  }
}
```

Restart the agent service after changing `appsettings.json`.

**Do not rely on manual runs for production validation.** If you start your plugin binary manually outside the agent, it must pass a certificate check to connect to MQTT or call Plugin Settings. A binary started by the orchestrator skips that check because it is already registered. Test manual invocations in a development environment before assuming production behavior matches.
{% endstep %}

{% step %}
**Deployment**

Adding a new plugin to a production agent is a more involved process than deploying a job. It typically requires:

1. **Packaging and signing** the binary according to your organization's release process.
2. **Placing the binary** at the `executablePath` specified in the plugin JSON on every target endpoint.
3. **Dropping the plugin JSON** at `{AgentRoot}/Plugins/{PluginId}.json` using a supported deployment mechanism — not a manual file copy if Last Known Good is enabled.
4. **Restarting the agent** or triggering a plugin reload so the orchestrator picks up the new registration.

Work with your Keeper administrator to confirm the supported insertion path for new plugins in your environment. Arbitrary file drops without coordination can conflict with Last Known Good protection or packaging expectations.

**Updating plugin settings** after deployment does not require restarting the agent. Use `PUT /api/PluginSettings/{PluginId}/{settingName}` from a Plugin-tier authenticated process, or deliver a settings policy update through the console.

**If settings are not taking effect**, use `POST /api/PluginSettings/{PluginId}/revert` to re-import the plugin's on-disk JSON into unified storage, then check the effective values with a `GET`.
{% endstep %}
{% endstepper %}

## Checklist

Run through every item before rolling out to your fleet.

<table data-header-hidden="false" data-header-sticky><thead><tr><th width="55.06671142578125">#</th><th>Check</th></tr></thead><tbody><tr><td>1</td><td>Plugin <code>id</code> matches filename; binary exists at <code>executablePath</code> on each supported platform</td></tr><tr><td>2</td><td><code>Subscription.Topic</code> is set and unique among plugins in the deployment</td></tr><tr><td>3</td><td><code>metadata.mqttTopics.publish</code> includes every topic the binary actually publishes to</td></tr><tr><td>4</td><td><code>metadata.mqttTopics.subscribe</code> includes every topic the binary actually subscribes to beyond <code>Subscription.Topic</code></td></tr><tr><td>5</td><td>MQTT client ID uses the plugin format (<code>PluginName_Pid</code>) — not the job triple</td></tr><tr><td>6</td><td>Binary is signed; thumbprint is in <code>AlternativeSignatures</code> if required</td></tr><tr><td>7</td><td><code>GET /api/PluginSettings/KeeperPrivilegeManager</code> returns <code>broker.host</code> and <code>broker.port</code> from inside the running plugin</td></tr><tr><td>8</td><td><code>GET /api/PluginSettings/{PluginId}</code> returns the expected plugin-scoped settings</td></tr><tr><td>9</td><td>MQTT connects, primary subscription is active, and publish to <code>KeeperLogger</code> produces visible log output</td></tr><tr><td>10</td><td><code>autoStart</code>, <code>requiresMonitoring</code>, and <code>autoRestart</code> settings are validated with operations — confirm CPU usage, session behavior, and restart cadence are acceptable</td></tr><tr><td>11</td><td>No lifecycle conflict with a job running the same binary — separate executables for separate roles</td></tr></tbody></table>

## Troubleshooting

<table data-header-hidden="false" data-header-sticky><thead><tr><th width="322.800048828125">Symptom</th><th>Where to look</th></tr></thead><tbody><tr><td>Plugin does not start after deployment</td><td>Check <code>executablePath</code> resolves on the target OS; verify binary is signed; check agent logs for orchestrator errors</td></tr><tr><td>MQTT connection refused</td><td>Binary not registered as trusted; confirm the orchestrator started it rather than a manual run</td></tr><tr><td>MQTT connects but publish is denied</td><td>Topic missing from <code>metadata.mqttTopics.publish</code>; verify topic string matches exactly</td></tr><tr><td><code>GET /api/PluginSettings/MyBridge</code> returns <code>403</code></td><td>Process not Plugin-authenticated; ensure the agent (not a manual invocation) started the binary</td></tr><tr><td>Settings changes not reflected</td><td>Unified storage may be overriding the JSON file; call <code>/revert</code> to re-import, then re-check</td></tr><tr><td>Plugin restarts unexpectedly in a loop</td><td><code>autoRestart: true</code> combined with a binary that exits on a startup error; fix the root cause before re-enabling auto-restart</td></tr><tr><td>Duplicate processes detected</td><td>Same binary registered as both a plugin and a job task; separate the roles into distinct executables</td></tr></tbody></table>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.keeper.io/keeperpam/endpoint-privilege-manager/integrations/custom-plugin-guide.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
