Skip to content

[gateway] list_commits on non-default branch: owner's commits get none integrity on personal repos #4250

@lpcox

Description

@lpcox

Problem

When a user calls list_commits specifying a non-default branch (e.g., dev), commits authored by the repo owner are labeled with none:all integrity instead of the expected approved or writer level. This causes the commits to be filtered out when the agent's min-integrity is set to approved (the public-repo default).

Example log entry for a blocked commit:

{
  "tool_name": "list_commits",
  "author_login": "ahmadabdalla",
  "integrity_tags": ["none:all"],
  "reason": "...lower integrity than agent requires...\"approved\""
}

The author is the repo owner on a public personal repository.

Source: github/gh-aw#27473

Analysis

Three compounding issues in guards/github-guard/rust-guard/src/labels/helpers.rs and guards/github-guard/rust-guard/src/labels/response_items.rs:

1. GitHub's list_commits API response does not include author_association

In response_items.rs (line ~15831), the list_commits/get_commit case calls commit_integrity(). Inside commit_integrity() in helpers.rs (~line 64719), the first step is author_association_floor(item, ...), which reads author_association / authorAssociation from the item. For commit items returned by list_commits, the GitHub REST API does not include this field. The function returns an empty vec (i.e., none integrity).

2. elevate_via_collaborator_permission skips personal (non-org) repos

After author_association_floor returns none, commit_integrity calls elevate_via_collaborator_permission() as a fallback for public repos. However, inside that function:

let is_org = super::backend::is_repo_org_owned(owner, repo).unwrap_or(false);
if !is_org {
    return integrity;  // early return — no elevation for personal repos
}

This guard was added to handle org owners whose author_association incorrectly shows NONE. But because list_commits never provides author_association at all, personal repo owners are equally affected and receive no elevation.

3. is_default_branch_ref hardcodes only main/master/HEAD

In is_default_branch_ref():

pub fn is_default_branch_ref(branch_ref: &str) -> bool {
    branch_ref.is_empty()
        || branch_ref.eq_ignore_ascii_case("main")
        || branch_ref.eq_ignore_ascii_case("master")
        || branch_ref.eq_ignore_ascii_case("HEAD")
}

Repos whose actual default branch is named something other than main/master (e.g., trunk, develop) would face the same issue even without specifying a non-default branch. Additionally, list_commits + any SHA is treated as non-default-branch context (see is_default_branch_commit_context).

Net effect: On a public personal repo, list_commits on any non-default branch always produces none integrity for every commit, regardless of authorship.

Relevant files:

  • guards/github-guard/rust-guard/src/labels/helpers.rscommit_integrity, elevate_via_collaborator_permission, is_default_branch_ref
  • guards/github-guard/rust-guard/src/labels/response_items.rslist_commits / get_commit labeling case (~line 15831)
  • guards/github-guard/rust-guard/src/labels/mod.rs — test test_default_branch_commit_context_helper

Proposed Solution

Primary fix — lift the is_org guard in elevate_via_collaborator_permission

The collaborator permission endpoint (GET /repos/{owner}/{repo}/collaborators/{username}/permission) works for both personal and org repos. Removing (or relaxing) the is_org early-return allows the function to correctly elevate personal repo owners to writer_integrity:

// Before:
let is_org = super::backend::is_repo_org_owned(owner, repo).unwrap_or(false);
if !is_org {
    return integrity;
}

// After: remove the is_org guard entirely, or extend it to non-org repos
// (The original motivation was avoiding extra API calls for personal repos,
// but commits never carry author_association so the fallback is equally needed.)

Secondary fix — owner-login shortcut (avoids extra API call)

As a lightweight alternative (no extra API call), add a check in commit_integrity: if author_login matches the repo owner (the first path segment of repo_full_name) and the repo is public, elevate to at least writer_integrity without needing the collaborator permission API call:

// In commit_integrity(), before calling elevate_via_collaborator_permission:
if !repo_private && !author_login.is_empty() {
    let owner = repo_full_name.split('/').next().unwrap_or("");
    if author_login.eq_ignore_ascii_case(owner) {
        integrity = max_integrity(repo_full_name, integrity, writer_integrity(repo_full_name, ctx), ctx);
    }
}

Tertiary fix — query actual repo default branch

For the is_default_branch_ref issue, response_items.rs already fetches the repo visibility via backend::is_repo_private. A similar backend call (backend::get_repo_default_branch) could return the actual default branch name, allowing accurate comparison instead of relying on the hardcoded main/master/HEAD list. This is a lower-priority improvement but would help repos using non-standard default branch names.

Documentation fix

The integrity docs should clarify that commits on non-default branches of public personal repos currently receive none integrity regardless of authorship, and that min-integrity: none is the current workaround until this is fixed.

Testing

  1. Configure a workflow with list_commits on a public personal repo's feature branch, as the repo owner.
  2. Verify that commits authored by the owner get at least approved (writer) integrity in the integrity_tags log field.
  3. Run the existing Rust guard unit tests: cd guards/github-guard/rust-guard && cargo test
  4. Add a new test case in mod.rs covering list_commits on a non-default branch for a personal repo where the author is the repo owner, asserting writer_integrity output from commit_integrity.

Generated by Gateway Issue Dispatcher · ● 1.3M ·

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions