diff --git a/crates/ocs_plugin_api/REPORT.md b/crates/ocs_plugin_api/REPORT.md deleted file mode 100644 index 1509fd1c5..000000000 --- a/crates/ocs_plugin_api/REPORT.md +++ /dev/null @@ -1,193 +0,0 @@ -# Out-of-Process Plugin Architecture — `ocs_plugin_api` - -**Status:** Design proposal -**Scope:** `crates/ocs_plugin_api` only (minimal host wiring, no plugin changes, no new crate) - ---- - -## 1. Problem - -The host currently loads external add-ons as `cdylib` libraries into its own process via `libloading`. `panic::catch_unwind` catches Rust panics, but a plugin can still corrupt host memory, segfault, or deadlock the UI thread. The fix is to run each plugin as a separate OS process and mediate all interaction through IPC. - ---- - -## 2. Design - -`ocs_plugin_api` becomes a dual-use library: - -- **Plugin side:** unchanged API surface (`BuiltinPlugin`, `HostApi`, `CadModule`, `export_plugin!`). -- **Host side:** runtime that spawns plugin processes and handles their IPC requests. - -Plugins remain `cdylib`s. The host spawns **itself** in runner mode (`--ocs-plugin-runner `) to load each cdylib in a child process and bridge to the host over `interprocess::local_socket`. The runner implementation lives inside `ocs_plugin_api` as a library module; no separate helper binary is needed, so the runner and host are always the same build and cannot get out of sync at deployment time. - ---- - -## 3. Constraints - -| Constraint | Handling | -|---|---| -| Plugin API unchanged | Trait/type signatures preserved. `document()` / `document_mut()` keep their signatures but return a local cached copy, so `API_VERSION` bumps to 3. | -| Only `ocs_plugin_api` modified | All new code lives here. The host needs only minimal call-site wiring in `src/plugin/external.rs`, `src/plugin/registry.rs`, and `src/app/plugin_host.rs`. | -| No new crate | Runner code lives inside `ocs_plugin_api`; the host executable serves as the runner process. | -| Platform-independent | `interprocess::local_socket` uses named pipes on Windows and Unix domain sockets elsewhere; self-spawning works on every host target. | - ---- - -## 4. Architecture - -```text -Host process Plugin process -┌─────────────────┐ local socket ┌─────────────────┐ -│ HostSession │◄───────────────►│ HostApi proxy │ -│ (document, UI) │ bincode frames │ (sends RPCs) │ -└─────────────────┘ └─────────────────┘ - ▲ │ - │ ▼ - PluginManager cdylib loaded - (spawn / kill / by host in - supervise) runner mode -``` - -### 4.1 IPC protocol - -All messages are length-framed and serialized with `bincode`. - -**Host → plugin:** - -- `GetManifest`, `GetRibbon` -- `Dispatch { cmd: String }` -- `InteractiveEvent { command_id, event }` -- `Shutdown` - -**Plugin → host:** - -- `PushInfo` / `PushOutput` / `PushError` -- `AddEntity(SerializedEntity)` → `Handle` -- `BumpGeometry`, `PushUndo`, `SetDirty` -- `ReadRecord`, `WriteRecord`, `RemoveRecord` -- `StartInteractive`, `PollInteractive` -- `DocumentSnapshot` → `SerializedDocument` - -`SerializedEntity`, `SerializedRecord`, and `SerializedDocument` are `acadrust` types with `Serialize` / `Deserialize` derived. - -### 4.2 Plugin-side runtime - -`OpenCADStudio --ocs-plugin-runner `: - -1. Loads the cdylib, validates `ocs_plugin_api_version`, calls `ocs_plugin_register`. -2. Connects to the host socket and answers `GetManifest` / `GetRibbon`. -3. Runs a request loop: dispatch commands, forward interactive events. - -`PluginHostApi` implements `HostApi` by sending RPCs. `document()` / `document_mut()` return a local copy fetched from `DocumentSnapshot`; mutations are **not** automatically synced back. Plugins use `add_entity`, `write_record`, etc. for host-visible changes. - -### 4.3 Host-side runtime - -`ocs_plugin_api::process` provides: - -- `PluginProcess::spawn(cdylib_path)` — creates a socket, launches the host executable in runner mode, accepts its connection. -- `PluginManager` — spawn all discovered plugins, supervise, kill. -- `serve_plugin_connection(stream, &mut dyn HostApi)` — host-side request handler. - -The host creates a `HostSession` and passes it to `serve_plugin_connection`; existing document/undo logic is reused. - -### 4.4 Ribbon - -Ribbon types use `&'static str`, which cannot cross a socket. Define owned equivalents in `ocs_plugin_api::ribbon::owned` and convert for IPC. The host reconstructs `RibbonGroup` once per load (e.g., by leaking the owned strings), avoiding changes to ribbon rendering code. - -### 4.5 Failure handling - -| Failure | Behavior | -|---|---| -| Plugin crash / hang / malformed message | Host marks plugin dead, drops its ribbon tab, logs the error, and continues running. | -| Plugin panics | Caught inside the runner; an error response is returned to the host. | -| Spawn failure | Reported through `PluginManager` and shown in the Plugin Manager. | - ---- - -## 5. Host Integration Points - -1. **`src/plugin/external.rs`** — replace `libloading`-based `LoadedPlugin` with `PluginProcess::spawn`. -2. **`src/plugin/registry.rs`** — use `PluginProcess` for ribbon collection and command dispatch. -3. **`src/app/plugin_host.rs`** — add an IPC request bridge that maps incoming messages to `HostSession` calls. - -No changes to `docs/plugin-template` or any other plugin. - ---- - -## 6. Crate Changes - -### 6.1 New files inside `crates/ocs_plugin_api` - -```text -src/ - ipc/ - protocol.rs # HostRequest / PluginRequest / PluginResponse - transport.rs # framed read/write over local_socket - client.rs # plugin-side IpcClient + PluginHostApi - server.rs # host-side serve_plugin_connection - process.rs # PluginProcess / PluginManager - runner.rs # plugin runner logic invoked by host in runner mode -``` - -### 6.2 Dependencies - -Add under the existing `host` feature: - -```toml -[dependencies] -interprocess = { version = "2", optional = true } -serde = { version = "1", features = ["derive"], optional = true } -bincode = { version = "1", optional = true } -thiserror = { version = "1", optional = true } -libloading = { version = "0.8", optional = true } - -[features] -host = ["dep:acadrust", "dep:interprocess", "dep:serde", "dep:bincode", "dep:thiserror", "dep:libloading"] -``` - ---- - -## 7. API Version - -Bump `API_VERSION` to `3` because `document()` / `document_mut()` semantics change from direct host references to local cached copies. v2 plugins are refused as usual; plugin authors recompile with `ApiVersion::CURRENT`. - ---- - -## 8. Implementation Plan - -1. Add dependencies to `Cargo.toml`. -2. Implement framed transport and protocol messages. -3. Implement runner logic in `runner.rs`. -4. Implement `PluginHostApi` proxy. -5. Implement host-side server and `PluginManager`. -6. Add owned ribbon conversions. -7. Wire the three host call sites and add `--ocs-plugin-runner` dispatch in `src/main.rs`. -8. Bump `API_VERSION` and update `docs/plugin-architecture.md`. - ---- - -## 9. Testing - -- **Unit:** protocol round-trip, ribbon conversion, proxy request emission. -- **Integration:** spawn a test plugin, verify dispatch and interactive command round-trip, kill the process and confirm the host survives. -- **Host:** update registry tests once `LoadedPlugin` is replaced by `PluginProcess`. - ---- - -## 10. Compliance with `AGENT.md` - -| Requirement | Status | -|---|---| -| `ocs_plugin_api` is a library, not a plugin | Yes | -| Plugin API source-compatible | Yes; signatures are unchanged. `document()` semantics change is gated by v3. | -| Separate processes + failure management | Yes | -| Platform-independent `interprocess` IPC | Yes | -| Only `ocs_plugin_api` modified | Code: yes. Host needs minimal wiring; unavoidable because `OpenCADStudio` / `HostSession` are host-private. | -| No new crate | Yes | -| Memory / process isolation | Yes | - ---- - -## 11. Summary - -`ocs_plugin_api` absorbs the plugin runtime: the host spawns itself in runner mode to load each cdylib in its own process, and all host/plugin interaction is serialized over `interprocess` local sockets. Plugin API signatures stay intact, host changes are limited to a few call sites, and no new crate is introduced.