forked from molecule-ai/molecule-core
Root cause: the github-app-auth plugin injects GH_TOKEN + GITHUB_TOKEN
into each workspace container's env at provision time (EnvMutator). Those
are GitHub App installation tokens with a fixed ~60 min TTL. The plugin
has an in-process cache that proactively refreshes 5 min before expiry —
but the workspace env is set once at container start and never updated.
Any workspace alive >60 min ends up with an expired token.
Fix (Option B — on-demand endpoint):
pkg/provisionhook:
- Add TokenProvider interface: Token(ctx) (token, expiresAt, error)
Lives in pkg/ (public) so the github-app-auth plugin can implement it.
- Add Registry.FirstTokenProvider() — discovers the first mutator that
also satisfies TokenProvider via interface assertion. Safe under
concurrent reads (existing RWMutex).
platform/internal/handlers/github_token.go:
- New GitHubTokenHandler serving GET /admin/github-installation-token
- Delegates to the registered TokenProvider (plugin cache — always fresh)
- 404 if no GitHub App configured, 500 + [github] prefix log on error
- Never logs the token itself
platform/internal/handlers/workspace.go:
- Add TokenRegistry() getter so the router can wire the handler without
coupling to WorkspaceHandler internals
platform/internal/router/router.go:
- Register GET /admin/github-installation-token under AdminAuth
workspace-template/:
- scripts/molecule-git-token-helper.sh — git credential helper; calls
the platform endpoint on every push/fetch; falls through to next
helper (operator PAT) if platform unreachable
- entrypoint.sh — configure the credential helper at startup
Why Option B over Option A (background goroutine):
- The plugin already has its own cache refresh; nothing to refresh here.
- Pushing env updates into running containers requires docker exec, which
the architecture explicitly rejects (issue #547 "Alternatives").
- Pull-based is stateless, trivially testable, zero extra goroutines.
Closes #547
Co-authored-by: Molecule AI DevOps Engineer <devops-engineer@agents.moleculesai.app>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
113 lines
3.6 KiB
Bash
Executable File
113 lines
3.6 KiB
Bash
Executable File
#!/bin/bash
|
|
# molecule-git-token-helper.sh — git credential helper for GitHub App tokens
|
|
#
|
|
# Fetches a fresh GitHub App installation token from the Molecule AI
|
|
# platform endpoint GET /admin/github-installation-token on every git
|
|
# push/fetch, so workspace containers never use an expired GH_TOKEN after
|
|
# the ~60 min GitHub App token TTL.
|
|
#
|
|
# # Setup (called once at provision time or initial_prompt)
|
|
#
|
|
# git config --global \
|
|
# "credential.https://github.com.helper" \
|
|
# "!/workspace-template/scripts/molecule-git-token-helper.sh"
|
|
#
|
|
# # How git calls this helper
|
|
#
|
|
# git passes the action as the first positional arg. The protocol is:
|
|
# get → output credentials on stdout (we handle this)
|
|
# store → persist credentials (no-op — we never cache)
|
|
# erase → revoke credentials (no-op — platform manages lifecycle)
|
|
#
|
|
# On `get`, git reads key=value pairs terminated by an empty line.
|
|
# We must emit at minimum:
|
|
# username=x-access-token
|
|
# password=<token>
|
|
# (blank line)
|
|
#
|
|
# # Auth
|
|
#
|
|
# The platform endpoint requires a valid workspace bearer token. The
|
|
# token is stored at ${CONFIGS_DIR}/.auth_token (written by platform_auth.py
|
|
# on first /registry/register). Workspace env var PLATFORM_URL defaults
|
|
# to http://platform:8080.
|
|
#
|
|
# # Fallback
|
|
#
|
|
# If the platform endpoint is unreachable (e.g. network partition) or
|
|
# returns non-200, the script exits 1 without printing credentials so git
|
|
# will fall through to the next helper in the chain (if any). This
|
|
# preserves the operator's fallback PAT from .env if present.
|
|
#
|
|
# # gh CLI re-auth (30-min cron)
|
|
#
|
|
# To also fix `gh` CLI auth, run this from a workspace cron prompt:
|
|
#
|
|
# token=$(bash /workspace-template/scripts/molecule-git-token-helper.sh _fetch_token)
|
|
# echo "$token" | gh auth login --with-token
|
|
#
|
|
# (The _fetch_token private action returns only the raw token string.)
|
|
#
|
|
set -euo pipefail
|
|
|
|
PLATFORM_URL="${PLATFORM_URL:-http://platform:8080}"
|
|
CONFIGS_DIR="${CONFIGS_DIR:-/configs}"
|
|
TOKEN_FILE="${CONFIGS_DIR}/.auth_token"
|
|
ENDPOINT="${PLATFORM_URL}/admin/github-installation-token"
|
|
|
|
# _fetch_token — internal helper; also callable directly from cron.
|
|
# Outputs the raw token string on success; exits non-zero on failure.
|
|
_fetch_token() {
|
|
if [ ! -f "${TOKEN_FILE}" ]; then
|
|
echo "[molecule-git-token-helper] .auth_token not found at ${TOKEN_FILE}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
bearer=$(cat "${TOKEN_FILE}" | tr -d '[:space:]')
|
|
if [ -z "${bearer}" ]; then
|
|
echo "[molecule-git-token-helper] .auth_token is empty" >&2
|
|
exit 1
|
|
fi
|
|
|
|
response=$(curl -sf \
|
|
-H "Authorization: Bearer ${bearer}" \
|
|
-H "Accept: application/json" \
|
|
--max-time 10 \
|
|
"${ENDPOINT}" 2>&1) || {
|
|
echo "[molecule-git-token-helper] platform request failed: ${response}" >&2
|
|
exit 1
|
|
}
|
|
|
|
# Parse {"token":"ghs_...","expires_at":"..."} with sed (no jq dependency).
|
|
token=$(echo "${response}" | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')
|
|
if [ -z "${token}" ]; then
|
|
echo "[molecule-git-token-helper] empty token in platform response: ${response}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "${token}"
|
|
}
|
|
|
|
ACTION="${1:-get}"
|
|
|
|
case "${ACTION}" in
|
|
get)
|
|
token=$(_fetch_token) || exit 1
|
|
# Emit git credential protocol response.
|
|
printf 'username=x-access-token\n'
|
|
printf 'password=%s\n' "${token}"
|
|
printf '\n'
|
|
;;
|
|
store|erase)
|
|
# No-op — the platform manages token lifecycle.
|
|
;;
|
|
_fetch_token)
|
|
# Private action for cron-based gh auth login --with-token.
|
|
_fetch_token
|
|
;;
|
|
*)
|
|
echo "[molecule-git-token-helper] unknown action: ${ACTION}" >&2
|
|
exit 1
|
|
;;
|
|
esac
|