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
19 changes: 19 additions & 0 deletions internal/guard/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package guard

import (
"context"
"errors"
"fmt"

"github.com/github/gh-aw-mcpg/internal/difc"
Expand Down Expand Up @@ -64,6 +65,24 @@ func (e *PipelineAccessDenied) Error() string {
return fmt.Sprintf("DIFC access denied: %s", e.EvalResult.Reason)
}

// HandlePrePhaseError classifies a RunPipelinePrePhases error.
//
// When err represents a coarse-grained access denial, it returns the canonical
// *PipelineAccessDenied value together with the fully formatted violation error.
// For non-denial errors, it returns (nil, nil).
func HandlePrePhaseError(err error) (*PipelineAccessDenied, error) {
var denied *PipelineAccessDenied
if !errors.As(err, &denied) {
return nil, nil
}
return denied, difc.FormatViolationError(
denied.EvalResult,
denied.AgentLabels.Secrecy,
denied.AgentLabels.Integrity,
denied.Resource,
)
}

// RunPipelinePrePhases executes phases 0–2 of the DIFC enforcement pipeline:
//
// - Phase 0: Get or create agent labels from the registry and store tool args in context.
Expand Down
29 changes: 29 additions & 0 deletions internal/guard/pipeline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package guard
import (
"context"
"errors"
"fmt"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -299,3 +300,31 @@ func TestPipelineAccessDenied_ErrorMessage(t *testing.T) {
assert.Contains(t, denied.Error(), "DIFC access denied")
assert.Contains(t, denied.Error(), "integrity tag missing")
}

func TestHandlePrePhaseError_ReturnsDetailedViolationForDeniedError(t *testing.T) {
agentLabels := difc.NewAgentLabels("test-agent")
agentLabels.AddSecrecyTags([]difc.Tag{"secret"})

resource := difc.NewLabeledResource("resource")
deniedErr := &PipelineAccessDenied{
EvalResult: &difc.EvaluationResult{
Decision: difc.AccessDeny,
SecrecyToAdd: []difc.Tag{"private"},
Reason: "secrecy violation",
},
Resource: resource,
AgentLabels: agentLabels,
}

denied, detailedErr := HandlePrePhaseError(fmt.Errorf("wrapped: %w", deniedErr))
assert.Same(t, deniedErr, denied)
assert.Error(t, detailedErr)
assert.Contains(t, detailedErr.Error(), "DIFC Violation:")
assert.Contains(t, detailedErr.Error(), "secrecy violation")
}

func TestHandlePrePhaseError_IgnoresNonDeniedError(t *testing.T) {
denied, detailedErr := HandlePrePhaseError(errors.New("resource labeling failed"))
assert.Nil(t, denied)
assert.Nil(t, detailedErr)
}
2 changes: 1 addition & 1 deletion internal/proxy/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ func (h *proxyHandler) handleWithDIFC(w http.ResponseWriter, r *http.Request, pa
}
ctx, pre, err := guard.RunPipelinePrePhases(ctx, pipelineIn)
if err != nil {
if denied, ok := err.(*guard.PipelineAccessDenied); ok {
if denied, _ := guard.HandlePrePhaseError(err); denied != nil {
logHandler.Printf("[DIFC] Phase 2: BLOCKED %s %s — %s", r.Method, path, denied.EvalResult.Reason)
deniedErr := fmt.Errorf("DIFC policy violation: %s", denied.EvalResult.Reason)
tracing.RecordSpanError(difcSpan, deniedErr, "access denied: "+denied.EvalResult.Reason)
Expand Down
4 changes: 1 addition & 3 deletions internal/server/unified.go
Original file line number Diff line number Diff line change
Expand Up @@ -428,11 +428,9 @@ func (us *UnifiedServer) callBackendTool(ctx context.Context, serverID, toolName
}
ctx, pre, err := guard.RunPipelinePrePhases(ctx, pipelineIn)
if err != nil {
if denied, ok := err.(*guard.PipelineAccessDenied); ok {
if denied, detailedErr := guard.HandlePrePhaseError(err); denied != nil {
logger.LogWarn("difc", "Access DENIED for agent %s to %s: %s",
agentID, denied.Resource.Description, denied.EvalResult.Reason)
detailedErr := difc.FormatViolationError(denied.EvalResult,
denied.AgentLabels.Secrecy, denied.AgentLabels.Integrity, denied.Resource)
tracing.RecordSpanError(toolSpan, detailedErr, "access denied: "+denied.EvalResult.Reason)
httpStatusCode = 403
return mcp.NewErrorCallToolResult(detailedErr)
Expand Down
Loading