diff --git a/manifest.json b/manifest.json index 55790ca2..1bba24ad 100644 --- a/manifest.json +++ b/manifest.json @@ -4,6 +4,7 @@ "plugins": [ {"name": "browser-automation", "repo": "Molecule-AI/molecule-ai-plugin-browser-automation", "ref": "main"}, {"name": "ecc", "repo": "Molecule-AI/molecule-ai-plugin-ecc", "ref": "main"}, + {"name": "gh-identity", "repo": "Molecule-AI/molecule-ai-plugin-gh-identity", "ref": "main"}, {"name": "molecule-audit", "repo": "Molecule-AI/molecule-ai-plugin-molecule-audit", "ref": "main"}, {"name": "molecule-audit-trail", "repo": "Molecule-AI/molecule-ai-plugin-molecule-audit-trail", "ref": "main"}, {"name": "molecule-careful-bash", "repo": "Molecule-AI/molecule-ai-plugin-molecule-careful-bash", "ref": "main"}, diff --git a/workspace-server/cmd/server/main.go b/workspace-server/cmd/server/main.go index 6ab47cc4..c1676e29 100644 --- a/workspace-server/cmd/server/main.go +++ b/workspace-server/cmd/server/main.go @@ -23,10 +23,13 @@ import ( "github.com/Molecule-AI/molecule-monorepo/platform/internal/supervised" "github.com/Molecule-AI/molecule-monorepo/platform/internal/ws" - // External plugin — registers an EnvMutator that injects GITHUB_TOKEN / - // GH_TOKEN from a GitHub App installation token. Soft-dep: only active - // when GITHUB_APP_ID env var is set (see main() for the gate). - pluginloader "github.com/Molecule-AI/molecule-ai-plugin-github-app-auth/pluginloader" + // External plugins — each registers EnvMutator(s) that run at workspace + // provision time. Loaded via soft-dep gates in main() so self-hosters + // without the App or without per-agent identity configured keep working. + githubappauth "github.com/Molecule-AI/molecule-ai-plugin-github-app-auth/pluginloader" + ghidentity "github.com/Molecule-AI/molecule-ai-plugin-gh-identity/pluginloader" + + "github.com/Molecule-AI/molecule-monorepo/platform/pkg/provisionhook" ) func main() { @@ -153,22 +156,49 @@ func main() { wh.SetCPProvisioner(cpProv) } + // External-plugin env mutators — each plugin contributes 0+ mutators + // onto a shared registry. Order matters: gh-identity populates + // MOLECULE_AGENT_ROLE-derived attribution env vars that downstream + // mutators and the workspace's install.sh can then read. Keep + // github-app-auth last because it fails loudly on misconfig and its + // failure mode is "no GITHUB_TOKEN" — worth surfacing after the + // cheaper mutators already ran. + envReg := provisionhook.NewRegistry() + + // gh-identity plugin — per-agent attribution via env injection + gh + // wrapper shipped as base64 env. Soft-dep: no config file is OK + // (plugin no-ops when no role is set on the workspace). + // Tracks molecule-core#1957. + if res, err := ghidentity.BuildRegistry(); err != nil { + log.Fatalf("gh-identity plugin: %v", err) + } else { + envReg.Register(res.Mutator) + log.Printf("gh-identity: registered (config file=%q)", os.Getenv("MOLECULE_GH_IDENTITY_CONFIG_FILE")) + } + // github-app-auth plugin — injects GITHUB_TOKEN + GH_TOKEN into every // workspace env using the App's installation access token (rotates ~hourly). // Soft-skip when GITHUB_APP_* env vars are absent so dev/self-hosters // without an App configured keep working; fail-loud only on MISCONFIG // (e.g. APP_ID set but key file missing), not on unset. if os.Getenv("GITHUB_APP_ID") != "" { - if reg, err := pluginloader.BuildRegistry(); err != nil { + if reg, err := githubappauth.BuildRegistry(); err != nil { log.Fatalf("github-app-auth plugin: %v", err) } else { - wh.SetEnvMutators(reg) - log.Printf("github-app-auth: registered, %d mutator(s) in chain", reg.Len()) + // Copy the plugin's mutators onto the shared registry so the + // TokenProvider probe (FirstTokenProvider) still finds them. + for _, m := range reg.Mutators() { + envReg.Register(m) + } + log.Printf("github-app-auth: registered, %d mutator(s) added to chain", reg.Len()) } } else { log.Println("github-app-auth: GITHUB_APP_ID unset — skipping plugin registration (agents will use any PAT from .env)") } + wh.SetEnvMutators(envReg) + log.Printf("env-mutator chain: %v", envReg.Names()) + // Offline handler: broadcast event + auto-restart the dead workspace onWorkspaceOffline := func(innerCtx context.Context, workspaceID string) { if err := broadcaster.RecordAndBroadcast(innerCtx, "WORKSPACE_OFFLINE", workspaceID, map[string]interface{}{}); err != nil { diff --git a/workspace-server/go.mod b/workspace-server/go.mod index 6c50916a..2c022c32 100644 --- a/workspace-server/go.mod +++ b/workspace-server/go.mod @@ -4,6 +4,7 @@ go 1.25.0 require ( github.com/DATA-DOG/go-sqlmock v1.5.2 + github.com/Molecule-AI/molecule-ai-plugin-gh-identity v0.0.0-20260424033845-4fd5ac7be30f github.com/Molecule-AI/molecule-ai-plugin-github-app-auth v0.0.0-20260421064811-7d98ae51e31d github.com/alicebob/miniredis/v2 v2.37.0 github.com/creack/pty v1.1.18 diff --git a/workspace-server/go.sum b/workspace-server/go.sum index 681bb0cd..75e6b911 100644 --- a/workspace-server/go.sum +++ b/workspace-server/go.sum @@ -4,6 +4,8 @@ github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7Oputl github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/Microsoft/go-winio v0.4.21 h1:+6mVbXh4wPzUrl1COX9A+ZCvEpYsOBZ6/+kwDnvLyro= github.com/Microsoft/go-winio v0.4.21/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Molecule-AI/molecule-ai-plugin-gh-identity v0.0.0-20260424033845-4fd5ac7be30f h1:YkLRhUg+9qr9OV9N8dG1Hj0Ml7TThHlRwh5F//oUJVs= +github.com/Molecule-AI/molecule-ai-plugin-gh-identity v0.0.0-20260424033845-4fd5ac7be30f/go.mod h1:NqdtlWZDJvpXNJRHnMkPhTKHdA1LZTNH+63TB66JSOU= github.com/Molecule-AI/molecule-ai-plugin-github-app-auth v0.0.0-20260421064811-7d98ae51e31d h1:GpYhP6FxaJZc1Ljy5/YJ9ZIVGvfOqZBmDolNr2S5x2g= github.com/Molecule-AI/molecule-ai-plugin-github-app-auth v0.0.0-20260421064811-7d98ae51e31d/go.mod h1:3a6LR/zd7FjR9ZwLTbytwYlWuCBsbCOVFlEg0WnoYiM= github.com/alicebob/miniredis/v2 v2.37.0 h1:RheObYW32G1aiJIj81XVt78ZHJpHonHLHW7OLIshq68= diff --git a/workspace-server/internal/handlers/workspace_provision.go b/workspace-server/internal/handlers/workspace_provision.go index 0ebb0503..dff410f6 100644 --- a/workspace-server/internal/handlers/workspace_provision.go +++ b/workspace-server/internal/handlers/workspace_provision.go @@ -96,6 +96,14 @@ func (h *WorkspaceHandler) provisionWorkspaceOpts(workspaceID, templatePath stri applyAgentGitIdentity(envVars, payload.Name) applyRuntimeModelEnv(envVars, payload.Runtime, payload.Model) + // Propagate the workspace's role into env so role-aware plugins + // (gh-identity — molecule-core#1957) can read it without the + // plugin interface having to carry the full payload. Role is + // cosmetic metadata — no auth weight on it — safe to surface as env. + if payload.Role != "" { + envVars["MOLECULE_AGENT_ROLE"] = payload.Role + } + // Plugin extension point: run any registered EnvMutators (e.g. // github-app-auth, vault-secrets) AFTER built-in identity injection so // plugins can override or augment GIT_AUTHOR_*, GITHUB_TOKEN, etc. @@ -678,6 +686,11 @@ func (h *WorkspaceHandler) provisionWorkspaceCP(workspaceID, templatePath string applyAgentGitIdentity(envVars, payload.Name) applyRuntimeModelEnv(envVars, payload.Runtime, payload.Model) + // Propagate role for role-aware plugins (#1957). See provisionWorkspace + // above for rationale. + if payload.Role != "" { + envVars["MOLECULE_AGENT_ROLE"] = payload.Role + } if err := h.envMutators.Run(ctx, workspaceID, envVars); err != nil { log.Printf("CPProvisioner: env mutator failed for %s: %v", workspaceID, err) // F1086 / #1206: env mutator errors (missing tokens, vault paths) must not diff --git a/workspace-server/pkg/provisionhook/mutator.go b/workspace-server/pkg/provisionhook/mutator.go index 504b5f54..9433467d 100644 --- a/workspace-server/pkg/provisionhook/mutator.go +++ b/workspace-server/pkg/provisionhook/mutator.go @@ -143,6 +143,21 @@ func (r *Registry) Names() []string { return names } +// Mutators returns a copy of the registered mutators in registration +// order. Used when multiple plugins build their own registries and need +// to merge onto a shared one at boot. Returns a copy so callers can't +// mutate internal state. +func (r *Registry) Mutators() []EnvMutator { + if r == nil { + return nil + } + r.mu.RLock() + defer r.mu.RUnlock() + out := make([]EnvMutator, len(r.mutators)) + copy(out, r.mutators) + return out +} + // FirstTokenProvider returns the first registered mutator that also // implements TokenProvider, or nil if none do. Used to back the // GET /admin/github-installation-token endpoint so long-running