Privileged actions are system operations that the plugin cannot perform directly. Instead, the plugin sends a structured request to the FDO host, which validates the request against its scope policy, executes the operation, and returns a stable response envelope.
Every privileged request carries a correlation ID for auditing. The host logs the plugin identity, scope, and outcome for each request.
Privileged actions only execute in the iframe UI runtime, where window.createBackendReq is available. Calling requestPrivilegedAction() outside of that context throws an error.
Action types
The SDK supports three privileged action types:
| Action | Purpose |
|---|
system.hosts.write | Add or update records in /etc/hosts |
system.fs.mutate | Perform structured filesystem operations within a host-defined scope |
system.process.exec | Execute an allowlisted executable within a host-defined process scope |
A fourth action, system.workflow.run, is used internally by the workflow helpers. See Workflows for details.
Requesting a privileged action
Use requestPrivilegedAction(request, options) to send a request to the host and await the response:
import {
createFilesystemMutateActionRequest,
requestPrivilegedAction,
isPrivilegedActionSuccessResponse,
isPrivilegedActionErrorResponse,
} from "@anikitenko/fdo-sdk";
const request = createFilesystemMutateActionRequest({
action: "system.fs.mutate",
payload: {
scope: "etc-hosts",
dryRun: true,
reason: "preview managed hosts block update",
operations: [
{
type: "writeFile",
path: "/etc/hosts",
content: "# managed by fdo plugin",
encoding: "utf8",
},
],
},
});
const response = await requestPrivilegedAction(request, {
correlationIdPrefix: "etc-hosts",
});
if (isPrivilegedActionSuccessResponse(response)) {
console.log("Success:", response.correlationId, response.result);
} else if (isPrivilegedActionErrorResponse(response)) {
console.error("Error:", response.error, response.code);
}
Options
requestPrivilegedAction accepts an optional RequestPrivilegedActionOptions object:
| Field | Type | Description |
|---|
correlationId | string | Provide an explicit correlation ID. If omitted, one is generated from the prefix. |
correlationIdPrefix | string | Prefix used to generate the correlation ID (e.g., "docker-cli" → "docker-cli-1712345678"). |
handler | string | Override the host handler name. Defaults to "requestPrivilegedAction". |
Building requests
Use the typed factory functions to build validated request objects before sending them.
system.hosts.write
createHostsWriteActionRequest(request) builds a request to write records into /etc/hosts:
import { createHostsWriteActionRequest } from "@anikitenko/fdo-sdk";
const request = createHostsWriteActionRequest({
action: "system.hosts.write",
payload: {
records: [
{ address: "127.0.0.1", hostname: "myapp.local", comment: "local dev" },
{ address: "::1", hostname: "myapp.local" },
],
dryRun: true,
tag: "myapp-dev",
},
});
The HostsRecord type:
| Field | Type | Required | Description |
|---|
address | string | Yes | IPv4 or IPv6 address |
hostname | string | Yes | DNS-style host token |
comment | string | No | Optional comment appended to the line |
Required capability: system.hosts.write
system.fs.mutate
createFilesystemMutateActionRequest(request) builds a request to perform structured filesystem operations within a host-defined scope:
import { createFilesystemMutateActionRequest } from "@anikitenko/fdo-sdk";
const request = createFilesystemMutateActionRequest({
action: "system.fs.mutate",
payload: {
scope: "etc-hosts",
dryRun: true,
reason: "preview managed hosts block update",
operations: [
{
type: "writeFile",
path: "/etc/hosts",
content: "# managed by fdo plugin",
encoding: "utf8",
},
],
},
});
FilesystemMutationOperation types
Each entry in operations is one of the following:
| { type: "mkdir"; path: string; recursive?: boolean; mode?: number }
| { type: "writeFile"; path: string; content: string; encoding?: "utf8" | "base64"; mode?: number }
| { type: "appendFile"; path: string; content: string; encoding?: "utf8" | "base64" }
| { type: "rename"; from: string; to: string }
| { type: "remove"; path: string; recursive?: boolean; force?: boolean }
Required capabilities: system.hosts.write (or host-equivalent broad grant) and system.fs.scope.<scope-id>
system.process.exec
createProcessExecActionRequest(request) builds a request to execute an allowlisted process within a host-defined scope:
import { createProcessExecActionRequest } from "@anikitenko/fdo-sdk";
const request = createProcessExecActionRequest({
action: "system.process.exec",
payload: {
scope: "docker-cli",
command: "/usr/local/bin/docker",
args: ["ps", "--format", "json"],
timeoutMs: 5000,
dryRun: true,
reason: "list running containers",
},
});
Payload fields
| Field | Type | Required | Description |
|---|
scope | string | Yes | Host-defined scope ID (e.g., "docker-cli") |
command | string | Yes | Absolute path to the executable |
args | string[] | No | Command arguments |
cwd | string | No | Absolute working directory path |
env | Record<string, string> | No | Additional environment variables |
timeoutMs | number | No | Execution timeout in milliseconds |
input | string | No | Standard input for the process |
encoding | "utf8" | "base64" | No | Input/output encoding |
dryRun | boolean | No | Validate the request without executing |
reason | string | No | Human-readable reason for auditing |
Required capabilities: system.process.exec and system.process.scope.<scope-id>
Response handling
All privileged actions return a PrivilegedActionResponse:
type PrivilegedActionResponse<TResult = unknown> =
| { ok: true; correlationId: string; result?: TResult }
| { ok: false; correlationId: string; error: string; code?: string };
Type guard helpers
Use these helpers to narrow the response type safely:
import {
isPrivilegedActionSuccessResponse,
isPrivilegedActionErrorResponse,
unwrapPrivilegedActionResponse,
} from "@anikitenko/fdo-sdk";
if (isPrivilegedActionSuccessResponse(response)) {
// response.ok === true
// response.result is available
}
if (isPrivilegedActionErrorResponse(response)) {
// response.ok === false
// response.error and response.code are available
}
// Unwrap throws on error, returns result on success:
const result = unwrapPrivilegedActionResponse(response);
unwrapPrivilegedActionResponse(response) returns result when ok is true, and throws an Error (with .code attached) when ok is false.
Correlation IDs
Every request and response carries a correlationId that the host uses for auditing. You can generate one explicitly with createPrivilegedActionCorrelationId():
import { createPrivilegedActionCorrelationId } from "@anikitenko/fdo-sdk";
const correlationId = createPrivilegedActionCorrelationId("docker-cli");
// e.g. "docker-cli-1712345678901"
When you pass correlationIdPrefix to requestPrivilegedAction, the SDK generates a correlation ID in the same format automatically. Providing an explicit correlationId overrides generation entirely.
Building the transport payload without sending
Use createPrivilegedActionBackendRequest(request, options) to build the request envelope without dispatching it. This is useful when you need to inspect or log the payload before it is sent:
import {
createFilesystemMutateActionRequest,
createPrivilegedActionBackendRequest,
} from "@anikitenko/fdo-sdk";
const request = createFilesystemMutateActionRequest({ /* ... */ });
const payload = createPrivilegedActionBackendRequest(request, {
correlationIdPrefix: "etc-hosts",
});
// payload = { correlationId: "etc-hosts-...", request: { action: "system.fs.mutate", payload: { ... } } }
Dry-run testing
All three action types support a dryRun option in the payload. When set to true, the host validates the request and returns a response envelope without performing the actual operation. Use this during development and for preflight checks before committing to a destructive action.
const request = createProcessExecActionRequest({
action: "system.process.exec",
payload: {
scope: "kubectl",
command: "/usr/local/bin/kubectl",
args: ["delete", "pod", "api-xyz"],
dryRun: true, // validate without executing
reason: "preflight check before pod deletion",
},
});
Full example
The following is adapted from examples/08-privileged-actions-plugin.ts:
import {
createFilesystemMutateActionRequest,
FDOInterface,
FDO_SDK,
isPrivilegedActionSuccessResponse,
requestPrivilegedAction,
} from "@anikitenko/fdo-sdk";
export class PrivilegedActionsPlugin extends FDO_SDK implements FDOInterface {
get metadata() {
return {
name: "PrivilegedActionsPlugin",
version: "1.0.0",
author: "FDO Team",
description: "Demonstrates the low-level host privileged action request flow.",
icon: "shield",
};
}
init(): void {
this.log("PrivilegedActionsPlugin initialized");
}
render(): string {
return `
<div style="padding: 16px;">
<h2>Privileged Actions Demo</h2>
<button id="run-privileged-action">Run Dry-Run</button>
<pre id="result-box"></pre>
</div>
`;
}
renderOnLoad(): string {
const request = createFilesystemMutateActionRequest({
action: "system.fs.mutate",
payload: {
scope: "etc-hosts",
dryRun: true,
reason: "preview managed hosts block update",
operations: [
{
type: "writeFile",
path: "/etc/hosts",
content: "# managed by fdo plugin",
encoding: "utf8",
},
],
},
});
return `
() => {
const btn = document.getElementById("run-privileged-action");
const resultBox = document.getElementById("result-box");
if (!btn || !resultBox) return;
btn.addEventListener("click", async () => {
let correlationId = "unknown";
try {
const response = await (${requestPrivilegedAction.toString()})(${JSON.stringify(request)}, {
correlationIdPrefix: "etc-hosts",
});
correlationId = response.correlationId;
if (${isPrivilegedActionSuccessResponse.toString()}(response)) {
resultBox.textContent = JSON.stringify({
status: "ok",
correlationId: response.correlationId,
result: response.result ?? null,
}, null, 2);
return;
}
resultBox.textContent = JSON.stringify({
status: "error",
correlationId: response?.correlationId ?? correlationId,
error: response?.error ?? "Unknown host error",
code: response?.code ?? "UNKNOWN",
}, null, 2);
} catch (error) {
resultBox.textContent = JSON.stringify({
status: "error",
correlationId,
error: error instanceof Error ? error.message : String(error),
code: "IPC_FAILURE",
}, null, 2);
}
});
}
`;
}
}
For known tool families like Docker, kubectl, and Terraform, prefer requestOperatorTool() over building ProcessExecActionRequest manually. See Tool Presets for the curated helper path.