Skip to main content
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:
ActionPurpose
system.hosts.writeAdd or update records in /etc/hosts
system.fs.mutatePerform structured filesystem operations within a host-defined scope
system.process.execExecute 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:
FieldTypeDescription
correlationIdstringProvide an explicit correlation ID. If omitted, one is generated from the prefix.
correlationIdPrefixstringPrefix used to generate the correlation ID (e.g., "docker-cli""docker-cli-1712345678").
handlerstringOverride 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:
FieldTypeRequiredDescription
addressstringYesIPv4 or IPv6 address
hostnamestringYesDNS-style host token
commentstringNoOptional 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

FieldTypeRequiredDescription
scopestringYesHost-defined scope ID (e.g., "docker-cli")
commandstringYesAbsolute path to the executable
argsstring[]NoCommand arguments
cwdstringNoAbsolute working directory path
envRecord<string, string>NoAdditional environment variables
timeoutMsnumberNoExecution timeout in milliseconds
inputstringNoStandard input for the process
encoding"utf8" | "base64"NoInput/output encoding
dryRunbooleanNoValidate the request without executing
reasonstringNoHuman-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.