Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
62 changes: 62 additions & 0 deletions internal/cmd/delegation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package cmd

import (
"context"
"fmt"
"net"
"net/http"

"github.com/github/gh-aw-mcpg/internal/delegation"
"github.com/github/gh-aw-mcpg/internal/logger"
"github.com/github/gh-aw-mcpg/internal/server"
)

func startUnifiedDelegationControl(
ctx context.Context,
cancel context.CancelFunc,
unifiedServer *server.UnifiedServer,
delegationConfig *delegation.RuntimeConfig,
) (*http.Server, <-chan error, error) {
controlListenerErrCh := make(chan error, 1)
if delegationConfig == nil {
return nil, controlListenerErrCh, nil
}
controlListener, err := net.Listen("tcp", delegationConfig.ControlListenAddr)
if err != nil {
return nil, controlListenerErrCh, fmt.Errorf("failed to listen on private delegation control channel %s: %w", delegationConfig.ControlListenAddr, err)
}
controlHTTPServer := &http.Server{
Handler: unifiedServer.ControlHandler(),
BaseContext: func(_ net.Listener) context.Context {
return ctx
},
}
go func() {
if err := controlHTTPServer.Serve(controlListener); err != nil && err != http.ErrServerClosed {
logger.LogError("delegation", "Private delegation control channel exited unexpectedly, shutting down: %v", err)
controlListenerErrCh <- err
cancel()
}
}()
logger.LogInfo("startup", "Private delegation control channel listening on %s", controlListener.Addr())
return controlHTTPServer, controlListenerErrCh, nil
}

func persistUnifiedDelegationState(delegationConfig *delegation.RuntimeConfig, delegationStatePath string) error {
if delegationConfig == nil {
return nil
}
if err := delegationConfig.Store.SaveState(delegationStatePath); err != nil {
return fmt.Errorf("failed to persist delegation state: %w", err)
}
return nil
}

func selectDelegationControlError(err error, controlListenerErrCh <-chan error) error {
select {
case controlErr := <-controlListenerErrCh:
return fmt.Errorf("private delegation control channel failed: %w", controlErr)
default:
return err
}
}
111 changes: 111 additions & 0 deletions internal/cmd/delegation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package cmd

import (
"bytes"
"context"
"encoding/json"
"net"
"net/http"
"testing"
"time"

"github.com/github/gh-aw-mcpg/internal/config"
"github.com/github/gh-aw-mcpg/internal/delegation"
"github.com/github/gh-aw-mcpg/internal/sanitize"
"github.com/github/gh-aw-mcpg/internal/server"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestStartUnifiedDelegationControlServesStatus(t *testing.T) {
previousRedaction := sanitize.PrivateSelectorRedactionEnabled()
t.Cleanup(func() { sanitize.SetPrivateSelectorRedaction(previousRedaction) })

const capabilityKey = "control-capability-key-32-bytes!!"
delegationConfig, err := testRuntimeDelegationConfig(t, availableLoopbackAddr(t), capabilityKey)
require.NoError(t, err)
cfg := &config.Config{
Servers: map[string]*config.ServerConfig{},
Delegation: delegationConfig,
}
us, err := server.NewUnified(context.Background(), cfg)
require.NoError(t, err)
defer us.Close()

ctx, cancel := context.WithCancel(context.Background())
defer cancel()
controlServer, controlErrCh, err := startUnifiedDelegationControl(ctx, cancel, us, delegationConfig)
require.NoError(t, err)
require.NotNil(t, controlServer)
defer func() { _ = controlServer.Shutdown(context.Background()) }()

body := bytes.NewBufferString(`{"run_id":"run-1","enclave_entry_id":"entry-1"}`)
req, err := http.NewRequest(http.MethodPost, "http://"+delegationConfig.ControlListenAddr+delegation.ControlPathPrefix+"status", body)
require.NoError(t, err)
req.Header.Set("Authorization", capabilityKey)

resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()

assert.Equal(t, http.StatusOK, resp.StatusCode)
var payload map[string]any
require.NoError(t, json.NewDecoder(resp.Body).Decode(&payload))
assert.InEpsilon(t, 1.0, payload["generation"], 0)
assert.NoError(t, selectDelegationControlError(nil, controlErrCh))
}

func TestStartUnifiedDelegationControlFailsWhenListenAddrOccupied(t *testing.T) {
previousRedaction := sanitize.PrivateSelectorRedactionEnabled()
t.Cleanup(func() { sanitize.SetPrivateSelectorRedaction(previousRedaction) })

const capabilityKey = "control-capability-key-32-bytes!!"
occupiedAddr := availableLoopbackAddr(t)
occupyingListener, err := net.Listen("tcp", occupiedAddr)
require.NoError(t, err)
defer occupyingListener.Close()

delegationConfig, err := testRuntimeDelegationConfig(t, occupiedAddr, capabilityKey)
require.NoError(t, err)
cfg := &config.Config{
Servers: map[string]*config.ServerConfig{},
Delegation: delegationConfig,
}
us, err := server.NewUnified(context.Background(), cfg)
require.NoError(t, err)
defer us.Close()

ctx, cancel := context.WithCancel(context.Background())
defer cancel()
controlServer, _, err := startUnifiedDelegationControl(ctx, cancel, us, delegationConfig)
require.Error(t, err)
assert.Nil(t, controlServer)
assert.Contains(t, err.Error(), "failed to listen on private delegation control channel")
}

func testRuntimeDelegationConfig(t *testing.T, listenAddr, capabilityKey string) (*delegation.RuntimeConfig, error) {
t.Helper()
envelope := &delegation.Envelope{
RunID: "run-1",
EnclaveBackend: "awf-enclave",
AllowedRepositories: []string{"github/gh-aw"},
ToolPolicy: delegation.ToolPolicyGitHubRepositoryReadV1,
AllowedSchemaHashes: []string{"sha256:test"},
MaxIdentityTTL: 120 * time.Second,
ExpiresAt: time.Now().Add(time.Hour),
}
store, err := delegation.NewStore(envelope, 1)
if err != nil {
return nil, err
}
capability, err := delegation.NewControlCapability(capabilityKey)
if err != nil {
return nil, err
}
return &delegation.RuntimeConfig{
Store: store,
Capability: capability,
StatePath: t.TempDir() + "/state.json",
ControlListenAddr: listenAddr,
}, nil
}
4 changes: 2 additions & 2 deletions internal/cmd/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func init() {
rootCmd.AddCommand(newProxyCmd())
}

func resolveDelegationProxyConfig() (*proxy.DelegationConfig, string, error) {
func resolveDelegationProxyConfig() (*delegation.RuntimeConfig, string, error) {
envelopeJSON := os.Getenv("MCP_GATEWAY_DELEGATION_ENVELOPE")
capabilityKey := os.Getenv(delegation.EnvControlCapabilityKey)
statePath := os.Getenv("MCP_GATEWAY_DELEGATION_STATE_PATH")
Expand Down Expand Up @@ -95,7 +95,7 @@ func resolveDelegationProxyConfig() (*proxy.DelegationConfig, string, error) {
if err != nil {
return nil, "", err
}
return &proxy.DelegationConfig{Store: store, Capability: capability, StatePath: statePath, ControlListenAddr: controlListenAddr}, statePath, nil
return &delegation.RuntimeConfig{Store: store, Capability: capability, StatePath: statePath, ControlListenAddr: controlListenAddr}, statePath, nil
}

func newProxyCmd() *cobra.Command {
Expand Down
26 changes: 24 additions & 2 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"context"
"fmt"
"log"
"os"
Expand Down Expand Up @@ -284,6 +285,12 @@ func run(cmd *cobra.Command, args []string) error {

debugLog.Printf("Server mode: %s, guards mode: %s", mode, cfg.DIFCMode)

delegationConfig, delegationStatePath, err := resolveDelegationProxyConfig()
if err != nil {
return err
}
cfg.Delegation = delegationConfig

// Apply tracing flags: CLI flags and env var overrides take precedence over config values.
applyTracingOverrides(cmd, cfg)

Expand Down Expand Up @@ -340,6 +347,16 @@ func run(cmd *cobra.Command, args []string) error {
debugLog.Printf("Unified MCP server created successfully")
defer unifiedServer.Close()

delegationControlServer, delegationControlErrCh, err := startUnifiedDelegationControl(ctx, cancel, unifiedServer, delegationConfig)
if err != nil {
return err
}
if delegationControlServer != nil {
defer func() {
_ = delegationControlServer.Shutdown(context.Background())
}()
}

// Handle graceful shutdown via context cancellation
go func() {
<-ctx.Done()
Expand Down Expand Up @@ -370,7 +387,7 @@ func run(cmd *cobra.Command, args []string) error {
log.Printf("Warning: failed to write gateway configuration to stdout: %v", err)
}

if err := serveAndWait(
err = serveAndWait(
ctx,
cancel,
httpServer,
Expand All @@ -381,10 +398,15 @@ func run(cmd *cobra.Command, args []string) error {
func() error {
return httpServer.Serve(listener)
},
); err != nil {
)
err = selectDelegationControlError(err, delegationControlErrCh)
if err != nil {
debugLog.Printf("Server exited with error: %v", err)
return err
}
if err := persistUnifiedDelegationState(delegationConfig, delegationStatePath); err != nil {
return err
}

return nil
}
Expand Down
6 changes: 6 additions & 0 deletions internal/config/config_core.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (

"github.com/BurntSushi/toml"

"github.com/github/gh-aw-mcpg/internal/delegation"
"github.com/github/gh-aw-mcpg/internal/logger"
)

Expand Down Expand Up @@ -94,6 +95,11 @@ type Config struct {

// GuardPolicySource describes where GuardPolicy was resolved from (cli|env|config|legacy).
GuardPolicySource string `toml:"-" json:"-"`

// Delegation optionally enables github-repository-delegation-v1 runtime
// control/data-plane authorization. It is resolved from environment
// activation inputs, never from user configuration.
Delegation *delegation.RuntimeConfig `toml:"-" json:"-"`
}

// GatewayConfig holds global gateway settings.
Expand Down
14 changes: 14 additions & 0 deletions internal/delegation/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package delegation

// ControlPathPrefix is the private AWF control-plane URL prefix for
// github-repository-delegation-v1 operations.
const ControlPathPrefix = "/internal/awf-enclave-mcp-control/"

// RuntimeConfig enables runtime repository-read delegation and its
// AWF-authenticated private control channel.
type RuntimeConfig struct {
Store *Store
Capability *ControlCapability
StatePath string
ControlListenAddr string
}
19 changes: 19 additions & 0 deletions internal/delegation/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,25 @@ func (s *Store) AuthorizeExecutor(executorBearer, repository, tool string) (stri
return identity.Handle, nil
}

// HasLiveExecutorBearer reports whether executorBearer currently identifies a
// live delegated executor identity. It intentionally does not authorize any
// repository or tool; callers must still use AuthorizeExecutor at the actual
// data-plane operation.
func (s *Store) HasLiveExecutorBearer(executorBearer string) bool {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now()
s.cleanupExpiredLocked(now)
if s.recoveryIncomplete || !now.Before(s.envelope.ExpiresAt) {
return false
}
if bearer, ok := strings.CutPrefix(executorBearer, "Bearer "); ok {
executorBearer = bearer
}
_, ok := s.byBearer[sha256.Sum256([]byte(executorBearer))]
return ok
}

func (s *Store) authorize(executorBearer, repository, tool string, bindingMatches func(*Identity) bool) (*Identity, error) {
s.mu.Lock()
defer s.mu.Unlock()
Expand Down
14 changes: 14 additions & 0 deletions internal/delegation/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,20 @@ func TestAuthorizeExecutor_ReturnsIdentityHandleForIsolation(t *testing.T) {
assert.Error(t, err)
}

func TestHasLiveExecutorBearerTracksRevocation(t *testing.T) {
store, _ := newTestStore(t)
req := validRequest()
created, err := store.CreateOrConfirm(req)
require.NoError(t, err)

assert.True(t, store.HasLiveExecutorBearer(created.ExecutorBearer))
assert.True(t, store.HasLiveExecutorBearer("Bearer "+created.ExecutorBearer))
assert.False(t, store.HasLiveExecutorBearer(created.Handle), "control handles must not be accepted as executor bearers")

require.NoError(t, store.Revoke(created.Handle))
assert.False(t, store.HasLiveExecutorBearer(created.ExecutorBearer))
}

func TestExpiry_AutomaticAndExplicit(t *testing.T) {
store, _ := newTestStore(t)
req := validRequest()
Expand Down
9 changes: 2 additions & 7 deletions internal/proxy/delegation.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (

var logDelegation = logger.ForFile()

const delegationControlPath = "/internal/awf-enclave-mcp-control/"
const delegationControlPath = delegation.ControlPathPrefix

type delegationState struct {
store *delegation.Store
Expand All @@ -28,12 +28,7 @@ type delegationState struct {

// DelegationConfig enables runtime repository-read delegation and its
// AWF-authenticated private control channel.
type DelegationConfig struct {
Store *delegation.Store
Capability *delegation.ControlCapability
StatePath string
ControlListenAddr string
}
type DelegationConfig = delegation.RuntimeConfig

func newDelegationState(cfg *DelegationConfig) (*delegationState, error) {
if cfg == nil {
Expand Down
Loading
Loading