mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 01:16:45 +08:00
zai-beta
8
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5bf10d485c |
MUL-6582: add durable scheduled Plugin hooks (#7448)
* MUL-6582 feat(plugins): add durable scheduled hooks Co-authored-by: multica-agent <[email protected]> * MUL-6582: add schedule-pulse example plugin Ship a real scheduled plugin that records each unique delivery_id in workspace storage, so a host retry does not look like a second pulse. The cmd/server test installs this example manifest and drives two scheduler replicas through a dropped first attempt. Co-authored-by: multica-agent <[email protected]> * MUL-6582 fix(migrations): make schedule indexes retry-safe Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]> |
||
|
|
f45b591623 |
MUL-6469: host plugin artifacts, bind installs to immutable versions (#7321)
* 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]> |
||
|
|
4bf3c73a15 |
feat(plugins): agent integration — skill resources, agent trigger, mcp transport (MUL-6350 PR 4) (#7242)
* feat(plugins): skill resources into the existing skill table (MUL-6350 PR 4, 1/3) A plugin's skill resource becomes an ordinary row in the skill table. Not a plugin-owned copy, not a bundle, not an artifact with a digest: the previous plugin system built all of that across fourteen tables and, at the end of it, delivered one SKILL.md that this table could already hold. The only thing the platform has to remember is which installation contributed which skill, so uninstall removes exactly those and nothing a person wrote. That is one nullable column. The upsert is scoped to rows this installation owns, so a plugin claiming a name somebody already used fails the install instead of silently replacing their work — and the test asserts the human's content survives. Upgrade prunes what the new manifest no longer declares, or a renamed skill would leave its predecessor behind with nothing to attribute it to. Fresh install is now a transaction too. It was a single INSERT; skills have to land in the same commit, because an installation whose skills half-arrived is worse than one that failed — the missing half is invisible. Skill resources flip on here rather than with the rest of PR 4: nothing executes, so it does not wait for the daemon-side MCP server that the agent trigger and mcp transport still need. Co-authored-by: multica-agent <[email protected]> * feat(plugins): agent trigger — hooks as MCP tools (MUL-6350 PR 4, 2/3) The fourth call site, and the only one that is not the host deciding to call something: an agent reads a tool description and chooses. That choice is why hooks may be reached from an agent at all — the alternative, a hook that must run before or after every turn, is a third party holding the product's main loop open, and no such position exists anywhere in this design. The daemon renders the tools but does not call the plugin. A tool call goes back to Multica, which makes the signed request. Two reasons, and the second matters more: the signing secret is derived from the deployment key and putting it on every laptop running an agent would hand out a credential that can impersonate the server to every plugin backend; and routing through the server keeps the rate limit, circuit breaker, `net:` destination check and invocation record on one code path for all four triggers instead of a daemon-side copy that would drift. A failing hook is a TOOL error, not a transport error. The agent reads it, works around it, and finishes the issue — an unreachable plugin endpoint must not fail somebody's task. The server returns 200 with a status for the same reason: a non-2xx would look to the daemon like the broker itself is broken. TestPluginToolNameSeparatorCannotBeForged caught a real flaw in my first naming scheme. Sanitizing both halves and joining with an underscore is not injective — a plugin key uses `.` and `-`, both of which have to become `_`, so `a.b` and `a-b` collapse together and `a.b_` + `c` collides with `a.b` + `c`. Two plugins would have been offering the agent one tool name. The prefix now carries a short digest of the full key rather than a lossy transliteration. Co-authored-by: multica-agent <[email protected]> * feat(plugins): mcp transport with pinned tool approval (MUL-6350 PR 4, 3/3) An `mcp` hook adopts tools from an MCP server the plugin author already runs. The difference from an `http` hook is who decides the shape: an http hook declares one endpoint in a manifest an administrator read, while an MCP server decides its own tool list at runtime. So installing the plugin is not the grant — an administrator discovers the tools and pins a specific set by name and schema digest, and a tool that appears later is not adopted until approved. Approved hooks reach the daemon as ordinary remotemcp.Connection values, so the existing broker proxies them and validatePinnedRemoteMCPTools already refuses to start when an approved tool went missing or its schema drifted. No new enforcement path. Plugin connections carry a `plugin:` contribution-id prefix. They share the broker and the resolver with a workspace's own Remote MCP connections but keep their credential in the Plugin's secret storage, which a different route serves, and the contribution id is all the broker hands back at dial time. Flips TransportMCP on in HostCapabilities, completing PR 4's capability set. Co-authored-by: multica-agent <[email protected]> * test(plugins): cover mcp connection startup via the dev-origin seam The `http` transport has had MULTICA_PLUGIN_DEV_ORIGINS since the hook engine landed: an operator names exact origins, and endpoints there skip the public-internet requirement so a plugin author can develop against a server on their own machine. The `mcp` transport never got it. Two symptoms, one cause. A plugin author could not point an mcp hook at a local MCP server, and — for the same reason — the daemon's connection startup could not be reached by a test, because it validates the endpoint as public HTTPS and the repo's MCP fixture serves plain HTTP on loopback. That left the layer carrying the entire approval guarantee at 0% coverage: startTaskRemoteMCPBrokers 0.0% -> 59.5% validatePinnedRemoteMCPTools 0.0% -> 100.0% The seam is off unless the variable is set, and a test asserts that: the same loopback connection is refused when it is empty. It skips only the public-internet requirement — the net: scope still applies, and the dial-time host pin still refuses a redirect that changes host. Tests cover what the approval is for: an approved tool the server dropped, an approved tool whose schema drifted, and an optional connection that fails without failing the task. Each was verified by breaking the guard it covers. Co-authored-by: multica-agent <[email protected]> * feat(plugins): a real example plugin, and the two bugs it found examples/plugins/deploy-sentinel is an incident-response plugin: it correlates an issue with recent deploys, files rollback requests under the team's own rules (refusing deploys past a window, and reasons too short to be evidence), pages on-call on issue events, and adopts an external metrics MCP server. Four triggers, both transports, and a skill that tells an agent when to use any of it. The other examples each demonstrate one mechanism. This one was written to find out whether the mechanisms compose, and it found two things that no unit test was ever going to: 1. mcp discovery could not succeed against ANY server. DiscoverMCPHookTools passed a nil protocol-version list, and Discover checks the server's answer against that same slice — empty means nothing is acceptable. Discover now falls back to SupportedProtocolVersions() when the caller passes none. 2. A hook handler had no way to read its own configuration. The manifest's config block drives a host-rendered form, the Action API has no config endpoint, and the hook body did not carry the values — so a plugin would have had to keep a second copy on its own server. The body now carries non-secret config. Secret-typed fields are excluded; they live in a separate table and are the plugin's credentials for its own services. Also lets an mcp dev origin trust MULTICA_PLUGIN_DEV_CA, mirroring the http transport. The manifest validator requires HTTPS, so without this a local MCP server could not be developed against at all. plugin_example_test.go installs the real manifest from examples/, not a copy, so a broken example fails the build rather than rotting quietly. Migrations renumbered to 368/369 after main advanced. 362 is added to the legacy-duplicate allowlist: two PRs took that prefix within the same hour and both merged, and the ledger keys on the full stem, so renaming one would re-run it everywhere it has already applied. main is red on this today. Co-authored-by: multica-agent <[email protected]> * fix(plugins): the deploy-sentinel example servers must serve HTTPS Both .mjs servers used node:http. A hook's transport URL has to be an https:// URL or the manifest fails validation, so the example as shipped could not actually be installed against a running Multica — the README told you to set MULTICA_PLUGIN_DEV_ORIGINS to https:// origins that nothing was listening on. Found by running it: install, config, skill contribution and live MCP tool discovery all work against the real servers once they speak TLS. They read TLS_CERT / TLS_KEY and the README carries the openssl one-liner. MULTICA_PLUGIN_DEV_CA changes which certificate is trusted; it never disables verification, which is why a self-signed certificate is needed rather than plain HTTP. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
659bb41abd |
MUL-6350: rebuild the plugin system (3/4) — hook engine (#7182)
* feat(plugins): hook engine — signed outbound calls, trigger-decided identity (MUL-6350) The direction reverses here. PR 1 and 2 only ever ran inbound: a sandboxed surface asked the host, and the host acted on the signed-in user's session, so nothing left our infrastructure on a plugin's say-so. A hook leaves, and every new check exists because of that. Destination: the target host must be inside the granted `net:` set, resolve to a public address, and be re-checked at dial. Body: HMAC-SHA256 over timestamp.body, so a receiver can tell our call from anyone else's and a captured one expires. Identity follows the trigger, not the plugin. A ui/manual hook runs because a person pressed something, so its writes stay theirs; an event hook has nobody behind it and writes as the installation — hence the fourth author_type. Two credentials, moving opposite ways, which is what decides how each is stored. The install token goes plugin -> host and is only ever verified, so it is hashed. The signing secret goes host -> plugin and must be reproduced to sign with, so it is derived from the deployment key and stored nowhere. Event hooks never block the host: Bus.Publish runs listeners inline on the publishing request's goroutine, so the dispatcher hands off to a worker pool immediately. Three attempts, then a circuit breaker, then silence. Co-authored-by: multica-agent <[email protected]> * feat(plugins): manual hook actions, ui trigger, and a worked hook example (MUL-6350) The host owns the manual menu entry rather than letting a surface draw one, because the trigger decides identity: a manual call acts as the person who picked it, so the entry has to live where the host can prove somebody did. The ui trigger goes through the bridge for the same reason a surface cannot fetch its own backend directly — routing through the host is what makes the call signed, rate limited, bounded to the granted net: domains and recorded. buildActorNameResolver gains a plugin branch. Without it an event-written comment rendered as "System", which is the one attribution that is actively misleading: it reads as the platform having said something. examples/plugins/triage-notify carries the half that does NOT run in Multica — a reference handler showing the four things a receiver must do, including the replay guard the host cannot do for it. TestShippedExamplesParseAndInstallOnThisHost closes the gap that let an example drift: nothing checked that shipped documentation still parses, or that it declares only what this build can run. TestCheckCapabilitiesReportsEveryUnavailableContribution rewritten the way the other two were in PR 2 — it named today's unshipped set, so flipping ui/manual/ event on turned it red with a message about the flip rather than about the gate. It now derives its expectation from HostCapabilities and survives a flip while still failing if the gate stops checking a category. Co-authored-by: multica-agent <[email protected]> * feat(plugins): modal surface, invocation cleanup, honest capability guards (MUL-6350) TestShippedHostCapabilitiesRunTheSurfacesTheHostMounts caught me flipping the modal surface on without a renderer — an install that succeeds and then never appears, which is the exact failure that guard exists to prevent. Built the mount point rather than weakening the guard, and rewrote it to name the host code behind each line so the next flip has to change both together. plugin_invocation was missing from workspace teardown and from uninstall. TestWorkspaceDeletionManifestCoversPublicSchema caught the first; the second would have left rows naming an installation that no longer exists. Deleted by workspace_id rather than through installation ids, so a record whose plugin was already uninstalled does not outlive the workspace it describes. A modal opens because a person picked it from the issue menu, never on the plugin's own initiative — same sandbox as a panel, differing only in where it appears. Co-authored-by: multica-agent <[email protected]> * test(plugins): prove the event path never blocks, and contain worker panics (MUL-6350) Writing the never-blocks test found a real bug rather than confirming one. Bus.Publish recovers panics in its listeners so one bad handler cannot kill the request that published; moving delivery onto a worker goroutine stepped outside that protection, where a panic is not a failed hook but a dead process. runGuarded restores the guarantee the hand-off gave up. The bridge now takes an EventSink rather than the concrete dispatcher, so the vocabulary mapping — which internal event becomes which published one — is asserted directly instead of through a live endpoint. That mapping is a published contract; the internal names are not. The backpressure test stops the workers rather than racing them, so it reproduces a saturated pool without depending on timing, and asserts the exact overflow instead of "something was dropped". Co-authored-by: multica-agent <[email protected]> * fix(plugins): two hook-contract bugs a live run found (MUL-6350) Both were invisible to the tests because the tests made one call each, and a real handler does not. The callback token was single-use. The reference handler reads the issue, decides, then posts a comment — and the second call died on a spent token. Read-then-write is the floor for a handler that does anything with what it read, so a single-use grant made the callback unusable. The trade that decides it is not one call versus several. It is this token versus the installation's standing token: a handler that cannot finish with the callback gets written against the install token instead, which never expires and is not scoped to an invocation. A control that pushes authors toward the stronger credential is worse than the looser one they will actually use. The grant is now invocation-scoped — minutes, this installation's scopes, the actor fixed at dispatch — and revoked as soon as the call it was issued for returns. The hook body also omitted the issue. The host resolves and permission-checks it, then said nothing, so a handler had to read a client-supplied field — unvalidated for ui/manual, and absent entirely for event, where there is no client. issue_id now travels in the body. Also adds MULTICA_PLUGIN_DEV_CA: a hook URL must be HTTPS, so an author testing against localhost needs its certificate trusted by something. A named CA file rather than InsecureSkipVerify, which survives into production behind a config flag nobody remembers and cannot be spotted by reading the code. Co-authored-by: multica-agent <[email protected]> * fix(plugins): enforce the callback token's issue scope, sweep invocations (MUL-6350) Addresses Fabel 5's review. The first item is the one that mattered. The callback grant's issue narrowing existed only in two doc comments and the PR description — pluginTokenCaller never read grant.IssueID, so a ui/manual token was worth every issue in the workspace its actor could see, for the whole five minutes it lived, on read and write alike. Now carried into the caller and checked in pluginIssueForUser, after resolution so an identifier and a uuid for the same issue agree. 404 rather than 403: "you are scoped elsewhere" confirms the id exists. DeleteExpiredPluginInvocations was generated and never called, while the table comment and the PR both said TTL-swept. A sweeper now runs on the dispatcher's lifecycle — same concern, bounded resources for something a third party drives. Event subscriptions bypassed the read scopes. issue.* pushes the description and comment.created pushes the body — the same content the Action API refuses without issues:read / comments:read. Subscribing was a way to receive what reading was not granted. Enforced at manifest validation so the consent screen shows the read scope a subscription implies. Migration 349's ADD CONSTRAINT copied 107's shape onto a table that is no longer 107's size: it revalidated every comment row under ACCESS EXCLUSIVE. NOT VALID plus a 352 that VALIDATEs under SHARE UPDATE EXCLUSIVE costs nothing and stops nothing. Plus the half-built breaker UI (a plugin could be enabled and silently delivering nothing), redirect refusal on the dev hook client (a 302 would have replayed the signed body and the callback token onward), and omitting callback_url rather than sending a relative one. Co-authored-by: multica-agent <[email protected]> * fix(plugins): the invocation sweeper must not touch the database at construction (MUL-6350) CI panicked in cmd/server: the sweeper I added last round fired immediately on its own goroutine, and that test builds a router over a Queries whose pool was never opened. pgxpool dereferenced the nil pool and the process died. Two things were wrong, and only one of them is the obvious one. The sweep no longer runs at construction. It waited for nothing and gained one hour of retention on a cold start against a 7-day TTL; the cost was a goroutine reaching for the database before the process is known to have one. The nil check was also the wrong instrument. sqlc's Queries wraps an executor, so a non-nil Queries over an unopened pool passes every comparison available at that layer and only fails inside pgxpool — no amount of nil-checking here can see it. The guard is a recover, for the same reason runGuarded exists: on a bare goroutine a panic is not a failed sweep, it is a dead server. I had already fixed that class once for delivery and then reintroduced it in the sweeper. The regression test builds the exact shape CI hit — a dispatcher over db.New(nil) — and fails if either the construction-time sweep or the recover comes back. cmd/server cannot reproduce it locally, where DATABASE_URL supplies a real pool. Co-authored-by: multica-agent <[email protected]> * fix(plugins): gate the event dispatch path behind plugins_v1 (MUL-6350) Answering "is this still behind the flag" turned up the one place it was not. Every other plugin surface is refused by a handler that reads the flag off a request. An event hook has no request: the dispatcher is subscribed to the bus at startup and delivers on a worker. Nothing in it consulted the flag, so turning plugins off hid the UI and refused the Action API while the OUTBOUND calls kept going — fail-open on the one path that reaches a third party. It only bites after a plugin has been installed, because installation is flag-gated, so a deployment that never enabled plugins was never exposed. A workspace that enabled it, installed something, then turned it off was. The check reads the flag per delivery rather than once at subscription, so the flip takes effect immediately instead of at the next restart. It also removes the flag-off cost: every dispatched event used to run a ListWorkspacePluginInstallations query to discover there was nothing to call. Nil flags read as disabled — a deployment that never wired the flag service must not get outbound hooks by omission. The test lives in internal/handler because it needs a real installed plugin to mean anything: with the flag on the endpoint is called, with it off nothing leaves while the same installation stays enabled and still declares the hook. Removing the check makes it fail on the second half. Co-authored-by: multica-agent <[email protected]> * perf(plugins): stop parsing event payloads on the publishing goroutine (MUL-6350) Found while answering whether merging can affect existing users, and the answer was "in one measurable way it can". The bus listener passed issueIDFromPayload(e.Payload) as an argument, so a JSON marshal plus unmarshal of the full event payload — for issue.created that is the whole issue body, description included — ran inline on the goroutine of whatever request published. Seven event types, every workspace, and the feature flag was checked far downstream on the worker, which then threw the result away. A deployment with plugins switched off paid for all of it. The listener now hands the payload over unexamined and the id is extracted on the worker, past the flag check. It also fits the rule this file already claimed: the publishing request does the least it possibly can. Not moved: the status_changed lookup, which is a map read, not a parse. TestPublishingDoesNotParseThePayload counts MarshalJSON calls during Publish and fails if any happen — restoring the old argument makes it fail with the count. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
6a17c00144 |
MUL-6350: rebuild the plugin system (1/4) — cleanup and foundations (#7144)
* MUL-6350 refactor(plugins): reset the plugin system to Action / Hook / Resource foundations The V1 plugin system shipped 14 tables, an append-only revision log per relationship, a capability-snapshot compiler, a per-task execution manifest pinned by an INSERT trigger on agent_task_queue, release signing, trust tiers, and a bundled catalog — and never left its feature flag. It holds no production data, so this removes it outright instead of migrating it forward. What replaces it, per the design in MUL-6338: - A plugin relates to Multica in exactly three ways. Action (plugin calls Multica), Hook (Multica calls plugin), Resource (a static contribution with no call at all). "Who triggers" and "what capability is called" are orthogonal: a hook is declared once and lists which triggers may invoke it. - We never execute third-party code. A plugin runs only in a sandboxed iframe in the user's browser or on the author's own server. - Distribution is "install by URL, admin reads the scopes". No signature, no trust tier, no publisher verification — the scope list IS the trust decision. This change is the foundation, with no user-visible surface yet: - Decouples remote MCP first. RemoteMCPConnection/RemoteMCPTool/DigestBytes move into pkg/remotemcp, where they belonged; the protocol layer and daemon broker survive intact and dormant, ready to be rewired for the agent trigger. - Drops all 14 plugin_* tables, their triggers, the manifest-composition function, and agent_task_queue.plugin_execution_manifest_id. Adds three: plugin_installation (one row per workspace+plugin, holding the manifest snapshot the admin consented to), plugin_storage, plugin_secret. - Defines the manifest schema in full, once — surfaces, hooks with their triggers and events, resources, and a closed scope list. Parsing stays strict (DisallowUnknownFields, no trailing JSON). Contribution kinds this build does not ship yet fail installation loudly rather than installing half-working. - Two-step install: preview parses and returns the scopes without writing, then install requires consent matching the manifest exactly. Manifest fetches reuse the remote MCP SSRF guard; MULTICA_PLUGIN_DIR adds a local source for self-hosting and plugin authors. - Storage quotas are enforced on write and never by eviction. Secrets live in a separate encrypted table with no read path that returns a value. Co-authored-by: multica-agent <[email protected]> * MUL-6350 fix(migrate): register the plugin index builds for invalid-index cleanup Every migration that builds an index CONCURRENTLY must be registered in concurrentIndexCleanups, or an interrupted build leaves a permanently INVALID index that the `IF NOT EXISTS` retry records as success. TestEveryConcurrentUp\ BuildHasCleanup caught all three new plugin index migrations. Co-authored-by: multica-agent <[email protected]> * MUL-6350 fix(plugins): address review — byte quotas, version bound, secret pruning, script entry From the review on #7144, plus one contract change the surface design forced. - Storage quotas were accounted in characters and enforced in bytes. The usage query and the size_bytes it reports now use octet_length, and so do the plugin_storage column checks. A UTF-8 value could otherwise spend up to a quarter of the budget it appeared to. - manifest version had no length bound while plugin_installation.version caps at 64, so a legal-but-long semver failed at INSERT as an opaque install error instead of a parse error naming the field. - Upgrading pruned config values the new manifest dropped but left the matching plugin_secret rows. Ciphertext nothing can reach is exactly the residue pruneConfig exists to prevent; upgrade now prunes both in one transaction. - SetConfig wrote secrets and config across two tables with no transaction, so a failed config write could leave a saved form half-applied. - A duplicate first install mapped a unique-index violation to 502; it is 409. - net: is now an exact host match. It was a suffix match in the hook transport check while the same scope becomes an exact-host CSP connect-src in the surface runtime — one scope string cannot mean two things across the two places the consent screen claims to describe. - surfaces[].entry must be a .js/.mjs script, not an HTML document. The host generates the surface's document so it can attach the CSP derived from net:; a plugin-authored document would carry its own server's policy and net: would be a claim rather than a control. Tests: the three storage limits get a canonical unit matrix plus DB-backed coverage of the byte counting and the exclude-the-candidate-key replacement semantics; upgrade-time secret pruning is pinned end to end (verified failing without the fix); and the admin-only routes now have a real middleware-level 403 check, which every direct-handler test bypassed. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
4d495056e8 |
[MUL-6139] Expand Plugin skills and hosted MCP connections (#6920)
* feat(plugins): add remote MCP runtime Co-authored-by: multica-agent <[email protected]> * chore(plugins): add private Exa example Co-authored-by: multica-agent <[email protected]> * docs(plugins): remove private plugin guide Co-authored-by: multica-agent <[email protected]> * fix(plugins): stabilize remote MCP tool digests Co-authored-by: multica-agent <[email protected]> * fix(plugins): preserve remote MCP notification status Co-authored-by: multica-agent <[email protected]> * fix(plugins): make Exa example runnable end to end Co-authored-by: multica-agent <[email protected]> * fix(codex): record MCP tool calls in agent log Co-authored-by: multica-agent <[email protected]> * chore(plugins): add community skills example Co-authored-by: multica-agent <[email protected]> * feat(plugins): support multi-file skill bundles Co-authored-by: multica-agent <[email protected]> * feat(plugins): add Mobbin MCP example Co-authored-by: multica-agent <[email protected]> * chore(plugins): bundle full Matt Pocock skill set Co-authored-by: multica-agent <[email protected]> * feat(plugins): add remote MCP OAuth connections Co-authored-by: multica-agent <[email protected]> * fix(plugins): gate enable on remote MCP readiness Co-authored-by: multica-agent <[email protected]> * fix(plugin): authenticate remote mcp credential broker Co-authored-by: multica-agent <[email protected]> * fix(plugins): harden remote mcp oauth flow Co-authored-by: multica-agent <[email protected]> * refactor(plugins): use a single feature flag Co-authored-by: multica-agent <[email protected]> * fix(plugins): clean up remote MCP OAuth state Co-authored-by: multica-agent <[email protected]> * chore(plugins): remove Matt Pocock example Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
31645cc510 |
[MUL-6125] Ship Private Skill Plugin developer loop (#6900)
* [MUL-6125] Ship private Skill Plugin developer loop Co-authored-by: multica-agent <[email protected]> * [MUL-6125] Address Private Plugin review findings Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
d467cc9069 |
MUL-6075: complete Plugin V1 execution chain (#6853)
* feat(server): add Plugin V1 lifecycle foundation Co-authored-by: multica-agent <[email protected]> * feat(server): complete Plugin V1 execution chain Co-authored-by: multica-agent <[email protected]> * feat(web): add temporary plugin settings demo Co-authored-by: multica-agent <[email protected]> * refactor(plugin): keep foundation free of demo content Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |