-
Couldn't load subscription status.
- Fork 84
adds dynamic ui #655
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 10-16-dynamic_plugin_architecture
Are you sure you want to change the base?
adds dynamic ui #655
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -600,13 +600,18 @@ func migrationAddVirtualKeyMCPConfigsTable(ctx context.Context, db *gorm.DB) err | |
| // migrationAddPluginPathColumn adds the path column to the plugin table | ||
| func migrationAddPluginPathColumn(ctx context.Context, db *gorm.DB) error { | ||
| m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{ | ||
| ID: "add_plugin_path_column", | ||
| ID: "update_plugins_table_for_custom_plugins", | ||
| Migrate: func(tx *gorm.DB) error { | ||
| tx = tx.WithContext(ctx) | ||
| migrator := tx.Migrator() | ||
| if !migrator.HasColumn(&tables.TablePlugin{}, "path") { | ||
| if err := migrator.AddColumn(&tables.TablePlugin{}, "path"); err != nil { | ||
| return err | ||
| } | ||
| } | ||
| if !migrator.HasColumn(&tables.TablePlugin{}, "is_custom") { | ||
| if err := migrator.AddColumn(&tables.TablePlugin{}, "is_custom"); err != nil { | ||
| return err | ||
| } | ||
| } | ||
|
Comment on lines
+612
to
616
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainVerify default value is applied to existing rows. When adding the Consider running this verification script after the migration to confirm existing rows have 🏁 Script executed: #!/bin/bash
# Verify that all existing plugins have is_custom set to false after migration
# This requires database access credentials to be configured
echo "This script requires manual execution with database access"
echo "Example SQL to verify after migration:"
echo ""
echo "SELECT name, is_custom, path FROM config_plugins WHERE is_custom IS NULL OR is_custom = true;"
echo ""
echo "Expected: No rows returned (all existing plugins should have is_custom = false)"Length of output: 273 Length of output: 170 🏁 Script executed: #!/bin/bash
# View the migration code context around lines 612-616
cat framework/configstore/migrations.go | sed -n '605,625p'Length of output: 694 🏁 Script executed: #!/bin/bash
# Check if there are any explicit ALTER statements or default value assignments in the migration
rg -n "is_custom" framework/configstore/migrations.go -B3 -A3Length of output: 483 Length of output: 170 🏁 Script executed: #!/bin/bash
# Once found, search for is_custom field definition with GORM tags
rg -n "is_custom" --type go -B2 -A2 | grep -A2 -B2 "is_custom"Length of output: 750 Length of output: 2320 🏁 Script executed: #!/bin/bash
# Search for is_custom across all files to find the struct definition
find . -name "*.go" -type f | xargs grep -l "is_custom" | head -10Length of output: 37 🏁 Script executed: #!/bin/bash
# Look for tables directory and plugin definitions
find . -path "*/tables/*.go" -type f 2>/dev/null | head -20Length of output: 802 Length of output: 1157 🏁 Script executed: #!/bin/bash
# Look for UPDATE statements in the migration function around line 612
sed -n '600,635p' framework/configstore/migrations.goLength of output: 1170 |
||
| return nil | ||
|
|
@@ -617,6 +622,9 @@ func migrationAddPluginPathColumn(ctx context.Context, db *gorm.DB) error { | |
| if err := migrator.DropColumn(&tables.TablePlugin{}, "path"); err != nil { | ||
| return err | ||
| } | ||
| if err := migrator.DropColumn(&tables.TablePlugin{}, "is_custom"); err != nil { | ||
| return err | ||
| } | ||
| return nil | ||
| }, | ||
| }}) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -825,12 +825,12 @@ func (s *RDBConfigStore) DeleteModelPrices(ctx context.Context, tx ...*gorm.DB) | |||||||||||||||||||||
|
|
||||||||||||||||||||||
| // PLUGINS METHODS | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| func (s *RDBConfigStore) GetPlugins(ctx context.Context) ([]tables.TablePlugin, error) { | ||||||||||||||||||||||
| var plugins []tables.TablePlugin | ||||||||||||||||||||||
| func (s *RDBConfigStore) GetPlugins(ctx context.Context) ([]*tables.TablePlugin, error) { | ||||||||||||||||||||||
| var plugins []*tables.TablePlugin | ||||||||||||||||||||||
| if err := s.db.WithContext(ctx).Find(&plugins).Error; err != nil { | ||||||||||||||||||||||
| return nil, err | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| return plugins, nil | ||||||||||||||||||||||
| return plugins, nil | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| func (s *RDBConfigStore) GetPlugin(ctx context.Context, name string) (*tables.TablePlugin, error) { | ||||||||||||||||||||||
|
|
@@ -851,6 +851,10 @@ func (s *RDBConfigStore) CreatePlugin(ctx context.Context, plugin *tables.TableP | |||||||||||||||||||||
| } else { | ||||||||||||||||||||||
| txDB = s.db | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| // Mark plugin as custom if path is not empty | ||||||||||||||||||||||
| if plugin.Path != nil && *plugin.Path != "" { | ||||||||||||||||||||||
| plugin.IsCustom = true | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+854
to
+857
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Derive IsCustom deterministically (also when path is cleared). Currently only sets true; if a plugin’s path becomes empty, -// Mark plugin as custom if path is not empty
-if plugin.Path != nil && *plugin.Path != "" {
- plugin.IsCustom = true
-}
+// Derive IsCustom from Path (trimmed) on every write
+if plugin.Path != nil && strings.TrimSpace(*plugin.Path) != "" {
+ plugin.IsCustom = true
+} else {
+ plugin.IsCustom = false
+}Add import: import (
"context"
"encoding/json"
"errors"
"fmt"
+ "strings"
)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
| return txDB.WithContext(ctx).Create(plugin).Error | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -866,6 +870,11 @@ func (s *RDBConfigStore) UpdatePlugin(ctx context.Context, plugin *tables.TableP | |||||||||||||||||||||
| localTx = true | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Mark plugin as custom if path is not empty | ||||||||||||||||||||||
| if plugin.Path != nil && *plugin.Path != "" { | ||||||||||||||||||||||
| plugin.IsCustom = true | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
Comment on lines
+873
to
+877
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Apply the same IsCustom derivation in UpdatePlugin. Mirror the deterministic computation here as well to avoid divergence. -// Mark plugin as custom if path is not empty
-if plugin.Path != nil && *plugin.Path != "" {
- plugin.IsCustom = true
-}
+// Derive IsCustom from Path consistently
+if plugin.Path != nil && strings.TrimSpace(*plugin.Path) != "" {
+ plugin.IsCustom = true
+} else {
+ plugin.IsCustom = false
+}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
| if err := txDB.WithContext(ctx).Delete(&tables.TablePlugin{}, "name = ?", plugin.Name).Error; err != nil { | ||||||||||||||||||||||
| if localTx { | ||||||||||||||||||||||
| txDB.Rollback() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ import ( | |
| type PluginsLoader interface { | ||
| ReloadPlugin(ctx context.Context, name string, path *string, pluginConfig any) error | ||
| RemovePlugin(ctx context.Context, name string) error | ||
| GetPluginStatus() []schemas.PluginStatus | ||
| } | ||
|
|
||
| // PluginsHandler is the handler for the plugins API | ||
|
|
@@ -41,11 +42,13 @@ type CreatePluginRequest struct { | |
| Name string `json:"name"` | ||
| Enabled bool `json:"enabled"` | ||
| Config map[string]any `json:"config"` | ||
| Path *string `json:"path"` | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // UpdatePluginRequest is the request body for updating a plugin | ||
| type UpdatePluginRequest struct { | ||
| Enabled bool `json:"enabled"` | ||
| Path *string `json:"path"` | ||
| Config map[string]any `json:"config"` | ||
| } | ||
|
|
||
|
|
@@ -66,10 +69,64 @@ func (h *PluginsHandler) getPlugins(ctx *fasthttp.RequestCtx) { | |
| SendError(ctx, 500, "Failed to retrieve plugins", h.logger) | ||
| return | ||
| } | ||
|
|
||
| // Fetching status | ||
| pluginStatus := h.pluginsLoader.GetPluginStatus() | ||
| // Creating ephemeral struct for the plugins | ||
| finalPlugins := []struct{ | ||
| Name string `json:"name"` | ||
| Enabled bool `json:"enabled"` | ||
| Config any `json:"config"` | ||
| IsCustom bool `json:"isCustom"` | ||
| Path *string `json:"path"` | ||
| Status schemas.PluginStatus `json:"status"` | ||
| }{} | ||
| // Iterating over plugin status to get the plugin info | ||
| for _,pluginStatus := range pluginStatus { | ||
| var pluginInfo *configstoreTables.TablePlugin | ||
| for _, plugin := range plugins { | ||
| if plugin.Name == pluginStatus.Name { | ||
| pluginInfo = plugin | ||
| break | ||
| } | ||
| } | ||
| if pluginInfo == nil { | ||
| finalPlugins = append(finalPlugins, struct{ | ||
| Name string `json:"name"` | ||
| Enabled bool `json:"enabled"` | ||
| Config any `json:"config"` | ||
| IsCustom bool `json:"isCustom"` | ||
| Path *string `json:"path"` | ||
| Status schemas.PluginStatus `json:"status"` | ||
| }{ | ||
| Name: pluginStatus.Name, | ||
| Enabled: pluginStatus.Status != schemas.PluginStatusDisabled, | ||
| Config: map[string]any{}, | ||
| IsCustom: false, | ||
| Path: nil, | ||
| Status: pluginStatus, | ||
| }) | ||
| continue | ||
| } | ||
| finalPlugins = append(finalPlugins, struct{ | ||
| Name string `json:"name"` | ||
| Enabled bool `json:"enabled"` | ||
| Config any `json:"config"` | ||
| IsCustom bool `json:"isCustom"` | ||
| Path *string `json:"path"` | ||
| Status schemas.PluginStatus `json:"status"` | ||
| }{ | ||
| Name: pluginInfo.Name, | ||
| Enabled: pluginInfo.Enabled, | ||
| Config: pluginInfo.Config, | ||
| IsCustom: pluginInfo.IsCustom, | ||
| Path: pluginInfo.Path, | ||
| Status: pluginStatus, | ||
| }) | ||
| } | ||
| // Creating ephemeral struct | ||
| SendJSON(ctx, map[string]any{ | ||
| "plugins": plugins, | ||
| "count": len(plugins), | ||
| "plugins": finalPlugins, | ||
| "count": len(plugins), | ||
| }, h.logger) | ||
|
Comment on lines
+72
to
130
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix count inconsistency and reduce duplication. Two issues:
Apply this diff to fix the count: SendJSON(ctx, map[string]any{
"plugins": finalPlugins,
- "count": len(plugins),
+ "count": len(finalPlugins),
}, h.logger)To reduce duplication, define a named type before the function: type PluginWithStatus struct {
Name string `json:"name"`
Enabled bool `json:"enabled"`
Config any `json:"config"`
IsCustom bool `json:"isCustom"`
Path *string `json:"path"`
Status schemas.PluginStatus `json:"status"`
}Then replace the anonymous struct with 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
|
|
@@ -134,6 +191,8 @@ func (h *PluginsHandler) createPlugin(ctx *fasthttp.RequestCtx) { | |
| Name: request.Name, | ||
| Enabled: request.Enabled, | ||
| Config: request.Config, | ||
| Path: request.Path, | ||
| IsCustom: true, | ||
| }); err != nil { | ||
| h.logger.Error("failed to create plugin: %v", err) | ||
| SendError(ctx, 500, "Failed to create plugin", h.logger) | ||
|
|
@@ -149,7 +208,7 @@ func (h *PluginsHandler) createPlugin(ctx *fasthttp.RequestCtx) { | |
|
|
||
| // We reload the plugin if its enabled | ||
| if request.Enabled { | ||
| if err := h.pluginsLoader.ReloadPlugin(ctx, request.Name, nil, request.Config); err != nil { | ||
| if err := h.pluginsLoader.ReloadPlugin(ctx, request.Name, request.Path, request.Config); err != nil { | ||
| h.logger.Error("failed to load plugin: %v", err) | ||
| SendJSON(ctx, map[string]any{ | ||
| "message": fmt.Sprintf("Plugin created successfully; but failed to load plugin with new config: %v", err), | ||
|
|
@@ -188,16 +247,21 @@ func (h *PluginsHandler) updatePlugin(ctx *fasthttp.RequestCtx) { | |
| SendError(ctx, 400, "Empty 'name' parameter not allowed", h.logger) | ||
| return | ||
| } | ||
|
|
||
| var plugin *configstoreTables.TablePlugin | ||
| var err error | ||
| // Check if plugin exists | ||
| if _, err := h.configStore.GetPlugin(ctx, name); err != nil { | ||
| plugin, err = h.configStore.GetPlugin(ctx, name) | ||
| if err != nil { | ||
| // If doesn't exist, create it | ||
| if errors.Is(err, configstore.ErrNotFound) { | ||
| if err := h.configStore.CreatePlugin(ctx, &configstoreTables.TablePlugin{ | ||
| plugin = &configstoreTables.TablePlugin{ | ||
| Name: name, | ||
| Enabled: false, | ||
| Config: map[string]any{}, | ||
| }); err != nil { | ||
| Path: nil, | ||
| IsCustom: true, | ||
| } | ||
|
Comment on lines
195
to
+263
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inconsistent auto-creation: IsCustom true but Path nil. When auto-creating a missing plugin, the code sets Consider either:
plugin = &configstoreTables.TablePlugin{
Name: name,
Enabled: false,
Config: map[string]any{},
Path: nil,
- IsCustom: true,
+ IsCustom: false, // No path means not custom
}🤖 Prompt for AI Agents |
||
| if err := h.configStore.CreatePlugin(ctx, plugin); err != nil { | ||
| h.logger.Error("failed to create plugin: %v", err) | ||
| SendError(ctx, 500, "Failed to create plugin", h.logger) | ||
| return | ||
|
|
@@ -209,24 +273,28 @@ func (h *PluginsHandler) updatePlugin(ctx *fasthttp.RequestCtx) { | |
| } | ||
| } | ||
|
|
||
| // Unmarshalling the request body | ||
| var request UpdatePluginRequest | ||
| if err := json.Unmarshal(ctx.PostBody(), &request); err != nil { | ||
| h.logger.Error("failed to unmarshal update plugin request: %v", err) | ||
| SendError(ctx, 400, "Invalid request body", h.logger) | ||
| return | ||
| } | ||
|
|
||
| // Updating the plugin | ||
| if err := h.configStore.UpdatePlugin(ctx, &configstoreTables.TablePlugin{ | ||
| Name: name, | ||
| Enabled: request.Enabled, | ||
| Config: request.Config, | ||
| Path: request.Path, | ||
| IsCustom: plugin.IsCustom, | ||
| }); err != nil { | ||
| h.logger.Error("failed to update plugin: %v", err) | ||
| SendError(ctx, 500, "Failed to update plugin", h.logger) | ||
| return | ||
| } | ||
|
|
||
| plugin, err := h.configStore.GetPlugin(ctx, name) | ||
| plugin, err = h.configStore.GetPlugin(ctx, name) | ||
| if err != nil { | ||
| if errors.Is(err, gorm.ErrRecordNotFound) { | ||
| SendError(ctx, fasthttp.StatusNotFound, "Plugin not found", h.logger) | ||
|
|
@@ -238,7 +306,7 @@ func (h *PluginsHandler) updatePlugin(ctx *fasthttp.RequestCtx) { | |
| } | ||
| // We reload the plugin if its enabled, otherwise we stop it | ||
| if request.Enabled { | ||
| if err := h.pluginsLoader.ReloadPlugin(ctx, name, nil, request.Config); err != nil { | ||
| if err := h.pluginsLoader.ReloadPlugin(ctx, name, request.Path, request.Config); err != nil { | ||
| h.logger.Error("failed to load plugin: %v", err) | ||
| SendJSON(ctx, map[string]any{ | ||
| "message": fmt.Sprintf("Plugin updated successfully; but failed to load plugin with new config: %v", err), | ||
|
|
@@ -247,6 +315,7 @@ func (h *PluginsHandler) updatePlugin(ctx *fasthttp.RequestCtx) { | |
| return | ||
| } | ||
| } else { | ||
| ctx.SetUserValue("isDisabled", true) | ||
| if err := h.pluginsLoader.RemovePlugin(ctx, name); err != nil { | ||
| h.logger.Error("failed to stop plugin: %v", err) | ||
| SendJSON(ctx, map[string]any{ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
CRITICAL: Verify migration ID change impact before merging.
Changing the migration ID from
"add_plugin_path_column"to"update_plugins_table_for_custom_plugins"will cause the migration system to treat this as a completely new migration. If the old ID was already executed in any environment (dev, staging, production), the system will attempt to run this migration again, potentially causing conflicts even with theHasColumnchecks.Please verify:
"add_plugin_path_column"been deployed to any environment?is_customcolumn)Run this script to check if the old migration ID exists in the migration history:
🏁 Script executed:
Length of output: 188
🏁 Script executed:
Length of output: 306
🏁 Script executed:
Length of output: 2291
🏁 Script executed:
Length of output: 2118
CRITICAL: Migration ID change will re-execute already-deployed migration in production.
The git history confirms that the migration ID
"add_plugin_path_column"was already deployed. Changing it to"update_plugins_table_for_custom_plugins"will cause the migration system to treat this as a completely new migration and attempt to execute it again across all environments, even though theHasColumnchecks exist.Action required: Either:
"add_plugin_path_column"and create a separate new migration for theis_customcolumn, ORSecondary issue: The rollback logic lacks defensive checks. Add
HasColumnguards beforeDropColumncalls (lines 625–627) to prevent failures if the migration partially executed.🤖 Prompt for AI Agents