Files
LangBot/plugins/misc/FAQManager/SHARED_RUNTIME.md
T

4.7 KiB

Shared runtime placement (shared-runtime-v1 + stateless-v1)

This source declares execution.sharedRuntime: shared-runtime-v1 and execution.componentModel: stateless-v1 in manifest.yaml. It is a source declaration only: it is not certified, has no signed archive, and has not been accepted against a live multi-Workspace deployment. The exact archive still needs independent review and acceptance runs.

Under shared placement one FAQManagerPlugin object serves every installation of the artifact digest, and the manager Page, dashboard Page, search_faq Tool and EventListener all reach their data through that single object.

Per invocation

  • get_installation_binding() is read per invocation and never retained beyond the call; it is the only authority for whose entries are served.
  • Every entry point (get_entries, search, add_entry, update_entry, delete_entry, persist, and therefore every Page/Tool call) resolves the invoking installation's state on entry, so no request can be answered from another installation's cache.
  • This plugin declares no config, so no tenant configuration is captured on the shared object.
  • The manager page renders entry text through DOM nodes and element properties (textContent / value) and binds its controls with addEventListener; no entry field is interpolated into markup, so a stored question or answer cannot add attributes or event handlers to the page.

Per binding, released on revocation

  • _states is keyed by the installation scope <instance_uuid>:<workspace_uuid>:<installation_uuid> and holds that installation's entries list and its async lock. runtime_revision is deliberately excluded from the token so a worker upgrade does not orphan a tenant's data.
  • BasePlugin.on_installation_revoked(binding) pops exactly that key. It touches process-local state only, because no Host API is available during revocation.
  • Durable state is written under faq_entries:<instance>:<workspace>:<installation>. The Host row key ([instance_uuid, workspace_uuid, owner_type, owner, key]) has no installation dimension, so the installation identity has to live in the plugin's own key; without it, two installations in one Workspace would share a row.
  • Dedicated (binding-less) workers keep the legacy faq_entries key and a single in-process list, i.e. their existing behaviour.

Known limits

  • initialize() is process-scoped and does nothing: it runs once per worker with an empty config and, under shared placement, before any installation is known.
  • A shared worker that gets an invocation without a trusted installation binding raises instead of falling back to a binding-less key, so such a request is refused rather than served with shared entries. A route that does not carry the installation binding therefore cannot work under shared placement.
  • Legacy data migration is not performed: on shared placement a pre-existing shared faq_entries row is no longer read (each installation starts empty). Dedicated workers keep reading the legacy key unchanged.
  • The entries list handed to a Page response is the live cached list; it is not copied, so a concurrent writer for the same installation can change it while a response is being serialized.
  • The per-installation cache is unbounded in size until revocation; the runtime revokes on uninstall, disable, Workspace removal and upgrade.
  • Revocation is serialized per installation but may overlap with sibling installations' invocations; per-installation state is locked, cross-installation state is not shared.

Tests

tests/test_installation_scoped_faq.py drives two distinct InstallationBindings (same instance and Workspace, different installation uuids) through one plugin object with the installed SDK's bind_invocation, over a synthetic Host KV whose row key mirrors LangBot's. It asserts that a write from one installation never appears in the other's entries, search results, Page or Tool output, that a fresh object ("restart of the loader") reloads each installation's own row, that revocation drops only the revoked binding, and that dedicated binding-less workers keep the legacy key.

tests/test_page_attribute_injection.py renders components/pages/manager/index.html with tests/page_dom_probe.js (Node) against a recording DOM, with a question of " onfocus="alert(1)" data-x=" and an answer containing </textarea><img src=x onerror=...>. It asserts that no markup string carries the entry text, that parsing every markup string yields no event-handler attribute, and that the list and the edit form still show the entry through element properties (pre-fix, the edit form produced an onfocus attribute).