forked from molecule-ai/molecule-core
feat(#1957): wire gh-identity plugin into workspace-server
Ships the monorepo side of molecule-core#1957 (agent identity collapse). Companion to molecule-ai-plugin-gh-identity (new repo, merged-and-tagged separately). Changes: - manifest.json: add gh-identity plugin to Tier 1 registry - workspace-server/go.mod: require github.com/Molecule-AI/molecule-ai-plugin-gh-identity - cmd/server/main.go: build a shared provisionhook.Registry, register gh-identity first (always), then github-app-auth (gated on GITHUB_APP_ID) - workspace_provision.go: propagate workspace.Role into env["MOLECULE_AGENT_ROLE"] before calling the mutator chain, so the gh-identity plugin can see which agent is booting - provisionhook/mutator.go: add Registry.Mutators() accessor so individual-plugin registries can be merged onto a shared one at boot Boot log gains a line like: env-mutator chain: [gh-identity github-app-auth] Effect per workspace: - env contains MOLECULE_AGENT_ROLE, MOLECULE_OWNER, MOLECULE_ATTRIBUTION_BADGE, MOLECULE_GH_WRAPPER_B64, MOLECULE_GH_WRAPPER_SHA - Each workspace template's install.sh can decode + install the wrapper at /usr/local/bin/gh, intercepting @me assignment and prepending agent attribution on PR/issue creates Does not break existing workspaces — absent workspace.role, the plugin is a no-op. Absent install.sh updates in each template, the env vars are simply unused. Follow-up template PRs (hermes, claude-code, langgraph, etc.) each add ~15 lines to install.sh to decode + install the wrapper. Ref: #1957 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6b62391e5d
commit
03e913db75
@ -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"},
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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=
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user