Skip to content

refactor(plugins): give the plugin capability surface its own module - #1027

Merged
rmyndharis merged 1 commit into
mainfrom
refactor/plugin-capability-context
Jul 31, 2026
Merged

refactor(plugins): give the plugin capability surface its own module#1027
rmyndharis merged 1 commit into
mainfrom
refactor/plugin-capability-context

Conversation

@rmyndharis

Copy link
Copy Markdown
Owner

Third step on plugin-loader.service.ts, and the one with the most value: it removes the
security-review audience.

Why

createPluginContext and its seven gate helpers are the surface a plugin can actually reach —
ctx.messages, ctx.engine, ctx.net, ctx.storage, ctx.conversations, ctx.handover,
ctx.mappings — plus the gates in front of each. A plugin supplies its own sessionId, so
assertSessionAllowed (the static manifest boundary) and the operator-activation check are the
security boundary.

They sat inside a loader otherwise concerned with directory discovery, lifecycle and worker plumbing.
Reviewing the boundary meant reading 1640 lines to find 330.

What moves

PluginCapabilityContext holds the seven gate helpers, createPluginContext, and the hook
AsyncLocalStorage. It holds no plugin registry and no worker hosts — I verified the moved
range references neither this.plugins nor this.sandboxHosts. Everything it needs comes from the
PluginInstance it is handed.

The constructor signature is untouched — the facade is built in the body, as PluginHostServices
already is. All 33 positional spec constructions and the three subclasses overriding
createSandboxHost compile and pass unchanged.

1640 → 1267 lines.

Two things this surfaced

The loader's logger is passed in, not recreated. A plugin's ctx.logger lines carry that logger's
context, and it is operator-visible. A fresh createLogger('PluginCapabilityContext') silently
retagged every plugin log line — the sandbox log-relay tests caught it, which is exactly what they are
for.

dispatchConversationMedia moved too. Leaving it in the loader would have made the new module
import the loader while the loader imports the new module — a require cycle, in a file that already
pays a lazy-require tax to avoid precisely that class of problem. The loader no longer references it.

Spec changes: pointer-only

Four files, +29/−18, and no assertion, fixture or harness touched:

  • three reach-in helpers (plugin-capability, plugin-loader-conversation-repair,
    plugin-loader-activation) now name the object that owns the surface;
  • one import line follows dispatchConversationMedia.

The plan this came from predicted zero spec edits. That was wrong for the same reason it was wrong
twice before this week: these specs reach into private members, so a member that moves takes its
reach-in with it. Recorded rather than glossed.

307 plugin tests pass; 4012 overall, unchanged.

Verification

Backend lint, prettier --check, tsc --noEmit over the full project including specs, build, 4012 unit
tests, 137 e2e tests, and the dashboard build all pass on Node 22.

This is the security-review surface: the verbs a plugin can actually reach —
ctx.messages, ctx.engine, ctx.net, ctx.storage, ctx.conversations, ctx.handover,
ctx.mappings — and the gates in front of every one of them. A plugin supplies
its own sessionId, so assertSessionAllowed (the static manifest boundary) and
the operator-activation check ARE the boundary, not a convenience.

It sat inside a loader that is otherwise about directory discovery, lifecycle
and worker plumbing, so reviewing the boundary meant reading 1640 lines to find
330. PluginCapabilityContext now holds the seven gate helpers, createPluginContext
and the hook AsyncLocalStorage. It holds no plugin registry and no worker hosts —
everything it needs comes from the PluginInstance it is handed.

The constructor SIGNATURE is untouched: the facade is built in the body, as
PluginHostServices already is. All 33 positional spec constructions and the three
subclasses overriding createSandboxHost compile and pass unchanged.

Two things this surfaced that are worth recording:

The loader's logger is passed in rather than a second one created. A plugin's
ctx.logger lines carry that logger's context, which is operator-visible; a fresh
createLogger('PluginCapabilityContext') silently retagged every plugin log line,
and the sandbox log-relay tests caught it.

dispatchConversationMedia moved too. Leaving it behind would have made the new
module import the loader while the loader imports it — a require cycle, in a file
that already pays a lazy-require tax to avoid exactly that. The loader no longer
references it.

1640 to 1267 lines. Spec changes are pointer-only across four files: three
reach-in helpers now name the object that owns the surface, and one import line
follows dispatchConversationMedia. No assertion, fixture or harness was touched;
307 plugin tests and 4012 overall pass unchanged.
@rmyndharis
rmyndharis merged commit 6975e56 into main Jul 31, 2026
15 checks passed
@rmyndharis
rmyndharis deleted the refactor/plugin-capability-context branch July 31, 2026 18:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant