feat: collapse plugin runtime to one tier behind a plugin SDK

This commit is contained in:
LukeGus committed 2026-09-22 14:11:50 -05:00
1 parent 6e058f6e9e
commit 4758016f41
96 files changed
+34631 -8551

No files matched your search

+71 -1
View File
@@ -689,6 +689,7 @@ async function initializeCompleteDatabase(): Promise<void> {
source TEXT NOT NULL DEFAULT 'community',
registry_id TEXT,
state TEXT NOT NULL DEFAULT 'disabled',
last_error TEXT,
installed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
auto_update INTEGER NOT NULL DEFAULT 0,
@@ -701,7 +702,9 @@ async function initializeCompleteDatabase(): Promise<void> {
plugin_id TEXT NOT NULL,
capability TEXT NOT NULL,
granted_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
granted_by TEXT NOT NULL,
-- Nullable: a bundled grant is made by the install, not by a user.
granted_by TEXT,
source TEXT NOT NULL DEFAULT 'admin',
UNIQUE (plugin_id, capability),
FOREIGN KEY (plugin_id) REFERENCES plugins (id) ON DELETE CASCADE,
FOREIGN KEY (granted_by) REFERENCES users (id) ON DELETE CASCADE
@@ -987,6 +990,63 @@ const addColumnIfNotExists = (
}
};
/**
* Drops the NOT NULL on plugin_permission_grants.granted_by.
*
* A bundled grant is made by the install rather than by a person, so there is
* no user id to record. SQLite cannot relax a constraint in place, so the
* table is rebuilt; it only ever holds a handful of rows.
*/
const relaxPluginGrantGrantedBy = () => {
try {
const sql = sqlite
.prepare(
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'plugin_permission_grants'`,
)
.get() as { sql?: string } | undefined;
if (!sql?.sql || !/granted_by\s+TEXT\s+NOT\s+NULL/i.test(sql.sql)) {
return;
}
sqlite.exec(`
PRAGMA foreign_keys=OFF;
BEGIN;
CREATE TABLE plugin_permission_grants_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
plugin_id TEXT NOT NULL,
capability TEXT NOT NULL,
granted_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
granted_by TEXT,
source TEXT NOT NULL DEFAULT 'admin',
UNIQUE (plugin_id, capability),
FOREIGN KEY (plugin_id) REFERENCES plugins (id) ON DELETE CASCADE,
FOREIGN KEY (granted_by) REFERENCES users (id) ON DELETE CASCADE
);
INSERT INTO plugin_permission_grants_new
(id, plugin_id, capability, granted_at, granted_by, source)
SELECT id, plugin_id, capability, granted_at, granted_by, 'admin'
FROM plugin_permission_grants;
DROP TABLE plugin_permission_grants;
ALTER TABLE plugin_permission_grants_new RENAME TO plugin_permission_grants;
COMMIT;
PRAGMA foreign_keys=ON;
`);
databaseLogger.info(
"Relaxed plugin_permission_grants.granted_by to allow bundled grants",
{ operation: "schema_migration" },
);
} catch (error) {
databaseLogger.warn(
`Failed to relax plugin_permission_grants.granted_by: ${
error instanceof Error ? error.message : String(error)
}`,
{ operation: "schema_migration" },
);
}
};
const migrateSchema = () => {
addColumnIfNotExists(
"session_recordings",
@@ -2836,6 +2896,16 @@ const migrateSchema = () => {
}
addColumnIfNotExists("homepage_items", "sync_id", "TEXT");
// Plugin runtime: lastError reports why a plugin is blocked or failed, and
// grants record whether a capability came from an admin or from bundling.
addColumnIfNotExists("plugins", "last_error", "TEXT");
addColumnIfNotExists(
"plugin_permission_grants",
"source",
"TEXT NOT NULL DEFAULT 'admin'",
);
relaxPluginGrantGrantedBy();
const syncIdTables = [
"ssh_data",
"ssh_credentials",
+13 -3
View File
@@ -2142,7 +2142,10 @@ export const plugins = mysqlTable(
tier: text("tier").notNull().default("available"),
source: text("source").notNull().default("community"),
registryId: varchar("registry_id", { length: 255 }),
/** enabled | disabled | blocked | failed */
state: text("state").notNull().default("disabled"),
/** Why the plugin is blocked or failed, for the admin UI. */
lastError: text("last_error"),
installedAt: text("installed_at")
.notNull()
.default(sql`(CURRENT_TIMESTAMP)`),
@@ -2168,9 +2171,16 @@ export const pluginPermissionGrants = mysqlTable(
grantedAt: text("granted_at")
.notNull()
.default(sql`(CURRENT_TIMESTAMP)`),
grantedBy: varchar("granted_by", { length: 255 })
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
/**
* Who granted it. Null for a bundled grant, which no user made: shipping
* in the install is the consent. Nullable also stops a user deletion from
* cascading a bundled plugin's capabilities away.
*/
grantedBy: varchar("granted_by", { length: 255 }).references(() => users.id, {
onDelete: "cascade",
}),
/** admin | bundled */
source: text("source").notNull().default("admin"),
},
// A plugin's grants are always read together, and re-granting the same
// capability should update the existing row rather than duplicate it.
+13 -3
View File
@@ -2143,7 +2143,10 @@ export const plugins = pgTable(
tier: text("tier").notNull().default("available"),
source: text("source").notNull().default("community"),
registryId: varchar("registry_id", { length: 255 }),
/** enabled | disabled | blocked | failed */
state: text("state").notNull().default("disabled"),
/** Why the plugin is blocked or failed, for the admin UI. */
lastError: text("last_error"),
installedAt: text("installed_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
@@ -2169,9 +2172,16 @@ export const pluginPermissionGrants = pgTable(
grantedAt: text("granted_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
grantedBy: varchar("granted_by", { length: 255 })
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
/**
* Who granted it. Null for a bundled grant, which no user made: shipping
* in the install is the consent. Nullable also stops a user deletion from
* cascading a bundled plugin's capabilities away.
*/
grantedBy: varchar("granted_by", { length: 255 }).references(() => users.id, {
onDelete: "cascade",
}),
/** admin | bundled */
source: text("source").notNull().default("admin"),
},
// A plugin's grants are always read together, and re-granting the same
// capability should update the existing row rather than duplicate it.
+13 -3
View File
@@ -2141,7 +2141,10 @@ export const plugins = sqliteTable(
tier: text("tier").notNull().default("available"),
source: text("source").notNull().default("community"),
registryId: text("registry_id"),
/** enabled | disabled | blocked | failed */
state: text("state").notNull().default("disabled"),
/** Why the plugin is blocked or failed, for the admin UI. */
lastError: text("last_error"),
installedAt: text("installed_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
@@ -2167,9 +2170,16 @@ export const pluginPermissionGrants = sqliteTable(
grantedAt: text("granted_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
grantedBy: text("granted_by")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
/**
* Who granted it. Null for a bundled grant, which no user made: shipping
* in the install is the consent. Nullable also stops a user deletion from
* cascading a bundled plugin's capabilities away.
*/
grantedBy: text("granted_by").references(() => users.id, {
onDelete: "cascade",
}),
/** admin | bundled */
source: text("source").notNull().default("admin"),
},
// A plugin's grants are always read together, and re-granting the same
// capability should update the existing row rather than duplicate it.