Skip to content

Commit 06c856a

Browse files
authored
feat: unified JSONL log file between proxy and gateway (#2350)
## Summary Unify the JSONL RPC message logs so both the DIFC proxy and MCP gateway append to the same `rpc-messages.jsonl` file, creating a single chronological timeline of all RPC messages and DIFC filtering events. ## Problem Previously the proxy wrote `proxy-rpc.jsonl` to `/tmp/gh-aw/proxy-logs/` and the gateway wrote `rpc-messages.jsonl` to `/tmp/gh-aw/mcp-logs/`. This made it harder to get a complete picture of DIFC events across both components. ## Changes ### Code - **`internal/cmd/proxy.go`**: Rename JSONL output from `proxy-rpc.jsonl` → `rpc-messages.jsonl` ### Workflow - **`repo-assist.lock.yml`**: Mount `/tmp/gh-aw/mcp-logs` into the proxy container and point `--log-dir` there - Update proxy.log reference path to match new log directory - Enable local build for testing ## Result Both proxy and gateway append to `/tmp/gh-aw/mcp-logs/rpc-messages.jsonl`: - Proxy writes first (pre-agent `gh` calls with DIFC filtering) - Gateway appends later (agent MCP tool calls with DIFC filtering) - Single file = unified timeline for analysis
2 parents 28f0b48 + 794a0f9 commit 06c856a

File tree

6 files changed

+450
-39
lines changed

6 files changed

+450
-39
lines changed

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

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

internal/cmd/proxy.go

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,16 @@ import (
1818

1919
// Proxy subcommand flag variables
2020
var (
21-
proxyGuardWasm string
22-
proxyPolicy string
23-
proxyToken string
24-
proxyListen string
25-
proxyLogDir string
26-
proxyDIFCMode string
27-
proxyAPIURL string
28-
proxyTLS bool
29-
proxyTLSDir string
21+
proxyGuardWasm string
22+
proxyPolicy string
23+
proxyToken string
24+
proxyListen string
25+
proxyLogDir string
26+
proxyDIFCMode string
27+
proxyAPIURL string
28+
proxyTLS bool
29+
proxyTLSDir string
30+
proxyTrustedBots []string
3031
)
3132

3233
func init() {
@@ -94,6 +95,7 @@ Local usage:
9495
cmd.Flags().StringVar(&proxyAPIURL, "github-api-url", proxy.DefaultGitHubAPIBase, "Upstream GitHub API URL")
9596
cmd.Flags().BoolVar(&proxyTLS, "tls", false, "Enable HTTPS with auto-generated self-signed certificates")
9697
cmd.Flags().StringVar(&proxyTLSDir, "tls-dir", "", "Directory for TLS certificates (default: <log-dir>/proxy-tls)")
98+
cmd.Flags().StringSliceVar(&proxyTrustedBots, "trusted-bots", nil, "Additional trusted bot usernames (comma-separated, extends built-in list)")
9799

98100
// Only require --guard-wasm when no baked-in guard is available
99101
if defaultGuard == "" {
@@ -111,7 +113,7 @@ func runProxy(cmd *cobra.Command, args []string) error {
111113
if err := logger.InitFileLogger(proxyLogDir, "proxy.log"); err != nil {
112114
log.Printf("Warning: Failed to initialize file logger: %v", err)
113115
}
114-
if err := logger.InitJSONLLogger(proxyLogDir, "proxy-rpc.jsonl"); err != nil {
116+
if err := logger.InitJSONLLogger(proxyLogDir, "rpc-messages.jsonl"); err != nil {
115117
log.Printf("Warning: Failed to initialize JSONL logger: %v", err)
116118
}
117119

@@ -141,6 +143,7 @@ func runProxy(cmd *cobra.Command, args []string) error {
141143
GitHubToken: token,
142144
GitHubAPIURL: proxyAPIURL,
143145
DIFCMode: proxyDIFCMode,
146+
TrustedBots: proxyTrustedBots,
144147
})
145148
if err != nil {
146149
return fmt.Errorf("failed to create proxy server: %w", err)

internal/proxy/graphql_rewrite.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package proxy
2+
3+
import (
4+
"encoding/json"
5+
"regexp"
6+
"strings"
7+
)
8+
9+
// guardRequiredFields lists the GraphQL selection fields the DIFC guard needs
10+
// for accurate integrity labeling. author{login} enables trusted-bot detection;
11+
// authorAssociation provides the integrity level directly (MEMBER, CONTRIBUTOR,
12+
// etc.) so the guard doesn't need extra enrichment REST round-trips.
13+
var guardRequiredFields = []struct {
14+
field string // field text to inject
15+
present *regexp.Regexp // pattern that indicates the field is already selected
16+
}{
17+
{"author{login}", regexp.MustCompile(`\bauthor\s*\{[^}]*\blogin\b`)},
18+
{"authorAssociation", regexp.MustCompile(`\bauthorAssociation\b`)},
19+
}
20+
21+
// allGuardFieldsPresent returns true if the query already contains every
22+
// required guard field.
23+
func allGuardFieldsPresent(query string) bool {
24+
for _, f := range guardRequiredFields {
25+
if !f.present.MatchString(query) {
26+
return false
27+
}
28+
}
29+
return true
30+
}
31+
32+
// missingGuardFields returns the field strings not yet present in the query.
33+
func missingGuardFields(query string) []string {
34+
var missing []string
35+
for _, f := range guardRequiredFields {
36+
if !f.present.MatchString(query) {
37+
missing = append(missing, f.field)
38+
}
39+
}
40+
return missing
41+
}
42+
43+
// InjectGuardFields rewrites a GraphQL request body to include fields
44+
// required by the DIFC guard (e.g. author{login} for trusted-bot detection).
45+
// Returns the (possibly modified) body. If injection is not needed or fails,
46+
// the original body is returned unchanged.
47+
func InjectGuardFields(body []byte, toolName string) []byte {
48+
// Only rewrite for tools that need author info
49+
switch toolName {
50+
case "list_issues", "list_pull_requests", "issue_read", "pull_request_read",
51+
"search_issues":
52+
default:
53+
return body
54+
}
55+
56+
var gql GraphQLRequest
57+
if err := json.Unmarshal(body, &gql); err != nil {
58+
return body
59+
}
60+
61+
if gql.Query == "" || allGuardFieldsPresent(gql.Query) {
62+
return body
63+
}
64+
65+
missing := missingGuardFields(gql.Query)
66+
modified := injectFieldsIntoQuery(gql.Query, missing)
67+
if modified == gql.Query {
68+
return body
69+
}
70+
71+
logGraphQL.Printf("injected %v into GraphQL query for %s", missing, toolName)
72+
73+
gql.Query = modified
74+
out, err := json.Marshal(gql)
75+
if err != nil {
76+
return body
77+
}
78+
return out
79+
}
80+
81+
// injectFieldsIntoQuery adds the given fields into the GraphQL query's node
82+
// selection or fragment. Each field string (e.g. "author{login}",
83+
// "authorAssociation") is comma-joined and injected as a single block.
84+
func injectFieldsIntoQuery(query string, fields []string) string {
85+
injection := strings.Join(fields, ",")
86+
87+
// Step 1: Check if the query uses a fragment spread in the nodes.
88+
// Pattern: nodes { ...fragmentName }
89+
fragmentInNodes := regexp.MustCompile(`nodes\s*\{\s*\.\.\.(\w+)`)
90+
if m := fragmentInNodes.FindStringSubmatch(query); m != nil {
91+
fragName := m[1]
92+
return injectIntoFragment(query, fragName, injection)
93+
}
94+
95+
// Step 2: No fragment — inject directly into nodes { ... }
96+
nodesPattern := regexp.MustCompile(`(nodes\s*\{)`)
97+
if nodesPattern.MatchString(query) {
98+
return nodesPattern.ReplaceAllString(query, "${1}"+injection+",")
99+
}
100+
101+
return query
102+
}
103+
104+
// injectIntoFragment adds a field to the end of a named fragment definition.
105+
// "fragment Name on Type { existing fields }" → "fragment Name on Type { existing fields field }"
106+
func injectIntoFragment(query, fragName, field string) string {
107+
// Match: fragment <name> on <Type> { ... }
108+
// We need to find the closing brace of this specific fragment.
109+
fragPrefix := "fragment " + fragName + " on "
110+
idx := strings.Index(query, fragPrefix)
111+
if idx == -1 {
112+
return query
113+
}
114+
115+
// Find the opening brace of the fragment body
116+
braceStart := strings.Index(query[idx:], "{")
117+
if braceStart == -1 {
118+
return query
119+
}
120+
braceStart += idx
121+
122+
// Find the matching closing brace (handle nested braces)
123+
depth := 0
124+
braceEnd := -1
125+
for i := braceStart; i < len(query); i++ {
126+
if query[i] == '{' {
127+
depth++
128+
} else if query[i] == '}' {
129+
depth--
130+
if depth == 0 {
131+
braceEnd = i
132+
break
133+
}
134+
}
135+
}
136+
137+
if braceEnd == -1 {
138+
return query
139+
}
140+
141+
// Insert field before the closing brace
142+
return query[:braceEnd] + "," + field + query[braceEnd:]
143+
}

0 commit comments

Comments
 (0)