diff --git a/workspace-server/go.sum b/workspace-server/go.sum index a31b0c4e..2936e4c8 100644 --- a/workspace-server/go.sum +++ b/workspace-server/go.sum @@ -154,6 +154,8 @@ github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +go.moleculesai.app/plugin/gh-identity v0.0.0-20260509010445-788988195fce h1:ftm0ba0ukLlfqeFes+/jWnXH8XULXmRpMy3fOCZ83/U= +go.moleculesai.app/plugin/gh-identity v0.0.0-20260509010445-788988195fce/go.mod h1:0aAqoDle2V7Cywso94MXdv1DH/HEe/0oZmcbqWYMK7g= go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= diff --git a/workspace-server/internal/plugins/drift_sweeper.go b/workspace-server/internal/plugins/drift_sweeper.go index 684b2f65..a7624793 100644 --- a/workspace-server/internal/plugins/drift_sweeper.go +++ b/workspace-server/internal/plugins/drift_sweeper.go @@ -61,15 +61,26 @@ const DriftSweepInterval = 1 * time.Hour // that handles Gitea instances on high-latency links. const ResolveRefDeadline = 60 * time.Second -// PluginResolver resolves plugin sources to installable directories. -// Satisfied by *Registry (which wraps GithubResolver + LocalResolver). +// PluginResolver is the registry-level abstraction the sweeper consumes: +// pick a per-scheme SourceResolver for a parsed Source, and enumerate the +// registered schemes so we can strip the prefix from a stored source_raw. +// +// Resolve returns the production SourceResolver from source.go (NOT another +// PluginResolver) — that's the actual shape of *Registry.Resolve, and the +// sweeper only needs the per-scheme resolver's identity, not its Fetch. +// // Named PluginResolver (not SourceResolver) to avoid redeclaring the -// SourceResolver interface defined in source.go (core#228 fix). +// per-scheme SourceResolver interface defined in source.go (core#228 fix). +// Satisfied by *Registry from source.go via Resolve + Schemes. type PluginResolver interface { - Resolve(source Source) (PluginResolver, error) + Resolve(source Source) (SourceResolver, error) Schemes() []string } +// Compile-time assertion: *Registry satisfies PluginResolver. Catches any +// future drift in Registry.Resolve / Schemes signatures at build time. +var _ PluginResolver = (*Registry)(nil) + // StartPluginDriftSweeper runs the drift-detection loop until ctx is cancelled. // Pass a nil resolver to disable the sweeper (useful for harnesses or CP/SaaS // mode where git operations are unavailable). diff --git a/workspace-server/internal/plugins/drift_sweeper_test.go b/workspace-server/internal/plugins/drift_sweeper_test.go index 3370dce1..7fe07525 100644 --- a/workspace-server/internal/plugins/drift_sweeper_test.go +++ b/workspace-server/internal/plugins/drift_sweeper_test.go @@ -2,12 +2,14 @@ package plugins import ( "context" - "database/sql" "errors" "testing" ) -// stubResolver is a SourceResolver that always returns a stub github resolver. +// stubResolver is a PluginResolver that always returns a stub github +// resolver. *GithubResolver satisfies the production SourceResolver from +// source.go via Scheme() + Fetch(); the sweeper only uses Schemes() and +// Resolve(), so the returned resolver's Fetch is never invoked here. type stubResolver struct { schemes []string } @@ -156,8 +158,9 @@ func TestPluginUpdateQueueRow_Struct(t *testing.T) { } } -// TestSourceResolverInterface_StubResolver verifies that a stub resolver -// satisfies the SourceResolver interface. -func TestSourceResolverInterface_StubResolver(t *testing.T) { - var _ SourceResolver = (*stubResolver)(nil) +// TestPluginResolverInterface_StubResolver verifies that a stub resolver +// satisfies the PluginResolver interface (the sweeper-side abstraction +// over *Registry — distinct from the per-scheme SourceResolver in source.go). +func TestPluginResolverInterface_StubResolver(t *testing.T) { + var _ PluginResolver = (*stubResolver)(nil) } diff --git a/workspace-server/internal/router/router.go b/workspace-server/internal/router/router.go index 9fec06dd..aac18c14 100644 --- a/workspace-server/internal/router/router.go +++ b/workspace-server/internal/router/router.go @@ -27,7 +27,15 @@ import ( "github.com/gin-gonic/gin" ) -func Setup(hub *ws.Hub, broadcaster *events.Broadcaster, prov *provisioner.Provisioner, platformURL, configsDir string, wh *handlers.WorkspaceHandler, channelMgr *channels.Manager, memBundle *memwiring.Bundle, pluginResolver plugins.SourceResolver) *gin.Engine { +// Setup wires the gin router. pluginResolver is the registry-level resolver +// (typically *plugins.Registry from main.go) reserved for future per-deploy +// customisation — currently passed only to satisfy the call-site contract; +// plgh (PluginsHandler) constructs its own internal registry with the +// default github+local resolvers via NewPluginsHandler. The drift sweeper +// (main.go) gets the same pluginResolver instance so it can share scheme +// enumeration if a deployment registers extra schemes externally. A nil +// pluginResolver is harmless: plgh still works with its built-in defaults. +func Setup(hub *ws.Hub, broadcaster *events.Broadcaster, prov *provisioner.Provisioner, platformURL, configsDir string, wh *handlers.WorkspaceHandler, channelMgr *channels.Manager, memBundle *memwiring.Bundle, pluginResolver plugins.PluginResolver) *gin.Engine { r := gin.Default() // Issue #179 — trust no reverse-proxy headers. Without this call Gin's @@ -499,6 +507,18 @@ func Setup(hub *ws.Hub, broadcaster *events.Broadcaster, prov *provisioner.Provi r.POST("/admin/workspace-images/refresh", middleware.AdminAuth(db.DB), imgH.Refresh) } + // dockerCli is shared across plugins, terminal, templates, and bundle + // handlers. Declared up-front (was at line ~594) because the plugins + // init block — moved here in 70f84823 to fix "undefined: plgh" — needs + // dockerCli at construction time (NewPluginsHandler signature). Moving + // only the plgh block left dockerCli used-before-declared. Same nil + // guard semantics: prov nil → dockerCli nil → handlers fall back to + // non-Docker paths or skip Docker-dependent routes. + var dockerCli *client.Client + if prov != nil { + dockerCli = prov.DockerClient() + } + // Plugins — plgh must be initialized before the drift handler that uses it. // Moved here (core#248 fix) because the drift handler block (core#123) was // registered before plgh was created, causing "undefined: plgh" on main. @@ -531,16 +551,17 @@ func Setup(hub *ws.Hub, broadcaster *events.Broadcaster, prov *provisioner.Provi ).Scan(&instanceID) return instanceID, err } - // pluginResolver: when provided (normal production), use it for plgh so - // the drift sweeper (which also gets the same resolver in main.go) uses - // identical resolver state. When nil (test / backward compat), let - // NewPluginsHandler create its own default registry. + // plgh constructs its own internal registry (github + local) inside + // NewPluginsHandler. The pluginResolver param is the SHARED registry the + // drift sweeper consumes (main.go); we don't graft it onto plgh because + // plgh's WithSourceResolver expects a per-scheme SourceResolver, not a + // PluginResolver/registry. Cross-wiring those types was the original + // "*Registry doesn't implement SourceResolver" build break (core#228). + // Use of pluginResolver here is intentionally read-side only. + _ = pluginResolver plgh := handlers.NewPluginsHandler(pluginsDir, dockerCli, wh.RestartByID). WithRuntimeLookup(runtimeLookup). WithInstanceIDLookup(instanceIDLookup) - if pluginResolver != nil { - plgh = plgh.WithSourceResolver(pluginResolver) - } r.GET("/plugins", plgh.ListRegistry) r.GET("/plugins/sources", plgh.ListSources) wsAuth.GET("/plugins", plgh.ListInstalled) @@ -590,11 +611,7 @@ func Setup(hub *ws.Hub, broadcaster *events.Broadcaster, prov *provisioner.Provi wsAuth.GET("/github-installation-token", ghTokH.GetInstallationToken) } - // Terminal — shares Docker client with provisioner - var dockerCli *client.Client - if prov != nil { - dockerCli = prov.DockerClient() - } + // Terminal — shares Docker client with provisioner (declared above). th := handlers.NewTerminalHandler(dockerCli) wsAuth.GET("/terminal", th.HandleConnect) wsAuth.GET("/terminal/diagnose", th.HandleDiagnose)