diff --git a/go.mod b/go.mod index 689918f..c9164be 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/sirerun/mint -go 1.26.1 +go 1.26.4 require ( cloud.google.com/go/artifactregistry v1.20.0 @@ -18,6 +18,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 github.com/aws/aws-sdk-go-v2 v1.41.3 github.com/aws/aws-sdk-go-v2/config v1.32.11 + github.com/aws/aws-sdk-go-v2/service/applicationautoscaling v1.41.12 github.com/aws/aws-sdk-go-v2/service/codebuild v1.68.11 github.com/aws/aws-sdk-go-v2/service/ecr v1.56.0 github.com/aws/aws-sdk-go-v2/service/ecs v1.73.1 @@ -29,7 +30,7 @@ require ( github.com/pb33f/libopenapi v0.34.0 go.yaml.in/yaml/v4 v4.0.0-rc.4 golang.org/x/oauth2 v0.36.0 - golang.org/x/term v0.40.0 + golang.org/x/term v0.43.0 google.golang.org/api v0.271.0 google.golang.org/grpc v1.79.2 ) @@ -53,7 +54,6 @@ require ( github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect - github.com/aws/aws-sdk-go-v2/service/applicationautoscaling v1.41.12 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect @@ -67,7 +67,7 @@ require ( github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-jose/go-jose/v4 v4.1.3 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect @@ -92,11 +92,11 @@ require ( go.opentelemetry.io/otel/sdk v1.40.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.40.0 // indirect - golang.org/x/crypto v0.48.0 // indirect - golang.org/x/net v0.51.0 // indirect + golang.org/x/crypto v0.51.0 // indirect + golang.org/x/net v0.55.0 // indirect golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.15.0 // indirect google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect diff --git a/go.sum b/go.sum index 418c804..30188bc 100644 --- a/go.sum +++ b/go.sum @@ -128,8 +128,8 @@ github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= -github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -199,21 +199,21 @@ go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZY go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= diff --git a/internal/mcpgen/converter.go b/internal/mcpgen/converter.go index f9e6509..f4220e0 100644 --- a/internal/mcpgen/converter.go +++ b/internal/mcpgen/converter.go @@ -1,6 +1,7 @@ package mcpgen import ( + "encoding/json" "fmt" "strings" "unicode" @@ -22,6 +23,7 @@ func Convert(doc *v3high.Document) (*MCPServer, error) { } server.Auth = extractAuth(doc) + server.Scoping = extractScoping(doc) if doc.Paths == nil || doc.Paths.PathItems == nil { return server, nil @@ -37,6 +39,32 @@ func Convert(doc *v3high.Document) (*MCPServer, error) { return server, nil } +// extractScoping reads the spec's top-level `x-sire-scoping` OpenAPI extension +// and returns it as canonical JSON, or nil when the extension is absent or +// cannot be decoded. mint does not interpret the value -- it is an opaque +// passthrough that the Sire scoping layer (ADR 121) parses and validates +// downstream. Failing to nil (rather than erroring) keeps spec parsing +// resilient: a malformed extension simply yields no descriptor, which the +// downstream layer treats as end-user-unavailable (fail-closed default-deny). +func extractScoping(doc *v3high.Document) json.RawMessage { + if doc == nil || doc.Extensions == nil { + return nil + } + node, ok := doc.Extensions.Get("x-sire-scoping") + if !ok || node == nil { + return nil + } + var v interface{} + if err := node.Decode(&v); err != nil { + return nil + } + raw, err := json.Marshal(v) + if err != nil { + return nil + } + return raw +} + func extractToolsFromPathItem(path string, item *v3high.PathItem) []MCPTool { var tools []MCPTool diff --git a/internal/mcpgen/model.go b/internal/mcpgen/model.go index bc4608a..266175d 100644 --- a/internal/mcpgen/model.go +++ b/internal/mcpgen/model.go @@ -1,5 +1,7 @@ package mcpgen +import "encoding/json" + // MCPServer represents a generated MCP server with its tools and configuration. type MCPServer struct { Name string `json:"name"` @@ -8,6 +10,11 @@ type MCPServer struct { BaseURL string `json:"base_url"` Tools []MCPTool `json:"tools"` Auth *MCPAuth `json:"auth,omitempty"` + // Scoping carries the spec's top-level `x-sire-scoping` OpenAPI extension + // (the end-user data-scoping descriptor, Sire ADR 121) as canonical JSON, or + // nil when the spec declares no such extension. mint treats it as an opaque + // passthrough; the Sire API's scoping layer parses and enforces it. + Scoping json.RawMessage `json:"scoping,omitempty"` } // MCPTool represents a single MCP tool derived from an OpenAPI operation. diff --git a/internal/mcpgen/scoping_test.go b/internal/mcpgen/scoping_test.go new file mode 100644 index 0000000..b3ce300 --- /dev/null +++ b/internal/mcpgen/scoping_test.go @@ -0,0 +1,88 @@ +package mcpgen + +import ( + "encoding/json" + "testing" + + "github.com/pb33f/libopenapi" +) + +// buildDoc parses an inline OpenAPI spec into the v3 model Convert consumes. +func buildScopingDoc(t *testing.T, spec string) *MCPServer { + t.Helper() + doc, err := libopenapi.NewDocument([]byte(spec)) + if err != nil { + t.Fatalf("NewDocument: %v", err) + } + model, buildErr := doc.BuildV3Model() + if buildErr != nil { + t.Fatalf("BuildV3Model: %v", buildErr) + } + server, err := Convert(&model.Model) + if err != nil { + t.Fatalf("Convert: %v", err) + } + return server +} + +// TestConvertScopingExtension_Present verifies the top-level `x-sire-scoping` +// extension is captured as canonical JSON on MCPServer.Scoping. +func TestConvertScopingExtension_Present(t *testing.T) { + spec := `openapi: 3.0.0 +info: + title: shopify + version: 1.0.0 +x-sire-scoping: + pattern: tenant_scoped + scopeParam: customer_id + derivation: external_principal +paths: {} +` + server := buildScopingDoc(t, spec) + if len(server.Scoping) == 0 { + t.Fatal("Scoping is empty; want the x-sire-scoping extension") + } + + var got map[string]any + if err := json.Unmarshal(server.Scoping, &got); err != nil { + t.Fatalf("Scoping is not valid JSON: %v (%s)", err, server.Scoping) + } + want := map[string]any{ + "pattern": "tenant_scoped", + "scopeParam": "customer_id", + "derivation": "external_principal", + } + for k, v := range want { + if got[k] != v { + t.Errorf("Scoping[%q] = %v, want %v", k, got[k], v) + } + } +} + +// TestConvertScopingExtension_Absent verifies a spec without the extension +// yields a nil Scoping (omitted from JSON), the default-deny posture. +func TestConvertScopingExtension_Absent(t *testing.T) { + spec := `openapi: 3.0.0 +info: + title: noscope + version: 1.0.0 +paths: {} +` + server := buildScopingDoc(t, spec) + if server.Scoping != nil { + t.Errorf("Scoping = %s, want nil for a spec with no x-sire-scoping", server.Scoping) + } + + // And it must be omitted from the marshaled server. + out, err := json.Marshal(server) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + var m map[string]json.RawMessage + if err := json.Unmarshal(out, &m); err != nil { + t.Fatalf("Unmarshal: %v", err) + } + if _, present := m["scoping"]; present { + t.Error("scoping key present in JSON; want omitted when absent") + } +}