6.2 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 DailyLimitPlugin object and one of every declared
component serve every installation of the artifact digest. Installation
attachments are lightweight contexts; they do not create another object graph.
Per invocation
self.get_config()is read on every invocation, and no config value, credential or installation identity is stored on the shared object. The invocation config is only a seed: it is read when an installation first loads its own row, and that installation's stored settings win from then on.get_installation_binding()is read per invocation and never retained beyond the call. It is the only authority for which installation's data is served.- The EventListener, Page and Tool paths all resolve the invoking installation's state on entry, so a request can never be answered from another installation's cached data.
- The management page renders session ids and labels through DOM nodes, element
properties and listener closures (
textContent,value,addEventListener); no session field is interpolated into markup, so a stored id cannot add attributes or event handlers to the page.
Per binding, released on revocation
_statesis keyed by the installation scope<instance_uuid>:<workspace_uuid>:<installation_uuid>and holds that installation'ssettings,sessionsand its async lock.runtime_revisionis 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
daily_limit_state:<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
daily_limit_statekey and a single in-process state, i.e. their existing behaviour.
Concurrency and durability
- Every change of an installation's state runs inside that installation's async
lock and consists of read, modify, serialize and await of the Host write: the
EventListener counting path and the management page's
update_settings,set_session_limit,reset_session,reset_allanddelete_sessionall go through_transaction. The stored row is overwritten whole, so an unlocked writer could commit a snapshot that a concurrent writer had already moved past and roll the newer state back when the two Host writes complete out of call order.snapshot()reads under the same lock. - If a mutation or its write fails, or the call is cancelled, the in-memory state is restored to the last snapshot that actually landed, so memory and the durable row do not diverge. The counting path opts out of that rollback on purpose: a message that was already answered keeps consuming quota, and the next successful write persists the whole snapshot again.
update_settingsvalidates a copy of the patched settings before swapping it in, so a rejected value cannot leave a partially updated settings dict behind.
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.- Settings are seeded from the invocation config when an installation first loads and are then owned by that installation's stored row (unchanged pre-existing semantics). Editing the plugin config of an installation whose row already exists does not override the stored values.
- 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 state. 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
daily_limit_staterow is no longer read (each installation starts from the config seed). Dedicated workers keep reading the legacy key unchanged. - 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_daily_limit.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 snapshot, 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_admin_write_serialization.py uses a synthetic Host that keeps
SET_PLUGIN_STORAGE pending and commits newest-first. It overlaps an admin write
with a runtime write and two admin writes of one installation, and asserts that
the durable row still holds both effects (pre-fix, the older snapshot was
committed last and rolled the newer state back).
tests/test_page_session_id_injection.py renders
components/pages/manage/index.html with tests/page_dom_probe.js (Node) against
a recording DOM, with a session id of x');alert(1);//. It asserts that no markup
string carries the id, that parsing every markup string yields no event-handler
attribute, and that the row's Reset/Remove/limit controls still call the Page API
with that exact id.