molecule-ai-plugin-gh-identity/pluginloader/pluginloader.go
Hongming Wang 4fd5ac7be3
Some checks failed
CI / Shellcheck + wrapper tests (push) Failing after 9s
CI / Go build + test + vet (push) Failing after 13m36s
feat(plugin): gh-identity — per-agent attribution via env injection + gh wrapper
Fixes molecule-core#1957: agent identity collapse where all agents share
one GitHub PAT and their writes attribute to the CEO.

This plugin takes the pragmatic "wrap, don't multiply identities" path:
- Injects MOLECULE_AGENT_ROLE / OWNER / ATTRIBUTION_BADGE per workspace
- Ships a shell wrapper for `gh` that:
  * prepends an attribution badge to issue/PR bodies on publish
  * rewrites --assignee @me to the role's designated human owner
  * emits an NDJSON audit log to /var/log/molecule-gh.ndjson
- Wrapper is shipped as base64 env var; each workspace template's
  install.sh decodes and writes it to /usr/local/bin/gh

Scales where GitHub Apps / machine users don't: adding a new agent role
is one entry in config.yaml, not a GitHub UI roundtrip per role.

See README + known-issues.md for the v2-architecture migration plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 20:38:45 -07:00

51 lines
1.7 KiB
Go

// Package pluginloader builds a provisionhook.Registry populated with
// the gh-identity Mutator so the platform's cmd/server can wire this
// plugin into the workspace provision chain.
//
// Operators integrating the plugin:
//
// reg, err := pluginloader.BuildRegistry()
// if err != nil { log.Fatalf("gh-identity: %v", err) }
// wh.SetEnvMutators(append(existingMutators, reg.Mutators()...))
//
// The plugin is INTENTIONALLY non-fatal on missing config: absent the
// optional MOLECULE_GH_IDENTITY_CONFIG_FILE env var, this still
// registers a Mutator that reads workspace-supplied roles and emits
// wrapper env — just without @me owner rewriting. Operators who want
// owner rewriting set MOLECULE_GH_IDENTITY_CONFIG_FILE.
package pluginloader
import (
"fmt"
"os"
"github.com/Molecule-AI/molecule-ai-plugin-gh-identity/internal/ghidentity"
)
// Result bundles what BuildRegistry returns — a single mutator plus
// whatever config it loaded, so test harnesses can inspect both.
type Result struct {
Mutator *ghidentity.Mutator
Config *ghidentity.Config
}
// BuildRegistry reads MOLECULE_GH_IDENTITY_CONFIG_FILE (optional),
// constructs the Mutator, and returns it.
//
// Error modes:
// - config file set but unreadable → error (operator bug; fail loud)
// - config file unset → fine, use empty map
// - config file set but non-existent → fine, use empty map (lets you
// point at a file that CI hasn't created yet without blocking boot)
func BuildRegistry() (*Result, error) {
cfgPath := os.Getenv("MOLECULE_GH_IDENTITY_CONFIG_FILE")
cfg, err := ghidentity.LoadConfig(cfgPath)
if err != nil {
return nil, fmt.Errorf("gh-identity: %w", err)
}
return &Result{
Mutator: &ghidentity.Mutator{Config: cfg},
Config: cfg,
}, nil
}