Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/PROXY_MODE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ awmg proxy \
# Trust the generated CA and point gh at the proxy
export GH_HOST=localhost:8443
export NODE_EXTRA_CA_CERTS=/tmp/gh-aw/mcp-logs/proxy-tls/ca.crt
export SSL_CERT_FILE=/tmp/gh-aw/mcp-logs/proxy-tls/ca.crt
export GIT_SSL_CAINFO=/tmp/gh-aw/mcp-logs/proxy-tls/ca.crt
gh issue list -R org/repo
```

Expand Down Expand Up @@ -167,6 +169,8 @@ docker run --rm -p 8443:8443 \
# Trust the CA cert from the mounted log volume
export GH_HOST=localhost:8443
export NODE_EXTRA_CA_CERTS=/tmp/proxy-logs/proxy-tls/ca.crt
export SSL_CERT_FILE=/tmp/proxy-logs/proxy-tls/ca.crt
export GIT_SSL_CAINFO=/tmp/proxy-logs/proxy-tls/ca.crt
gh issue list -R org/repo
```

Expand Down Expand Up @@ -219,8 +223,13 @@ When `--tls` is enabled, the proxy writes to `--tls-dir` (default: `<log-dir>/pr
**gh CLI / Node.js**:
```bash
export NODE_EXTRA_CA_CERTS=/tmp/gh-aw/mcp-logs/proxy-tls/ca.crt
export SSL_CERT_FILE=/tmp/gh-aw/mcp-logs/proxy-tls/ca.crt
export GIT_SSL_CAINFO=/tmp/gh-aw/mcp-logs/proxy-tls/ca.crt
```

When `GITHUB_ENV` is present (GitHub Actions), `awmg proxy --tls` appends these TLS trust
variables automatically for downstream steps.

**System-wide (Ubuntu)**:
```bash
cp /tmp/gh-aw/mcp-logs/proxy-tls/ca.crt /usr/local/share/ca-certificates/mcpg-proxy.crt
Expand Down
49 changes: 49 additions & 0 deletions internal/cmd/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import (
"context"
"crypto/tls"
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"

Expand All @@ -24,6 +26,14 @@ import (

var logProxyCmd = logger.New("cmd:proxy")

var tlsTrustEnvKeys = []string{
"NODE_EXTRA_CA_CERTS",
"SSL_CERT_FILE",
"GIT_SSL_CAINFO",
"CURL_CA_BUNDLE",
"REQUESTS_CA_BUNDLE",
}

// Proxy subcommand flag variables
var (
proxyGuardWasm string
Expand Down Expand Up @@ -223,6 +233,9 @@ func runProxy(cmd *cobra.Command, args []string) error {
if err != nil {
return fmt.Errorf("failed to generate TLS certificates: %w", err)
}
if err := configureTLSTrustEnvironment(tlsCfg.CACertPath); err != nil {
return err
}
logger.LogInfo("startup", "TLS certificates generated: ca=%s", tlsCfg.CACertPath)
}

Expand Down Expand Up @@ -268,6 +281,8 @@ func runProxy(cmd *cobra.Command, args []string) error {
fmt.Fprintf(os.Stderr, "\nConnect with:\n")
fmt.Fprintf(os.Stderr, " export GH_HOST=%s\n", clientAddr(actualAddr))
fmt.Fprintf(os.Stderr, " export NODE_EXTRA_CA_CERTS=%s\n", tlsCfg.CACertPath)
fmt.Fprintf(os.Stderr, " export SSL_CERT_FILE=%s\n", tlsCfg.CACertPath)
fmt.Fprintf(os.Stderr, " export GIT_SSL_CAINFO=%s\n", tlsCfg.CACertPath)
fmt.Fprintf(os.Stderr, " gh issue list -R org/repo\n\n")
} else {
fmt.Fprintf(os.Stderr, "\nConnect with:\n")
Expand Down Expand Up @@ -302,3 +317,37 @@ func clientAddr(addr string) string {
}
return addr
}

func configureTLSTrustEnvironment(caCertPath string) error {
if strings.ContainsAny(caCertPath, "\r\n") {
return fmt.Errorf("invalid TLS CA cert path contains newline")
}

for _, key := range tlsTrustEnvKeys {
if err := os.Setenv(key, caCertPath); err != nil {
return fmt.Errorf("failed to set %s: %w", key, err)
}
}

githubEnvPath := os.Getenv("GITHUB_ENV")
if githubEnvPath == "" {
return nil
}

// Best-effort append: the proxy should still start even if GITHUB_ENV cannot be opened.
f, err := os.OpenFile(githubEnvPath, os.O_APPEND|os.O_WRONLY, 0o644)
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

os.OpenFile(githubEnvPath, os.O_APPEND|os.O_WRONLY, 0o644) does not use the provided permissions unless os.O_CREATE is set, so 0o644 is effectively ignored here. To avoid implying that permissions are being applied, consider passing 0 (or adding os.O_CREATE if you actually want to create the file when missing).

Suggested change
f, err := os.OpenFile(githubEnvPath, os.O_APPEND|os.O_WRONLY, 0o644)
f, err := os.OpenFile(githubEnvPath, os.O_APPEND|os.O_WRONLY, 0)

Copilot uses AI. Check for mistakes.
if err != nil {
logger.LogWarn("startup", "Skipping GITHUB_ENV TLS trust export: open failed for %s: %v", githubEnvPath, err)
return nil
}
defer f.Close()

for _, key := range tlsTrustEnvKeys {
if _, err := io.WriteString(f, key+"="+caCertPath+"\n"); err != nil {
return fmt.Errorf("failed writing %s to GITHUB_ENV file: %w", key, err)
}
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

configureTLSTrustEnvironment treats opening GITHUB_ENV as best-effort (logs a warning and continues), but write failures currently return an error and will abort proxy startup. This conflicts with the intended/ documented behavior of not failing startup when GITHUB_ENV is unavailable/unwritable. Consider making write failures best-effort too (log a warning and continue/return nil), or at least gate it behind the same best-effort policy as open failures.

Copilot uses AI. Check for mistakes.
}

logProxyCmd.Printf("Appended TLS trust environment to GITHUB_ENV: %s", githubEnvPath)
return nil
}
49 changes: 49 additions & 0 deletions internal/cmd/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,3 +409,52 @@ func TestClientAddr(t *testing.T) {
})
}
}

func TestConfigureTLSTrustEnvironment(t *testing.T) {
caPath := "/tmp/proxy-tls/ca.crt"

t.Run("sets trust environment variables in process", func(t *testing.T) {
assert := assert.New(t)
t.Setenv("GITHUB_ENV", "")
for _, key := range tlsTrustEnvKeys {
t.Setenv(key, "")
}

err := configureTLSTrustEnvironment(caPath)
require.NoError(t, err)

for _, key := range tlsTrustEnvKeys {
assert.Equal(caPath, os.Getenv(key), "expected %s to be set", key)
}
})

t.Run("skips GITHUB_ENV append when env var is unset", func(t *testing.T) {
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subtest name says "env var is unset" but the test uses t.Setenv("GITHUB_ENV", ""), which sets it to an empty string rather than truly unsetting it. Since os.Getenv treats unset and empty equivalently this still exercises the code path, but the name is misleading; consider renaming to "unset or empty" (or use os.Unsetenv if you specifically want to test "unset").

Suggested change
t.Run("skips GITHUB_ENV append when env var is unset", func(t *testing.T) {
t.Run("skips GITHUB_ENV append when env var is unset or empty", func(t *testing.T) {

Copilot uses AI. Check for mistakes.
t.Setenv("GITHUB_ENV", "")
require.NoError(t, configureTLSTrustEnvironment(caPath))
})

t.Run("appends trust environment variables to GITHUB_ENV", func(t *testing.T) {
assert := assert.New(t)
githubEnvFile := t.TempDir() + "/github_env"
require.NoError(t, os.WriteFile(githubEnvFile, []byte{}, 0o644))
t.Setenv("GITHUB_ENV", githubEnvFile)
for _, key := range tlsTrustEnvKeys {
t.Setenv(key, "")
}

err := configureTLSTrustEnvironment(caPath)
require.NoError(t, err)

content, err := os.ReadFile(githubEnvFile)
require.NoError(t, err)
for _, key := range tlsTrustEnvKeys {
assert.Contains(string(content), key+"="+caPath+"\n")
}
})

t.Run("rejects CA cert path with newline", func(t *testing.T) {
err := configureTLSTrustEnvironment("/tmp/ca.crt\nMALICIOUS=1")
require.Error(t, err)
assert.Contains(t, err.Error(), "contains newline")
})
}
Loading