* feat(plugins): host plugin artifacts, bind installs to immutable versions A plugin used to be a URL. The manifest was frozen at install, but the surface script was fetched from the author's server every time a panel opened, with no integrity check — so an administrator consented to a manifest while the browser ran whatever that host served that day, inside the scopes already granted. The author's uptime was our uptime, every panel open leaked the reader's IP and "who read which issue" to the author, and publishing at all required a public HTTPS domain, which is why the repo's own deploy-sentinel example never ran. The author now uploads an artifact bundle and Multica stores it: - plugin_package / plugin_package_version / plugin_package_file. A version is insert-only, and the (package_id, version) unique index is what makes immutability a database rule rather than a convention. - An installation names one version. Publishing another changes nothing for a workspace until an administrator upgrades, which is a second consent. - The bundle is validated once, at publish: the manifest parses, every file it declares is present and loadable, and a surface entry with a top-level import is refused with the line named. That failure used to surface months later in a reader's browser. - The host inlines the surface script into the document it generates, so the CSP names no remote script origin at all. `connect-src 'none'` now means it: a surface with no net: scope cannot reach anywhere, including its author. - Install-by-URL is gone rather than kept alongside. Two paths would make "is this code frozen?" depend on how the plugin was installed. - MULTICA_PLUGIN_DIR stays the development channel, but publishes an ordinary immutable version instead of being a second render path; re-publishing an unchanged version lands as `+dev.N`. Hook endpoints and MCP servers remain on the author's own infrastructure. Migration 376 drops existing installations: they name a source URL that is no longer a concept, and the code they would run was never published here. plugins_v1 has never left its feature flag and holds no production data — the same call migration 344 made. Closes MUL-6469 Co-authored-by: multica-agent <[email protected]> * fix(plugins): serialize publish/install/delete, correct the isolation claim Review of #7321 found five problems. Four are fixed here; the fifth is real, needs its own design pass, and is now tracked and described honestly in the code instead of claimed away. Migration prefix collision. main took 376 for agent_task_durable_work_dir while this branch was open, so `migration prefix 376 is reused` failed backend-tests. Rebased and renumbered the six migrations to 377-382. TOCTOU between delete and install/publish. Relationships are application-owned by repository policy, so nothing made "the version this installation names still exists" true across statements: `delete counts zero installs` -> `install reads the version` -> `delete commits` -> `install commits` left an installation whose panel 404s forever, and a publish racing a delete left a version whose package row was gone. Publish, install and delete now hold a (workspace, plugin key) advisory lock for their whole transaction, and delete counts installations inside it. Both interleavings have regression tests that reproduce the bug with the lock removed. Failed publishes moved package state. upsertPackage ran before the version transaction, so a republish that lost the unique index had already renamed the package, and a first publish that failed afterwards left a plugin with zero versions. It now runs inside the same transaction. The top-level import check was wrong in BOTH directions. It read line prefixes, so it missed ` import x` and `/* c */ import x` — and it refused a valid file whose template literal contained the word at a line start, which is ordinary in a surface that renders code samples. A refusal blocks a publish with no way around it, so that false positive was the worse half. Replaced with a scanner that skips comments and string/template literals; where it is imprecise it is imprecise safely (`/` always reads as division), so a mistake can only cost a detection, never invent one. Sandbox self-navigation is NOT fixed. `sandbox="allow-scripts"` permits `_self` navigation by design, no shipped CSP directive covers it, and contentWindow is unchanged across it — so a hostile artifact can still reach its author once and hand the replacement document the bridge port. Closing it needs an embedder frame-src policy or a handshake a navigated document cannot complete; both are decisions of their own. What lands here is damage control, labelled as such: the generated document beacons on pagehide from a listener the plugin cannot detach, and the embedder drops the bridge and unmounts the frame. The "a surface cannot reach its author" wording is removed from the SDK README and the tests. Closes MUL-6469 Refs MUL-6485 Co-authored-by: multica-agent <[email protected]> * fix(plugins): harden surface validation and error reporting * fix(migrations): renumber plugin package migrations * fix(plugins): validate classic surfaces and reset failures --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]>
5.3 KiB
Deploy Sentinel
A production-incident plugin. It correlates an incident issue with recent deploys, lets an agent file a rollback request under the team's own rules, pages on-call when an incident is filed, and adopts the team's existing metrics MCP server.
It exists because the other examples in this directory each demonstrate one thing. This one is the shape of a plugin somebody would actually write: four triggers, both transports, real business rules that refuse things, and a skill that teaches an agent when to use any of it.
What it contributes
| Contribution | Kind | What it demonstrates |
|---|---|---|
incident-response |
skill resource | A SKILL.md becomes an ordinary workspace skill. Uninstall removes it. |
correlate_deploys |
hook, agent + manual + ui |
One hook, three callers. The agent gets a tool; a person gets a button; the panel gets a function. |
request_rollback |
hook, agent + manual |
A write action that refuses: too old, or no written reason. |
page_oncall |
hook, event |
Fires on issue.created / issue.status_changed. Never blocks anything. |
metrics |
hook, mcp transport |
Adopts an external MCP server's tools, subject to admin approval. |
incident |
issue_panel surface |
The human view, in a sandboxed iframe with no credential. |
The part worth copying
request_rollback says no. It refuses a deploy past the configured window,
and it refuses a reason shorter than 20 characters. An agent that has to write
evidence before it can file a change request produces better change requests —
and the refusal text explains why, so the agent can act on it rather than retry
blindly. Hook handlers that only ever succeed are the easy case; this one is the
useful one.
correlate_deploys reports "nothing changed" as an answer, not as an empty
list. The skill then tells the agent that an empty result means it should stop
looking at deploys. A tool that returns [] and a tool that says "no deploys in
this window, this is unlikely to be a deploy" cost the same to write and behave
very differently in an agent's hands.
The skill and the hooks are designed together. SKILL.md names the tools and
says what order to use them in. Neither half works as well alone.
Running it locally
Two servers, both the plugin author's own. Multica never runs either.
Both serve HTTPS, because a hook's transport URL must be an https:// URL or
the manifest will not install. Make a certificate once:
openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
-keyout dev-key.pem -out dev-cert.pem \
-subj "/CN=127.0.0.1" -addext "subjectAltName=IP:127.0.0.1"
# The hook endpoint. The signing secret is the whsec_… shown once when you
# issued the plugin's token in workspace settings.
MULTICA_SIGNING_SECRET=whsec_... node server/handler.mjs # :8788
# The MCP server behind the `metrics` hook.
METRICS_TOKEN=metrics-dev-token node server/metrics-mcp.mjs # :8789
Both read TLS_CERT / TLS_KEY, defaulting to dev-cert.pem / dev-key.pem
in the working directory.
Then point Multica at them. Both endpoints are on loopback, which the outbound guard refuses by design, so name them explicitly:
export MULTICA_PLUGIN_DIR=examples/plugins
export MULTICA_PLUGIN_DEV_ORIGINS=https://127.0.0.1:8788,https://127.0.0.1:8789
export MULTICA_PLUGIN_DEV_CA=/path/to/your-dev-ca.pem
Publish it from MULTICA_PLUGIN_DIR as deploy-sentinel, install the version
that appears, fill in the config form, then open the metrics approval panel
and tick the tools you want agents to reach. Nothing under metrics is callable
until you do — that is the whole difference between the mcp transport and an
http hook.
Note what MULTICA_PLUGIN_DIR does and does not cover. It is a publishing
shortcut for the FRONTEND artifact only; the two servers above are still yours
to run, and the net: scopes are still what authorises reaching them.
Both endpoints must be HTTPS even locally: the manifest validator requires it,
and MULTICA_PLUGIN_DEV_CA is how you get a self-signed certificate trusted.
The dev switches change which certificate is trusted and whether the address
must be public. They never disable verification, and they never widen the
net: scopes — a hook still cannot reach a host the administrator did not
approve.
Try breaking it
- Ask an agent to roll back
d-4802. It is 380 minutes old; the plugin refuses and explains why. - Ask for a rollback with the reason "broken". Refused — too short.
- Add a tool to
server/metrics-mcp.mjsand reopen the approval panel. It appears unapproved, and agents cannot call it until somebody ticks it. - Change an approved tool's
inputSchemaand restart. The agent's task starts fine, but that connection is refused: the schema no longer matches what was approved.
Tested
server/internal/handler/plugin_example_test.go installs this manifest,
not a copy, and drives it: the skill lands in the skill table, the agent tool
list contains exactly the hooks that declare the agent trigger, an agent hook
call goes out signed and comes back, and the metrics tools are discovered,
refused before approval, and pinned by schema digest after it.
The .mjs files are what you run by hand. The test uses Go servers that answer
the same contract, so CI does not need node.