Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
80 changes: 80 additions & 0 deletions guards/github-guard/rust-guard/src/labels/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4712,4 +4712,84 @@ mod tests {

assert_eq!(integrity, writer_integrity(repo_id, &ctx), "fork_repository should have writer integrity");
}

#[test]
fn test_apply_tool_labels_edit_repository_writer_integrity() {
let ctx = default_ctx();
let repo_id = "github/copilot";
let tool_args = json!({
"owner": "github",
"repo": "copilot",
});

let (_secrecy, integrity, _desc) = apply_tool_labels(
"edit_repository",
&tool_args,
repo_id,
vec![],
vec![],
String::new(),
&ctx,
);

assert_eq!(integrity, writer_integrity(repo_id, &ctx), "edit_repository should have writer integrity");
}

#[test]
fn test_apply_tool_labels_revert_pull_request_writer_integrity() {
let ctx = default_ctx();
let repo_id = "github/copilot";
let tool_args = json!({
"owner": "github",
"repo": "copilot",
"pullNumber": 42,
});

let (_secrecy, integrity, _desc) = apply_tool_labels(
"revert_pull_request",
&tool_args,
repo_id,
vec![],
vec![],
String::new(),
&ctx,
);

assert_eq!(integrity, writer_integrity(repo_id, &ctx), "revert_pull_request should have writer integrity");
}

#[test]
fn test_apply_tool_labels_deploy_key_operations_private_secrecy() {
let ctx = default_ctx();
let repo_id = "github/copilot";
let tool_args = json!({
"owner": "github",
"repo": "copilot",
});

for tool_name in &["add_deploy_key", "delete_deploy_key"] {
let (secrecy, integrity, _desc) = apply_tool_labels(
tool_name,
&tool_args,
repo_id,
vec![],
vec![],
String::new(),
&ctx,
);

assert_eq!(
secrecy,
super::helpers::policy_private_scope_label("github", "copilot", repo_id, &ctx),
"{} should have private-scoped secrecy",
tool_name
);
assert_eq!(
integrity,
writer_integrity(repo_id, &ctx),
"{} should have writer integrity",
tool_name
);
}
}
}
25 changes: 25 additions & 0 deletions guards/github-guard/rust-guard/src/labels/tool_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,31 @@ pub fn apply_tool_labels(
integrity = writer_integrity(repo_id, ctx);
}

// === Repository settings edit (can change visibility) ===
"edit_repository" => {
// Can change repo visibility, security settings, default branch.
// S = S(repo); I = writer (requires admin access)
secrecy = apply_repo_visibility_secrecy(&owner, &repo, repo_id, secrecy, ctx);
integrity = writer_integrity(repo_id, ctx);
}

// === PR revert (creates revert branch + PR) ===
"revert_pull_request" => {
// Creates a new branch + PR reverting a merged PR.
// S = S(repo); I = writer
secrecy = apply_repo_visibility_secrecy(&owner, &repo, repo_id, secrecy, ctx);
integrity = writer_integrity(repo_id, ctx);
}

// === Deploy key management (SSH key with optional write access) ===
"add_deploy_key" | "delete_deploy_key" => {
// Manages SSH deploy keys — `add_deploy_key` may grant persistent write access.
// S = private:owner/repo (deploy key secrets should be restricted)
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

The comment claims secrecy is private:owner/repo, but this arm uses policy_private_scope_label(...), which can return unscoped private (or owner-scoped) depending on the cached policy scope kind. Update the comment to reflect the actual behavior (i.e., always at least private, scope may vary).

Suggested change
// S = private:owner/repo (deploy key secrets should be restricted)
// S = at least private; scope is policy-dependent (may be unscoped, owner-scoped, or repo-scoped)

Copilot uses AI. Check for mistakes.
// I = writer (requires admin access)
secrecy = policy_private_scope_label(&owner, &repo, repo_id, ctx);
integrity = writer_integrity(repo_id, ctx);
}

// === Star/unstar operations (public metadata) ===
"star_repository" | "unstar_repository" => {
// Starring is a public action; response is minimal metadata.
Expand Down
23 changes: 23 additions & 0 deletions guards/github-guard/rust-guard/src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ pub const WRITE_OPERATIONS: &[&str] = &[
"rerun_workflow_run", // gh run rerun — reruns a completed workflow run
"rerun_failed_jobs", // gh run rerun --failed — reruns only failed jobs
"rerun_workflow_job", // gh run rerun --job — reruns a specific job
// Pre-emptive: gh repo edit (PATCH /repos/{owner}/{repo}) — can change visibility, security settings
"edit_repository",
// Pre-emptive: gh pr revert (GraphQL revertPullRequest) — creates revert branch + PR
"revert_pull_request",
// Pre-emptive: gh repo deploy-key add/delete — SSH key with optional write access
"add_deploy_key",
"delete_deploy_key",
];

/// Read-write operations that both read and modify data
Expand Down Expand Up @@ -210,6 +217,22 @@ mod tests {
}
}

#[test]
fn test_cli_gap_operations_are_write_operations() {
for op in &[
"edit_repository",
"revert_pull_request",
"add_deploy_key",
"delete_deploy_key",
] {
assert!(
is_write_operation(op),
"{} must be classified as a write operation",
op
);
}
}

#[test]
fn test_create_agent_task_is_read_write_and_blocked() {
assert!(
Expand Down
Loading