Skip to content

Commit 348b45a

Browse files
authored
feat: proxy pre-agent gh CLI calls through DIFC gateway (#2294)
## Summary Routes all pre-agent `gh` CLI calls in `repo-assist` through the MCP gateway DIFC proxy, applying the same `min-integrity: merged` policy that governs the agent's MCP tool calls. ## Problem The repo-assist workflow runs several `gh` CLI calls **before** the agent starts (issue list, PR list, repo-memory clone). These calls bypass the DIFC pipeline entirely — the agent's guard policy only applies to MCP tool calls during the agent phase. ## Solution Add two steps to `repo-assist.lock.yml`: ### Start DIFC Proxy (before first `GH_TOKEN` step) - Runs `ghcr.io/github/gh-aw-mcpg:v0.1.19` in proxy mode with TLS on port 18443 - Same policy as agent: `{"allow-only":{"repos":["github/*"],"min-integrity":"merged"}}` - Adds proxy CA cert to system trust store (`update-ca-certificates`) so `gh` CLI (Go) trusts it - Writes `GH_HOST=localhost:18443` to `$GITHUB_ENV` — all subsequent steps inherit it ### Stop DIFC Proxy (before agent execution) - Stops the proxy container - Clears `GH_HOST` from `$GITHUB_ENV` to prevent leaking into `awf --env-all` ## Steps Covered | Pre-Agent Step | Uses `GH_TOKEN` | Proxied | Mechanism | |---|---|---|---| | Configure gh CLI for GHE | ✅ | ✅ | `$GITHUB_ENV` | | Fetch repo data (gh issue/pr list) | ✅ | ✅ | `$GITHUB_ENV` | | Clone repo-memory branch | ✅ | ✅ | `$GITHUB_ENV` | | Checkout PR branch | ✅ | ❌ | Uses Octokit, not `gh` CLI | | Install Copilot CLI | — | ❌ | Explicit `GH_HOST: github.com` override | | Agent (awf container) | — | ❌ | `GH_HOST` cleared before agent | ## Design Decisions - **`$GITHUB_ENV` over per-step env vars**: Propagates to framework-injected steps (clone-repo-memory) that we can't modify from the `.md` source - **System CA trust store**: `gh` CLI uses Go's `http.DefaultTransport` which reads system CAs — `NODE_EXTRA_CA_CERTS` only works for Node.js - **Port 18443**: Avoids conflict with the agent's MCP gateway (port 80) - **Graceful fallback**: If proxy fails to start, workflow continues with direct API access - **Lock file only**: Editing `.lock.yml` directly for debugging; `.md` source changes will follow once validated ## Testing This is a lock file change — needs to be validated by running the repo-assist workflow in CI.
2 parents 68537a8 + f348b3b commit 348b45a

6 files changed

Lines changed: 166 additions & 25 deletions

File tree

.github/workflows/repo-assist.lock.yml

Lines changed: 69 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

guards/github-guard/rust-guard/src/labels/helpers.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,12 +1034,16 @@ pub fn commit_integrity(
10341034
/// Trusted bots:
10351035
/// - dependabot[bot]: GitHub dependency updater
10361036
/// - github-actions[bot]: GitHub Actions workflow actor (GITHUB_TOKEN)
1037+
/// - github-actions: GitHub Actions workflow actor (without [bot] suffix, as returned by some APIs)
1038+
/// - app/github-actions: GitHub Actions workflow actor (with app/ prefix, as returned by gh CLI)
10371039
/// - github-merge-queue[bot]: GitHub merge queue automation
10381040
/// - copilot: GitHub Copilot AI assistant
10391041
pub fn is_trusted_first_party_bot(username: &str) -> bool {
10401042
let lower = username.to_lowercase();
10411043
lower == "dependabot[bot]"
10421044
|| lower == "github-actions[bot]"
1045+
|| lower == "github-actions"
1046+
|| lower == "app/github-actions"
10431047
|| lower == "github-merge-queue[bot]"
10441048
|| lower == "copilot"
10451049
}

guards/github-guard/rust-guard/src/labels/mod.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -847,7 +847,8 @@ mod tests {
847847
// Not bots
848848
assert!(!is_trusted_first_party_bot("octocat"));
849849
assert!(!is_trusted_first_party_bot("dependabot"));
850-
assert!(!is_trusted_first_party_bot("github-actions"));
850+
assert!(is_trusted_first_party_bot("github-actions"));
851+
assert!(is_trusted_first_party_bot("app/github-actions"));
851852
assert!(!is_trusted_first_party_bot(""));
852853
}
853854

@@ -893,6 +894,16 @@ mod tests {
893894
writer_integrity(repo, &ctx)
894895
);
895896

897+
// github-actions without [bot] suffix (as returned by some APIs)
898+
let actions_no_bot_issue = json!({
899+
"user": {"login": "github-actions"},
900+
"author_association": "NONE"
901+
});
902+
assert_eq!(
903+
issue_integrity(&actions_no_bot_issue, repo, false, &ctx),
904+
writer_integrity(repo, &ctx)
905+
);
906+
896907
// Non-trusted bot still gets none integrity on public repo
897908
let renovate_issue = json!({
898909
"user": {"login": "renovate[bot]"},

internal/proxy/graphql.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ type graphqlPattern struct {
3535

3636
// graphqlPatterns is the ordered list of GraphQL operation → tool name mappings.
3737
var graphqlPatterns = []graphqlPattern{
38+
// Schema introspection queries (safe read-only metadata, no repo data)
39+
{queryPattern: regexp.MustCompile(`(?i)__type\s*\(`), toolName: "graphql_introspection"},
40+
{queryPattern: regexp.MustCompile(`(?i)__schema\b`), toolName: "graphql_introspection"},
41+
3842
// Issue operations (singular before plural — more specific first)
3943
{queryPattern: regexp.MustCompile(`(?i)repository\s*\([^)]*\)\s*\{[^}]*\bissue\s*\(`), toolName: "issue_read"},
4044
{queryPattern: regexp.MustCompile(`(?i)repository\s*\([^)]*\)\s*\{[^}]*\bissues\s*[\({]`), toolName: "list_issues"},
@@ -170,7 +174,9 @@ func extractOwnerRepo(variables map[string]interface{}, query string) (string, s
170174
}
171175

172176
// IsGraphQLPath returns true if the request path is the GraphQL endpoint.
177+
// Accepts /graphql (after prefix strip), /api/v3/graphql (before strip),
178+
// and /api/graphql (GHES-style path used by gh CLI with GH_HOST).
173179
func IsGraphQLPath(path string) bool {
174180
cleaned := strings.TrimSuffix(path, "/")
175-
return cleaned == "/graphql" || cleaned == "/api/v3/graphql"
181+
return cleaned == "/graphql" || cleaned == "/api/v3/graphql" || cleaned == "/api/graphql"
176182
}

0 commit comments

Comments
 (0)