Skip to content

Commit 8f3714c

Browse files
ChesterHsuclaude
andcommitted
feat: complete interactive plugin system with UI support
- @flyto/plugin-sdk: add UIServer, uiStep(), waitForUI() for interactive plugins - @flyto/plugin-ui-tokens: CSS design tokens matching flyto-cloud dark theme - @flyto/plugin-ui-bridge: iframe communication bridge (postMessage + HTTP) - @flyto/plugin-form-builder: 16 field types, wizard mode, approval form - @flyto/plugin-image-crop: canvas-based crop tool with aspect ratio constraints - plugin.yaml spec: documented schema with UI step configuration - 47 tests passing (30 unit + 9 E2E + 8 runtime integration) - Security: body size limits, origin validation, iframe sandbox hardening Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0b33972 commit 8f3714c

35 files changed

Lines changed: 4885 additions & 7 deletions

LICENSE

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
Apache License
2+
Version 2.0, January 2004
3+
http://www.apache.org/licenses/
4+
5+
Copyright 2026 Flyto2
6+
7+
Licensed under the Apache License, Version 2.0 (the "License");
8+
you may not use this file except in compliance with the License.
9+
You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing, software
14+
distributed under the License is distributed on an "AS IS" BASIS,
15+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
See the License for the specific language governing permissions and
17+
limitations under the License.

PLUGIN_SPEC.md

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# plugin.yaml Specification
2+
3+
Every Flyto2 plugin must include a `plugin.yaml` at the package root.
4+
5+
## Naming Convention
6+
7+
A plugin has two identifiers:
8+
9+
| Identifier | Where | Example | Purpose |
10+
|---|---|---|---|
11+
| **npm package name** | `package.json` `"name"` | `@flyto/plugin-slack` | npm install/publish |
12+
| **plugin runtime ID** | `plugin.yaml` `id` | `flyto-community/slack` | Runtime dispatch, module registry |
13+
14+
- npm name uses `@flyto/plugin-*` scope for official plugins
15+
- Runtime ID uses `vendor/name` format (e.g., `flyto-community/slack`, `acme-corp/custom-tool`)
16+
- The module registry key becomes `plugin.{runtime_id}/{step_id}`
17+
18+
## Schema
19+
20+
```yaml
21+
# ── Plugin identity ──────────────────────────────────────
22+
id: string # Runtime ID — "vendor/name" format (e.g., "flyto-community/slack")
23+
name: string # Display name
24+
version: string # SemVer
25+
description: string # One-line description
26+
27+
# ── Runtime ──────────────────────────────────────────────
28+
runtime:
29+
language: node # "node" (future: "deno", "bun")
30+
entry_point: dist/index.js # Compiled entry
31+
min_flyto_version: "2.25.0" # Minimum flyto-core version
32+
33+
# ── Steps ────────────────────────────────────────────────
34+
steps:
35+
- id: string # Step identifier
36+
label: string # Display label
37+
description: string # What this step does
38+
category: string # Category for grouping
39+
icon: string # Icon name (Lucide icons)
40+
color: string # Hex color for the node
41+
42+
# Connection rules
43+
can_receive_from: ["*"] # Which step types can connect to this
44+
can_connect_to: ["*"] # Which step types this can connect to
45+
46+
# Input parameters
47+
params_schema:
48+
param_name:
49+
type: string | number | boolean | array | object
50+
label: string
51+
description: string
52+
required: boolean # default: false
53+
default: any # default value
54+
placeholder: string # input placeholder
55+
options: # for select/enum types
56+
- value: string
57+
label: string
58+
59+
# Output schema
60+
output_schema:
61+
field_name:
62+
type: string | number | boolean | array | object
63+
description: string
64+
65+
# ── UI Configuration (optional) ──────────────────────
66+
# When present, this step opens an interactive UI page
67+
# instead of running headlessly.
68+
ui:
69+
type: page | panel | dialog # How the UI is displayed
70+
# page — full-screen overlay
71+
# panel — side panel (default 400px)
72+
# dialog — centered modal
73+
page: string # Path to UI directory (relative to plugin root)
74+
# Must contain index.html
75+
width: number # Default width in pixels (optional)
76+
height: number # Default height in pixels (optional)
77+
timeout_ms: number # Max wait time in ms (default: 300000)
78+
79+
# ── Secrets ──────────────────────────────────────────────
80+
required_secrets: # List of secret names the plugin needs
81+
- SECRET_NAME
82+
```
83+
84+
## UI Step Lifecycle
85+
86+
```
87+
flyto-core Plugin SDK Plugin UI (iframe)
88+
│ │ │
89+
├── invoke(step) ──────────>│ │
90+
│ ├── start HTTP server │
91+
│ ├── ui.open {url} ──────────> │
92+
│ │ ├── render UI
93+
│ │ ├── user interacts
94+
│ │ <── POST /callback ───────┤ flyto.submit(data)
95+
│ ├── ui.close ────────────────>│
96+
│ <── result ─────────────┤ │
97+
│ │ │
98+
```
99+
100+
## Example: UI Step in plugin.yaml
101+
102+
```yaml
103+
steps:
104+
- id: crop_image
105+
label: Crop Image
106+
description: Interactive image cropping tool
107+
category: media
108+
icon: Crop
109+
color: "#10B981"
110+
params_schema:
111+
image_url:
112+
type: string
113+
label: Image URL
114+
required: true
115+
aspect_ratio:
116+
type: string
117+
label: Aspect Ratio
118+
options:
119+
- { value: "free", label: "Free" }
120+
- { value: "1:1", label: "Square" }
121+
- { value: "16:9", label: "Widescreen" }
122+
- { value: "4:3", label: "Standard" }
123+
output_schema:
124+
cropped_data_url:
125+
type: string
126+
description: Base64 data URL of the cropped image
127+
crop_rect:
128+
type: object
129+
description: "{ x, y, width, height } of the crop area"
130+
ui:
131+
type: dialog
132+
page: ui/dist
133+
width: 900
134+
height: 650
135+
```

README.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# flyto-plugins-js
2+
3+
Interactive plugin system for Flyto2 workflows. Plugins run as Node.js processes, communicate with flyto-core via JSON-RPC, and can serve interactive UIs (forms, tools, approvals) that appear inline during workflow execution.
4+
5+
## Architecture
6+
7+
```
8+
flyto-core (Python) ←── JSON-RPC stdin/stdout ──→ Plugin (Node.js)
9+
10+
├── Headless steps (like Slack send_message)
11+
└── UI steps (serve HTML via local HTTP server)
12+
13+
└── iframe in flyto-cloud frontend
14+
```
15+
16+
## Packages
17+
18+
| Package | Description |
19+
|---------|-------------|
20+
| [`@flyto/plugin-sdk`](packages/sdk/) | Core SDK — JSON-RPC runtime, UI server, step registration |
21+
| [`@flyto/plugin-ui-tokens`](packages/ui-tokens/) | CSS design tokens matching flyto-cloud's look & feel |
22+
| [`@flyto/plugin-ui-bridge`](packages/ui-bridge/) | Communication bridge for plugin UI iframes |
23+
24+
## Plugins
25+
26+
| Plugin | Type | Description |
27+
|--------|------|-------------|
28+
| [`@flyto/plugin-slack`](plugins/slack/) | Headless | Send messages, list channels |
29+
| [`@flyto/plugin-form-builder`](plugins/form-builder/) | Interactive | Dynamic forms, wizard, approval |
30+
| [`@flyto/plugin-image-crop`](plugins/image-crop/) | Interactive | Image cropping tool |
31+
32+
## Quick Start
33+
34+
```bash
35+
# Install dependencies
36+
npm install
37+
38+
# Build all packages
39+
npm run build
40+
41+
# Run tests
42+
npm test
43+
```
44+
45+
### Create a Plugin
46+
47+
```typescript
48+
import { createPlugin } from '@flyto/plugin-sdk';
49+
50+
const plugin = createPlugin({ id: 'my-org/my-plugin', version: '1.0.0' });
51+
52+
// Headless step
53+
plugin.step('do_something', async (input, ctx) => {
54+
return { ok: true, data: { result: input.value * 2 } };
55+
});
56+
57+
// UI step (opens interactive page)
58+
plugin.uiStep('configure', { page: 'ui', type: 'dialog', width: 600, height: 400 },
59+
async (input, ctx) => {
60+
const result = await ctx.waitForUI({ page: 'ui', props: { ...input } });
61+
return { ok: true, data: result.data };
62+
}
63+
);
64+
65+
plugin.start();
66+
```
67+
68+
See [PLUGIN_SPEC.md](PLUGIN_SPEC.md) for the full `plugin.yaml` specification.
69+
70+
## Development
71+
72+
```
73+
flyto-plugins-js/
74+
├── packages/
75+
│ ├── sdk/ @flyto/plugin-sdk
76+
│ ├── ui-tokens/ @flyto/plugin-ui-tokens
77+
│ └── ui-bridge/ @flyto/plugin-ui-bridge
78+
├── plugins/
79+
│ ├── slack/ @flyto/plugin-slack
80+
│ ├── form-builder/ @flyto/plugin-form-builder
81+
│ └── image-crop/ @flyto/plugin-image-crop
82+
├── tests/ E2E integration tests
83+
└── PLUGIN_SPEC.md plugin.yaml specification
84+
```
85+
86+
## License
87+
88+
Apache-2.0

package-lock.json

Lines changed: 48 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/sdk/README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# @flyto/plugin-sdk
2+
3+
SDK for building Flyto2 plugins in TypeScript/JavaScript.
4+
5+
## Usage
6+
7+
```typescript
8+
import { createPlugin } from '@flyto/plugin-sdk';
9+
10+
const plugin = createPlugin({ id: 'my-org/my-plugin', version: '1.0.0' });
11+
12+
// Register a headless step
13+
plugin.step('echo', async (input, ctx) => {
14+
return { ok: true, data: { message: input.text } };
15+
});
16+
17+
// Register a UI step (opens interactive page during workflow execution)
18+
plugin.uiStep('configure',
19+
{ page: 'ui', type: 'dialog', width: 800, height: 600 },
20+
async (input, ctx) => {
21+
const result = await ctx.waitForUI({
22+
page: 'ui',
23+
props: { initialValue: input.value },
24+
});
25+
if (!result.submitted) {
26+
return { ok: false, error: { code: 'CANCELLED', message: 'User cancelled' } };
27+
}
28+
return { ok: true, data: result.data };
29+
}
30+
);
31+
32+
plugin.start(); // Listen on stdin for JSON-RPC messages
33+
```
34+
35+
## API
36+
37+
### `createPlugin(config)`
38+
39+
Create a new plugin instance.
40+
41+
- `config.id` — Plugin runtime ID (`vendor/name` format)
42+
- `config.version` — SemVer version string
43+
- `config.name` — Optional display name
44+
45+
### `plugin.step(stepId, handler)`
46+
47+
Register a headless step handler.
48+
49+
### `plugin.uiStep(stepId, uiConfig, handler)`
50+
51+
Register a UI-enabled step. `uiConfig`:
52+
53+
- `page` — Path to the UI directory (must contain `index.html`)
54+
- `type``"page"` | `"panel"` | `"dialog"`
55+
- `width` / `height` — Default dimensions in pixels
56+
57+
### `plugin.start()`
58+
59+
Begin listening for JSON-RPC messages on stdin.
60+
61+
## Protocol
62+
63+
Communicates with flyto-core via JSON-RPC 2.0 over stdin/stdout:
64+
65+
- `handshake` — Negotiate protocol version, report available steps
66+
- `invoke` — Execute a step with input and context
67+
- `ping` — Health check
68+
- `shutdown` — Graceful shutdown

packages/sdk/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,18 @@
1818
*/
1919

2020
export { FlytoPlugin } from "./plugin.js";
21+
export { UIServer } from "./ui-server.js";
2122
export type {
2223
PluginConfig,
2324
StepHandler,
2425
StepContext,
2526
StepResult,
27+
StepUIConfig,
28+
UIResult,
29+
UIStepContext,
30+
UIStepHandler,
31+
UIServerConfig,
32+
UIWaitOptions,
2633
JsonRpcRequest,
2734
JsonRpcResponse,
2835
} from "./types.js";

0 commit comments

Comments
 (0)