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
9 changes: 8 additions & 1 deletion pkg/github/context_tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"time"

ghErrors "github.com/github/github-mcp-server/pkg/errors"
"github.com/github/github-mcp-server/pkg/ifc"
"github.com/github/github-mcp-server/pkg/inventory"
"github.com/github/github-mcp-server/pkg/scopes"
"github.com/github/github-mcp-server/pkg/translations"
Expand Down Expand Up @@ -103,7 +104,13 @@ func GetMe(t translations.TranslationHelperFunc) inventory.ServerTool {
},
}

return MarshalledTextResult(minimalUser), nil, nil
result := MarshalledTextResult(minimalUser)
if deps.GetFlags(ctx).InsidersMode {
result.Meta = mcp.Meta{
"ifc": ifc.LabelGetMe(),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Heads-up: the wire format here is _meta.ifc with {integrity: "high"|"low", confidentiality: ["public"|...]}, but original spec _meta.ifc_label with {integrity: "trusted"|"untrusted", confidentiality: {readers: [...]}}. I'm fine matching your shape on the cli side), but should we update the issues to lock in ifc + high/low + flat list as the contract before we annotate more tools?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also this assigns result.Meta outright. If MarshalledTextResult ever starts populating Meta (or another decorator runs before this), we'd silently clobber it. cheaper to be defensive now and merge into an existing map than to debug later, something like if result.Meta == nil { result.Meta = mcp.Meta{} }; result.Meta["ifc"] = ifc.LabelGetMe(). wdyt?

}
}
return result, nil, nil
Comment on lines +107 to +113
},
)
}
Expand Down
60 changes: 60 additions & 0 deletions pkg/github/context_tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,66 @@ func Test_GetMe(t *testing.T) {
}
}

func Test_GetMe_IFC_InsidersMode(t *testing.T) {
t.Parallel()

serverTool := GetMe(translations.NullTranslationHelper)

mockUser := &github.User{
Login: github.Ptr("testuser"),
HTMLURL: github.Ptr("https://github.com/testuser"),
CreatedAt: &github.Timestamp{Time: time.Now()},
}
mockedHTTPClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
GetUser: mockResponse(t, http.StatusOK, mockUser),
})

t.Run("insiders mode disabled omits ifc label from result meta", func(t *testing.T) {
deps := BaseDeps{
Client: github.NewClient(mockedHTTPClient),
Flags: FeatureFlags{InsidersMode: false},
}
handler := serverTool.Handler(deps)

request := createMCPRequest(map[string]any{})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
require.False(t, result.IsError)

assert.Nil(t, result.Meta, "result meta should be nil when insiders mode is disabled")
})

t.Run("insiders mode enabled includes ifc label in result meta", func(t *testing.T) {
deps := BaseDeps{
Client: github.NewClient(mockedHTTPClient),
Flags: FeatureFlags{InsidersMode: true},
}
handler := serverTool.Handler(deps)

request := createMCPRequest(map[string]any{})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
require.False(t, result.IsError)

require.NotNil(t, result.Meta, "result meta should be set when insiders mode is enabled")
ifcLabel, ok := result.Meta["ifc"]
require.True(t, ok, "result meta should contain ifc key")

ifcJSON, err := json.Marshal(ifcLabel)
require.NoError(t, err)

var ifcMap map[string]any
err = json.Unmarshal(ifcJSON, &ifcMap)
require.NoError(t, err)

assert.Equal(t, "high", ifcMap["integrity"])
confList, ok := ifcMap["confidentiality"].([]any)
require.True(t, ok, "confidentiality should be a list")
require.Len(t, confList, 1)
assert.Equal(t, "public", confList[0])
})
}

func Test_GetTeams(t *testing.T) {
t.Parallel()

Expand Down
6 changes: 6 additions & 0 deletions pkg/ifc/labelling_engine_readers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package ifc

// LabelGetMe returns a label for get_me: trusted, universal readers.
func LabelGetMe() ReadersSecurityLabel {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

handler-level test in context_tools_test.go covers the integration nicely. Worth a one-liner unit test in pkg/ifc too. assert.True(t, LabelGetMe().IsHighIntegrity() && LabelGetMe().IsPublicConfidentiality())

I think it is very cheap to add now, and as labelling_engine_readers.go grows (LabelListIssues, LabelGetFileContents, …) we'll have a single place tracking per-tool label correctness without spinning up the full handler.

return PublicTrusted()
}
221 changes: 221 additions & 0 deletions pkg/ifc/lattice.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
// Package ifc implements Information Flow Control (IFC) lattices and security labels.
//
// This package provides the fundamental lattice structures used for IFC:
// - Confidentiality lattice (LOW, HIGH)
// - Integrity lattice (TRUSTED, UNTRUSTED)
package ifc

import "fmt"

type Lattice[T any] interface {
Leq(other T) bool // self <= other
Join(other T) T // least upper bound
Meet(other T) T // greatest lower bound
fmt.Stringer // String() string
}

type ConfidentialityLevel int

const (
ConfidentialityLow ConfidentialityLevel = iota
ConfidentialityHigh
)

func (l ConfidentialityLevel) String() string {
switch l {
case ConfidentialityLow:
return "LOW"
case ConfidentialityHigh:
return "HIGH"
default:
return fmt.Sprintf("ConfidentialityLevel(%d)", int(l))
}
}

type ConfidentialityLabel struct {
Level ConfidentialityLevel
}

func LowConfidentiality() ConfidentialityLabel {
return ConfidentialityLabel{Level: ConfidentialityLow}
}

func HighConfidentiality() ConfidentialityLabel {
return ConfidentialityLabel{Level: ConfidentialityHigh}
}

type SecurityLabel struct {
ProductLabel[ConfidentialityLabel, IntegrityLabel]
}

func NewSecurityLabel(c ConfidentialityLabel, i IntegrityLabel) SecurityLabel {
return SecurityLabel{
ProductLabel: ProductLabel[ConfidentialityLabel, IntegrityLabel]{
Left: c,
Right: i,
},
}
}

func (s SecurityLabel) Leq(other SecurityLabel) bool {
return s.ProductLabel.Leq(other.ProductLabel)
}

func (s SecurityLabel) Join(other SecurityLabel) SecurityLabel {
return SecurityLabel{
ProductLabel: s.ProductLabel.Join(other.ProductLabel),
}
}

func (s SecurityLabel) Meet(other SecurityLabel) SecurityLabel {
return SecurityLabel{
ProductLabel: s.ProductLabel.Meet(other.ProductLabel),
}
}

func (s SecurityLabel) String() string {
return s.ProductLabel.String()
}

var _ Lattice[SecurityLabel] = SecurityLabel{}

var LabelHighConfidentialityTrusted = NewSecurityLabel(HighConfidentiality(), Trusted())
var LabelPublicTrusted = NewSecurityLabel(LowConfidentiality(), Trusted())
var LabelUserUntrusted = NewSecurityLabel(HighConfidentiality(), Untrusted())
var LabelPublicUntrusted = NewSecurityLabel(LowConfidentiality(), Untrusted())

func (c ConfidentialityLabel) Leq(other ConfidentialityLabel) bool {
return int(c.Level) <= int(other.Level)
}

func (c ConfidentialityLabel) Join(other ConfidentialityLabel) ConfidentialityLabel {
if c.Leq(other) {
return other
}
return c
}

func (c ConfidentialityLabel) Meet(other ConfidentialityLabel) ConfidentialityLabel {
if c.Leq(other) {
return c
}
return other
}

func (c ConfidentialityLabel) String() string {
return c.Level.String()
}

var _ Lattice[ConfidentialityLabel] = ConfidentialityLabel{}

type IntegrityLevel int

const (
IntegrityTrusted IntegrityLevel = iota
IntegrityUntrusted
)

func (l IntegrityLevel) String() string {
switch l {
case IntegrityTrusted:
return "TRUSTED"
case IntegrityUntrusted:
return "UNTRUSTED"
default:
return fmt.Sprintf("IntegrityLevel(%d)", int(l))
}
}

type IntegrityLabel struct {
Level IntegrityLevel
}

// Trusted: content originating from the user, from trusted collaborators, or system prompts.
func Trusted() IntegrityLabel {
return IntegrityLabel{Level: IntegrityTrusted}
}

// Untrusted: content from untrusted users (e.g., no push access), or from external/public sources.
func Untrusted() IntegrityLabel {
return IntegrityLabel{Level: IntegrityUntrusted}
}

func (i IntegrityLabel) Leq(other IntegrityLabel) bool {
return int(i.Level) <= int(other.Level)
}

func (i IntegrityLabel) Join(other IntegrityLabel) IntegrityLabel {
if i.Leq(other) {
return other
}
return i
}

func (i IntegrityLabel) Meet(other IntegrityLabel) IntegrityLabel {
if i.Leq(other) {
return i
}
return other
}

func (i IntegrityLabel) String() string {
return i.Level.String()
}

var _ Lattice[IntegrityLabel] = IntegrityLabel{}

// ProductLabel is a product lattice of two lattices L1 × L2.
type ProductLabel[L1 Lattice[L1], L2 Lattice[L2]] struct {
Left L1
Right L2
}

func (p ProductLabel[L1, L2]) Leq(other ProductLabel[L1, L2]) bool {
return p.Left.Leq(other.Left) && p.Right.Leq(other.Right)
}

func (p ProductLabel[L1, L2]) Join(other ProductLabel[L1, L2]) ProductLabel[L1, L2] {
return ProductLabel[L1, L2]{
Left: p.Left.Join(other.Left),
Right: p.Right.Join(other.Right),
}
}

func (p ProductLabel[L1, L2]) Meet(other ProductLabel[L1, L2]) ProductLabel[L1, L2] {
return ProductLabel[L1, L2]{
Left: p.Left.Meet(other.Left),
Right: p.Right.Meet(other.Right),
}
}

func (p ProductLabel[L1, L2]) String() string {
return fmt.Sprintf("(%s, %s)", p.Left.String(), p.Right.String())
}

var ProductLabelLattice Lattice[ProductLabel[ConfidentialityLabel, IntegrityLabel]] = ProductLabel[ConfidentialityLabel, IntegrityLabel]{}

// InverseLattice inverts the order of an underlying lattice.
type InverseLattice[L Lattice[L]] struct {
Inner L
}

func (i InverseLattice[L]) Leq(other InverseLattice[L]) bool {
// Invert order: i <= other ⇔ other.Inner <= i.Inner
return other.Inner.Leq(i.Inner)
}

func (i InverseLattice[L]) Join(other InverseLattice[L]) InverseLattice[L] {
// join in inverse is meet in the original
return InverseLattice[L]{Inner: i.Inner.Meet(other.Inner)}
}

func (i InverseLattice[L]) Meet(other InverseLattice[L]) InverseLattice[L] {
// meet in inverse is join in the original
return InverseLattice[L]{Inner: i.Inner.Join(other.Inner)}
}

func (i InverseLattice[L]) String() string {
return fmt.Sprintf("Inverse(%s)", i.Inner.String())
}

var _ Lattice[InverseLattice[ConfidentialityLabel]] = InverseLattice[ConfidentialityLabel]{}
Loading
Loading