Standalone Salamatrix runtime providers

Runtime providers are optional Salamander plugins (.SPL, i.e. DLLs) and do not depend on the Automation plugin. Python, PowerShell, PHP, and Node discover a separately installed interpreter. Lua Runtime instead includes a pinned Lua interpreter built by vcpkg, while retaining an explicit override and PATH fallback. Every provider owns its worker bootstrap and registers an adapter with the already loaded Salamatrix.Runtime broker.

The runtime providers and the standalone SalamatrixAI.SPL use the conventional plugin metadata structure: their resource script includes versinfo.rh2 and versinfo.rc2, their entry point uses the VERSINFO_* metadata constants, and their Plugin Manager homepage is https://samandarin.net/. SalamatrixAI uses the shared Framework icon at src/res/sal_r.ico; it remains a separate Menu Extension plugin and does not move AI ownership back into Automation.

See Developing a Salamatrix language runtime provider for the native ABI, SMX1 worker, lifecycle, packaging, and verification requirements for adding another language. See Salamatrix.UI framework and custom dialog guide for the shared native control catalog and examples in every supported runtime.

Native UI dark mode is owned by the Salamatrix Framework provider. It reads the host's explicit Windows Dark Mode (experimental) scheme and current scheme colors, configures the existing USE_DARKMODELIB=1 integration, refreshes dialog controls and title bars on theme/configuration broadcasts, and routes runtime/UI-service Unicode message boxes through the same dark-aware helper.

Provider lifecycle

void WINAPI SalamanderPluginEntry(...)
{
    IRuntimeService* broker = QueryService(
        SalamanderGeneral,
        SALAMATRIX_SERVICE_RUNTIME,
        SALAMATRIX_RUNTIME_VERSION_1_0);
    // If Salamatrix.SPL is not loaded yet, keep the plugin valid and retry
    // from Connect/Event after the framework provider appears.
    TryRegisterProvider();
    return &PluginInterface;
}

BOOL WINAPI CPluginInterface::Release(HWND, BOOL)
{
    providerRegistration.Unregister();
    return TRUE;
}

RuntimeProviderRegistration in salamatrix_runtime_api.h retains the exact broker/adapter pair and unregisters it during provider release. This keeps unload independent from Automation and lets native plugins, Automation, and other runtime providers resolve the same descriptor through IRuntimeService.

Package split

Extension packages are framework-owned directories, not Automation scripts. The default installed package root is $(SalDir)\extensions; additional roots are configured by Salamatrix under ExtensionRoots. Each package contains extension.json, one runtime entry point, and optional package-owned SVG assets. Salamatrix owns recursive discovery, validation, publication and worker lifecycle. The native Plugin Manager consumes the shared Salamatrix.Extensions catalog: it shows the package row and icon, activates or deactivates the worker, and exposes declared commands in the Plugin menu, panel context menu, and toolbar. Automation is not required for package discovery or activation.

Four complete package fixtures live in src/extensions/demos: Node.js, CPython, PowerShell, and PHP CLI. Their run commands demonstrate the common notification and typed-storage API.

The intended packages are:

Package Runtime id Interpreter discovery
PythonRuntime.SPL Python.CPython SALAMATRIX_PYTHON, python.exe, python3.exe
PowerShellRuntime.SPL PowerShell SALAMATRIX_POWERSHELL, pwsh.exe, powershell.exe
PHPRuntime.SPL PHP.CLI SALAMATRIX_PHP, php.exe
JavaScriptRuntime.SPL JavaScript.Node SALAMATRIX_NODE, node.exe, node
LuaRuntime.SPL Lua SALAMATRIX_LUA, bundled runtime\lua.exe, then lua.exe/lua55.exe/lua54.exe on PATH

Python candidates must successfully identify themselves as Python 3 before the provider publishes them as available. Discovery continues past unusable PATH entries, including the Windows Microsoft Store placeholder aliases, while a working Store-installed Python execution alias remains supported.

Automation keeps its legacy JScript/VBScript ActiveScript adapters. It becomes just another broker consumer for the providers above; installing or loading a provider must not require Automation. A manifest's runtime id and minimum version select the provider through the broker, while a missing provider is a clear unavailable-runtime result.

The standalone SalamatrixAI.SPL is likewise separate from Automation. It owns the local assistant provider, chat window, and localized Ask Salamatrix AI... menu command; its menu extension uses the same text as a bounded fallback when the plugin resource lookup is unavailable. Automation does not publish a duplicate AI menu item or own the chat flow; it retains only the shared Salamatrix.AI host/API bridge needed by runtime scripts and the Salamatrix.ScriptRunner compatibility service.

The current branch has the broker contract, worker protocol, provider lifecycle helper, and framework-owned manifest parser in src/plugins/salamatrix/salamatrix_manifest.*. PythonRuntime.SPL, PowerShellRuntime.SPL, PHPRuntime.SPL, JavaScriptRuntime.SPL, and LuaRuntime.SPL now have their own projects, adapters, worker assets, and load/unload registration paths. Debug and Release x64 builds produce the standalone .SPL binaries; provider registration is deferred safely when Salamatrix is loaded later. No provider should be made a dependency of Automation.

Current worker UI surface

The five modern workers expose the same Salamatrix dialog surface. Their language facades differ only in naming and async syntax; they send the same salamander.ui.dialog.* methods and option fields to the package dispatcher owned by Salamatrix Framework. Automation is not a dependency of this path. All five facades expose the same optional resizable dialog flag. Resizable dialogs retain compact native controls while the framework stretches content and keeps lower/right controls anchored; font, DPI, and selected color scheme remain framework-owned.

The common control set contains labels, host static text, text boxes, check/radio buttons, combo boxes, ordinary buttons, ListView/TreeView/TabControl, folder/file pickers, group boxes, host hyperlinks and progress bars, arrow/text-arrow/color-arrow buttons, and toolbar headers. The generic add operation forwards explicit bounds plus style flags, path separators, tooltips, hyperlink actions, known/indeterminate progress, 64-bit progress values, colors, and toolbar-header button masks.

Each bundled runtime demo builds the complete 463 x 236 capabilities gallery itself and identifies its runtime and extension in the Created by group. That makes runtime parity directly inspectable in JavaScript, Python, PowerShell, PHP, and Lua source instead of hiding the layout behind a prebuilt showcase call. The complete cross-language reference is in doc/salamatrix-ui.md.

Each worker also exposes a folder picker embedded in a dialog:

Runtime Dialog method
Python dialog.add_folder_picker(id, path="")
PowerShell $dialog.AddFolderPicker(id, path)
PHP $dialog->addFolderPicker(id, path)
Node await dialog.addFolderPicker(id, path)
Lua dialog.add_folder_picker(id, path)

It maps to the runtime protocol control kind folderpicker, opens the standard native folder browser when clicked, and returns the chosen UTF-8 path through the normal dialog get/control-text mechanism. For editable file paths, the same workers additionally expose add_file_picker/ AddFilePicker/addFilePicker; this maps to filepicker, keeps the path in an editable native edit control, and places a separate wide Win32 browse button next to it.

The editable file picker accepts optional filter and save-mode values. The SMX1 payload stays flat and appends filter (UTF-8 pipe-separated description/pattern pairs) and save (boolean) to the existing dialog-add payload:

Runtime Dialog method
Python dialog.add_file_picker(id, path="", layout=None, filter="", save=False)
PowerShell $dialog.AddFilePicker(id, path, filter, save)
PHP $dialog->addFilePicker(id, path, filter, save)
Node await dialog.addFilePicker(id, path, layout=null, filter="", save=false)
Lua dialog.add_file_picker(id, path, filter, save)

An omitted or empty filter uses the all-files fallback. save=true selects the native save dialog and enables overwrite prompting; the selected UTF-8 path continues to use the normal dialog control-text/get contract.

All five facades also expose a host-uptime helper backed by salamander.host.uptime. It returns a decimal millisecond string so 32-bit language integer limits do not truncate long-running systems.

Command state

All modern workers accept optional enabled and visible fields when registering commands. They also expose the same append-only state update operation:

Runtime Registration State update
Python commands.register(..., enabled=True, visible=True) commands.set_state(id, enabled=None, visible=None)
PowerShell $Salamander.Commands.Register(..., $Enabled, $Visible) $Salamander.Commands.SetState(id, $Enabled, $Visible)
PHP $Salamander->commands->register(..., $enabled, $visible) $Salamander->commands->setState($id, $enabled, $visible)
Node commands.register(..., enabled, visible) commands.setState(id, enabled, visible)
Lua commands.register(id, title, options) commands.set_state(id, enabled, visible)

The host applies these values to the existing Automation command record and posts the normal Plugin Manager/menu refresh. Hidden commands are omitted from the native menu and disabled commands remain visible but non-invokable. This does not add a public vtable method or require a separate Extension Manager.

Verification at the current pause point: all five provider projects have verified x64 build paths and their worker files pass available Python, PowerShell, PHP, Node, and Lua syntax/load checks. The isolated process-runtime integration run now also passes with the standalone provider worker assets: with SALAMATRIX_WORKER_ROOT explicitly set to build\verification\command-state\worker-root, the Python/PowerShell/PHP process test executable returned exit code 0 and completed the SMX1 host-call, persistent-session, UI, storage, event, picker, command-state, shutdown, output-capture, and timeout scenarios. The lifecycle assertions verify the append-only IRuntimeSession::GetDiagnostic contract for running, explicit host stop, clean exit, and nonzero failed exit, including cached process id, exit code, error code, and bounded message. The provider projects contain the same diagnostic behavior even though the process-runtime executable exercises the Automation-side adapter. No Salamander process was started or controlled.

The file-picker option slice was additionally rebuilt into build\verification\file-picker-options. The explicit worker-root run verified filter and save=true for Python, PowerShell, and PHP without starting or controlling Salamander.

The Plugin Manager/AI integration slice was verified separately in build\verification\regressions: the core Debug x64 build and standalone SalamatrixAI.SPL Debug x64 build both passed, and the source-contract test verified the four runtime labels, the localized AI chat command, and the framework-first unload guard. The AI helper does not add a runtime dependency or installer; it continues to consume the shared Salamatrix services. No Salamander process was started or controlled.

Manifest settings migrations are intentionally host-side: the Salamatrix package manager applies the bounded typed rename/remove chain before publishing the shared storage.schema() view. All four provider workers therefore keep the same storage wire contract and receive already-migrated values without a runtime-specific migration implementation. The same workers expose storage.keys() (Python), Storage.Keys() (PowerShell), storage->keys() (PHP), and Storage.keys() (Node); the host returns typed UTF-8 key records in deterministic case-insensitive order.

RuntimeSessionDiagnostic is a bounded value snapshot. It reports lifecycle state, process id, exit code, and a host/provider error code without exposing a process handle or provider-owned string. Current process providers additionally retain the process id and exit code after Stop(), report explicit host stops as Stopped, clean exits as Exited, nonzero exits as Failed, and include a bounded message. The default ABI-compatible implementation still derives the running/exited state from the existing session methods, so older providers remain usable while newer providers can append richer diagnostics.

Tab lifecycle event bridge

The native Salamatrix.Events service keeps its original vtable and event payload prefix. Event kinds 16–20 are append-only:

Event name Meaning
tabCreated A tab id exists in the current side snapshot but not the previous one.
tabClosed A previous tab id is absent from the current side snapshot.
tabReordered The tab order changed while the side retained the same tab ids.
windowDetached A tab's detached flag changed from clear to set.
windowAttached A tab's detached flag changed from set to clear.

The host compares heap-backed left/right tab snapshots when the core emits PLUGINEVENT_TABCHANGED. The first snapshot is only a baseline, including an empty side; the next transition from zero tabs therefore reports tabCreated. Inferred lifecycle events are published before the existing tabChanged notification. Their appended payload fields are changedTabId, tabIndex, and previousTabIndex; tabId in the worker JSON frame remains the active tab id for compatibility. Older V1 payloads are still accepted for legacy events and receive 0/-1 defaults for the appended JSON fields.

Manifest extensions may subscribe only to event names in the existing Plugin Manager manifest allow-list. The native Events schema, runtime event bridge, and SalamatrixAI focused events API slice publish the same names. This is snapshot inference, not a new core Salamander event ABI; direct create/close/reorder/window hooks remain a later GAP item.

The slice was rebuilt into build\verification\tab-lifecycle and verified by the native event tests, manifest parser tests, runtime protocol/schema tests, Automation/Salamatrix Debug x64 builds, all five standalone provider builds, all five worker syntax/load checks, and the explicit isolated SALAMATRIX_WORKER_ROOT Python/PowerShell/PHP process-runtime test. No Salamander process was started or controlled.

Tab mutation worker contract

The implemented Sides version 1.3 contract exposes these host calls:

Host method Arguments Return
salamander.sides.createTab {side:string, path?:string, index?:int} {created:true, tabId:string}; tabId is decimal text.
salamander.sides.closeTab {tabId:string} {ok:true}
salamander.sides.reorderTab {tabId:string, index:int} {ok:true}
salamander.sides.moveTab {tabId:string, side:string, index?:int} {ok:true}
salamander.sides.setDetached {detached:bool} {ok:true, detached:bool}

Python exposes these as source_side.create_tab(path=None, index=-1), close_tab, reorder_tab, move_tab(side, index=-1), and set_detached. Node, PHP, and PowerShell retain their existing camelCase/PascalCase naming conventions. The deterministic bootstrap and dispatcher-count test is isolated under build\verification\tab-object-model; it does not start or control Salamander.

SalamatrixAI ownership

SalamatrixAI.SPL is the sole native owner of the local assistant workflow. Its menu extension opens the chat and owns the complete context, localization, repair/refinement, preview, clipboard, save, run, and extension-package export flow. The package writer emits the manifest and runtime entry script for the existing Plugin Manager discovery path.

The Automation plugin remains an independent legacy JavaScript/VBScript host and shared Salamatrix runtime client. It no longer contains package discovery, package lifecycle, or the assistant command workflow. It continues to provide the shared Salamatrix.AI and Salamatrix.ScriptRunner services used by compatible clients. IScriptRunner::RefreshExtensions remains an append-only compatibility seam for helper clients.

The ownership restoration was verified by the source-contract test and isolated Debug x64 builds in build\verification\ai-restored-logic-final and build\verification\automation-restored-logic-final; no Salamander process was started or controlled.

Shutdown and unload safety

The runtime providers borrow the Salamatrix.Runtime broker; they do not own it. During application shutdown the Framework provider can therefore be unloaded before a standalone runtime provider. Each provider now validates the currently published SALAMATRIX_SERVICE_RUNTIME pointer before unregistering its adapter. A missing or replaced broker causes only the provider's local registration state to be reset; UnregisterAdapter is called only while the original broker is still live.

This prevents the stale-broker virtual call reported by the JavaScript runtime shutdown crash. The four providers were rebuilt in the isolated build\verification\shutdown-guard-* directories and the source regression test passed. No Salamander process was started or controlled.

The shared runtime header includes src/darkmode.h directly instead of depending on a provider-specific precompiled header to expose dark-mode declarations. This keeps Salamatrix.Poc consumers such as demoplug self-contained while retaining each project's existing USE_DARKMODELIB configuration. The isolated demoplug Debug x64 verification build passed.

The shared src/res/sal_r.ico artwork is intentionally used only by the Salamatrix Framework and SalamatrixAI plugins. The five standalone runtime providers do not define a custom plugin icon resource and therefore keep the standard Plugin Manager fallback icon. SalamatrixAI registers resource ID 1030 through its normal GUI icon-list callbacks during Connect.

The standalone SalamatrixAI provider initializes its borrowed CSalamanderGUIAbstract pointer during SalamanderPluginEntry, before Connect() is reached. Its dynamic Ask Salamatrix AI... command is added with MENU_SKILLLEVEL_ALL and remains enabled while the shared Framework services are resolved lazily. Selecting the menu item or its Plugin Manager keyboard shortcut therefore reaches the same ExecuteMenuItem() path; ShowChat() still reports the existing Framework-not-loaded warning when the optional services are unavailable.