Summary
Evolve ghpool into a credential-swapping MCP reverse proxy in front of GitHub's official hosted MCP server (https://api.githubcopilot.com/mcp/), giving AI agents full GitHub MCP tool access without ever exposing a GitHub credential to the agent.
Revision 2 (2026-07-12): Major update incorporating community review feedback. Key changes: GitHub App installation token as recommended credential backend (resolving PAT-pooling compliance concern); per-agent default-deny policy model with explicit tool + repository allowlists; revised phase gates with Phase 0 compatibility spike; strengthened security boundaries throughout.
Revision 1 (2026-07-12): Earlier drafts proposed ghpool implementing its own MCP tool surface. This revision narrowed scope: ghpool does not define any MCP tools. It proxies GitHub's official MCP server, swaps credentials, and enforces per-agent policy. Zero tool-schema maintenance.
Motivation
Current approaches for AI agents (e.g. OpenAB on ECS/EKS) to interact with GitHub:
| Approach |
Limitation |
| Fine-grained PAT |
Single org only; long-lived; can be exfiltrated |
| Token Vending Machine |
Agent still receives a short-lived token — can be misused during its lifetime |
| ghpool as MCP reverse proxy |
Agent never sees any GitHub credential. Access bounded by per-agent tool + resource policy. |
The MCP proxy approach provides the strongest security boundary: the GitHub credential never leaves ghpool, every request is tied to a verified agent identity, both the action and target resource are authorized, the upstream credential is independently least-privileged, and every mutation has a durable audit trail.
Design Goals
- No GitHub credential exposed to the agent — the upstream bearer token (GitHub App installation token) is injected by ghpool; agents hold only a ghpool API key or use ambient IAM identity
- Default-deny per-agent policy — each agent has an explicit allowlist of tools and repositories; unlisted tools/repos are denied; new upstream tools are denied until explicitly added to policy
- Resource-level authorization — toolset categories (
X-MCP-Toolsets) provide coarse upstream filtering; ghpool enforces fine-grained per-tool + per-repo policy at the proxy layer
- Simple agent authn — per-agent API keys (delivered via existing secrets infra) mapped to policy; optional secretless SigV4 identity proof via a
ghp mcp stdio shim for AWS environments
- Centralized durable audit — ghpool logs
(agent, tool, target repo, allow/deny decision) per JSON-RPC frame; writes require durable audit (fail-closed when audit backend unavailable)
- No git/gh CLI dependency — agents only need MCP client capability
- No MCP tool implementation in ghpool — GitHub's official server owns the tool schemas; ghpool proxies, authorizes, and audits
Credential Architecture
Recommended: GitHub App Installation Token
AWS Secrets Manager
└── GitHub App private key (PEM)
│
▼
ghpool (on startup / on-demand refresh)
→ Sign JWT with App private key
→ POST /app/installations/{id}/access_tokens
→ Receive installation token (1hr TTL)
→ Use as upstream bearer for MCP / REST / GraphQL
→ Auto-refresh before expiry (5min margin)
Why GitHub App over PAT pool:
| Aspect |
PAT Pool |
GitHub App |
| Rate limit compliance |
⚠️ Aggregating multiple users' buckets may violate GitHub API Terms §H |
✅ Installation token rate limits scale legitimately with repos |
| Attribution |
Collapses to PAT owner |
✅ App name visible in GitHub audit log |
| Token lifetime |
Long-lived (unless rotated) |
Short-lived (1hr auto-expiry) |
| Scope |
User-level |
✅ Repository-scoped via installation |
| Rotation |
Manual |
✅ Automatic (new token every hour) |
Configuration:
[credentials]
type = "github-app"
app_id = 123456
private_key = "aws:secretsmanager:ghpool/github-app-key"
installation_id = 78901234
# Legacy PAT support (not recommended for production)
# type = "pat-pool"
# [[identities]]
# id = "bot-1"
# token = "aws:secretsmanager:ghpool/pat-bot-1"
Session-Credential Binding
- Session lifetime must not exceed credential lifetime
- ghpool tracks installation token expiry; sessions pinned to an expiring token are proactively terminated (client receives 404, re-initializes with fresh token)
- No silent mid-session credential rotation
Per-Agent Policy Model
Default-Deny Architecture
Every agent has a policy that explicitly lists allowed tools and allowed repositories. Anything not listed is denied.
[[agents]]
id = "agent-a"
key = "aws:secretsmanager:ghpool/agent-a-key"
# OR for IAM-based authn:
# iam_role = "arn:aws:iam::123456789:role/agent-a-task-role"
[agents.policy]
readonly = true
tools = ["issue_read", "get_pull_request", "list_issues", "search_code"]
repos = ["openabdev/ghpool", "openabdev/openab"]
[[agents]]
id = "agent-b"
key = "aws:secretsmanager:ghpool/agent-b-key"
[agents.policy]
readonly = false
tools = ["issue_read", "create_issue", "add_comment", "create_pull_request"]
repos = ["openabdev/*"] # wildcard: all repos in org
Enforcement Flow
Agent → POST /mcp { tools/call: create_issue, params: { owner, repo, ... } }
│
▼
1. AUTHN — verify X-Ghpool-Key → resolve agent identity
│ (or SigV4 proof → STS → IAM role → agent mapping)
│
▼
2. PARSE — extract JSON-RPC method + tool name + target resource from params
│
▼
3. AUTHZ — check policy (all must pass):
│ ├── tool in agent.policy.tools? → deny if not listed
│ ├── target repo in agent.policy.repos? → deny if not listed
│ ├── is_write_tool AND agent.readonly? → deny
│ └── resource resolvable? → deny if ambiguous
│
▼
4. FORWARD — inject upstream headers:
│ ├── Authorization: Bearer <GitHub App installation token>
│ ├── X-MCP-Toolsets: <from agent policy, coarse filter>
│ └── X-MCP-Readonly: <from agent policy>
│
▼
5. AUDIT — structured log:
{ agent, session, rpc_id, tool, repo, decision: allow/deny,
upstream_credential_id (not secret), timestamp, duration }
Policy Principles
- Default-deny: unlisted tool → denied; unlisted repo → denied
- New tools are safe: when GitHub adds a new upstream tool, no agent can use it until an admin adds it to that agent's
tools list
- Resource validation: if target resource cannot be resolved from tool arguments, the call is denied (safe fallback)
- Outbound header allowlist: ghpool strips ALL client-supplied
X-MCP-* and Authorization headers; policy-derived headers are injected from scratch — a client cannot widen its own policy
Proposed Architecture
┌───────────────────────────── Private Network / VPC ─────────────────────────────┐
│ │
│ ┌──────────────────────────┐ ┌──────────────────────────┐ │
│ │ Agent A (ECS task role) │ │ Agent B (EKS + IRSA) │ │
│ │ no GitHub credential │ │ no GitHub credential │ │
│ │ holds: X-Ghpool-Key │ │ holds: ambient IAM only │ │
│ └────────────┬─────────────┘ └────────────┬─────────────┘ │
│ │ │ │
│ │ MCP Streamable HTTP │ │
│ │ + X-Ghpool-Key header │ │
│ │ (or SigV4 proof via ghp shim) │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ghpool (MCP reverse proxy) │ │
│ │ │ │
│ │ 1. AUTHN — verify caller │ │
│ │ API key → agent (or SigV4 proof → STS) │───► AWS STS │
│ │ │ │
│ │ 2. AUTHZ — per-agent policy (default-deny) │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ agent-a: │ │ │
│ │ │ tools = [issue_read, get_pull_request] │ │ │
│ │ │ repos = [openabdev/ghpool, openabdev/openab] │ │ │
│ │ │ readonly = true │ │ │
│ │ │ agent-b: │ │ │
│ │ │ tools = [issue_read, create_issue, add_comment]│ │ │
│ │ │ repos = [openabdev/*] │ │ │
│ │ │ readonly = false │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 3. REWRITE headers (from scratch — never forward client's) │ │
│ │ inject: Authorization: Bearer <App installation token> │ │
│ │ X-MCP-Toolsets: (from agent policy) │◄── GitHub App │
│ │ X-MCP-Readonly: (from agent policy) │ private key │
│ │ │ in AWS SM │
│ │ 4. AUDIT — structured log per JSON-RPC frame │ │
│ │ (agent, tool, repo, decision, session, timestamp) │ │
│ └────────────────────────────┬────────────────────────────────┘ │
│ │ │
└───────────────────────────────┼─────────────────────────────────────────────────┘
│ same MCP protocol, GitHub's own tool schemas
▼
┌──────────────────────────────────┐
│ https://api.githubcopilot.com/ │
│ mcp/ (hosted) │
│ mcp/readonly (or per-toolset │
│ mcp/x/issues URL variants) │
└──────────────────────────────────┘
Agent Authentication
MCP clients are plain HTTP clients — their config supports a URL and static headers, not a per-request signing loop. So agent authn is layered:
Baseline: per-agent API keys
ghpool issues one key per agent, mapped to policy. Delivery via existing secrets infra (ECS task secret / K8s Secret → env var); MCP clients expand env vars in config headers:
{ "url": "https://ghpool:8080/mcp",
"headers": { "X-Ghpool-Key": "${GHPOOL_KEY}" } }
Transport security: TLS is required for production (terminated by ghpool, ALB, or service mesh). The key is a bearer credential and must not transit in cleartext.
Key properties: high entropy (256-bit), constant-time comparison, excluded from all logs/traces, support two simultaneously valid values for zero-downtime rotation.
Blast radius: this is a ghpool credential, not a GitHub credential. A leaked key is bounded by that agent's tool+repo policy and revoked centrally in ghpool config without touching GitHub — categorically better than a leaked PAT or vended token.
Optional: secretless via ghp mcp stdio shim
For zero static secrets: a ghp mcp subcommand runs as a stdio MCP server inside the agent container, piping frames to ghpool over HTTP while attaching a periodically-refreshed presigned sts:GetCallerIdentity proof signed with ambient IAM credentials (ECS task role / EKS IRSA).
Security controls for SigV4 proof:
- TLS required (proof is not encryption)
- Very short validity window (≤ 60s)
- Allowlisted STS host and region only
- Strict validation of signed method/body/headers
- Replay mitigation via nonce or timestamp check
ghpool forwards the proof to STS and maps the caller ARN to agent policy. The signing capability lives in the shim, so any MCP client works unchanged.
Phasing
Phase 0: Compliance & Compatibility Spike
Phase 1: Read-Only Proxy (PR #20)
Security boundary: network-trust model (same as existing ghpool read routes). No agent authn required — all workloads on the network share the same read-only access.
Scope:
Limitations (documented & accepted for Phase 1):
allowed_owners not enforced on MCP path (requires tool-argument inspection — Phase 2)
- Single-replica only (in-memory session cache; rolling deployment = session re-initialization)
- Network trust — any workload that can reach
/mcp gets read access to all repos available to the credential
- PAT pool used (GitHub App backend is Phase 2 prerequisite for production)
Phase 2: Agent Authn + Per-Agent Policy + Writes
Gate: ALL of the following must be implemented before any write tool is exposed.
Phase 3: Operational Hardening
Tracking
Comparison with Token Vending
|
Token Vending |
ghpool MCP proxy |
| Agent gets GitHub token? |
✅ Short-lived |
❌ Never |
| Access boundary |
Token scope |
Per-agent tool + repo allowlist (default-deny) |
| git CLI compatible |
✅ Direct |
❌ MCP tools only |
| Tool schema maintenance |
n/a |
None (GitHub-owned) |
| New tools |
Auto-available |
Default-denied until allowlisted |
| Best for |
git-heavy coding agents |
Issues/PRs/reviews with strict audit |
Complementary Design
The two patterns coexist behind ghpool:
- MCP proxy path — issues, PRs, comments, reviews, workflow ops. GitHub credential never leaves ghpool. Per-agent tool+repo policy enforced.
- Token vending path —
git clone/push for coding agents that need a real checkout, using the Token Vending Machine pattern internally (GitHub App → scoped installation token → agent).
ghpool's existing REST/GraphQL read proxy (caching + credential isolation) is unchanged.
Known Tradeoffs
- Proxy vs. direct access: ghpool adds latency and a single point of failure, but provides the only viable path to zero-credential agents with per-tool policy
- GitHub-side attribution collapses to the GitHub App — ghpool's structured audit log is the authoritative record; commit trailers / PR body attribution supplement but don't replace it
- Hosted endpoint dependency: requires egress to
api.githubcopilot.com. Fallback: run a self-hosted github-mcp-server container next to ghpool — identical headers and schemas
- Session state is in-memory (Phase 1-2): process restart or rolling deployment terminates all sessions; clients must re-initialize. Shared state is Phase 3.
- No tool schema ownership means no schema validation: ghpool trusts upstream tool definitions; if upstream adds a dangerous tool, defense is the per-agent allowlist (not auto-exposed)
Security Invariants
These properties must hold across all phases:
- No GitHub credential leaves ghpool — agent never receives a PAT, App token, or any GitHub bearer
- Default-deny — unlisted tool or repo is always denied, regardless of upstream availability
- Session integrity — unknown/stale session → 404 (per MCP spec); no silent identity rotation
- Outbound header control — client cannot inject or override
Authorization, X-MCP-Toolsets, X-MCP-Readonly, or any policy-affecting header
- Audit before action — every
tools/call is logged with agent identity, tool, target, and decision before forwarding
- Credential isolation — upstream token lifetime bounds session lifetime; no session outlives its credential
Open Questions (Resolved & Remaining)
Resolved
Remaining
Related
Summary
Evolve ghpool into a credential-swapping MCP reverse proxy in front of GitHub's official hosted MCP server (
https://api.githubcopilot.com/mcp/), giving AI agents full GitHub MCP tool access without ever exposing a GitHub credential to the agent.Motivation
Current approaches for AI agents (e.g. OpenAB on ECS/EKS) to interact with GitHub:
The MCP proxy approach provides the strongest security boundary: the GitHub credential never leaves ghpool, every request is tied to a verified agent identity, both the action and target resource are authorized, the upstream credential is independently least-privileged, and every mutation has a durable audit trail.
Design Goals
X-MCP-Toolsets) provide coarse upstream filtering; ghpool enforces fine-grained per-tool + per-repo policy at the proxy layerghp mcpstdio shim for AWS environments(agent, tool, target repo, allow/deny decision)per JSON-RPC frame; writes require durable audit (fail-closed when audit backend unavailable)Credential Architecture
Recommended: GitHub App Installation Token
Why GitHub App over PAT pool:
Configuration:
Session-Credential Binding
Per-Agent Policy Model
Default-Deny Architecture
Every agent has a policy that explicitly lists allowed tools and allowed repositories. Anything not listed is denied.
Enforcement Flow
Policy Principles
toolslistX-MCP-*andAuthorizationheaders; policy-derived headers are injected from scratch — a client cannot widen its own policyProposed Architecture
Agent Authentication
MCP clients are plain HTTP clients — their config supports a URL and static headers, not a per-request signing loop. So agent authn is layered:
Baseline: per-agent API keys
ghpool issues one key per agent, mapped to policy. Delivery via existing secrets infra (ECS task secret / K8s Secret → env var); MCP clients expand env vars in config headers:
{ "url": "https://ghpool:8080/mcp", "headers": { "X-Ghpool-Key": "${GHPOOL_KEY}" } }Transport security: TLS is required for production (terminated by ghpool, ALB, or service mesh). The key is a bearer credential and must not transit in cleartext.
Key properties: high entropy (256-bit), constant-time comparison, excluded from all logs/traces, support two simultaneously valid values for zero-downtime rotation.
Blast radius: this is a ghpool credential, not a GitHub credential. A leaked key is bounded by that agent's tool+repo policy and revoked centrally in ghpool config without touching GitHub — categorically better than a leaked PAT or vended token.
Optional: secretless via
ghp mcpstdio shimFor zero static secrets: a
ghp mcpsubcommand runs as a stdio MCP server inside the agent container, piping frames to ghpool over HTTP while attaching a periodically-refreshed presignedsts:GetCallerIdentityproof signed with ambient IAM credentials (ECS task role / EKS IRSA).Security controls for SigV4 proof:
ghpool forwards the proof to STS and maps the caller ARN to agent policy. The signing capability lives in the shim, so any MCP client works unchanged.
Phasing
Phase 0: Compliance & Compatibility Spike
Mcp-Session-Id, SSE resumption,Last-Event-ID, session terminationPhase 1: Read-Only Proxy (PR #20)
Security boundary: network-trust model (same as existing ghpool read routes). No agent authn required — all workloads on the network share the same read-only access.
Scope:
POST/GET/DELETE /mcp→ upstream/mcp/readonlyenabled = falsedefault)Limitations (documented & accepted for Phase 1):
allowed_ownersnot enforced on MCP path (requires tool-argument inspection — Phase 2)/mcpgets read access to all repos available to the credentialPhase 2: Agent Authn + Per-Agent Policy + Writes
Gate: ALL of the following must be implemented before any write tool is exposed.
X-Ghpool-Keyor SigV4)X-MCP-*from client)tools/listresponse filtering (prune tools not in agent's allowlist)Phase 3: Operational Hardening
ghp mcpstdio shim + SigV4)Tracking
Comparison with Token Vending
Complementary Design
The two patterns coexist behind ghpool:
git clone/pushfor coding agents that need a real checkout, using the Token Vending Machine pattern internally (GitHub App → scoped installation token → agent).ghpool's existing REST/GraphQL read proxy (caching + credential isolation) is unchanged.
Known Tradeoffs
api.githubcopilot.com. Fallback: run a self-hostedgithub-mcp-servercontainer next to ghpool — identical headers and schemasSecurity Invariants
These properties must hold across all phases:
Authorization,X-MCP-Toolsets,X-MCP-Readonly, or any policy-affecting headertools/callis logged with agent identity, tool, target, and decision before forwardingOpen Questions (Resolved & Remaining)
Resolved
/mcpon existing listener (conditional route registration)Remaining
config.toml([[agents]]) or separate policy file?tools/listresponses be cached, or rely on upstream caching?Related
api.githubcopilot.com/mcp/)