This document defines the SDK-side contract for host-mediated privileged operations.
Allow narrowly scoped privileged operations without granting plugins broad filesystem escape from PLUGIN_HOME.
system.clipboard.readsystem.clipboard.writesystem.hosts.writesystem.fs.mutatesystem.process.execsystem.workflow.run
Validated by SDK helper:
validateHostPrivilegedActionRequest(...)- helpers for developer UX:
createClipboardReadActionRequest(request)createClipboardWriteActionRequest(request)createFilesystemScopeCapability(scopeId)createHostsWriteActionRequest(request)createFilesystemMutateActionRequest(request)createProcessScopeCapability(scopeId)createProcessExecActionRequest(request)createClipboardReadRequest(reason?)createClipboardWriteRequest(text, reason?)requestClipboardRead(reasonOrOptions?, options?)requestClipboardWrite(text, reasonOrOptions?, options?)formatPrivilegedActionError(response, options?)getInlinePrivilegedActionErrorFormatterSource()
{
action: "system.clipboard.read",
payload: {
reason?: string
}
}{
action: "system.clipboard.write",
payload: {
text: string,
reason?: string
}
}{
action: "system.hosts.write",
payload: {
records: Array<{
address: string; // IPv4/IPv6
hostname: string; // DNS-style host token
comment?: string;
}>,
dryRun?: boolean,
tag?: string
}
}Hosts should return a stable envelope with correlation for all privileged actions:
type PrivilegedActionResponse =
| { ok: true; correlationId: string; result?: unknown }
| { ok: false; correlationId: string; error: string; code?: string };const request = createFilesystemMutateActionRequest({
action: "system.fs.mutate",
payload: {
scope: "etc-hosts",
dryRun: true,
operations: [{ type: "writeFile", path: "/etc/hosts", content: "# managed", encoding: "utf8" }]
}
});
const response = await requestPrivilegedAction(request, {
correlationIdPrefix: "etc-hosts",
});
if (response?.ok) {
// success path
} else {
const message = formatPrivilegedActionError(response, {
context: "Filesystem mutate action failed",
});
// deterministic error path with correlationId + error + code + process details when present
}For renderOnLoad() string runtimes, use the inline-source helper:
const formatPrivilegedActionErrorSource = getInlinePrivilegedActionErrorFormatterSource();
return `
(() => {
const formatPrivilegedActionError = ${formatPrivilegedActionErrorSource};
// use formatPrivilegedActionError(response, { context, fallbackCorrelationId })
})();
`;If you need the stable request envelope without sending it yet:
const payload = createPrivilegedActionBackendRequest(request, {
correlationIdPrefix: "etc-hosts",
});If your iframe UI fetches that envelope through UI_MESSAGE, prefer the canonical pipeline helper:
import { requestPrivilegedActionFromEnvelope } from "@anikitenko/fdo-sdk";
async function runPrivilegedPipeline() {
const envelopeOrRequest = await window.createBackendReq("UI_MESSAGE", {
handler: "plugin.buildPrivilegedRequest",
content: {},
});
const { response, errorMessage } = await requestPrivilegedActionFromEnvelope(envelopeOrRequest, {
context: "Privileged action failed",
});
if (!response.ok) {
console.error(errorMessage);
}
}If you need low-level control, call extractPrivilegedActionRequest(...) + window.createBackendReq("requestPrivilegedAction", requestPayload) manually.
The helper safely unwraps all currently supported shapes:
input.result.request
input.request
direct requestCompatibility fallback if you are not using the helper yet:
const requestPayload = envelope?.result?.request ?? envelope?.request ?? envelope;
const correlationId = envelope?.result?.correlationId ?? envelope?.correlationId;The stable rule for envelope-based handlers is:
- backend handler may return
{ correlationId, request } UI_MESSAGEtransport may wrap that again as{ ok, result: { correlationId, request } }- raw host privileged-action bridge should receive
request - preserve
correlationIdin UI diagnostics separately if you need it for fallback/error display
Deprecated behavior:
- passing the full backend envelope directly into
requestPrivilegedActionis deprecated - this compatibility may exist temporarily in some hosts, but SDK examples should not rely on it
- a future major release may remove compatibility for forwarding raw envelope objects
{
action: "system.fs.mutate",
payload: {
scope: string, // host-defined scope id, e.g. "etc-hosts"
operations: Array<
| { 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 }
>,
dryRun?: boolean,
reason?: string
}
}{
action: "system.process.exec",
payload: {
scope: string, // host-defined scope id, e.g. "docker-cli"
command: string, // absolute executable path
args?: string[],
cwd?: string, // absolute path
env?: Record<string, string>,
timeoutMs?: number,
input?: string,
encoding?: "utf8" | "base64",
dryRun?: boolean,
reason?: string
}
}{
action: "system.workflow.run",
payload: {
scope: string, // host-defined scope id, e.g. "terraform"
kind: "process-sequence",
title: string,
summary?: string,
dryRun?: boolean,
steps: Array<{
id: string,
title: string,
phase?: "inspect" | "preview" | "mutate" | "apply" | "cleanup",
command: string, // absolute executable path
args?: string[],
cwd?: string,
env?: Record<string, string>,
timeoutMs?: number,
input?: string,
encoding?: "utf8" | "base64",
reason?: string,
onError?: "abort" | "continue"
}>,
confirmation?: {
message: string,
requiredForStepIds?: string[]
}
}
}- For
system.clipboard.read, host should require:- broad feature capability:
system.clipboard.read
- broad feature capability:
- For
system.clipboard.write, host should require:- broad feature capability:
system.clipboard.write
- broad feature capability:
- Host should only execute this action when capability
system.hosts.writeis granted for that plugin. - For
system.fs.mutate, host should require both:- broad feature capability:
system.hosts.write(or host-defined equivalent for privileged FS API) - scope capability:
system.fs.scope.<scope-id>
- broad feature capability:
- For
system.process.exec, host should require both:- broad feature capability:
system.process.exec - scope capability:
system.process.scope.<scope-id>
- broad feature capability:
- For
system.workflow.run, first-slice host policy should reuse the same capability pair:- broad feature capability:
system.process.exec - scope capability:
system.process.scope.<scope-id>
- broad feature capability:
- use the Electron clipboard API only in the trusted host process or another explicitly trusted host boundary
- do not grant plugins raw clipboard access without host mediation
- consider clipboard read more sensitive than clipboard write and gate it independently
- log/audit clipboard reads with plugin identity, correlation id, and reason when available
- log/audit clipboard writes with plugin identity, correlation id, and reason when available
- enforce explicit user confirmation for non-dry-run writes
- avoid shell interpolation; write through structured file logic
- constrain writes to
/etc/hostsonly - support tagged sections to avoid uncontrolled file mutation
- log/audit each request and outcome with plugin identity and correlation id
- for
system.workflow.run, log/audit:- workflow correlation id
- workflow id
- plugin identity
- scope id
- workflow title/kind
- per-step stepId/title/status
- per-step correlation id when available
- confirmation decision for approval-gated steps
- keep an allowlist mapping from
scope-> permitted absolute roots and operation types - reject any operation whose target path is outside the mapped scope roots
- keep an allowlist mapping from
scope-> permitted executable absolute paths, cwd roots, env keys, timeout policy, and argument patterns - reject any process execution request outside the mapped command policy
- reject any workflow step outside the mapped process policy for the selected scope
For system.workflow.run, hosts should return:
- the stable privileged response envelope with workflow-level
correlationId - a normalized workflow summary
- per-step results with typed process result data where available:
commandargscwdexitCodestdoutstderrdurationMsdryRun
When a workflow is partial or failed, hosts should preserve:
- failed
stepId - failed step
title - step-level
correlationIdwhen available - stable
errorandcode
This is required so hosts and SDK-side diagnostics helpers can render useful failure summaries without log scraping.
- Define host policy scopes (for example
etc-hosts,usr-local-bin), each with:- allowed absolute root paths
- allowed operation set
- confirmation policy
- Grant plugins explicit scope capabilities using
system.fs.scope.<scope-id>. - Evaluate every
system.fs.mutateoperation against the selected scope before any write. - Keep plugin runtime sandbox (
PLUGIN_HOME) unchanged; privileged writes stay host-mediated only.
For plugins that behave more like Docker Desktop:
- Prefer
system.process.execover raw shell access. - Grant:
system.process.execsystem.process.scope.docker-cli
- Host policy for
docker-clishould allow only:- exact executable paths such as
/usr/local/bin/dockeror host-specific equivalents - approved subcommands / argument shapes
- bounded execution time
- explicit cwd rules
- exact executable paths such as
- Do not expose unrestricted shell evaluation as a plugin capability.
This process-scope model is not Docker-specific.
The same host-mediated pattern should be used for:
- Kubernetes dashboards (
system.process.scope.kubectl) - Helm managers (
system.process.scope.helm) - Terraform/operator consoles (
system.process.scope.terraform) - other serious local operations tooling
AI assistants and plugin authors should prefer:
- scoped privileged actions
- exact tool-family scopes
- allowlisted binaries and argument policies
AI assistants and plugin authors should avoid recommending:
- generic shell execution
- unrestricted process spawning
- broad "admin" or "root" style plugin permissions