Files
multica/server/pkg/db/generated/plugin.sql.go
T
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]>
2026-08-20 02:35:29 +08:00

957 lines
28 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: plugin.sql
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const countRecentPluginFailures = `-- name: CountRecentPluginFailures :one
SELECT count(*) FROM plugin_invocation
WHERE installation_id = $1 AND hook_key = $2 AND created_at > $3 AND status <> 'ok'
`
type CountRecentPluginFailuresParams struct {
InstallationID pgtype.UUID `json:"installation_id"`
HookKey string `json:"hook_key"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
// Consecutive-failure signal for the event circuit breaker. Bounded by time so
// a breaker that tripped hours ago does not keep a hook shut forever.
func (q *Queries) CountRecentPluginFailures(ctx context.Context, arg CountRecentPluginFailuresParams) (int64, error) {
row := q.db.QueryRow(ctx, countRecentPluginFailures, arg.InstallationID, arg.HookKey, arg.CreatedAt)
var count int64
err := row.Scan(&count)
return count, err
}
const countRecentPluginInvocations = `-- name: CountRecentPluginInvocations :one
SELECT count(*) FROM plugin_invocation
WHERE installation_id = $1 AND hook_key = $2 AND created_at > $3
`
type CountRecentPluginInvocationsParams struct {
InstallationID pgtype.UUID `json:"installation_id"`
HookKey string `json:"hook_key"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
// Feeds the per-hook rate limit. Counts attempts, not distinct calls: a hook
// retrying into a dead endpoint is exactly the traffic the limit exists to cap.
func (q *Queries) CountRecentPluginInvocations(ctx context.Context, arg CountRecentPluginInvocationsParams) (int64, error) {
row := q.db.QueryRow(ctx, countRecentPluginInvocations, arg.InstallationID, arg.HookKey, arg.CreatedAt)
var count int64
err := row.Scan(&count)
return count, err
}
const createPluginInstallation = `-- name: CreatePluginInstallation :one
INSERT INTO plugin_installation (
workspace_id, plugin_key, source_url, version, manifest, granted_scopes, installed_by
) VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, workspace_id, plugin_key, source_url, version, manifest, granted_scopes, config, enabled, installed_by, created_at, updated_at, token_hash, token_rotated_at, mcp_approvals
`
type CreatePluginInstallationParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
PluginKey string `json:"plugin_key"`
SourceUrl string `json:"source_url"`
Version string `json:"version"`
Manifest []byte `json:"manifest"`
GrantedScopes []byte `json:"granted_scopes"`
InstalledBy pgtype.UUID `json:"installed_by"`
}
func (q *Queries) CreatePluginInstallation(ctx context.Context, arg CreatePluginInstallationParams) (PluginInstallation, error) {
row := q.db.QueryRow(ctx, createPluginInstallation,
arg.WorkspaceID,
arg.PluginKey,
arg.SourceUrl,
arg.Version,
arg.Manifest,
arg.GrantedScopes,
arg.InstalledBy,
)
var i PluginInstallation
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.PluginKey,
&i.SourceUrl,
&i.Version,
&i.Manifest,
&i.GrantedScopes,
&i.Config,
&i.Enabled,
&i.InstalledBy,
&i.CreatedAt,
&i.UpdatedAt,
&i.TokenHash,
&i.TokenRotatedAt,
&i.McpApprovals,
)
return i, err
}
const createPluginInvocation = `-- name: CreatePluginInvocation :one
INSERT INTO plugin_invocation (
installation_id, workspace_id, hook_key, trigger, status, event_type, attempt, latency_ms, error
) VALUES ($1, $2, $3, $4, $5, $8, $6, $7, $9)
RETURNING id, installation_id, workspace_id, hook_key, trigger, status, event_type, attempt, latency_ms, error, created_at
`
type CreatePluginInvocationParams struct {
InstallationID pgtype.UUID `json:"installation_id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
HookKey string `json:"hook_key"`
Trigger string `json:"trigger"`
Status string `json:"status"`
Attempt int32 `json:"attempt"`
LatencyMs int32 `json:"latency_ms"`
EventType pgtype.Text `json:"event_type"`
Error pgtype.Text `json:"error"`
}
func (q *Queries) CreatePluginInvocation(ctx context.Context, arg CreatePluginInvocationParams) (PluginInvocation, error) {
row := q.db.QueryRow(ctx, createPluginInvocation,
arg.InstallationID,
arg.WorkspaceID,
arg.HookKey,
arg.Trigger,
arg.Status,
arg.Attempt,
arg.LatencyMs,
arg.EventType,
arg.Error,
)
var i PluginInvocation
err := row.Scan(
&i.ID,
&i.InstallationID,
&i.WorkspaceID,
&i.HookKey,
&i.Trigger,
&i.Status,
&i.EventType,
&i.Attempt,
&i.LatencyMs,
&i.Error,
&i.CreatedAt,
)
return i, err
}
const deleteExpiredPluginInvocations = `-- name: DeleteExpiredPluginInvocations :execrows
DELETE FROM plugin_invocation WHERE created_at < $1
`
// TTL sweep. This table is operational telemetry, not history to keep.
func (q *Queries) DeleteExpiredPluginInvocations(ctx context.Context, createdAt pgtype.Timestamptz) (int64, error) {
result, err := q.db.Exec(ctx, deleteExpiredPluginInvocations, createdAt)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const deletePluginInstallation = `-- name: DeletePluginInstallation :exec
DELETE FROM plugin_installation WHERE id = $1
`
func (q *Queries) DeletePluginInstallation(ctx context.Context, id pgtype.UUID) error {
_, err := q.db.Exec(ctx, deletePluginInstallation, id)
return err
}
const deletePluginInvocationsByInstallation = `-- name: DeletePluginInvocationsByInstallation :exec
DELETE FROM plugin_invocation WHERE installation_id = $1
`
func (q *Queries) DeletePluginInvocationsByInstallation(ctx context.Context, installationID pgtype.UUID) error {
_, err := q.db.Exec(ctx, deletePluginInvocationsByInstallation, installationID)
return err
}
const deletePluginSecret = `-- name: DeletePluginSecret :execrows
DELETE FROM plugin_secret WHERE installation_id = $1 AND key = $2
`
type DeletePluginSecretParams struct {
InstallationID pgtype.UUID `json:"installation_id"`
Key string `json:"key"`
}
func (q *Queries) DeletePluginSecret(ctx context.Context, arg DeletePluginSecretParams) (int64, error) {
result, err := q.db.Exec(ctx, deletePluginSecret, arg.InstallationID, arg.Key)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const deletePluginSecretsByInstallation = `-- name: DeletePluginSecretsByInstallation :exec
DELETE FROM plugin_secret WHERE installation_id = $1
`
func (q *Queries) DeletePluginSecretsByInstallation(ctx context.Context, installationID pgtype.UUID) error {
_, err := q.db.Exec(ctx, deletePluginSecretsByInstallation, installationID)
return err
}
const deletePluginSkillsByInstallation = `-- name: DeletePluginSkillsByInstallation :exec
DELETE FROM skill WHERE plugin_installation_id = $1
`
func (q *Queries) DeletePluginSkillsByInstallation(ctx context.Context, pluginInstallationID pgtype.UUID) error {
_, err := q.db.Exec(ctx, deletePluginSkillsByInstallation, pluginInstallationID)
return err
}
const deletePluginSkillsNotIn = `-- name: DeletePluginSkillsNotIn :exec
DELETE FROM skill
WHERE plugin_installation_id = $1 AND name <> ALL($2::text[])
`
type DeletePluginSkillsNotInParams struct {
PluginInstallationID pgtype.UUID `json:"plugin_installation_id"`
KeepNames []string `json:"keep_names"`
}
// Upgrade pruning: a skill this installation used to contribute but no longer
// declares must go, or a renamed skill leaves its predecessor behind forever.
func (q *Queries) DeletePluginSkillsNotIn(ctx context.Context, arg DeletePluginSkillsNotInParams) error {
_, err := q.db.Exec(ctx, deletePluginSkillsNotIn, arg.PluginInstallationID, arg.KeepNames)
return err
}
const deletePluginStorageByInstallation = `-- name: DeletePluginStorageByInstallation :exec
DELETE FROM plugin_storage WHERE installation_id = $1
`
func (q *Queries) DeletePluginStorageByInstallation(ctx context.Context, installationID pgtype.UUID) error {
_, err := q.db.Exec(ctx, deletePluginStorageByInstallation, installationID)
return err
}
const deletePluginStorageValue = `-- name: DeletePluginStorageValue :execrows
DELETE FROM plugin_storage
WHERE installation_id = $1 AND scope_type = $2 AND scope_id = $3 AND key = $4
`
type DeletePluginStorageValueParams struct {
InstallationID pgtype.UUID `json:"installation_id"`
ScopeType string `json:"scope_type"`
ScopeID pgtype.UUID `json:"scope_id"`
Key string `json:"key"`
}
func (q *Queries) DeletePluginStorageValue(ctx context.Context, arg DeletePluginStorageValueParams) (int64, error) {
result, err := q.db.Exec(ctx, deletePluginStorageValue,
arg.InstallationID,
arg.ScopeType,
arg.ScopeID,
arg.Key,
)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const getPluginInstallation = `-- name: GetPluginInstallation :one
SELECT id, workspace_id, plugin_key, source_url, version, manifest, granted_scopes, config, enabled, installed_by, created_at, updated_at, token_hash, token_rotated_at, mcp_approvals FROM plugin_installation WHERE id = $1
`
func (q *Queries) GetPluginInstallation(ctx context.Context, id pgtype.UUID) (PluginInstallation, error) {
row := q.db.QueryRow(ctx, getPluginInstallation, id)
var i PluginInstallation
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.PluginKey,
&i.SourceUrl,
&i.Version,
&i.Manifest,
&i.GrantedScopes,
&i.Config,
&i.Enabled,
&i.InstalledBy,
&i.CreatedAt,
&i.UpdatedAt,
&i.TokenHash,
&i.TokenRotatedAt,
&i.McpApprovals,
)
return i, err
}
const getPluginInstallationByTokenHash = `-- name: GetPluginInstallationByTokenHash :one
SELECT id, workspace_id, plugin_key, source_url, version, manifest, granted_scopes, config, enabled, installed_by, created_at, updated_at, token_hash, token_rotated_at, mcp_approvals FROM plugin_installation WHERE token_hash = $1
`
// Looked up by hash, so the plaintext token exists only in the caller's request.
func (q *Queries) GetPluginInstallationByTokenHash(ctx context.Context, tokenHash pgtype.Text) (PluginInstallation, error) {
row := q.db.QueryRow(ctx, getPluginInstallationByTokenHash, tokenHash)
var i PluginInstallation
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.PluginKey,
&i.SourceUrl,
&i.Version,
&i.Manifest,
&i.GrantedScopes,
&i.Config,
&i.Enabled,
&i.InstalledBy,
&i.CreatedAt,
&i.UpdatedAt,
&i.TokenHash,
&i.TokenRotatedAt,
&i.McpApprovals,
)
return i, err
}
const getPluginSecret = `-- name: GetPluginSecret :one
SELECT id, installation_id, key, ciphertext, created_at, updated_at FROM plugin_secret
WHERE installation_id = $1 AND key = $2
`
type GetPluginSecretParams struct {
InstallationID pgtype.UUID `json:"installation_id"`
Key string `json:"key"`
}
func (q *Queries) GetPluginSecret(ctx context.Context, arg GetPluginSecretParams) (PluginSecret, error) {
row := q.db.QueryRow(ctx, getPluginSecret, arg.InstallationID, arg.Key)
var i PluginSecret
err := row.Scan(
&i.ID,
&i.InstallationID,
&i.Key,
&i.Ciphertext,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getPluginStorageUsage = `-- name: GetPluginStorageUsage :one
SELECT COUNT(*)::bigint AS key_count,
COALESCE(SUM(octet_length(value)), 0)::bigint AS total_bytes
FROM plugin_storage
WHERE installation_id = $1 AND scope_type = $2 AND scope_id = $3 AND key <> $4
`
type GetPluginStorageUsageParams struct {
InstallationID pgtype.UUID `json:"installation_id"`
ScopeType string `json:"scope_type"`
ScopeID pgtype.UUID `json:"scope_id"`
Key string `json:"key"`
}
type GetPluginStorageUsageRow struct {
KeyCount int64 `json:"key_count"`
TotalBytes int64 `json:"total_bytes"`
}
// Quota accounting for one (installation, scope) pair. The candidate key is
// excluded so overwriting an existing key is measured as a replacement rather
// than an addition. octet_length, not char_length: the service compares these
// against byte budgets, and a UTF-8 character is up to 4 bytes.
func (q *Queries) GetPluginStorageUsage(ctx context.Context, arg GetPluginStorageUsageParams) (GetPluginStorageUsageRow, error) {
row := q.db.QueryRow(ctx, getPluginStorageUsage,
arg.InstallationID,
arg.ScopeType,
arg.ScopeID,
arg.Key,
)
var i GetPluginStorageUsageRow
err := row.Scan(&i.KeyCount, &i.TotalBytes)
return i, err
}
const getPluginStorageValue = `-- name: GetPluginStorageValue :one
SELECT id, installation_id, scope_type, scope_id, key, value, created_at, updated_at FROM plugin_storage
WHERE installation_id = $1 AND scope_type = $2 AND scope_id = $3 AND key = $4
`
type GetPluginStorageValueParams struct {
InstallationID pgtype.UUID `json:"installation_id"`
ScopeType string `json:"scope_type"`
ScopeID pgtype.UUID `json:"scope_id"`
Key string `json:"key"`
}
func (q *Queries) GetPluginStorageValue(ctx context.Context, arg GetPluginStorageValueParams) (PluginStorage, error) {
row := q.db.QueryRow(ctx, getPluginStorageValue,
arg.InstallationID,
arg.ScopeType,
arg.ScopeID,
arg.Key,
)
var i PluginStorage
err := row.Scan(
&i.ID,
&i.InstallationID,
&i.ScopeType,
&i.ScopeID,
&i.Key,
&i.Value,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getWorkspacePluginInstallation = `-- name: GetWorkspacePluginInstallation :one
SELECT id, workspace_id, plugin_key, source_url, version, manifest, granted_scopes, config, enabled, installed_by, created_at, updated_at, token_hash, token_rotated_at, mcp_approvals FROM plugin_installation
WHERE workspace_id = $1 AND id = $2
`
type GetWorkspacePluginInstallationParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
ID pgtype.UUID `json:"id"`
}
func (q *Queries) GetWorkspacePluginInstallation(ctx context.Context, arg GetWorkspacePluginInstallationParams) (PluginInstallation, error) {
row := q.db.QueryRow(ctx, getWorkspacePluginInstallation, arg.WorkspaceID, arg.ID)
var i PluginInstallation
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.PluginKey,
&i.SourceUrl,
&i.Version,
&i.Manifest,
&i.GrantedScopes,
&i.Config,
&i.Enabled,
&i.InstalledBy,
&i.CreatedAt,
&i.UpdatedAt,
&i.TokenHash,
&i.TokenRotatedAt,
&i.McpApprovals,
)
return i, err
}
const getWorkspacePluginInstallationByKey = `-- name: GetWorkspacePluginInstallationByKey :one
SELECT id, workspace_id, plugin_key, source_url, version, manifest, granted_scopes, config, enabled, installed_by, created_at, updated_at, token_hash, token_rotated_at, mcp_approvals FROM plugin_installation
WHERE workspace_id = $1 AND plugin_key = $2
`
type GetWorkspacePluginInstallationByKeyParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
PluginKey string `json:"plugin_key"`
}
func (q *Queries) GetWorkspacePluginInstallationByKey(ctx context.Context, arg GetWorkspacePluginInstallationByKeyParams) (PluginInstallation, error) {
row := q.db.QueryRow(ctx, getWorkspacePluginInstallationByKey, arg.WorkspaceID, arg.PluginKey)
var i PluginInstallation
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.PluginKey,
&i.SourceUrl,
&i.Version,
&i.Manifest,
&i.GrantedScopes,
&i.Config,
&i.Enabled,
&i.InstalledBy,
&i.CreatedAt,
&i.UpdatedAt,
&i.TokenHash,
&i.TokenRotatedAt,
&i.McpApprovals,
)
return i, err
}
const listPluginInvocations = `-- name: ListPluginInvocations :many
SELECT id, installation_id, workspace_id, hook_key, trigger, status, event_type, attempt, latency_ms, error, created_at FROM plugin_invocation
WHERE installation_id = $1
ORDER BY created_at DESC
LIMIT $2
`
type ListPluginInvocationsParams struct {
InstallationID pgtype.UUID `json:"installation_id"`
Limit int32 `json:"limit"`
}
func (q *Queries) ListPluginInvocations(ctx context.Context, arg ListPluginInvocationsParams) ([]PluginInvocation, error) {
rows, err := q.db.Query(ctx, listPluginInvocations, arg.InstallationID, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
items := []PluginInvocation{}
for rows.Next() {
var i PluginInvocation
if err := rows.Scan(
&i.ID,
&i.InstallationID,
&i.WorkspaceID,
&i.HookKey,
&i.Trigger,
&i.Status,
&i.EventType,
&i.Attempt,
&i.LatencyMs,
&i.Error,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listPluginSecretKeys = `-- name: ListPluginSecretKeys :many
SELECT key, updated_at FROM plugin_secret
WHERE installation_id = $1
ORDER BY key ASC
`
type ListPluginSecretKeysRow struct {
Key string `json:"key"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
// Deliberately never selects ciphertext: the presence of a secret is readable,
// its value is not.
func (q *Queries) ListPluginSecretKeys(ctx context.Context, installationID pgtype.UUID) ([]ListPluginSecretKeysRow, error) {
rows, err := q.db.Query(ctx, listPluginSecretKeys, installationID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListPluginSecretKeysRow{}
for rows.Next() {
var i ListPluginSecretKeysRow
if err := rows.Scan(&i.Key, &i.UpdatedAt); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listPluginSkills = `-- name: ListPluginSkills :many
SELECT id, workspace_id, name, description, content, config, created_by, created_at, updated_at, plugin_installation_id FROM skill WHERE plugin_installation_id = $1 ORDER BY name ASC
`
func (q *Queries) ListPluginSkills(ctx context.Context, pluginInstallationID pgtype.UUID) ([]Skill, error) {
rows, err := q.db.Query(ctx, listPluginSkills, pluginInstallationID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Skill{}
for rows.Next() {
var i Skill
if err := rows.Scan(
&i.ID,
&i.WorkspaceID,
&i.Name,
&i.Description,
&i.Content,
&i.Config,
&i.CreatedBy,
&i.CreatedAt,
&i.UpdatedAt,
&i.PluginInstallationID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listPluginStorageKeys = `-- name: ListPluginStorageKeys :many
SELECT key, octet_length(value)::bigint AS size_bytes, updated_at
FROM plugin_storage
WHERE installation_id = $1 AND scope_type = $2 AND scope_id = $3
ORDER BY key ASC
`
type ListPluginStorageKeysParams struct {
InstallationID pgtype.UUID `json:"installation_id"`
ScopeType string `json:"scope_type"`
ScopeID pgtype.UUID `json:"scope_id"`
}
type ListPluginStorageKeysRow struct {
Key string `json:"key"`
SizeBytes int64 `json:"size_bytes"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) ListPluginStorageKeys(ctx context.Context, arg ListPluginStorageKeysParams) ([]ListPluginStorageKeysRow, error) {
rows, err := q.db.Query(ctx, listPluginStorageKeys, arg.InstallationID, arg.ScopeType, arg.ScopeID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListPluginStorageKeysRow{}
for rows.Next() {
var i ListPluginStorageKeysRow
if err := rows.Scan(&i.Key, &i.SizeBytes, &i.UpdatedAt); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listWorkspacePluginInstallations = `-- name: ListWorkspacePluginInstallations :many
SELECT id, workspace_id, plugin_key, source_url, version, manifest, granted_scopes, config, enabled, installed_by, created_at, updated_at, token_hash, token_rotated_at, mcp_approvals FROM plugin_installation
WHERE workspace_id = $1
ORDER BY created_at ASC
`
func (q *Queries) ListWorkspacePluginInstallations(ctx context.Context, workspaceID pgtype.UUID) ([]PluginInstallation, error) {
rows, err := q.db.Query(ctx, listWorkspacePluginInstallations, workspaceID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []PluginInstallation{}
for rows.Next() {
var i PluginInstallation
if err := rows.Scan(
&i.ID,
&i.WorkspaceID,
&i.PluginKey,
&i.SourceUrl,
&i.Version,
&i.Manifest,
&i.GrantedScopes,
&i.Config,
&i.Enabled,
&i.InstalledBy,
&i.CreatedAt,
&i.UpdatedAt,
&i.TokenHash,
&i.TokenRotatedAt,
&i.McpApprovals,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const setPluginInstallationEnabled = `-- name: SetPluginInstallationEnabled :one
UPDATE plugin_installation
SET enabled = $2,
updated_at = now()
WHERE id = $1
RETURNING id, workspace_id, plugin_key, source_url, version, manifest, granted_scopes, config, enabled, installed_by, created_at, updated_at, token_hash, token_rotated_at, mcp_approvals
`
type SetPluginInstallationEnabledParams struct {
ID pgtype.UUID `json:"id"`
Enabled bool `json:"enabled"`
}
func (q *Queries) SetPluginInstallationEnabled(ctx context.Context, arg SetPluginInstallationEnabledParams) (PluginInstallation, error) {
row := q.db.QueryRow(ctx, setPluginInstallationEnabled, arg.ID, arg.Enabled)
var i PluginInstallation
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.PluginKey,
&i.SourceUrl,
&i.Version,
&i.Manifest,
&i.GrantedScopes,
&i.Config,
&i.Enabled,
&i.InstalledBy,
&i.CreatedAt,
&i.UpdatedAt,
&i.TokenHash,
&i.TokenRotatedAt,
&i.McpApprovals,
)
return i, err
}
const setPluginInstallationToken = `-- name: SetPluginInstallationToken :exec
UPDATE plugin_installation
SET token_hash = $2, token_rotated_at = now(), updated_at = now()
WHERE id = $1
`
type SetPluginInstallationTokenParams struct {
ID pgtype.UUID `json:"id"`
TokenHash pgtype.Text `json:"token_hash"`
}
func (q *Queries) SetPluginInstallationToken(ctx context.Context, arg SetPluginInstallationTokenParams) error {
_, err := q.db.Exec(ctx, setPluginInstallationToken, arg.ID, arg.TokenHash)
return err
}
const setPluginMCPApprovals = `-- name: SetPluginMCPApprovals :one
UPDATE plugin_installation
SET mcp_approvals = $2, updated_at = now()
WHERE id = $1
RETURNING id, workspace_id, plugin_key, source_url, version, manifest, granted_scopes, config, enabled, installed_by, created_at, updated_at, token_hash, token_rotated_at, mcp_approvals
`
type SetPluginMCPApprovalsParams struct {
ID pgtype.UUID `json:"id"`
McpApprovals []byte `json:"mcp_approvals"`
}
func (q *Queries) SetPluginMCPApprovals(ctx context.Context, arg SetPluginMCPApprovalsParams) (PluginInstallation, error) {
row := q.db.QueryRow(ctx, setPluginMCPApprovals, arg.ID, arg.McpApprovals)
var i PluginInstallation
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.PluginKey,
&i.SourceUrl,
&i.Version,
&i.Manifest,
&i.GrantedScopes,
&i.Config,
&i.Enabled,
&i.InstalledBy,
&i.CreatedAt,
&i.UpdatedAt,
&i.TokenHash,
&i.TokenRotatedAt,
&i.McpApprovals,
)
return i, err
}
const updatePluginInstallationConfig = `-- name: UpdatePluginInstallationConfig :one
UPDATE plugin_installation
SET config = $2,
updated_at = now()
WHERE id = $1
RETURNING id, workspace_id, plugin_key, source_url, version, manifest, granted_scopes, config, enabled, installed_by, created_at, updated_at, token_hash, token_rotated_at, mcp_approvals
`
type UpdatePluginInstallationConfigParams struct {
ID pgtype.UUID `json:"id"`
Config []byte `json:"config"`
}
func (q *Queries) UpdatePluginInstallationConfig(ctx context.Context, arg UpdatePluginInstallationConfigParams) (PluginInstallation, error) {
row := q.db.QueryRow(ctx, updatePluginInstallationConfig, arg.ID, arg.Config)
var i PluginInstallation
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.PluginKey,
&i.SourceUrl,
&i.Version,
&i.Manifest,
&i.GrantedScopes,
&i.Config,
&i.Enabled,
&i.InstalledBy,
&i.CreatedAt,
&i.UpdatedAt,
&i.TokenHash,
&i.TokenRotatedAt,
&i.McpApprovals,
)
return i, err
}
const updatePluginInstallationManifest = `-- name: UpdatePluginInstallationManifest :one
UPDATE plugin_installation
SET source_url = $2,
version = $3,
manifest = $4,
granted_scopes = $5,
config = $6,
updated_at = now()
WHERE id = $1
RETURNING id, workspace_id, plugin_key, source_url, version, manifest, granted_scopes, config, enabled, installed_by, created_at, updated_at, token_hash, token_rotated_at, mcp_approvals
`
type UpdatePluginInstallationManifestParams struct {
ID pgtype.UUID `json:"id"`
SourceUrl string `json:"source_url"`
Version string `json:"version"`
Manifest []byte `json:"manifest"`
GrantedScopes []byte `json:"granted_scopes"`
Config []byte `json:"config"`
}
// Upgrade path: the re-consented manifest snapshot replaces the old one in
// place. Config values survive on purpose; fields the new manifest dropped are
// pruned by the service before this runs.
func (q *Queries) UpdatePluginInstallationManifest(ctx context.Context, arg UpdatePluginInstallationManifestParams) (PluginInstallation, error) {
row := q.db.QueryRow(ctx, updatePluginInstallationManifest,
arg.ID,
arg.SourceUrl,
arg.Version,
arg.Manifest,
arg.GrantedScopes,
arg.Config,
)
var i PluginInstallation
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.PluginKey,
&i.SourceUrl,
&i.Version,
&i.Manifest,
&i.GrantedScopes,
&i.Config,
&i.Enabled,
&i.InstalledBy,
&i.CreatedAt,
&i.UpdatedAt,
&i.TokenHash,
&i.TokenRotatedAt,
&i.McpApprovals,
)
return i, err
}
const upsertPluginSecret = `-- name: UpsertPluginSecret :exec
INSERT INTO plugin_secret (installation_id, key, ciphertext)
VALUES ($1, $2, $3)
ON CONFLICT (installation_id, key)
DO UPDATE SET ciphertext = EXCLUDED.ciphertext, updated_at = now()
`
type UpsertPluginSecretParams struct {
InstallationID pgtype.UUID `json:"installation_id"`
Key string `json:"key"`
Ciphertext []byte `json:"ciphertext"`
}
func (q *Queries) UpsertPluginSecret(ctx context.Context, arg UpsertPluginSecretParams) error {
_, err := q.db.Exec(ctx, upsertPluginSecret, arg.InstallationID, arg.Key, arg.Ciphertext)
return err
}
const upsertPluginSkill = `-- name: UpsertPluginSkill :one
INSERT INTO skill (workspace_id, name, description, content, config, created_by, plugin_installation_id)
VALUES ($1, $2, $3, $4, '{}'::jsonb, $6, $5)
ON CONFLICT (workspace_id, name) DO UPDATE SET
description = EXCLUDED.description,
content = EXCLUDED.content,
updated_at = now()
WHERE skill.plugin_installation_id = EXCLUDED.plugin_installation_id
RETURNING id, workspace_id, name, description, content, config, created_by, created_at, updated_at, plugin_installation_id
`
type UpsertPluginSkillParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
Name string `json:"name"`
Description string `json:"description"`
Content string `json:"content"`
PluginInstallationID pgtype.UUID `json:"plugin_installation_id"`
CreatedBy pgtype.UUID `json:"created_by"`
}
// A plugin's skill resource, as an ordinary workspace skill.
//
// Upsert on (workspace_id, name) because that is the table's own uniqueness
// rule and an upgrade re-installs the same skill. The WHERE clause is the
// important half: it refuses to overwrite a skill a PERSON created, or one
// another installation owns. A plugin claiming a name someone already used must
// fail the install loudly, not silently replace their work.
func (q *Queries) UpsertPluginSkill(ctx context.Context, arg UpsertPluginSkillParams) (Skill, error) {
row := q.db.QueryRow(ctx, upsertPluginSkill,
arg.WorkspaceID,
arg.Name,
arg.Description,
arg.Content,
arg.PluginInstallationID,
arg.CreatedBy,
)
var i Skill
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.Name,
&i.Description,
&i.Content,
&i.Config,
&i.CreatedBy,
&i.CreatedAt,
&i.UpdatedAt,
&i.PluginInstallationID,
)
return i, err
}
const upsertPluginStorageValue = `-- name: UpsertPluginStorageValue :one
INSERT INTO plugin_storage (installation_id, scope_type, scope_id, key, value)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (installation_id, scope_type, scope_id, key)
DO UPDATE SET value = EXCLUDED.value, updated_at = now()
RETURNING id, installation_id, scope_type, scope_id, key, value, created_at, updated_at
`
type UpsertPluginStorageValueParams struct {
InstallationID pgtype.UUID `json:"installation_id"`
ScopeType string `json:"scope_type"`
ScopeID pgtype.UUID `json:"scope_id"`
Key string `json:"key"`
Value string `json:"value"`
}
func (q *Queries) UpsertPluginStorageValue(ctx context.Context, arg UpsertPluginStorageValueParams) (PluginStorage, error) {
row := q.db.QueryRow(ctx, upsertPluginStorageValue,
arg.InstallationID,
arg.ScopeType,
arg.ScopeID,
arg.Key,
arg.Value,
)
var i PluginStorage
err := row.Scan(
&i.ID,
&i.InstallationID,
&i.ScopeType,
&i.ScopeID,
&i.Key,
&i.Value,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}