Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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"])
})
Comment thread
RossTarrant marked this conversation as resolved.

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()
Comment thread
RossTarrant marked this conversation as resolved.
}
return PrivateUntrusted()
return PublicUntrusted()
}

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