mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 01:16:45 +08:00
* 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]>
92 lines
4.6 KiB
SQL
92 lines
4.6 KiB
SQL
-- Plugin system reset.
|
|
--
|
|
-- The previous design shipped 14 tables, an append-only revision log per
|
|
-- relationship, a capability-snapshot compiler, and a per-task execution
|
|
-- manifest pinned by an INSERT trigger on agent_task_queue. It never left its
|
|
-- feature flag and holds no production data, so this migration removes it
|
|
-- outright rather than migrating it forward.
|
|
--
|
|
-- What replaces it: one installation row per (workspace, plugin), holding the
|
|
-- manifest snapshot the administrator actually consented to, plus two leaf
|
|
-- tables for plugin-owned state. Relationships stay application-owned by
|
|
-- repository policy: no foreign keys, no cascades.
|
|
|
|
DROP TRIGGER IF EXISTS trg_agent_task_queue_plugin_execution_manifest ON agent_task_queue;
|
|
DROP FUNCTION IF EXISTS pin_plugin_execution_manifest();
|
|
|
|
DROP TRIGGER IF EXISTS trg_plugin_installation_config_append_only ON plugin_installation_config;
|
|
DROP TRIGGER IF EXISTS trg_plugin_health_append_only ON plugin_health;
|
|
DROP TRIGGER IF EXISTS trg_plugin_execution_manifest_append_only ON plugin_execution_manifest;
|
|
DROP TRIGGER IF EXISTS trg_plugin_capability_snapshot_append_only ON plugin_capability_snapshot;
|
|
DROP TRIGGER IF EXISTS trg_plugin_artifact_file_append_only ON plugin_artifact_file;
|
|
DROP TRIGGER IF EXISTS trg_plugin_binding_append_only ON plugin_binding;
|
|
DROP TRIGGER IF EXISTS trg_plugin_grant_append_only ON plugin_grant;
|
|
DROP TRIGGER IF EXISTS trg_plugin_contribution_append_only ON plugin_contribution;
|
|
DROP TRIGGER IF EXISTS trg_plugin_release_immutable ON plugin_release;
|
|
DROP FUNCTION IF EXISTS reject_plugin_append_only_update();
|
|
DROP FUNCTION IF EXISTS enforce_plugin_release_immutable();
|
|
|
|
ALTER TABLE agent_task_queue DROP COLUMN IF EXISTS plugin_execution_manifest_id;
|
|
|
|
DROP TABLE IF EXISTS plugin_remote_mcp_oauth_state;
|
|
DROP TABLE IF EXISTS plugin_remote_mcp_secret;
|
|
DROP TABLE IF EXISTS plugin_installation_config;
|
|
DROP TABLE IF EXISTS plugin_health;
|
|
DROP TABLE IF EXISTS plugin_execution_manifest;
|
|
DROP TABLE IF EXISTS plugin_capability_snapshot;
|
|
DROP TABLE IF EXISTS plugin_workspace_capability_state;
|
|
DROP TABLE IF EXISTS plugin_artifact_file;
|
|
DROP TABLE IF EXISTS plugin_binding;
|
|
DROP TABLE IF EXISTS plugin_grant;
|
|
DROP TABLE IF EXISTS plugin_installation;
|
|
DROP TABLE IF EXISTS plugin_contribution;
|
|
DROP TABLE IF EXISTS plugin_release;
|
|
DROP TABLE IF EXISTS plugin_identity;
|
|
|
|
-- One row per installed plugin. `manifest` is the snapshot the administrator
|
|
-- saw and approved on the consent screen, not whatever the source URL serves
|
|
-- today: an upgrade re-fetches, re-consents, and overwrites it. That is why
|
|
-- there is no separate version table.
|
|
CREATE TABLE plugin_installation (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
workspace_id UUID NOT NULL,
|
|
plugin_key TEXT NOT NULL CHECK (char_length(plugin_key) BETWEEN 3 AND 255),
|
|
source_url TEXT NOT NULL CHECK (char_length(source_url) BETWEEN 1 AND 2048),
|
|
version TEXT NOT NULL CHECK (char_length(version) BETWEEN 1 AND 64),
|
|
manifest JSONB NOT NULL CHECK (jsonb_typeof(manifest) = 'object'),
|
|
granted_scopes JSONB NOT NULL DEFAULT '[]'::jsonb CHECK (jsonb_typeof(granted_scopes) = 'array'),
|
|
config JSONB NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(config) = 'object'),
|
|
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
|
installed_by UUID,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- Plugin-owned key/value state. Exactly two scopes: workspace state shared by
|
|
-- the team, and per-member state. Quotas are enforced on write in application
|
|
-- code; nothing here is evicted behind the plugin's back.
|
|
CREATE TABLE plugin_storage (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
installation_id UUID NOT NULL,
|
|
scope_type TEXT NOT NULL CHECK (scope_type IN ('workspace', 'user')),
|
|
scope_id UUID NOT NULL,
|
|
-- Byte-counted, matching the service-side quota: key and value are
|
|
-- plugin-supplied and routinely non-ASCII.
|
|
key TEXT NOT NULL CHECK (octet_length(key) BETWEEN 1 AND 1024),
|
|
value TEXT NOT NULL CHECK (octet_length(value) <= 102400),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- Secrets live in their own table so no storage read path can reach them by
|
|
-- construction. Values are encrypted with MULTICA_PLUGIN_SECRET_KEY and are
|
|
-- never returned by any endpoint.
|
|
CREATE TABLE plugin_secret (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
installation_id UUID NOT NULL,
|
|
key TEXT NOT NULL CHECK (char_length(key) BETWEEN 1 AND 128),
|
|
ciphertext BYTEA NOT NULL CHECK (octet_length(ciphertext) > 0),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|