Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ The supported lifecycle contract is synchronous:
- `renderOnLoad()` optionally returns an on-load string
- the SDK serializes those values for host transport separately

DOM helper rule:

- if `render()` uses SDK DOM helpers that generate goober-backed classes/styles, wrap the final helper output with `renderHTML(...)`
- this is mandatory for styled DOM-helper output, because helper-generated class names are only useful when the extracted CSS is emitted with the markup
- returning raw DOM-helper markup without `renderHTML(...)` can leave the UI unstyled even though class names are present
- DOM helpers emit raw HTML attribute names such as `class`, `for`, and `readonly`; JSX-style aliases such as `className`, `htmlFor`, and `readOnly` are accepted as input for compatibility and normalized to HTML output
- if both the HTML form and JSX-style alias are provided for the same attribute, the HTML form wins explicitly (`class` over `className`, `for` over `htmlFor`, `readonly` over `readOnly`)

Plugin metadata is also part of the host contract. In particular, `metadata.icon` must be a valid BlueprintJS v6 icon name because FDO uses BlueprintJS v6 in the host application.

For the detailed render/runtime contract, see [docs/RENDER_RUNTIME_CONTRACT.md](./docs/RENDER_RUNTIME_CONTRACT.md).
Expand Down Expand Up @@ -130,15 +138,17 @@ new MyPlugin();

### Example Usage

See `examples/fixtures/minimal-plugin.fixture.ts` for the primary minimal plugin scaffold.
See `examples/fixtures/minimal-plugin.fixture.ts` for the primary minimal plugin scaffold: metadata, `init()`, and `render()` only, with no extra bridge or UI-helper concepts mixed in.
See `examples/01-basic-plugin.ts` for the teaching-oriented basic lifecycle example.

See `examples/dom_elements_plugin.ts` for comprehensive examples of using the new DOM element creation capabilities including tables, media, semantic HTML, lists, and form controls.
See `examples/05-advanced-dom-plugin.ts` for the numbered DOM-helper learning example.

See `examples/08-privileged-actions-plugin.ts` for the low-level host privileged action request flow using `requestPrivilegedAction(...)` with correlation IDs and stable response envelope handling.

See `examples/09-operator-plugin.ts` for a curated operator helper example for a known tool family built on scoped host process execution.

See `examples/10-system-file-plugin.ts` for the next logical low-level filesystem example after `/etc/hosts`: a scoped `system.fs.mutate` flow targeting another system file (`/etc/motd`) through the same host-mediated privileged transport path.

For public SDK guidance, follow the fixture-first and curated-helper-first recommendation order documented in [docs/OPERATOR_PLUGIN_PATTERNS.md](./docs/OPERATOR_PLUGIN_PATTERNS.md).

The SDK also provides curated operator presets for common DevOps/SRE tooling such as Docker, kubectl, Helm, Terraform, Ansible, AWS CLI, gcloud, Azure CLI, Podman, Kustomize, GitHub CLI, Git, Vault, and Nomad, while still supporting generic custom scopes for host-specific tools.
Expand All @@ -157,27 +167,37 @@ Core capabilities:

- `storage.json` - required for `PluginRegistry.useStore("json")`
- `sudo.prompt` - required for `runWithSudo(...)`
- `system.clipboard.read` - required for host-mediated clipboard reads
- `system.clipboard.write` - required for host-mediated clipboard writes
- `system.hosts.write` - required for host-mediated hosts updates and scoped privileged fs API
- `system.fs.scope.<scope-id>` - host-defined scoped permission for `system.fs.mutate`
- `system.process.exec` - required for host-mediated process execution
- `system.process.scope.<scope-id>` - host-defined scoped permission for `system.process.exec`

Privileged action contracts:

- `system.clipboard.read`
- `system.clipboard.write`
- `system.hosts.write`
- `system.fs.mutate`
- `system.process.exec`

Public helpers exported from root package:

- `validateHostPrivilegedActionRequest(...)`
- `createClipboardReadActionRequest(...)`
- `createClipboardWriteActionRequest(...)`
- `createHostsWriteActionRequest(...)`
- `createFilesystemMutateActionRequest(...)`
- `createFilesystemScopeCapability(...)`
- `createProcessExecActionRequest(...)`
- `createProcessScopeCapability(...)`
- `createPrivilegedActionBackendRequest(...)`
- `requestPrivilegedAction(...)`
- `createClipboardReadRequest(...)`
- `createClipboardWriteRequest(...)`
- `requestClipboardRead(...)`
- `requestClipboardWrite(...)`
- `createScopedProcessExecActionRequest(...)`
- `requestScopedProcessExec(...)`
- `createProcessCapabilityBundle(...)`
Expand Down Expand Up @@ -233,6 +253,14 @@ Preferred pattern:
- expect workflow step results to expose typed process outcome data (`command`, `args`, `exitCode`, `stdout`, `stderr`, `durationMs`) rather than opaque blobs
- implement `declareCapabilities()` for operator plugins so declared vs granted capabilities can be compared during preflight and diagnostics

Rule of thumb for many commands:

- one command: `requestOperatorTool(...)`
- many independent inspections gathered by one backend method: loop in backend code
- one named troubleshooting runbook or inspect/act sequence: `requestScopedWorkflow(...)`

For example, an AWS troubleshooting plugin that needs 10+ CLI calls should not default to ten UI actions or raw shell chaining. Keep orchestration in backend code, declare capabilities for `aws-cli`, and promote the sequence to `requestScopedWorkflow(...)` once it becomes one logical operator run rather than a bag of unrelated commands.

Examples of suitable process scopes:

- `system.process.scope.docker-cli`
Expand Down
37 changes: 31 additions & 6 deletions docs/EXAMPLES_AND_FIXTURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,51 @@ If you are starting a new plugin, prefer the fixture set under `examples/fixture

1. `examples/fixtures/minimal-plugin.fixture.ts`
Use when you need the smallest valid plugin scaffold.
It is intentionally plain: metadata, `init()`, and `render()` only, with no extra bridge or UI-helper concepts mixed in.
2. `examples/fixtures/error-handling-plugin.fixture.ts`
Use when you need deterministic render fallback behavior.
It demonstrates `@handleError`, real `UI_MESSAGE` handler invocation, and runtime-safe fallback UI without extra UI abstraction.
3. `examples/fixtures/storage-plugin.fixture.ts`
Use when you need plugin-scoped storage with graceful JSON-store handling.
4. `examples/fixtures/advanced-ui-plugin.fixture.ts`
Use when you need richer UI composition with DOM helpers.
5. `examples/fixtures/operator-kubernetes-plugin.fixture.ts`
It demonstrates backend storage handlers, session fallback when JSON storage is unavailable, and UI_MESSAGE-driven storage actions instead of a static snapshot only.
4. `examples/fixtures/operator-kubernetes-plugin.fixture.ts`
Use when building a `kubectl`-style operator plugin.
6. `examples/fixtures/operator-terraform-plugin.fixture.ts`
It demonstrates the current curated operator pattern: declare preset capabilities, build validated operator/workflow envelopes in backend code, fetch them through `UI_MESSAGE`, and send them through the host privileged-action path.
5. `examples/fixtures/operator-terraform-plugin.fixture.ts`
Use when building a Terraform preview/apply plugin.
7. `examples/fixtures/operator-custom-tool-plugin.fixture.ts`
It demonstrates the same curated operator pattern for Terraform: declare preset capabilities, build validated plan/workflow envelopes in backend code, fetch them through `UI_MESSAGE`, and send them through the host privileged-action path.
6. `examples/fixtures/operator-custom-tool-plugin.fixture.ts`
Use when you need a host-specific scoped tool that is not covered by a curated operator preset.
It demonstrates the generic scoped-process pattern: declare the broad execution capability plus a narrow custom process scope, build a validated scoped-process envelope in backend code, fetch it through `UI_MESSAGE`, and send it through the host privileged-action path.

The numbered examples under `examples/01-...` to `examples/09-...` are learning references. They are not the default production starting point.
The numbered examples under `examples/01-...` to `examples/10-...` are learning references. They are not the default production starting point.

## Production-Grade Rules

Use these rules for examples, fixtures, and AI-generated plugin scaffolds:

- Keep backend orchestration in plugin methods and registered handlers.
- Keep `renderOnLoad()` thin and UI-focused.
- If `render()` uses styled SDK DOM-helper output, wrap the final helper markup with `renderHTML(...)`.
- Treat `renderHTML(...)` as mandatory for styled DOM-helper output because it emits the extracted goober CSS alongside the markup.
- Treat DOM helpers as the preferred SDK pattern for general structured UI.
- Use plain markup intentionally only when the example is isolating a different lesson, such as injected libraries or the iframe/backend boundary.
- The numbered learning examples are an intentional progression:
- `01` plain markup for the minimal lifecycle contract
- `02` plain markup for handlers, `renderOnLoad()`, and `UI_MESSAGE`
- `03` plain markup for storage and backend/UI separation
- `04` plain markup for quick actions and side-panel routing
- `05` DOM helpers directly, including the `renderHTML(...)` rule
- `06` plain markup for error handling and runtime-safe fallback behavior
- `07` plain markup for injected iframe libraries and browser-only helpers
- `08` plain markup for low-level privileged-action transport and response handling
- `09` plain markup for the curated operator-helper path, declared preset capabilities, and host-mediated execution
- `10` plain markup for generic scoped filesystem mutation when a plugin needs to edit a system file other than `/etc/hosts`
- Even plain markup examples must remain JSX-compatible for the FDO host transform.
- Escape JSX-sensitive code samples and prefer JSX-safe tags such as `<br />`.
- Prefer descriptive text over raw guard-sensitive capability/runtime tokens in `render()` when the literal token is not required for the lesson.
- Example: in `09`, describe the broad capability plus narrow scope instead of embedding raw `system.process.*` strings in the rendered UI, because host fail-fast guards may conservatively flag those tokens.
- Do not embed raw JSON snapshots directly in `render()`; use a safe placeholder and load the structured data after iframe initialization.
- For UI-to-backend calls, use the real bridge contract:

```ts
Expand All @@ -55,6 +79,7 @@ The examples surface is considered stable only when all of the following stay tr
- public example docs do not reference local-only or internal planning files
- public example docs do not reference stale docs domains
- `createBackendReq(...)` examples reflect the real `UI_MESSAGE` handler pattern
- DOM-helper examples that rely on helper-generated styles call `renderHTML(...)`
- the canonical fixtures remain the primary recommended starting point

## Canonical Operator Patterns
Expand Down
36 changes: 36 additions & 0 deletions docs/HOST_PRIVILEGED_ACTIONS_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Allow narrowly scoped privileged operations without granting plugins broad files

## Current Actions

- `system.clipboard.read`
- `system.clipboard.write`
- `system.hosts.write`
- `system.fs.mutate`
- `system.process.exec`
Expand All @@ -17,14 +19,39 @@ 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?)`

## Request Shape

```ts
{
action: "system.clipboard.read",
payload: {
reason?: string
}
}
```

```ts
{
action: "system.clipboard.write",
payload: {
text: string,
reason?: string
}
}
```

```ts
{
action: "system.hosts.write",
Expand Down Expand Up @@ -150,6 +177,10 @@ const payload = createPrivilegedActionBackendRequest(request, {

## Capability Requirement

- For `system.clipboard.read`, host should require:
- broad feature capability: `system.clipboard.read`
- For `system.clipboard.write`, host should require:
- broad feature capability: `system.clipboard.write`
- Host should only execute this action when capability `system.hosts.write` is 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)
Expand All @@ -163,6 +194,11 @@ const payload = createPrivilegedActionBackendRequest(request, {

## Security Requirements For Hosts

- 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/hosts` only
Expand Down
45 changes: 45 additions & 0 deletions docs/INJECTED_LIBRARIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,51 @@ If you need a new UI library in iframe code, it must be added by FDO host inject

Backend/plugin runtime code can still import npm dependencies that are bundled with the plugin artifact.

## Teaching Boundary: Injected Libraries vs DOM Helpers

SDK DOM helpers are still a best practice for general SDK-native structured UI because they give authors a typed, reusable way to build consistent markup and styling.

But this injected-libraries guide serves a different purpose:

- teach iframe-only host-injected globals
- teach browser-only runtime behavior
- teach the `UI_MESSAGE` backend bridge pattern

So examples in this guide may intentionally use plain markup plus `renderOnLoad()` wiring instead of DOM helpers, in order to keep the lesson focused.

Use this rule:

- general SDK-native structured UI: DOM helpers are preferred
- injected-library/runtime-boundary teaching: plain markup is acceptable when it keeps the example focused

If you do use styled DOM helpers in `render()`, `renderHTML(...)` remains mandatory so the extracted goober CSS is emitted with the markup.

## Important Boundary: JSX-Compatible Markup

Even when you use plain markup in injected-library examples, remember that `render()` output is consumed by the FDO host transform, not inserted as unconstrained raw HTML.

That means the safest mental model is:

- plain markup is acceptable here for teaching focus
- but it still needs to be JSX-compatible for the host render pipeline

Examples of what to prefer:

- `<br />` instead of `<br>`
- inline element styles or DOM-helper styling instead of raw `<style>` blocks in `render()`
- escaped code samples inside `<code>` blocks, for example `&#123;` and `&#125;`

Examples of what to avoid:

- raw `<style>` tags in `render()`
- raw object/function braces inside code samples shown directly in JSX-visible markup

Clipboard note:

- if UI code wants to copy data outside the iframe sandbox, prefer a host-mediated clipboard path
- if UI code wants to read clipboard data, also prefer a host-mediated clipboard path
- in the SDK, use the clipboard privileged-action contract instead of assuming direct Electron clipboard access from plugin UI code

## Table of Contents

- [CSS Libraries](#css-libraries)
Expand Down
93 changes: 93 additions & 0 deletions docs/OPERATOR_PLUGIN_PATTERNS.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,99 @@ const response = await requestScopedWorkflow("terraform", {

Use this path for preview/apply and inspect/act style flows when one request is no longer enough.

## Many Commands And Troubleshooting Runbooks

Do not model a serious troubleshooting plugin around ten separate UI buttons or around raw shell chaining.

Use this decision rule instead:

1. one command: `requestOperatorTool(...)`
2. several independent read-only commands collected by one backend method: loop over `requestOperatorTool(...)` or `requestScopedProcessExec(...)` in backend code
3. one named multi-step troubleshooting runbook with ordering, shared summary, audit visibility, or step-level failure handling: `requestScopedWorkflow(...)`

This distinction matters because a real troubleshooting runbook is not just "many commands". It is one operator flow with:

- a shared intent
- ordered steps
- step-level auditability
- a normalized summary
- clear failure and remediation points

If the commands are just independent inspections, a backend loop is acceptable:

```ts
const commands = [
["sts", "get-caller-identity"],
["ec2", "describe-instances"],
["ecs", "list-clusters"],
] as const;

const results = [];
for (const args of commands) {
results.push(
await requestOperatorTool("aws-cli", {
command: "/usr/local/bin/aws",
args: [...args, "--output", "json"],
timeoutMs: 5000,
dryRun: true,
reason: `inspect aws state: ${args.join(" ")}`,
})
);
}
```

If the commands are one troubleshooting runbook, prefer a workflow:

```ts
const response = await requestScopedWorkflow("aws-cli", {
kind: "process-sequence",
title: "AWS troubleshooting workflow",
summary: "Collect identity, EC2, ECS, and CloudWatch diagnostics",
dryRun: true,
steps: [
{
id: "whoami",
title: "Get caller identity",
phase: "inspect",
command: "/usr/local/bin/aws",
args: ["sts", "get-caller-identity", "--output", "json"],
timeoutMs: 5000,
reason: "inspect current AWS identity",
onError: "abort",
},
{
id: "ec2",
title: "Describe EC2 instances",
phase: "inspect",
command: "/usr/local/bin/aws",
args: ["ec2", "describe-instances", "--output", "json"],
timeoutMs: 5000,
reason: "inspect EC2 state",
onError: "continue",
},
{
id: "ecs",
title: "List ECS clusters",
phase: "inspect",
command: "/usr/local/bin/aws",
args: ["ecs", "list-clusters", "--output", "json"],
timeoutMs: 5000,
reason: "inspect ECS state",
onError: "continue",
},
],
});
```

For AWS-style troubleshooting plugins, best practice is:

- declare capabilities up front with `declareCapabilities()`
- prefer `createOperatorToolCapabilityPreset("aws-cli")` for the capability bundle
- keep the CLI scope narrow (`system.process.scope.aws-cli`)
- keep orchestration in backend code, not in iframe UI code
- avoid `sh -c`, shell interpolation, and ad hoc command chaining
- promote the sequence to `requestScopedWorkflow(...)` once it becomes a real runbook rather than a bag of unrelated commands

Capability model for the first workflow slice:

- reuse `system.process.exec`
Expand Down
Loading