Skip to content
Docs

Gateway Plugin System

AgentWatch provides a sandboxed plugin system that lets administrators extend gateway behavior with custom JavaScript logic — no core code changes, no restarts. Plugins run in an isolated Node.js vm context and cannot access the filesystem, network, environment variables, require, import, process, Buffer, fs, or timers.

Inbound Request
│
▼
┌─────────────────────────────────────────────────────────────┐
│ pre-request plugins (sorted by priority, lowest runs first) │
│ Can: modify request body/headers, short-circuit with error │
└─────────────────────────────────────────────────────────────┘
│
▼ (forwarded to upstream LLM)
│
┌─────────────────────────────────────────────────────────────┐
│ post-request plugins │
│ Can: inspect request metadata after forwarding │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ pre-response plugins │
│ Can: modify response body/headers before sending to client │
└─────────────────────────────────────────────────────────────┘
│
▼ (response sent to client)
│
┌─────────────────────────────────────────────────────────────┐
│ post-response plugins (async, non-blocking) │
│ Can: fire-and-forget logging, metrics │
└─────────────────────────────────────────────────────────────┘
on-error plugins fire only when a gateway error occurs at any stage.

Fail-open design: If a plugin throws an unhandled exception, the error is logged and the request proceeds normally. Plugins can never crash the gateway.

Execution order: Within the same hook, plugins run by priority ascending (lower number = runs first).


Field Required Description
Name Yes Identifier for the plugin (e.g., add-request-id)
Description No What the plugin does
Hook Yes Which lifecycle stage to run at (see hooks below)
Route Filter No Glob pattern to limit which routes trigger this plugin. Default * = all routes
Priority No Execution order within the same hook. Lower = runs first. Default 0
Timeout (ms) No Max execution time before the plugin is killed. Default 5000ms, max 30000ms
Enabled Yes Toggle on/off without deleting
Plugin Code Yes JavaScript source. Must return { proceed: true } or { proceed: false, ... }

Hook When It Fires Context Available Typical Use
pre-request Before forwarding to upstream ctx.request, ctx.route Add/remove headers, validate body, block requests
post-request After upstream receives request, before response processing ctx.request, ctx.route Log request metadata
pre-response Before sending upstream response back to client ctx.request, ctx.response, ctx.route Transform or redact response body
post-response After response sent to client (async) ctx.request, ctx.response, ctx.route Fire-and-forget logging, metrics
on-error When any gateway error occurs ctx.request, ctx.route Custom error responses, alerting

Pattern Matches
* All routes
chat-* Routes whose name starts with chat-
*/production Routes ending in /production
** All routes including nested names
openai-gpt4 Exactly the route named openai-gpt4

Every plugin receives a ctx object (also aliased as context):

ctx.request = {
method, // "POST"
path, // "/v1/chat/completions"
headers, // { "authorization": "Bearer ...", ... }
body, // parsed JSON body object
query // query string params as object
}
ctx.response = { // only in post-request, pre-response, post-response
statusCode, // 200, 400, etc.
headers, // response headers
body // parsed JSON body
}
ctx.route = {
name, // route display name
id, // route UUID
upstream // upstream URL
}
ctx.config // values from plugin's config field (JSON object)
ctx.logger.info("message", { optional: "data" })
ctx.logger.warn("message")
ctx.logger.error("message")
ctx.metrics.increment("my-counter")
ctx.metrics.record("latency-ms", 42)

Every plugin must return an object with at least proceed:

Continue normally:

return { proceed: true };

Modify the request:

return {
proceed: true,
modifiedRequest: {
method: ctx.request.method,
path: ctx.request.path,
headers: { ...ctx.request.headers, "X-Request-Id": "abc123" },
body: ctx.request.body,
query: ctx.request.query
}
};

Modify the response:

return {
proceed: true,
modifiedResponse: {
statusCode: ctx.response.statusCode,
headers: ctx.response.headers,
body: { ...ctx.response.body, _source: "agentwatch" }
}
};

Short-circuit (block the request):

return {
proceed: false,
statusCode: 403,
body: { error: "Blocked by plugin", code: "PLUGIN_BLOCKED" }
};

Step 1: Navigate to Plugins

  1. Log in to the AgentWatch Admin Panel
  2. In the left sidebar, go to Gateway → Plugins
  3. Click + New Plugin

Step 2: Set the Name and Description

  • Name: Short, kebab-case identifier, e.g., add-correlation-id
  • Description: What the plugin does, e.g., Injects X-Correlation-Id header on every request

Step 3: Choose a Hook

Pick the hook that matches when your logic should run:

  • Modifying the request before it reaches the LLM → pre-request
  • Transforming the LLM response → pre-response
  • Logging after the response is sent → post-response
  • Custom error handling → on-error

Step 4: Set the Route Filter

  • Leave as * to apply to all gateway routes
  • Enter a specific pattern like openai-* to target only matching routes

Step 5: Set Priority and Timeout

  • Priority: If multiple plugins share the same hook, lower priority number runs first. Use 0 for most plugins.
  • Timeout: Default 5000ms is appropriate for most logic. Increase for plugins making external calls via shared state. Decrease for lightweight header manipulations.

Step 6: Write the Plugin Code

Start from a template (see below) or write custom logic. The code must always return { proceed: true } or { proceed: false, ... }.

Step 7: Save and Test

  1. Click Save Plugin
  2. On the plugin row, click Test to run the plugin against a sample context
  3. Click Logs to view execution logs (last 100 entries per plugin)

Template 1: Log Request Logs every request’s method, path, and headers.

ctx.logger.info("Incoming request", {
method: ctx.request.method,
path: ctx.request.path,
headers: ctx.request.headers,
query: ctx.request.query
});
return { proceed: true };

Template 2: Add Header Adds custom headers to the request (reads from ctx.config.headers).

var headers = ctx.request.headers || {};
var configHeaders = ctx.config.headers || {};
var keys = Object.keys(configHeaders);
for (var i = 0; i < keys.length; i++) {
headers[keys[i]] = configHeaders[keys[i]];
}
return {
proceed: true,
modifiedRequest: {
method: ctx.request.method,
path: ctx.request.path,
headers: headers,
body: ctx.request.body,
query: ctx.request.query
}
};

Template 3: Rate Limit by Header Blocks requests missing a required header.

var headerName = ctx.config.headerName || "x-api-key";
var key = ctx.request.headers[headerName];
if (!key) {
return {
proceed: false,
statusCode: 401,
body: { error: "Missing required header: " + headerName }
};
}
ctx.logger.info("Header present", { key: key });
return { proceed: true };

Template 4: Transform Body Adds or removes fields from the request body.

var body = ctx.request.body || {};
var addFields = ctx.config.addFields || {};
var removeFields = ctx.config.removeFields || [];
var keys = Object.keys(addFields);
for (var i = 0; i < keys.length; i++) {
body[keys[i]] = addFields[keys[i]];
}
for (var j = 0; j < removeFields.length; j++) {
delete body[removeFields[j]];
}
return {
proceed: true,
modifiedRequest: {
method: ctx.request.method,
path: ctx.request.path,
headers: ctx.request.headers,
body: body,
query: ctx.request.query
}
};

The following are not available inside plugin code:

Blocked Reason
require / import No module loading
fetch / XMLHttpRequest No outbound network calls
process / global No server environment access
fs / child_process No filesystem or shell access
setTimeout / setInterval No async timers
Buffer No raw binary access

Use ctx.config to pass configuration values into the plugin rather than hardcoding secrets.


Method Endpoint Description
GET /api/admin/gateway/plugins List all plugins
GET /api/admin/gateway/plugins/:id Get a single plugin
POST /api/admin/gateway/plugins Create a new plugin
PUT /api/admin/gateway/plugins/:id Update a plugin
DELETE /api/admin/gateway/plugins/:id Delete a plugin
POST /api/admin/gateway/plugins/:id/test Test-execute a plugin in sandbox
GET /api/admin/gateway/plugins/:id/logs Get last 100 execution logs