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: 46 additions & 4 deletions pkg/github/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,19 +296,61 @@ Options are:
return utils.NewToolResultErrorFromErr("failed to get GitHub graphql client", err), nil, nil
}

// attachIFC adds the IFC label to a successful tool result when
// InsidersMode is enabled. The visibility and (for private
// repositories) collaborators lookups are performed lazily on
// first use. If the visibility lookup fails the label is omitted
// rather than misclassifying the result; the failure is not
// cached so a subsequent dispatch branch could retry. If only
// the collaborators lookup fails for a private repo we fall back
// to the owner so the reader set is never empty. The label
// matches list_issues semantics: per-repo visibility, integrity
// always untrusted.
var (
ifcLabelKnown bool
ifcIsPrivate bool
ifcReaders []string
)
attachIFC := func(r *mcp.CallToolResult) *mcp.CallToolResult {
if r == nil || r.IsError || !deps.GetFlags(ctx).InsidersMode {
return r
}
if !ifcLabelKnown {
isPrivate, err := FetchRepoIsPrivate(ctx, client, owner, repo)
if err != nil {
return r
}
ifcIsPrivate = isPrivate
if ifcIsPrivate {
if collaborators, err := FetchRepoCollaborators(ctx, client, owner, repo); err == nil {
ifcReaders = collaborators
}
if len(ifcReaders) == 0 {
ifcReaders = []string{owner}
}
}
ifcLabelKnown = true
}
if r.Meta == nil {
r.Meta = mcp.Meta{}
}
r.Meta["ifc"] = ifc.LabelListIssues(ifcIsPrivate, ifcReaders)
return r
}

switch method {
case "get":
result, err := GetIssue(ctx, client, deps, owner, repo, issueNumber)
return result, nil, err
return attachIFC(result), nil, err
case "get_comments":
result, err := GetIssueComments(ctx, client, deps, owner, repo, issueNumber, pagination)
return result, nil, err
return attachIFC(result), nil, err
case "get_sub_issues":
result, err := GetSubIssues(ctx, client, deps, owner, repo, issueNumber, pagination)
return result, nil, err
return attachIFC(result), nil, err
case "get_labels":
result, err := GetIssueLabels(ctx, gqlClient, owner, repo, issueNumber)
return result, nil, err
return attachIFC(result), nil, err
default:
return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
}
Expand Down
121 changes: 121 additions & 0 deletions pkg/github/issues_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,127 @@ func Test_GetIssue(t *testing.T) {
}
}

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

serverTool := IssueRead(translations.NullTranslationHelper)

mockIssue := &github.Issue{
Number: github.Ptr(1),
Title: github.Ptr("Test"),
Body: github.Ptr("body"),
State: github.Ptr("open"),
HTMLURL: github.Ptr("https://github.com/octocat/repo/issues/1"),
User: &github.User{Login: github.Ptr("u")},
}

mockComments := []*github.IssueComment{
{Body: github.Ptr("hello"), User: &github.User{Login: github.Ptr("u")}},
}

makeMockClient := func(isPrivate bool, repoStatus int) *http.Client {
handlers := map[string]http.HandlerFunc{
GetReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockIssue),
GetReposIssuesCommentsByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockComments),
GetReposCollaboratorsByOwnerByRepo: mockResponse(t, http.StatusOK, []*github.User{
{Login: github.Ptr("octocat")},
{Login: github.Ptr("alice")},
}),
}
if repoStatus != 0 && repoStatus != http.StatusOK {
handlers[GetReposByOwnerByRepo] = mockResponse(t, repoStatus, "boom")
} else {
handlers[GetReposByOwnerByRepo] = mockResponse(t, http.StatusOK, map[string]any{
"name": "repo",
"private": isPrivate,
})
}
return MockHTTPClientWithHandlers(handlers)
}

getReq := map[string]any{
"method": "get",
"owner": "octocat",
"repo": "repo",
"issue_number": float64(1),
}
commentsReq := map[string]any{
"method": "get_comments",
"owner": "octocat",
"repo": "repo",
"issue_number": float64(1),
}

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

request := createMCPRequest(getReq)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
require.False(t, result.IsError)

assert.Nil(t, result.Meta)
})

t.Run("insiders mode enabled on public repo emits public untrusted", func(t *testing.T) {
deps := BaseDeps{
Client: github.NewClient(makeMockClient(false, 0)),
Flags: FeatureFlags{InsidersMode: true},
}
handler := serverTool.Handler(deps)

request := createMCPRequest(getReq)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
require.False(t, result.IsError)

require.NotNil(t, result.Meta)
ifcMap := unmarshalIFC(t, result.Meta["ifc"])
assert.Equal(t, "untrusted", ifcMap["integrity"])
assert.Equal(t, []any{"public"}, ifcMap["confidentiality"])
})

t.Run("insiders mode enabled on private repo with get_comments emits private untrusted", func(t *testing.T) {
deps := BaseDeps{
Client: github.NewClient(makeMockClient(true, 0)),
Flags: FeatureFlags{InsidersMode: true},
}
handler := serverTool.Handler(deps)

request := createMCPRequest(commentsReq)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
require.False(t, result.IsError)

require.NotNil(t, result.Meta)
ifcMap := unmarshalIFC(t, result.Meta["ifc"])
assert.Equal(t, "untrusted", ifcMap["integrity"])
assert.Equal(t, []any{"octocat", "alice"}, ifcMap["confidentiality"])
})

t.Run("insiders mode skips ifc label when visibility lookup fails", func(t *testing.T) {
deps := BaseDeps{
Client: github.NewClient(makeMockClient(false, http.StatusInternalServerError)),
Flags: FeatureFlags{InsidersMode: true},
}
handler := serverTool.Handler(deps)

request := createMCPRequest(getReq)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
require.False(t, result.IsError, "tool call should still succeed when visibility lookup fails")

if result.Meta != nil {
_, hasIFC := result.Meta["ifc"]
assert.False(t, hasIFC, "ifc label should be omitted when visibility lookup fails")
}
})
}

func Test_AddIssueComment(t *testing.T) {
// Verify tool definition once
serverTool := AddIssueComment(translations.NullTranslationHelper)
Expand Down
Loading