mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 09:26:50 +08:00
* 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]>
1359 lines
41 KiB
Go
1359 lines
41 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 countInstallationsOfPackageVersions = `-- name: CountInstallationsOfPackageVersions :one
|
|
SELECT count(*) FROM plugin_installation
|
|
WHERE package_version_id IN (
|
|
SELECT id FROM plugin_package_version WHERE package_id = $1
|
|
)
|
|
`
|
|
|
|
// Whether any workspace still runs a version of this package. Publishing is
|
|
// workspace-private, so this is scoped to the same workspace by construction.
|
|
func (q *Queries) CountInstallationsOfPackageVersions(ctx context.Context, packageID pgtype.UUID) (int64, error) {
|
|
row := q.db.QueryRow(ctx, countInstallationsOfPackageVersions, packageID)
|
|
var count int64
|
|
err := row.Scan(&count)
|
|
return count, err
|
|
}
|
|
|
|
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, package_version_id, version, manifest, granted_scopes, installed_by
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
RETURNING id, workspace_id, plugin_key, version, manifest, granted_scopes, config, enabled, installed_by, created_at, updated_at, token_hash, token_rotated_at, mcp_approvals, package_version_id
|
|
`
|
|
|
|
type CreatePluginInstallationParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
PluginKey string `json:"plugin_key"`
|
|
PackageVersionID pgtype.UUID `json:"package_version_id"`
|
|
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.PackageVersionID,
|
|
arg.Version,
|
|
arg.Manifest,
|
|
arg.GrantedScopes,
|
|
arg.InstalledBy,
|
|
)
|
|
var i PluginInstallation
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.PluginKey,
|
|
&i.Version,
|
|
&i.Manifest,
|
|
&i.GrantedScopes,
|
|
&i.Config,
|
|
&i.Enabled,
|
|
&i.InstalledBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.TokenHash,
|
|
&i.TokenRotatedAt,
|
|
&i.McpApprovals,
|
|
&i.PackageVersionID,
|
|
)
|
|
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 createPluginPackage = `-- name: CreatePluginPackage :one
|
|
INSERT INTO plugin_package (workspace_id, plugin_key, name, created_by)
|
|
VALUES ($1, $2, $3, $4)
|
|
RETURNING id, workspace_id, plugin_key, name, created_by, created_at, updated_at
|
|
`
|
|
|
|
type CreatePluginPackageParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
PluginKey string `json:"plugin_key"`
|
|
Name string `json:"name"`
|
|
CreatedBy pgtype.UUID `json:"created_by"`
|
|
}
|
|
|
|
func (q *Queries) CreatePluginPackage(ctx context.Context, arg CreatePluginPackageParams) (PluginPackage, error) {
|
|
row := q.db.QueryRow(ctx, createPluginPackage,
|
|
arg.WorkspaceID,
|
|
arg.PluginKey,
|
|
arg.Name,
|
|
arg.CreatedBy,
|
|
)
|
|
var i PluginPackage
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.PluginKey,
|
|
&i.Name,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const createPluginPackageFile = `-- name: CreatePluginPackageFile :exec
|
|
INSERT INTO plugin_package_file (version_id, path, content, size_bytes, sha256)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
`
|
|
|
|
type CreatePluginPackageFileParams struct {
|
|
VersionID pgtype.UUID `json:"version_id"`
|
|
Path string `json:"path"`
|
|
Content []byte `json:"content"`
|
|
SizeBytes int64 `json:"size_bytes"`
|
|
Sha256 string `json:"sha256"`
|
|
}
|
|
|
|
func (q *Queries) CreatePluginPackageFile(ctx context.Context, arg CreatePluginPackageFileParams) error {
|
|
_, err := q.db.Exec(ctx, createPluginPackageFile,
|
|
arg.VersionID,
|
|
arg.Path,
|
|
arg.Content,
|
|
arg.SizeBytes,
|
|
arg.Sha256,
|
|
)
|
|
return err
|
|
}
|
|
|
|
const createPluginPackageVersion = `-- name: CreatePluginPackageVersion :one
|
|
INSERT INTO plugin_package_version (
|
|
package_id, workspace_id, version, manifest, digest, size_bytes, published_by
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
RETURNING id, package_id, workspace_id, version, manifest, digest, size_bytes, published_by, created_at
|
|
`
|
|
|
|
type CreatePluginPackageVersionParams struct {
|
|
PackageID pgtype.UUID `json:"package_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Version string `json:"version"`
|
|
Manifest []byte `json:"manifest"`
|
|
Digest string `json:"digest"`
|
|
SizeBytes int64 `json:"size_bytes"`
|
|
PublishedBy pgtype.UUID `json:"published_by"`
|
|
}
|
|
|
|
// Published versions are only ever inserted. Nothing updates one, and the
|
|
// (package_id, version) unique index is what makes a second publish of the same
|
|
// version a conflict instead of a silent overwrite.
|
|
func (q *Queries) CreatePluginPackageVersion(ctx context.Context, arg CreatePluginPackageVersionParams) (PluginPackageVersion, error) {
|
|
row := q.db.QueryRow(ctx, createPluginPackageVersion,
|
|
arg.PackageID,
|
|
arg.WorkspaceID,
|
|
arg.Version,
|
|
arg.Manifest,
|
|
arg.Digest,
|
|
arg.SizeBytes,
|
|
arg.PublishedBy,
|
|
)
|
|
var i PluginPackageVersion
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.PackageID,
|
|
&i.WorkspaceID,
|
|
&i.Version,
|
|
&i.Manifest,
|
|
&i.Digest,
|
|
&i.SizeBytes,
|
|
&i.PublishedBy,
|
|
&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 deletePluginPackage = `-- name: DeletePluginPackage :exec
|
|
DELETE FROM plugin_package WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) DeletePluginPackage(ctx context.Context, id pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deletePluginPackage, id)
|
|
return err
|
|
}
|
|
|
|
const deletePluginPackageFilesByPackage = `-- name: DeletePluginPackageFilesByPackage :exec
|
|
DELETE FROM plugin_package_file
|
|
WHERE version_id IN (SELECT id FROM plugin_package_version WHERE package_id = $1)
|
|
`
|
|
|
|
func (q *Queries) DeletePluginPackageFilesByPackage(ctx context.Context, packageID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deletePluginPackageFilesByPackage, packageID)
|
|
return err
|
|
}
|
|
|
|
const deletePluginPackageVersionsByPackage = `-- name: DeletePluginPackageVersionsByPackage :exec
|
|
DELETE FROM plugin_package_version WHERE package_id = $1
|
|
`
|
|
|
|
func (q *Queries) DeletePluginPackageVersionsByPackage(ctx context.Context, packageID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deletePluginPackageVersionsByPackage, packageID)
|
|
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, version, manifest, granted_scopes, config, enabled, installed_by, created_at, updated_at, token_hash, token_rotated_at, mcp_approvals, package_version_id 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.Version,
|
|
&i.Manifest,
|
|
&i.GrantedScopes,
|
|
&i.Config,
|
|
&i.Enabled,
|
|
&i.InstalledBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.TokenHash,
|
|
&i.TokenRotatedAt,
|
|
&i.McpApprovals,
|
|
&i.PackageVersionID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getPluginInstallationByTokenHash = `-- name: GetPluginInstallationByTokenHash :one
|
|
SELECT id, workspace_id, plugin_key, version, manifest, granted_scopes, config, enabled, installed_by, created_at, updated_at, token_hash, token_rotated_at, mcp_approvals, package_version_id 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.Version,
|
|
&i.Manifest,
|
|
&i.GrantedScopes,
|
|
&i.Config,
|
|
&i.Enabled,
|
|
&i.InstalledBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.TokenHash,
|
|
&i.TokenRotatedAt,
|
|
&i.McpApprovals,
|
|
&i.PackageVersionID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getPluginPackageFile = `-- name: GetPluginPackageFile :one
|
|
SELECT id, version_id, path, content, size_bytes, sha256, created_at FROM plugin_package_file
|
|
WHERE version_id = $1 AND path = $2
|
|
`
|
|
|
|
type GetPluginPackageFileParams struct {
|
|
VersionID pgtype.UUID `json:"version_id"`
|
|
Path string `json:"path"`
|
|
}
|
|
|
|
func (q *Queries) GetPluginPackageFile(ctx context.Context, arg GetPluginPackageFileParams) (PluginPackageFile, error) {
|
|
row := q.db.QueryRow(ctx, getPluginPackageFile, arg.VersionID, arg.Path)
|
|
var i PluginPackageFile
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.VersionID,
|
|
&i.Path,
|
|
&i.Content,
|
|
&i.SizeBytes,
|
|
&i.Sha256,
|
|
&i.CreatedAt,
|
|
)
|
|
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, version, manifest, granted_scopes, config, enabled, installed_by, created_at, updated_at, token_hash, token_rotated_at, mcp_approvals, package_version_id 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.Version,
|
|
&i.Manifest,
|
|
&i.GrantedScopes,
|
|
&i.Config,
|
|
&i.Enabled,
|
|
&i.InstalledBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.TokenHash,
|
|
&i.TokenRotatedAt,
|
|
&i.McpApprovals,
|
|
&i.PackageVersionID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getWorkspacePluginInstallationByKey = `-- name: GetWorkspacePluginInstallationByKey :one
|
|
SELECT id, workspace_id, plugin_key, version, manifest, granted_scopes, config, enabled, installed_by, created_at, updated_at, token_hash, token_rotated_at, mcp_approvals, package_version_id 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.Version,
|
|
&i.Manifest,
|
|
&i.GrantedScopes,
|
|
&i.Config,
|
|
&i.Enabled,
|
|
&i.InstalledBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.TokenHash,
|
|
&i.TokenRotatedAt,
|
|
&i.McpApprovals,
|
|
&i.PackageVersionID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getWorkspacePluginPackage = `-- name: GetWorkspacePluginPackage :one
|
|
SELECT id, workspace_id, plugin_key, name, created_by, created_at, updated_at FROM plugin_package
|
|
WHERE workspace_id = $1 AND id = $2
|
|
`
|
|
|
|
type GetWorkspacePluginPackageParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
ID pgtype.UUID `json:"id"`
|
|
}
|
|
|
|
func (q *Queries) GetWorkspacePluginPackage(ctx context.Context, arg GetWorkspacePluginPackageParams) (PluginPackage, error) {
|
|
row := q.db.QueryRow(ctx, getWorkspacePluginPackage, arg.WorkspaceID, arg.ID)
|
|
var i PluginPackage
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.PluginKey,
|
|
&i.Name,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getWorkspacePluginPackageByKey = `-- name: GetWorkspacePluginPackageByKey :one
|
|
SELECT id, workspace_id, plugin_key, name, created_by, created_at, updated_at FROM plugin_package
|
|
WHERE workspace_id = $1 AND plugin_key = $2
|
|
`
|
|
|
|
type GetWorkspacePluginPackageByKeyParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
PluginKey string `json:"plugin_key"`
|
|
}
|
|
|
|
func (q *Queries) GetWorkspacePluginPackageByKey(ctx context.Context, arg GetWorkspacePluginPackageByKeyParams) (PluginPackage, error) {
|
|
row := q.db.QueryRow(ctx, getWorkspacePluginPackageByKey, arg.WorkspaceID, arg.PluginKey)
|
|
var i PluginPackage
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.PluginKey,
|
|
&i.Name,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getWorkspacePluginPackageVersion = `-- name: GetWorkspacePluginPackageVersion :one
|
|
SELECT id, package_id, workspace_id, version, manifest, digest, size_bytes, published_by, created_at FROM plugin_package_version
|
|
WHERE workspace_id = $1 AND id = $2
|
|
`
|
|
|
|
type GetWorkspacePluginPackageVersionParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
ID pgtype.UUID `json:"id"`
|
|
}
|
|
|
|
func (q *Queries) GetWorkspacePluginPackageVersion(ctx context.Context, arg GetWorkspacePluginPackageVersionParams) (PluginPackageVersion, error) {
|
|
row := q.db.QueryRow(ctx, getWorkspacePluginPackageVersion, arg.WorkspaceID, arg.ID)
|
|
var i PluginPackageVersion
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.PackageID,
|
|
&i.WorkspaceID,
|
|
&i.Version,
|
|
&i.Manifest,
|
|
&i.Digest,
|
|
&i.SizeBytes,
|
|
&i.PublishedBy,
|
|
&i.CreatedAt,
|
|
)
|
|
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 listPluginPackageFilePaths = `-- name: ListPluginPackageFilePaths :many
|
|
SELECT path, size_bytes, sha256 FROM plugin_package_file
|
|
WHERE version_id = $1
|
|
ORDER BY path ASC
|
|
`
|
|
|
|
type ListPluginPackageFilePathsRow struct {
|
|
Path string `json:"path"`
|
|
SizeBytes int64 `json:"size_bytes"`
|
|
Sha256 string `json:"sha256"`
|
|
}
|
|
|
|
// Paths and sizes only: the publisher's file list never needs the bytes.
|
|
func (q *Queries) ListPluginPackageFilePaths(ctx context.Context, versionID pgtype.UUID) ([]ListPluginPackageFilePathsRow, error) {
|
|
rows, err := q.db.Query(ctx, listPluginPackageFilePaths, versionID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListPluginPackageFilePathsRow{}
|
|
for rows.Next() {
|
|
var i ListPluginPackageFilePathsRow
|
|
if err := rows.Scan(&i.Path, &i.SizeBytes, &i.Sha256); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listPluginPackageVersions = `-- name: ListPluginPackageVersions :many
|
|
SELECT id, package_id, workspace_id, version, manifest, digest, size_bytes, published_by, created_at FROM plugin_package_version
|
|
WHERE package_id = $1
|
|
ORDER BY created_at DESC
|
|
`
|
|
|
|
func (q *Queries) ListPluginPackageVersions(ctx context.Context, packageID pgtype.UUID) ([]PluginPackageVersion, error) {
|
|
rows, err := q.db.Query(ctx, listPluginPackageVersions, packageID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []PluginPackageVersion{}
|
|
for rows.Next() {
|
|
var i PluginPackageVersion
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.PackageID,
|
|
&i.WorkspaceID,
|
|
&i.Version,
|
|
&i.Manifest,
|
|
&i.Digest,
|
|
&i.SizeBytes,
|
|
&i.PublishedBy,
|
|
&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, version, manifest, granted_scopes, config, enabled, installed_by, created_at, updated_at, token_hash, token_rotated_at, mcp_approvals, package_version_id 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.Version,
|
|
&i.Manifest,
|
|
&i.GrantedScopes,
|
|
&i.Config,
|
|
&i.Enabled,
|
|
&i.InstalledBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.TokenHash,
|
|
&i.TokenRotatedAt,
|
|
&i.McpApprovals,
|
|
&i.PackageVersionID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listWorkspacePluginPackages = `-- name: ListWorkspacePluginPackages :many
|
|
SELECT id, workspace_id, plugin_key, name, created_by, created_at, updated_at FROM plugin_package
|
|
WHERE workspace_id = $1
|
|
ORDER BY name ASC
|
|
`
|
|
|
|
func (q *Queries) ListWorkspacePluginPackages(ctx context.Context, workspaceID pgtype.UUID) ([]PluginPackage, error) {
|
|
rows, err := q.db.Query(ctx, listWorkspacePluginPackages, workspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []PluginPackage{}
|
|
for rows.Next() {
|
|
var i PluginPackage
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.PluginKey,
|
|
&i.Name,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const lockPluginPackageKey = `-- name: LockPluginPackageKey :exec
|
|
SELECT pg_advisory_xact_lock(hashtextextended($1::text, 0))
|
|
`
|
|
|
|
// Serializes publish, install and delete for one (workspace, plugin key).
|
|
//
|
|
// Relationships are application-owned by repository policy, so there are no
|
|
// foreign keys to make "this version still exists" true across statements.
|
|
// Without this lock the interleaving `delete counts 0 installs` → `install
|
|
// reads the version` → `delete commits` → `install commits` leaves an
|
|
// installation pointing at a version that no longer exists, and its panel 404s
|
|
// forever with nothing in the product able to explain why.
|
|
//
|
|
// Keyed on the plugin key rather than the package id because publish has no
|
|
// package id yet — the row it would lock is the one it may be about to create.
|
|
func (q *Queries) LockPluginPackageKey(ctx context.Context, dollar_1 string) error {
|
|
_, err := q.db.Exec(ctx, lockPluginPackageKey, dollar_1)
|
|
return err
|
|
}
|
|
|
|
const setPluginInstallationEnabled = `-- name: SetPluginInstallationEnabled :one
|
|
UPDATE plugin_installation
|
|
SET enabled = $2,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING id, workspace_id, plugin_key, version, manifest, granted_scopes, config, enabled, installed_by, created_at, updated_at, token_hash, token_rotated_at, mcp_approvals, package_version_id
|
|
`
|
|
|
|
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.Version,
|
|
&i.Manifest,
|
|
&i.GrantedScopes,
|
|
&i.Config,
|
|
&i.Enabled,
|
|
&i.InstalledBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.TokenHash,
|
|
&i.TokenRotatedAt,
|
|
&i.McpApprovals,
|
|
&i.PackageVersionID,
|
|
)
|
|
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, version, manifest, granted_scopes, config, enabled, installed_by, created_at, updated_at, token_hash, token_rotated_at, mcp_approvals, package_version_id
|
|
`
|
|
|
|
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.Version,
|
|
&i.Manifest,
|
|
&i.GrantedScopes,
|
|
&i.Config,
|
|
&i.Enabled,
|
|
&i.InstalledBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.TokenHash,
|
|
&i.TokenRotatedAt,
|
|
&i.McpApprovals,
|
|
&i.PackageVersionID,
|
|
)
|
|
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, version, manifest, granted_scopes, config, enabled, installed_by, created_at, updated_at, token_hash, token_rotated_at, mcp_approvals, package_version_id
|
|
`
|
|
|
|
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.Version,
|
|
&i.Manifest,
|
|
&i.GrantedScopes,
|
|
&i.Config,
|
|
&i.Enabled,
|
|
&i.InstalledBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.TokenHash,
|
|
&i.TokenRotatedAt,
|
|
&i.McpApprovals,
|
|
&i.PackageVersionID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updatePluginInstallationManifest = `-- name: UpdatePluginInstallationManifest :one
|
|
UPDATE plugin_installation
|
|
SET package_version_id = $2,
|
|
version = $3,
|
|
manifest = $4,
|
|
granted_scopes = $5,
|
|
config = $6,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING id, workspace_id, plugin_key, version, manifest, granted_scopes, config, enabled, installed_by, created_at, updated_at, token_hash, token_rotated_at, mcp_approvals, package_version_id
|
|
`
|
|
|
|
type UpdatePluginInstallationManifestParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
PackageVersionID pgtype.UUID `json:"package_version_id"`
|
|
Version string `json:"version"`
|
|
Manifest []byte `json:"manifest"`
|
|
GrantedScopes []byte `json:"granted_scopes"`
|
|
Config []byte `json:"config"`
|
|
}
|
|
|
|
// Upgrade path: the installation is re-pointed at another published version and
|
|
// takes that version's consented manifest snapshot. 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.PackageVersionID,
|
|
arg.Version,
|
|
arg.Manifest,
|
|
arg.GrantedScopes,
|
|
arg.Config,
|
|
)
|
|
var i PluginInstallation
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.PluginKey,
|
|
&i.Version,
|
|
&i.Manifest,
|
|
&i.GrantedScopes,
|
|
&i.Config,
|
|
&i.Enabled,
|
|
&i.InstalledBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.TokenHash,
|
|
&i.TokenRotatedAt,
|
|
&i.McpApprovals,
|
|
&i.PackageVersionID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updatePluginPackageName = `-- name: UpdatePluginPackageName :one
|
|
UPDATE plugin_package
|
|
SET name = $2,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING id, workspace_id, plugin_key, name, created_by, created_at, updated_at
|
|
`
|
|
|
|
type UpdatePluginPackageNameParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// The display name follows the newest published version. The key never moves:
|
|
// it is the identity an installation was consented under.
|
|
func (q *Queries) UpdatePluginPackageName(ctx context.Context, arg UpdatePluginPackageNameParams) (PluginPackage, error) {
|
|
row := q.db.QueryRow(ctx, updatePluginPackageName, arg.ID, arg.Name)
|
|
var i PluginPackage
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.PluginKey,
|
|
&i.Name,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
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
|
|
}
|