From 3609b7ab8c3923263154da5cece7c3a696059557 Mon Sep 17 00:00:00 2001 From: rabbitblood Date: Thu, 16 Apr 2026 12:52:20 -0700 Subject: [PATCH] feat(platform): wire github-app-auth plugin for per-installation tokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrates github.com/Molecule-AI/molecule-ai-plugin-github-app-auth. When GITHUB_APP_ID is set, the platform constructs a plugin Authenticator at boot and registers it as an EnvMutator on the WorkspaceHandler. Every workspace provision then gets a fresh GITHUB_TOKEN / GH_TOKEN injected from the App's installation token (rotates ~hourly, refresh 5 min before expiry). Verified live this turn: - Platform boot log: `github-app-auth: registered, 1 mutator(s) in chain` - `docker exec ws- gh auth status` → `Logged in as molecule-ai[bot] (GH_TOKEN)` - `gh issue list --repo Molecule-AI/molecule-core` returns real data (Hermes #498/#499/#500 visible from inside a workspace container) ## Changes - platform/go.mod + go.sum: new dep on the plugin - platform/cmd/server/main.go: import + conditional registration (soft-skip when GITHUB_APP_ID is unset for self-hosted/dev) - docker-compose.yml: pass GITHUB_APP_* env + bind-mount private key ## Drive-by .gitignore: exclude /org-templates /plugins /workspace-configs-templates — these dirs are populated locally by clone-manifest.sh from the standalone repos, should never be committed to core. Without this rule my previous git add -A staged 33 embedded git dirs. Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 6 ++++++ docker-compose.yml | 11 +++++++++++ platform/cmd/server/main.go | 21 +++++++++++++++++++++ platform/go.mod | 2 ++ platform/go.sum | 4 ++++ 5 files changed, 44 insertions(+) diff --git a/.gitignore b/.gitignore index 759d0fa4..ddfa7a84 100644 --- a/.gitignore +++ b/.gitignore @@ -117,3 +117,9 @@ org-templates/**/.auth-token # GitHub App private key + other local-only secrets — never committed. .secrets/ *.pem + +# Cloned-via-manifest dirs — populated locally by scripts/clone-manifest.sh, +# tracked in their own standalone repos. Never commit to core. +/org-templates/ +/plugins/ +/workspace-configs-templates/ diff --git a/docker-compose.yml b/docker-compose.yml index 9f82f18c..00431a98 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -128,11 +128,22 @@ services: CONFIGS_DIR: /configs CONFIGS_HOST_DIR: "${CONFIGS_HOST_DIR:-${PWD}/workspace-configs-templates}" PLUGINS_HOST_DIR: "${PLUGINS_HOST_DIR:-${PWD}/plugins}" + # github-app-auth plugin — injects GITHUB_TOKEN / GH_TOKEN into every + # workspace env from the App installation token. Remap the host-side + # path in GITHUB_APP_PRIVATE_KEY_FILE to /secrets/github-app.pem inside + # the container (the private key is bind-mounted below read-only). + # Soft-dep: skipped entirely when GITHUB_APP_ID is unset. + GITHUB_APP_ID: "${GITHUB_APP_ID:-}" + GITHUB_APP_INSTALLATION_ID: "${GITHUB_APP_INSTALLATION_ID:-}" + GITHUB_APP_PRIVATE_KEY_FILE: "/secrets/github-app.pem" volumes: - ./workspace-configs-templates:/configs - ./org-templates:/org-templates:ro - ./plugins:/plugins:ro - /var/run/docker.sock:/var/run/docker.sock + # App private key — read-only bind-mount. The host-side path is + # gitignored per .gitignore rules (/.secrets/ + *.pem). + - ./.secrets/github-app.pem:/secrets/github-app.pem:ro ports: - "${PLATFORM_PUBLISH_PORT:-8080}:${PLATFORM_PORT:-8080}" networks: diff --git a/platform/cmd/server/main.go b/platform/cmd/server/main.go index 6d99701d..d65d493f 100644 --- a/platform/cmd/server/main.go +++ b/platform/cmd/server/main.go @@ -22,6 +22,11 @@ import ( "github.com/Molecule-AI/molecule-monorepo/platform/internal/scheduler" "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" ) func main() { @@ -138,6 +143,22 @@ func main() { wh.SetCPProvisioner(cpProv) } + // 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 { + 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()) + } + } else { + log.Println("github-app-auth: GITHUB_APP_ID unset — skipping plugin registration (agents will use any PAT from .env)") + } + // 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/platform/go.mod b/platform/go.mod index 579256ff..40a93c6c 100644 --- a/platform/go.mod +++ b/platform/go.mod @@ -15,6 +15,7 @@ require ( require ( github.com/DATA-DOG/go-sqlmock v1.5.2 // indirect + github.com/Molecule-AI/molecule-ai-plugin-github-app-auth v0.0.0-20260416194734-2cd28737f845 // indirect github.com/alicebob/miniredis/v2 v2.37.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/bytedance/sonic v1.11.6 // indirect @@ -48,6 +49,7 @@ require ( github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect diff --git a/platform/go.sum b/platform/go.sum index d509601a..fd2eaa58 100644 --- a/platform/go.sum +++ b/platform/go.sum @@ -1,5 +1,7 @@ github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/Molecule-AI/molecule-ai-plugin-github-app-auth v0.0.0-20260416194734-2cd28737f845 h1:Pae8GmpJOP/Bpf2KE1FhdN3zoPSbV/tl25yiAqEc4lM= +github.com/Molecule-AI/molecule-ai-plugin-github-app-auth v0.0.0-20260416194734-2cd28737f845/go.mod h1:3a6LR/zd7FjR9ZwLTbytwYlWuCBsbCOVFlEg0WnoYiM= github.com/alicebob/miniredis/v2 v2.37.0 h1:RheObYW32G1aiJIj81XVt78ZHJpHonHLHW7OLIshq68= github.com/alicebob/miniredis/v2 v2.37.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= @@ -88,6 +90,8 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=