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
6 changes: 3 additions & 3 deletions pkg/github/discussions.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ func ListDiscussions(t translations.TranslationHelperFunc) inventory.ServerTool
result := utils.NewToolResultText(string(out))
// Discussion content is user-authored (untrusted); confidentiality
// follows repo visibility.
result = attachRepoVisibilityIFCLabelLazy(ctx, deps, owner, repo, result, ifc.LabelListIssues)
result = attachRepoVisibilityIFCLabelLazy(ctx, deps, owner, repo, result, ifc.LabelRepoUserContent)
return result, nil, nil
},
)
Expand Down Expand Up @@ -384,7 +384,7 @@ func GetDiscussion(t translations.TranslationHelperFunc) inventory.ServerTool {
result := utils.NewToolResultText(string(out))
// Discussion content is user-authored (untrusted); confidentiality
// follows repo visibility.
result = attachRepoVisibilityIFCLabelLazy(ctx, deps, params.Owner, params.Repo, result, ifc.LabelListIssues)
result = attachRepoVisibilityIFCLabelLazy(ctx, deps, params.Owner, params.Repo, result, ifc.LabelRepoUserContent)
return result, nil, nil
},
)
Expand Down Expand Up @@ -592,7 +592,7 @@ func GetDiscussionComments(t translations.TranslationHelperFunc) inventory.Serve
result := utils.NewToolResultText(string(out))
// Discussion comments are user-authored (untrusted); confidentiality
// follows repo visibility.
result = attachRepoVisibilityIFCLabelLazy(ctx, deps, params.Owner, params.Repo, result, ifc.LabelListIssues)
result = attachRepoVisibilityIFCLabelLazy(ctx, deps, params.Owner, params.Repo, result, ifc.LabelRepoUserContent)
return result, nil, nil
},
)
Expand Down
12 changes: 2 additions & 10 deletions pkg/github/gists.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,7 @@ func ListGists(t translations.TranslationHelperFunc) inventory.ServerTool {
}

result := utils.NewToolResultText(string(r))
// Gist contents are user-authored (untrusted); confidentiality is
// the IFC join of each gist's own public/secret flag.
visibilities := make([]bool, 0, len(gists))
for _, g := range gists {
visibilities = append(visibilities, g.GetPublic())
}
result = attachJoinedIFCLabel(ctx, deps, result, visibilities, ifc.LabelGistList)
result = attachStaticIFCLabel(ctx, deps, result, ifc.LabelGistList())
return result, nil, nil
},
)
Expand Down Expand Up @@ -167,9 +161,7 @@ func GetGist(t translations.TranslationHelperFunc) inventory.ServerTool {
}

result := utils.NewToolResultText(string(r))
// Gist contents are user-authored (untrusted); confidentiality
// derives from the gist's own public/secret flag.
result = attachStaticIFCLabel(ctx, deps, result, ifc.LabelGist(gist.GetPublic()))
result = attachStaticIFCLabel(ctx, deps, result, ifc.LabelGist())
return result, nil, nil
},
)
Expand Down
11 changes: 5 additions & 6 deletions pkg/github/ifc_labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,11 @@ func attachRepoVisibilityIFCLabelLazy(
}

// attachJoinedIFCLabel attaches an IFC label computed by joining a set of
// per-item visibilities (true == private for repositories, true == public for
// gists) when IFC labels are enabled. joinFn is the lattice join for the
// relevant item kind (e.g. ifc.LabelSearchIssues or ifc.LabelGistList). The
// visibility slice is cheap to build from an already-fetched response, so
// callers may construct it unconditionally and let this helper own the
// feature-flag gate.
// per-item visibilities (true == private) when IFC labels are enabled. joinFn
// is the lattice join for the relevant item kind (e.g. ifc.LabelSearchIssues or
// ifc.LabelGistList). The visibility slice is cheap to build from an
// already-fetched response, so callers may construct it unconditionally and let
// this helper own the feature-flag gate.
func attachJoinedIFCLabel(
ctx context.Context,
deps ToolDependencies,
Expand Down
2 changes: 1 addition & 1 deletion pkg/github/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -804,7 +804,7 @@ Options are:
// attachIFC adds the IFC label to a successful tool result when
// IFC labels are enabled. If the visibility lookup fails the
// label is omitted rather than misclassifying the result.
attachIFC := newRepoVisibilityIFCLabeler(ctx, deps, client, owner, repo, ifc.LabelListIssues)
attachIFC := newRepoVisibilityIFCLabeler(ctx, deps, client, owner, repo, ifc.LabelRepoUserContent)

switch method {
case "get":
Expand Down
8 changes: 4 additions & 4 deletions pkg/github/issues_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ func Test_IssueRead_IFC_InsidersMode(t *testing.T) {
assert.Equal(t, "public", ifcMap["confidentiality"])
})

t.Run("insiders mode enabled on private repo with get_comments emits private untrusted", func(t *testing.T) {
t.Run("insiders mode enabled on private repo with get_comments emits private trusted", func(t *testing.T) {
deps := BaseDeps{
Client: mustNewGHClient(t, makeMockClient(true, 0)),
featureChecker: featureCheckerFor(FeatureFlagIFCLabels),
Expand All @@ -370,7 +370,7 @@ func Test_IssueRead_IFC_InsidersMode(t *testing.T) {

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

Expand Down Expand Up @@ -2852,7 +2852,7 @@ func Test_ListIssues_IFC_InsidersMode(t *testing.T) {
assert.Equal(t, "public", ifcMap["confidentiality"])
})

t.Run("insiders mode enabled on private repo emits private untrusted label", func(t *testing.T) {
t.Run("insiders mode enabled on private repo emits private trusted label", func(t *testing.T) {
matcher := githubv4mock.NewQueryMatcher(query, vars, makeResponse(true))
gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(matcher))
deps := BaseDeps{
Expand All @@ -2875,7 +2875,7 @@ func Test_ListIssues_IFC_InsidersMode(t *testing.T) {
var ifcMap map[string]any
require.NoError(t, json.Unmarshal(ifcJSON, &ifcMap))

assert.Equal(t, "untrusted", ifcMap["integrity"])
assert.Equal(t, "trusted", ifcMap["integrity"])
assert.Equal(t, "private", ifcMap["confidentiality"])
Comment thread
RossTarrant marked this conversation as resolved.
})
}
Expand Down
8 changes: 4 additions & 4 deletions pkg/github/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,9 @@ Use this tool to list projects for a user or organization, or list project field
// labels are enabled. Project titles, item content, field
// definitions, and status updates are user-authored free text
// (untrusted); confidentiality is conservatively private since the
// project's public flag is not available across every sub-result.
// project's privacy is not available across every sub-result.
attachIFC := func(r *mcp.CallToolResult) *mcp.CallToolResult {
return attachStaticIFCLabel(ctx, deps, r, ifc.LabelProject(false))
return attachStaticIFCLabel(ctx, deps, r, ifc.LabelProject(true))
}

switch method {
Expand Down Expand Up @@ -349,9 +349,9 @@ Use this tool to get details about individual projects, project fields, and proj
// attachIFC adds the IFC label to a successful result when IFC
// labels are enabled. Project data is user-authored free text
// (untrusted); confidentiality is conservatively private since the
// project's public flag is not available across every sub-result.
// project's privacy is not available across every sub-result.
attachIFC := func(r *mcp.CallToolResult) *mcp.CallToolResult {
return attachStaticIFCLabel(ctx, deps, r, ifc.LabelProject(false))
return attachStaticIFCLabel(ctx, deps, r, ifc.LabelProject(true))
}

// Handle get_project_status_update early — it only needs status_update_id
Expand Down
4 changes: 2 additions & 2 deletions pkg/github/pullrequests.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ Possible options:
// visibility lookup fails the label is omitted rather than
// misclassifying the result.
attachIFC := func(r *mcp.CallToolResult) *mcp.CallToolResult {
return attachRepoVisibilityIFCLabel(ctx, deps, client, owner, repo, r, ifc.LabelListIssues)
return attachRepoVisibilityIFCLabel(ctx, deps, client, owner, repo, r, ifc.LabelRepoUserContent)
}

switch method {
Expand Down Expand Up @@ -1339,7 +1339,7 @@ func ListPullRequests(t translations.TranslationHelperFunc) inventory.ServerTool
result := utils.NewToolResultText(string(r))
// Pull request titles/bodies are user-authored (untrusted);
// confidentiality follows repo visibility.
result = attachRepoVisibilityIFCLabel(ctx, deps, client, owner, repo, result, ifc.LabelListIssues)
result = attachRepoVisibilityIFCLabel(ctx, deps, client, owner, repo, result, ifc.LabelRepoUserContent)
return result, nil, nil
})
}
Expand Down
7 changes: 4 additions & 3 deletions pkg/github/repositories.go
Original file line number Diff line number Diff line change
Expand Up @@ -2121,9 +2121,10 @@ func ListStarredRepositories(t translations.TranslationHelperFunc) inventory.Ser
result := utils.NewToolResultText(string(r))
// A starred-repository listing exposes repository data across many
// repos; reuse the multi-repo join shared with search_repositories
// (untrusted integrity; confidentiality private if any matched repo
// is private). Visibility is read directly from the response, so no
// extra API call is needed.
// (public-only results stay public-untrusted, mixed-visibility
// results become private-untrusted, all-private results become
// private-trusted). Visibility is read directly from the response,
// so no extra API call is needed.
visibilities := make([]bool, 0, len(minimalRepos))
for _, mr := range minimalRepos {
visibilities = append(visibilities, mr.Private)
Expand Down
17 changes: 9 additions & 8 deletions pkg/github/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,9 @@ func SearchRepositories(t translations.TranslationHelperFunc) inventory.ServerTo
// every matched repository and attaches the result to callResult when IFC
// labels are enabled. Visibility is read directly from the search response —
// no extra API call. The join math is shared with search_issues via
// ifc.LabelSearchIssues: integrity is always untrusted; confidentiality is
// private if any matched repository is private, otherwise public. The
// ifc.LabelSearchIssues: public-only results stay public-untrusted,
// mixed-visibility results become private-untrusted, and all-private results
// become private-trusted. The
// feature-flag check is centralized here (mirroring the attach* helpers in
// ifc_labels.go) so the handler can call this unconditionally.
func attachSearchRepositoriesIFCLabel(ctx context.Context, deps ToolDependencies, repos []*github.Repository, callResult *mcp.CallToolResult) {
Expand Down Expand Up @@ -302,9 +303,9 @@ func SearchCode(t translations.TranslationHelperFunc) inventory.ServerTool {
}

callResult := utils.NewToolResultText(string(r))
// Code search spans repositories and exposes file contents
// (untrusted). Confidentiality is the IFC join across every matched
// repository's visibility, read directly from the search response.
// Code search spans repositories; the IFC label is the conservative
// join across every matched repository's visibility, read directly
// from the search response.
visibilities := make([]bool, 0, len(result.CodeResults))
for _, code := range result.CodeResults {
if code.Repository != nil {
Expand Down Expand Up @@ -593,9 +594,9 @@ func SearchCommits(t translations.TranslationHelperFunc) inventory.ServerTool {
}

callResult := utils.NewToolResultText(string(r))
// Commit search spans repositories and exposes commit content
// (untrusted). Confidentiality is the IFC join across every matched
// repository's visibility, read directly from the search response.
// Commit search spans repositories; the IFC label is the conservative
// join across every matched repository's visibility, read directly
// from the search response.
visibilities := make([]bool, 0, len(result.Commits))
for _, commit := range result.Commits {
if commit.Repository != nil {
Expand Down
2 changes: 1 addition & 1 deletion pkg/github/search_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ func Test_SearchRepositories_IFC_InsidersMode(t *testing.T) {
assert.Equal(t, "public", ifcMap["confidentiality"])
})

t.Run("insiders mode any private match emits private untrusted", func(t *testing.T) {
t.Run("insiders mode mixed public and private emits private untrusted", func(t *testing.T) {
deps := BaseDeps{
Client: mustNewGHClient(t, makeMockClient([]repoFixture{
{owner: "octocat", name: "private-repo", isPrivate: true},
Expand Down
79 changes: 46 additions & 33 deletions pkg/ifc/ifc.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,23 @@ func LabelGetMe() SecurityLabel {
// LabelListIssues returns the IFC label for a list_issues result.
// Public repositories are universally readable; private repositories are
// restricted to their collaborators (resolved client-side from the marker).
// Issue contents are attacker-controllable, so integrity is always untrusted.
// Public repository issue contents are attacker-controllable, while private
// repository issues are treated as trusted collaborator-authored data.
func LabelListIssues(isPrivate bool) SecurityLabel {
if isPrivate {
return PrivateUntrusted()
return PrivateTrusted()
Comment thread
RossTarrant marked this conversation as resolved.
}
return PublicUntrusted()
}

// LabelRepoUserContent returns the IFC label for user-authored content scoped
// to a repository when that tool has not opted into a more specific integrity
// policy. Public repository content is untrusted because it may be authored by
// outside contributors. Private repository content is trusted because users who
// can read it are trusted collaborators.
func LabelRepoUserContent(isPrivate bool) SecurityLabel {
if isPrivate {
return PrivateTrusted()
}
return PublicUntrusted()
}
Expand All @@ -99,11 +112,12 @@ func LabelGetFileContents(isPrivate bool) SecurityLabel {
// result, joining per-repository labels across all matched repositories.
// Used by both search_issues and search_repositories.
//
// Integrity is always untrusted because results expose user-authored content.
//
// Confidentiality follows the IFC meet (greatest lower bound): if any matched
// repository is private the joined label is private; otherwise public. The
// reader set is opaque (the "private" marker); the client engine resolves
// Public-only results are untrusted and public. All-private results are trusted
// and private because private repository content is treated as trusted
// collaborator-authored data. Mixed public/private results are untrusted and
// private: the public items keep the joined payload's integrity untrusted,
// while the private items keep the joined payload's confidentiality private.
// The reader set is opaque (the "private" marker); the client engine resolves
// concrete readers on demand at egress decision time.
//
// An empty result set is treated as public-untrusted (no repository data is
Expand All @@ -119,12 +133,22 @@ func LabelGetFileContents(isPrivate bool) SecurityLabel {
// until then they would invite unsafe declassification of a "public" item that
// actually arrived alongside private data.
func LabelSearchIssues(repoVisibilities []bool) SecurityLabel {
var anyPrivate, anyPublic bool
for _, isPrivate := range repoVisibilities {
if isPrivate {
return PrivateUntrusted()
anyPrivate = true
} else {
anyPublic = true
}
Comment thread
RossTarrant marked this conversation as resolved.
}
return PublicUntrusted()
switch {
case anyPrivate && anyPublic:
return PrivateUntrusted()
case anyPrivate:
return PrivateTrusted()
default:
return PublicUntrusted()
}
}

// LabelRepoMetadata returns the IFC label for structural repository metadata
Expand Down Expand Up @@ -261,32 +285,21 @@ func LabelRepositorySecurityAdvisory(isPrivate bool, allPublished bool) Security
// LabelGist returns the IFC label for gist content.
//
// Integrity is untrusted: gist contents are arbitrary user-authored text.
// Confidentiality derives from the gist's own visibility rather than any
// repository — public gists are universally readable, while secret gists are
// restricted to those who hold the gist URL (modeled with the opaque "private"
// marker).
func LabelGist(isPublic bool) SecurityLabel {
if isPublic {
return PublicUntrusted()
}
return PrivateUntrusted()
// Confidentiality is public because secret gists are URL-accessible and cannot
// be modeled as private to a GitHub reader set.
func LabelGist() SecurityLabel {
return PublicUntrusted()
}

// LabelGistList returns the IFC label for a list of gists belonging to a user,
// joining the per-gist confidentiality across the result set.
//
// Integrity is untrusted (user-authored content). Confidentiality follows the
// IFC meet: if any gist in the result is secret the joined label is private;
// otherwise public. An empty result is treated as public-untrusted.
// Integrity is untrusted (user-authored content). Confidentiality is public
// because even secret gists are URL-accessible.
//
// See LabelSearchIssues for why list results carry a single joined label
// rather than one label per item.
func LabelGistList(gistVisibilities []bool) SecurityLabel {
for _, isPublic := range gistVisibilities {
if !isPublic {
return PrivateUntrusted()
}
}
func LabelGistList() SecurityLabel {
return PublicUntrusted()
}

Expand All @@ -295,13 +308,13 @@ func LabelGistList(gistVisibilities []bool) SecurityLabel {
//
// Integrity is untrusted: project titles, item content, and status update
// bodies are user-authored free text. Confidentiality derives from the
// project's own public flag — public projects are universally readable, while
// private projects restrict the reader set.
func LabelProject(isPublic bool) SecurityLabel {
if isPublic {
return PublicUntrusted()
// project's own privacy — private projects restrict the reader set, while
// public projects are universally readable.
func LabelProject(isPrivate bool) SecurityLabel {
if isPrivate {
return PrivateUntrusted()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
return PrivateUntrusted()
return PrivateTrusted()

}
return PrivateUntrusted()
return PublicUntrusted()
}

// LabelTeam returns the IFC label for organization team membership data
Expand Down
Loading
Loading