@@ -347,6 +347,12 @@ type serverApp struct {
347347 currentConfig * config.WorkflowConfig // last loaded config, used by dynamic config watcher
348348}
349349
350+ // ReconfigureModules delegates to the current engine, ensuring the reloader
351+ // always targets the active engine even after a full reload replaces it.
352+ func (app * serverApp ) ReconfigureModules (ctx context.Context , changes []config.ModuleConfigChange ) ([]string , error ) {
353+ return app .engine .ReconfigureModules (ctx , changes )
354+ }
355+
350356// setup initializes all server components: engine, AI services, and HTTP mux.
351357func setup (logger * slog.Logger , cfg * config.WorkflowConfig ) (* serverApp , error ) {
352358 app := & serverApp {
@@ -1232,9 +1238,9 @@ func run(ctx context.Context, app *serverApp, listenAddr string) error {
12321238
12331239 var reloaderErr error
12341240 reloader , reloaderErr = config .NewConfigReloader (
1235- app .currentConfig , // the loaded WorkflowConfig
1236- app .reloadEngine , // existing full reload function
1237- app . engine , // implements ModuleReconfigurer
1241+ app .currentConfig , // the loaded WorkflowConfig
1242+ app .reloadEngine , // existing full reload function
1243+ app , // stable adapter — delegates to current app.engine
12381244 app .logger ,
12391245 )
12401246 if reloaderErr != nil {
@@ -1269,7 +1275,7 @@ func run(ctx context.Context, app *serverApp, listenAddr string) error {
12691275 // Reuse or create a reloader for the DB poller.
12701276 if reloader == nil {
12711277 var reloaderErr error
1272- reloader , reloaderErr = config .NewConfigReloader (app .currentConfig , app .reloadEngine , app . engine , app .logger )
1278+ reloader , reloaderErr = config .NewConfigReloader (app .currentConfig , app .reloadEngine , app , app .logger )
12731279 if reloaderErr != nil {
12741280 app .logger .Error ("Failed to create config reloader for DB poller" , "error" , reloaderErr )
12751281 }
@@ -1590,40 +1596,56 @@ func runMultiWorkflow(logger *slog.Logger) error {
15901596
15911597 // Module reconfiguration endpoint — allows runtime hot-reload of individual
15921598 // modules that implement interfaces.Reconfigurable without a full engine restart.
1593- mux .HandleFunc ("PUT /api/v1/modules/{name}/config" , func (w http.ResponseWriter , r * http.Request ) {
1599+ // Wrapped with the same RequireAuth middleware used by the API router.
1600+ reconfigPerms := apihandler .NewPermissionService (stores .Memberships , stores .Workflows , stores .Projects )
1601+ reconfigMw := apihandler .NewMiddleware ([]byte (secret ), stores .Users , reconfigPerms )
1602+ mux .Handle ("PUT /api/v1/modules/{name}/config" , reconfigMw .RequireAuth (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
15941603 moduleName := r .PathValue ("name" )
15951604 if moduleName == "" {
1596- http .Error (w , `{"error":"module name required"}` , http .StatusBadRequest )
1605+ w .Header ().Set ("Content-Type" , "application/json" )
1606+ w .WriteHeader (http .StatusBadRequest )
1607+ _ = json .NewEncoder (w ).Encode (map [string ]string {"error" : "module name required" })
15971608 return
15981609 }
15991610
1611+ const maxConfigBytes = 1 << 20 // 1 MiB
1612+ limitedBody := http .MaxBytesReader (w , r .Body , maxConfigBytes )
1613+ defer limitedBody .Close () //nolint:errcheck
1614+
16001615 var newConfig map [string ]any
1601- if err := json .NewDecoder (r .Body ).Decode (& newConfig ); err != nil {
1602- http .Error (w , fmt .Sprintf (`{"error":"invalid JSON: %v"}` , err ), http .StatusBadRequest )
1616+ if err := json .NewDecoder (limitedBody ).Decode (& newConfig ); err != nil {
1617+ w .Header ().Set ("Content-Type" , "application/json" )
1618+ w .WriteHeader (http .StatusBadRequest )
1619+ _ = json .NewEncoder (w ).Encode (map [string ]string {"error" : fmt .Sprintf ("invalid JSON: %v" , err )})
16031620 return
16041621 }
16051622
16061623 mod := app .engine .GetApp ().GetModule (moduleName )
16071624 if mod == nil {
1608- http .Error (w , fmt .Sprintf (`{"error":"module %q not found"}` , moduleName ), http .StatusNotFound )
1625+ w .Header ().Set ("Content-Type" , "application/json" )
1626+ w .WriteHeader (http .StatusNotFound )
1627+ _ = json .NewEncoder (w ).Encode (map [string ]string {"error" : fmt .Sprintf ("module %q not found" , moduleName )})
16091628 return
16101629 }
16111630
16121631 reconf , ok := mod .(interfaces.Reconfigurable )
16131632 if ! ok {
1614- http .Error (w , fmt .Sprintf (`{"error":"module %q does not support runtime reconfiguration"}` , moduleName ), http .StatusNotImplemented )
1633+ w .Header ().Set ("Content-Type" , "application/json" )
1634+ w .WriteHeader (http .StatusNotImplemented )
1635+ _ = json .NewEncoder (w ).Encode (map [string ]string {"error" : fmt .Sprintf ("module %q does not support runtime reconfiguration" , moduleName )})
16151636 return
16161637 }
16171638
16181639 if err := reconf .Reconfigure (r .Context (), newConfig ); err != nil {
1619- http .Error (w , fmt .Sprintf (`{"error":"reconfiguration failed: %v"}` , err ), http .StatusInternalServerError )
1640+ w .Header ().Set ("Content-Type" , "application/json" )
1641+ w .WriteHeader (http .StatusInternalServerError )
1642+ _ = json .NewEncoder (w ).Encode (map [string ]string {"error" : fmt .Sprintf ("reconfiguration failed: %v" , err )})
16201643 return
16211644 }
16221645
16231646 w .Header ().Set ("Content-Type" , "application/json" )
1624- resp , _ := json .Marshal (map [string ]string {"status" : "ok" , "module" : moduleName })
1625- w .Write (resp ) //nolint:errcheck
1626- })
1647+ _ = json .NewEncoder (w ).Encode (map [string ]string {"status" : "ok" , "module" : moduleName })
1648+ })))
16271649
16281650 mux .Handle ("/" , http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
16291651 w .Header ().Set ("Content-Type" , "application/json" )
0 commit comments