Skip to content
Open
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
50 changes: 40 additions & 10 deletions internal/contracts/intent.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,20 +76,20 @@ type IntentDeclaration struct {

// IntentValidationError represents intent validation failures
type IntentValidationError struct {
Code string `json:"code"` // Error code for programmatic handling
Message string `json:"message"` // Human-readable error message
Details map[string]interface{} `json:"details" swaggertype:"object"` // Additional context
Code string `json:"code"` // Error code for programmatic handling
Message string `json:"message"` // Human-readable error message
Details map[string]interface{} `json:"details" swaggertype:"object"` // Additional context
}

// Error codes for intent validation
const (
IntentErrorCodeMissing = "MISSING_INTENT"
IntentErrorCodeMissing = "MISSING_INTENT"
IntentErrorCodeMissingOperationType = "MISSING_OPERATION_TYPE"
IntentErrorCodeInvalidOperationType = "INVALID_OPERATION_TYPE"
IntentErrorCodeMismatch = "INTENT_MISMATCH"
IntentErrorCodeServerMismatch = "SERVER_MISMATCH"
IntentErrorCodeInvalidSensitivity = "INVALID_SENSITIVITY"
IntentErrorCodeReasonTooLong = "REASON_TOO_LONG"
IntentErrorCodeMismatch = "INTENT_MISMATCH"
IntentErrorCodeServerMismatch = "SERVER_MISMATCH"
IntentErrorCodeInvalidSensitivity = "INVALID_SENSITIVITY"
IntentErrorCodeReasonTooLong = "REASON_TOO_LONG"
)

// Error implements the error interface
Expand All @@ -115,8 +115,8 @@ func (i *IntentDeclaration) Validate() *IntentValidationError {
IntentErrorCodeInvalidSensitivity,
fmt.Sprintf("Invalid intent.data_sensitivity '%s': must be public, internal, private, or unknown", i.DataSensitivity),
map[string]interface{}{
"provided": i.DataSensitivity,
"valid_values": ValidDataSensitivities,
"provided": i.DataSensitivity,
"valid_values": ValidDataSensitivities,
},
)
}
Expand Down Expand Up @@ -227,6 +227,36 @@ func DeriveCallWith(annotations *config.ToolAnnotations) string {
return ToolVariantRead
}

// Content trust constants for open-world hint scanning (Spec 035)
const (
// ContentTrustUntrusted marks tool output as untrusted (open-world tool, data from external sources)
ContentTrustUntrusted = "untrusted"
// ContentTrustTrusted marks tool output as trusted (closed-world tool, data from controlled sources)
ContentTrustTrusted = "trusted"
)

// IsOpenWorldTool checks whether a tool's annotations indicate it operates in an
// open-world context (fetching data from external/untrusted sources). Per the MCP spec,
// openWorldHint defaults to true when nil or when annotations are nil entirely.
func IsOpenWorldTool(annotations *config.ToolAnnotations) bool {
if annotations == nil {
return true // spec default: openWorldHint=true
}
if annotations.OpenWorldHint == nil {
return true // nil defaults to true per spec
}
return *annotations.OpenWorldHint
}

// ContentTrustForTool returns the content trust level based on tool annotations.
// Open-world tools return "untrusted", closed-world tools return "trusted".
func ContentTrustForTool(annotations *config.ToolAnnotations) string {
if IsOpenWorldTool(annotations) {
return ContentTrustUntrusted
}
return ContentTrustTrusted
}

// isValidDataSensitivity checks if the data sensitivity is valid
func isValidDataSensitivity(sensitivity string) bool {
for _, valid := range ValidDataSensitivities {
Expand Down
118 changes: 112 additions & 6 deletions internal/contracts/intent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,112 @@ import (
"github.com/smart-mcp-proxy/mcpproxy-go/internal/config"
)

func TestIsOpenWorldTool(t *testing.T) {
trueVal := true
falseVal := false

tests := []struct {
name string
annotations *config.ToolAnnotations
want bool
}{
{
name: "nil annotations - defaults to true (open world)",
annotations: nil,
want: true,
},
{
name: "empty annotations (no hints set) - defaults to true",
annotations: &config.ToolAnnotations{},
want: true,
},
{
name: "openWorldHint nil - defaults to true",
annotations: &config.ToolAnnotations{
ReadOnlyHint: &trueVal, // other hint set but not openWorldHint
},
want: true,
},
{
name: "openWorldHint explicitly true",
annotations: &config.ToolAnnotations{
OpenWorldHint: &trueVal,
},
want: true,
},
{
name: "openWorldHint explicitly false",
annotations: &config.ToolAnnotations{
OpenWorldHint: &falseVal,
},
want: false,
},
{
name: "openWorldHint false with other hints",
annotations: &config.ToolAnnotations{
OpenWorldHint: &falseVal,
ReadOnlyHint: &trueVal,
DestructiveHint: &falseVal,
},
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := IsOpenWorldTool(tt.annotations)
if got != tt.want {
t.Errorf("IsOpenWorldTool() = %v, want %v", got, tt.want)
}
})
}
}

func TestContentTrustForTool(t *testing.T) {
trueVal := true
falseVal := false

tests := []struct {
name string
annotations *config.ToolAnnotations
want string
}{
{
name: "nil annotations - untrusted",
annotations: nil,
want: ContentTrustUntrusted,
},
{
name: "empty annotations - untrusted",
annotations: &config.ToolAnnotations{},
want: ContentTrustUntrusted,
},
{
name: "openWorldHint true - untrusted",
annotations: &config.ToolAnnotations{
OpenWorldHint: &trueVal,
},
want: ContentTrustUntrusted,
},
{
name: "openWorldHint false - trusted",
annotations: &config.ToolAnnotations{
OpenWorldHint: &falseVal,
},
want: ContentTrustTrusted,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ContentTrustForTool(tt.annotations)
if got != tt.want {
t.Errorf("ContentTrustForTool() = %v, want %v", got, tt.want)
}
})
}
}

func TestIntentDeclaration_Validate(t *testing.T) {
// Note: Validate() only checks optional fields (data_sensitivity, reason)
// operation_type is inferred from tool variant, not validated here
Expand Down Expand Up @@ -99,12 +205,12 @@ func TestIntentDeclaration_ValidateForToolVariant(t *testing.T) {
// Note: ValidateForToolVariant now SETS operation_type from tool variant (inference)
// It no longer validates that operation_type matches - it always overwrites with inferred value
tests := []struct {
name string
intent IntentDeclaration
toolVariant string
wantErr bool
wantErrCode string
wantOpType string // expected operation_type after call
name string
intent IntentDeclaration
toolVariant string
wantErr bool
wantErrCode string
wantOpType string // expected operation_type after call
}{
{
name: "empty intent with call_tool_read - sets operation_type",
Expand Down
Loading
Loading