diff --git a/docs/plugins/examples/multi_capability_skeleton.py b/docs/plugins/examples/multi_capability_skeleton.py index c29f8f6952..ff47b500ce 100644 --- a/docs/plugins/examples/multi_capability_skeleton.py +++ b/docs/plugins/examples/multi_capability_skeleton.py @@ -27,8 +27,6 @@ See ``plugin_development.md`` for the full reference, and ``host_ui_panel.py`` f richer worked example built on the ``orca.host`` read-only API. """ -import json - import orca @@ -85,11 +83,15 @@ class ExamplePostProcess(orca.gcode.GCodePluginCapabilityBase): # --------------------------------------------------------------------------- # # Capability 3 - a printer-connection (agent) capability -# Registers a network printer agent on load. The host serialises each native -# agent call into a single JSON request envelope -# ({command, request_id, dev_id, payload}); you dispatch on request["command"] -# and return a JSON response envelope string. Delete this whole class if your -# plugin is not a printer agent. +# Registers a network printer agent on load. Unlike the other capabilities, an +# agent is driven through the native printer-agent surface: the host calls +# individual operations (connect_printer, start_discovery, start_print, ...) +# directly on your object. orca.printer_agent.PrinterAgentBase declares ~30 +# pure-virtual operations and EVERY one must be overridden - an operation you +# leave out raises RuntimeError the moment the host calls it. This skeleton +# implements just enough to load and be discovered; see +# resources/orca_plugins/BBLPrinterAgentPlugin.py for a complete working agent +# and the full method list. Delete this whole class if you are not writing one. # --------------------------------------------------------------------------- # class ExamplePrinterAgent(orca.printer_agent.PrinterAgentBase): def get_name(self): @@ -103,28 +105,54 @@ class ExamplePrinterAgent(orca.printer_agent.PrinterAgentBase): description="Skeleton printer agent.", ) - def send_command(self, request_json): - # Parse the request envelope and dispatch on its "command". - try: - request = json.loads(request_json or "{}") - except json.JSONDecodeError: - request = {} - command = request.get("command", "") - request_id = request.get("request_id", "") + # --- connection ------------------------------------------------------- # + def connect_printer(self, dev_id, dev_ip, username, password, use_ssl) -> int: + # TODO: open your transport (MQTT/HTTP/serial/...). Return 0 on success. + return 0 - # TODO: handle the commands your device supports and build a real response. - return json.dumps({ - "request_id": request_id, - "status": "error", - "message": f"unhandled command: {command}", - }) + def disconnect_printer(self) -> int: + # TODO: tear the transport down. Return 0 on success. + return 0 - def send_command_with_progress(self, request_json, update_fn, cancel_fn): - # Optional. Override only for long-running commands (uploads, prints). - # update_fn(stage, percent, message) - push progress to the host UI. - # cancel_fn() -> bool - poll it; abort if it returns True. - # Default behaviour is to run as a plain send_command. - return self.send_command(request_json) + # --- discovery -------------------------------------------------------- # + def start_discovery(self, start=True, sending=False) -> bool: + # TODO: start/stop scanning the network for printers. Return True on success. + return True + + # --- messaging -------------------------------------------------------- # + def send_message(self, dev_id, json_str, qos=0, flag=0) -> int: + # TODO: publish a control message to the device. Return 0 on success. + return 0 + + def get_user_selected_machine(self) -> str: + return "" + + def set_user_selected_machine(self, dev_id) -> int: + return 0 + + # --- printing --------------------------------------------------------- # + def start_print(self, params=None, update_fn=None, cancel_fn=None, wait_fn=None) -> int: + # params is an orca.printer_agent.PrintParams. The host also passes callbacks: + # update_fn(stage, percent, message) - report progress to the host UI + # cancel_fn() -> bool - poll it; abort if it returns True + # wait_fn(...) - host-provided wait hook + # Return 0 on success. + return 0 + + # --- filament sync ---------------------------------------------------- # + def get_filament_sync_mode(self): + return orca.printer_agent.FilamentSyncMode.None_ + + def fetch_filament_info(self, dev_id) -> bool: + return False + + # NOTE: PrinterAgentBase has more pure-virtual operations a real agent must + # implement, e.g. send_message_to_printer, bind_detect, bind/unbind, ping_bind, + # check_cert/install_device_cert, request_bind_ticket, start_local_print, + # start_local_print_with_record, start_sdcard_print, start_send_gcode_to_sdcard, + # and the host-callback setters (set_server_callback, set_on_message_fn, + # set_on_printer_connected_fn, set_queue_on_main_fn, ...). See + # BBLPrinterAgentPlugin.py for the full set and expected signatures. # --------------------------------------------------------------------------- # diff --git a/docs/plugins/plugin_audit_hook.md b/docs/plugins/plugin_audit_hook.md index 9d0ee143de..826f7654d4 100644 --- a/docs/plugins/plugin_audit_hook.md +++ b/docs/plugins/plugin_audit_hook.md @@ -87,13 +87,13 @@ allows everything. ```cpp enum class AuditMode { - // Permissive reads, restricted writes. Python must be able to read stdlib - // modules and the plugin file during import/on-load, so reads are allowed; - // only writes outside the allowed roots are blocked. + // Import/loading phase: allow reads anywhere, only block writes + // outside allowed roots. Python needs to read stdlib modules + // during import and those are not inside plugin directories. Loading, - // Restricted reads AND writes: every file path must resolve inside an - // allowed root, or it is blocked. + // Execution phase: block both reads and writes outside allowed + // roots, plus subprocess/socket/ctypes. Enforcing, }; ``` @@ -318,8 +318,10 @@ This version is deliberately minimal. Do **not** treat it as a hardened sandbox. - **Only the `open` event is enforced.** `subprocess.Popen`, `os.system`, `socket.*`, `ctypes.*` and friends are *not* blocked. (The `Enforcing` enum comment describes an aspiration, not current behavior.) -- **`os.open` slips through.** It raises the `open` event with `mode = None`, so the - `"s|si"` parse fails and the call is allowed. Low‑level opens are currently unaudited. +- **Non‑string paths slip through the `open` check.** The audit callback parses only a + string path (`"s|si"`); any `open`‑event call whose first argument is bytes or an integer + file descriptor — including `os.open`, which additionally passes `mode = None` — fails the + parse and is allowed. Low‑level and non‑`str` opens are currently unaudited. - **`open(path, "x")`** (exclusive create — a write) contains no `w`/`a`/`+`, so it is classified as a read and allowed under `Loading`. - **Non‑`open` filesystem mutations are unaudited.** `os.remove`, `os.rename`, `os.mkdir`, diff --git a/docs/plugins/plugin_development.md b/docs/plugins/plugin_development.md index aa3150b3b6..37de4da6ba 100644 --- a/docs/plugins/plugin_development.md +++ b/docs/plugins/plugin_development.md @@ -103,7 +103,7 @@ fields live at the TOML root. Parsing is implemented in | `author` | `[tool.orcaslicer.plugin]` | optional | | | `version` | `[tool.orcaslicer.plugin]` | recommended | | | `dependencies` | TOML root | optional | array of pip requirements (see [Dependencies](#dependencies)) | -| `requires-python` | TOML root | optional | **stored but not enforced** against the bundled interpreter today | +| `requires-python` | TOML root | optional | **read but not stored or enforced** against the bundled interpreter today | > **The metadata block no longer declares a `type`.** A plugin's type(s) are derived from the > capability classes it registers (each capability's `get_type()`), so the same metadata block @@ -396,14 +396,15 @@ class SamplePlugin(orca.base): OrcaSlicer instantiates the package class (it must be callable as `SamplePlugin()` with no arguments), calls `register_capabilities()`, then instantiates each registered capability. -Rules enforced by `PythonPluginBridge.cpp`: +Rules enforced when a plugin loads (most in `PythonPluginBridge.cpp`): - The `@orca.plugin` class **must** subclass `orca.base`, and there must be **exactly one** per file — a second `@orca.plugin` fails the load. - Each class passed to `orca.register_capability` must subclass a capability base (ultimately `orca.PythonPluginBase`); otherwise it raises `value_error`. -- Every capability must resolve `get_name()`, and the resulting `(type, name)` pair must be - unique across the plugin — a duplicate is rejected. +- Every capability must resolve `get_name()` (checked in the bridge); the loader + (`PluginLoader.cpp`) additionally rejects the plugin if the resulting `(type, name)` pair is + not unique across it. - A capability class you never pass to `register_capability` is **invisible** to OrcaSlicer, even if it is defined in the file. @@ -913,7 +914,7 @@ For your new type, add a call site (or an on‑capability‑load callback) that: List your new `.hpp` / `.cpp` files in `src/slic3r/CMakeLists.txt`, alongside the existing plugin‑type sources (search for `plugin/pluginTypes/gcode/GCodePluginCapability.cpp` — the block is -around lines 613–621): +around lines 615–623): ```cmake plugin/pluginTypes//PluginCapability.hpp @@ -1011,5 +1012,5 @@ best covered by the manual steps above. | `src/slic3r/plugin/PythonInterpreter.cpp` | interpreter init, audit‑hook install, traceback formatting, `stderr` → log file | | `src/slic3r/GUI/PluginsDialog.cpp` | Plugins dialog: details/error area, script **Run**, error dialogs | | `src/slic3r/GUI/PostProcessor.cpp` | resolves the preset's plugin refs and invokes post‑processing (G‑code) capabilities during export | -| `src/slic3r/CMakeLists.txt` (~602–610) | build list for plugin sources | +| `src/slic3r/CMakeLists.txt` (~609–623) | build list for plugin sources | | [`plugin_audit_hook.md`](plugin_audit_hook.md) | the audit hook: modes, allow‑list, extending it | diff --git a/docs/plugins/plugin_system.md b/docs/plugins/plugin_system.md index e8375c3387..330e1519a7 100644 --- a/docs/plugins/plugin_system.md +++ b/docs/plugins/plugin_system.md @@ -255,26 +255,33 @@ Each entry is a single string with three `;`‑separated fields: entries drop out. - Parsing/serialization lives in `Config.cpp` (`parse_capability_ref` → `PluginCapabilityRef{ name, capability_name, uuid }`); the `plugins` option is defined in - `PrintConfig.cpp` and is a **process/print** preset setting. See + `PrintConfig.cpp` and is tracked on **process, printer, and filament** presets. See `tests/libslic3r/test_config.cpp` and `tests/slic3rutils/test_plugin_capability_identifier.cpp`. ## Restoring missing plugins -When a slice is started (`Plater::reslice`), OrcaSlicer resolves the active preset's `plugins` -array against the loaded catalog. Any reference that is not installed is **missing**, and a -dialog appears before slicing continues. Missing references are split by whether they carry a -cloud UUID: +When you prepare to slice, OrcaSlicer resolves the active process, printer, and filament +presets' `plugins` arrays against the loaded catalog (`Plater::refresh_missing_plugin_block`). +Any reference it cannot satisfy is surfaced as a **non-closable notification**, and while any +remain the **Slice button stays blocked** — there is no "slice anyway" path; you resolve the +reference (or change the setting that pulls it in). References are sorted into four buckets: -- **Missing OrcaCloud plugins** (have a UUID) — the dialog offers **Install plugins**, which - subscribes to, installs, loads, and enables each one so it is usable immediately, or - **Continue without plugins**. -- **Missing local plugins** (no UUID) — these cannot be fetched automatically, so the dialog - offers **Open OrcaCloud** (a browser search for similarly named plugins on the OrcaCloud - plugins explore page) or **Continue without plugins**. +- **Missing OrcaCloud plugins** (ref carries a UUID) — notification action **Install Plugins**, + which subscribes to, installs, loads, and enables each one so it becomes usable immediately. +- **Missing local plugins** (no UUID) — cannot be fetched automatically; the action **Find on + OrcaCloud** just opens a browser search on the OrcaCloud plugins page. It is a suggestion + only: it neither closes the notification nor unblocks slicing. +- **Inactive plugins** — the package is installed locally but the referenced capability is not + active (plugin not loaded, or capability disabled). Action **Activate Now** loads/enables it + locally, with no download. +- **Broken references** — the plugin is installed and loaded but no longer provides the + referenced capability (renamed/removed/outdated). Activation cannot fix this, so it is + informational, with **Find on OrcaCloud** to look for an update. -Choosing *Continue without plugins* proceeds with the slice; the functionality those plugins -would have provided is simply skipped. +The bucketing lives in `PluginResolver` (`get_missing_cloud_plugins`, `get_missing_local_plugins`, +`get_inactive_plugins`, `get_broken_plugins`); the notifications and the slice block are driven +from `Plater.cpp` (`refresh_missing_plugin_block`). ## The Plugins dialog diff --git a/src/slic3r/plugin/PluginManager.hpp b/src/slic3r/plugin/PluginManager.hpp index 5b2120947f..5655835757 100644 --- a/src/slic3r/plugin/PluginManager.hpp +++ b/src/slic3r/plugin/PluginManager.hpp @@ -31,7 +31,7 @@ public: ~PluginManager(); - // Initialize plugin system (no longer initializes Python — that happens lazily on first load_plugin) + // Initialize the plugin system, eagerly starting the embedded Python interpreter on the main thread. bool initialize(); // Stop discovery and unload Python plugin objects before Python finalizes. diff --git a/src/slic3r/plugin/PythonInterpreter.cpp b/src/slic3r/plugin/PythonInterpreter.cpp index c3f9313c1f..be65f0467e 100644 --- a/src/slic3r/plugin/PythonInterpreter.cpp +++ b/src/slic3r/plugin/PythonInterpreter.cpp @@ -622,8 +622,9 @@ bool PythonInterpreter::initialize() BOOST_LOG_TRIVIAL(info) << "Bundled uv executable not found"; // Install the CPython audit hook for plugin policy enforcement. - // This is defense-in-depth: it monitors file/subprocess/socket/ctypes - // access from plugin code. It is NOT a full security sandbox. + // This is defense-in-depth: today it only inspects the `open` audit event + // and blocks writes outside the allowed roots; subprocess/socket/ctypes and + // other events are not yet handled. It is NOT a full security sandbox. PluginAuditManager::instance().install_hook(); // Persist Python stderr (plugin tracebacks, including uncaught