Safe Plugin Authoring
This guide is for plugin authors building on@anikitenko/fdo-sdk.
Runtime Separation
- Backend runtime:
- plugin class lifecycle (
init, handlers, storage, logging)
- plugin class lifecycle (
- Iframe UI runtime:
- rendered UI source and browser-side helpers
Markup and Text Safety
- Treat
DOM.createElement(..., ...children)children as trusted JSX-like markup fragments. - For untrusted/user-provided text, use
DOMTexthelpers (createText,createPText,createSpanText, etc.) so JSX-sensitive characters are escaped. - Do not pass unsanitized user input as raw child markup into generic DOM helpers.
- DOM helpers emit raw HTML attributes in the final string. Compatibility aliases such as
className,htmlFor, andreadOnlyare accepted on input and normalized toclass,for, andreadonlyin output. - When both forms are provided, the native HTML form wins explicitly:
classoverclassName,foroverhtmlFor, andreadonlyoverreadOnly.
DOM Helper Rule: renderHTML() Is Mandatory For Styled Helper Output
If your render() method uses SDK DOM helpers and expects goober-backed styling/classes to appear in the output, you must wrap the final helper markup with renderHTML(...).
Why this is mandatory:
- DOM helpers generate class names through goober
- those class names are not enough by themselves
- the extracted CSS must be emitted into the render output alongside the markup
renderHTML(...)is the SDK helper that emits that CSS and the expected script placeholder boundary
- build the full helper-composed UI first, then call
renderHTML(...)once on the final root content - use the same helper instance for composition and
renderHTML(...)when practical - keep
renderHTML(...)inrender(), not inrenderOnLoad() - if you are returning plain manual JSX-like markup with no DOM-helper styling/classes,
renderHTML(...)is not required
Logging In Plugins
Use the built-inFDO_SDK logging methods from your plugin class:
this.log(message)this.info(message, ...meta)this.warn(message, ...meta)this.debug(message, ...meta)this.verbose(message, ...meta)this.silly(message, ...meta)this.error(error)this.event(name, payload)returns a correlation IDthis.getLogDirectory()resolves the current log directory for the plugin
FDO_SDK_LOG_ROOTconfigures log root directory- default root is
./logs - log files are written directly into the configured log directory
- in FDO host runtime this is typically
PLUGIN_HOME/logs/ - plugin identity is preserved in structured log metadata rather than an extra nested log folder
Privileged Error Formatting Rule
Do not surface rawresponse.error only. Use the SDK formatter so users see correlation IDs and host process details (stderr, stdout, exitCode, command, cwd) when present.
Use in normal runtime code:
renderOnLoad() string runtimes:
renderOnLoad() needs multiple UI bindings, prefer defineRenderOnLoadActions(...) over hand-written long listener strings. It gives typed handler/binding configuration and consistent runtime guard behavior (selector mismatch, unknown handler id, async handler failure).
Production baseline for new plugins:
- Default to
defineRenderOnLoadActions(...)for event wiring. - Use
strict: trueso missing selectors and unknown handlers fail fast during development. - Keep
setupfocused on state/bootstrap only; put user interactions in declarativebindings.
listRenderOnLoadTemplates(...) and getRenderOnLoadTemplate(id) instead of hardcoded renderOnLoad snippets.
For host UX that needs deterministic “copy exact fix” guidance by error code, use:
getDiagnosticFixTemplate(code)formatDiagnosticExactFix(code)
Metadata Rules
- Define full
metadata(name,version,author,description,icon) metadata.iconmust be a valid BlueprintJS v6 icon name- Prefer setting
metadata.idfor stable plugin-scoped storage/logging paths
Storage Rules
- Use
PluginRegistry.useStore("default")for in-memory scoped data - Use
PluginRegistry.useStore("json")for persistent scoped data - Configure JSON storage root:
PluginRegistry.configureStorage({ rootDir })- or
FDO_SDK_STORAGE_ROOT
- Do not call
PluginRegistry.useStore(...)in class-field initializers. - Acquire stores lazily (helper getter) or inside
init()/handlers after metadata is ready. - Reason: some host/runtime validations read plugin metadata during store acquisition. If store calls run before metadata assignment, startup can fail with errors like
Plugin metadata must be an object.and terminate the plugin process.
Capability Grants (Host-Managed)
Privileged SDK features are capability-gated. The host should grant capabilities at init-time throughPLUGIN_INIT.content.capabilities.
storage: base storage capability family; required with any concrete storage backend capabilitystorage.json: JSON backend leaf capability; required withstorageforPluginRegistry.useStore("json")system.network: base network capability family; required with concrete network transport capabilitiessystem.network.https: required for outbound HTTPS requests (for examplefetch("https://..."))system.network.http: required for outbound plaintext HTTP requests (for examplefetch("http://..."))system.network.websocket: required for outboundWebSocketusagesystem.network.tcp: required for raw TCP socket modules/APIssystem.network.udp: required for raw UDP socket modules/APIssystem.network.dns: required for DNS modules/APIssudo.prompt: required forrunWithSudo(...)system.clipboard.read: required for host-mediated clipboard readssystem.clipboard.write: required for host-mediated clipboard writessystem.hosts.write: reserved for host-mediated/etc/hostsupdates (do not implement direct filesystem writes in plugins)system.fs.scope.<scope-id>: host-defined scope capability for controlled external filesystem mutations throughsystem.fs.mutatesystem.process.exec: required for host-mediated process executionsystem.process.scope.<scope-id>: host-defined scope capability for controlled process execution throughsystem.process.exec
- declare expected capabilities in code via
declareCapabilities() - treat that declaration as an early intent manifest for host preflight and diagnostics
- do not treat declared capabilities as actual grants; the host remains authoritative
- keep runtime
requireCapability(...)and scoped helper enforcement for real authorization
/etc/hosts, use a host-mediated action contract with strict payload validation and host-side confirmation/auditing.
For Docker-style plugins, prefer host-mediated system.process.exec with a narrow scope such as system.process.scope.docker-cli and a host allowlist for exact command paths and argument patterns.
Operator-Style Plugin Pattern
The SDK supports larger operational plugins as long as they keep the host boundary explicit. Good examples:- Docker Desktop analogue
- Kubernetes dashboard / cluster console
- Helm release manager
- Terraform operator console
- local cluster/dev-environment manager
- UI and interaction in the iframe runtime
- backend orchestration in plugin runtime
- privileged execution through host-mediated scoped actions
- no generic unrestricted shell access
system.process.scope.docker-clisystem.process.scope.kubectlsystem.process.scope.helmsystem.process.scope.terraformsystem.process.scope.ansiblesystem.process.scope.aws-clisystem.process.scope.gcloudsystem.process.scope.azure-clisystem.process.scope.podmansystem.process.scope.kustomizesystem.process.scope.ghsystem.process.scope.gitsystem.process.scope.vaultsystem.process.scope.nomad
createOperatorToolCapabilityPreset(...)createOperatorToolActionRequest(...)requestOperatorTool(...)
createProcessScopeCapability(...)createScopedProcessExecActionRequest(...)requestScopedProcessExec(...)
createProcessCapabilityBundle(...)createFilesystemCapabilityBundle(...)describeCapability(...)parseMissingCapabilityError(...)runCapabilityPreflight(...)
- missing broad capability such as
system.process.exec - missing narrow scope capability such as
system.process.scope.kubectl
PluginRegistry.callInit() already logs declared-capability preflight diagnostics, but calling runCapabilityPreflight(...) directly is useful when you want a deterministic report object for custom UI, telemetry, or AI-assisted remediation.
Many-Command Troubleshooting Best Practice
If a plugin needs to run many tool commands, do not default to:- ten separate UI actions
- raw shell chaining
- ad hoc orchestration in iframe code
- one command:
requestOperatorTool(...) - several independent commands gathered by one backend method: loop in backend code
- one named troubleshooting or inspect/act runbook:
requestScopedWorkflow(...)
aws CLI commands. Best practice is:
- declare capabilities via
declareCapabilities() - use
createOperatorToolCapabilityPreset("aws-cli") - keep execution in backend methods and registered handlers
- use a backend loop only when the commands are independent inspections
- switch to
requestScopedWorkflow(...)when the sequence is one logical operator run with ordered steps, shared summary, and step-level diagnostics
sh -c- shell interpolation
- unstructured command concatenation
- repeating the same low-level request code in many UI handlers
Error-Path Safety
- Keep render error fallbacks simple and runtime-safe
- Avoid iframe-only helpers in backend failure paths
- Prefer deterministic fallback UI over brittle styling dependencies