Skip to content
Merged
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
23 changes: 23 additions & 0 deletions guards/github-guard/rust-guard/src/labels/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4640,6 +4640,29 @@ mod tests {
assert_eq!(integrity, project_github_label(&ctx), "star_repository should have project:github integrity");
}

#[test]
fn test_apply_tool_labels_enable_toolset_public_secrecy_writer_integrity() {
let ctx = default_ctx();
let tool_args = json!({ "toolset": "advanced" });

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

assert!(secrecy.is_empty(), "enable_toolset should have empty (public) secrecy");
assert_eq!(
integrity,
writer_integrity("github", &ctx),
"enable_toolset should have writer-level integrity on github scope"
);
}

#[test]
fn test_apply_tool_labels_assign_copilot_to_issue_writer_integrity() {
let ctx = default_ctx();
Expand Down
45 changes: 43 additions & 2 deletions guards/github-guard/rust-guard/src/labels/tool_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,9 @@ pub fn apply_tool_labels(
}

// === Projects write operations (org-scoped) ===
"projects_write" => {
"projects_write"
// Deprecated aliases that map to projects_write
| "add_project_item" | "update_project_item" | "delete_project_item" => {
// Projects are org-scoped; write responses carry the same labels as reads.
// I = approved:<owner>
if !owner.is_empty() {
Expand All @@ -609,7 +611,9 @@ pub fn apply_tool_labels(
}

// === Actions: Workflow run triggers ===
"actions_run_trigger" => {
"actions_run_trigger"
// Deprecated aliases that map to actions_run_trigger
| "run_workflow" | "delete_workflow_run_logs" => {
// Triggering a workflow run returns repo-scoped metadata.
// S = S(repo); I = writer
secrecy = apply_repo_visibility_secrecy(&owner, &repo, repo_id, secrecy, ctx);
Expand Down Expand Up @@ -670,6 +674,16 @@ pub fn apply_tool_labels(
integrity = writer_integrity(repo_id, ctx);
}

// === Dynamic toolset enablement (capability expansion) ===
"enable_toolset" => {
// Enabling a toolset expands the agent's runtime capability set.
// Requires writer-level integrity to prevent low-trust agents from
// self-escalating by enabling additional tool groups.
// S = public (empty — no repository-scoped data); I = writer (global)
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

enable_toolset sets integrity to writer_integrity("github"), but label_resource later applies ensure_integrity_baseline(infer_scope_for_baseline(...)). For tools with empty repo_id (like enable_toolset), infer_scope_for_baseline currently returns an empty scope, which can downgrade scoped labels like approved:github to unscoped none under scoped allowlists (no Public/All token). Please ensure the final baseline scope matches "github" for this tool (e.g., teach infer_scope_for_baseline about enable_toolset, or switch this rule to use unscoped integrity if that’s the intended policy).

Suggested change
// S = public (empty — no repository-scoped data); I = writer (global)
// S = public (empty — no repository-scoped data); I = writer (global)
// Keep `repo_id` aligned with the intended global scope so later
// baseline inference does not treat this as unscoped and weaken
// `approved:github` / writer-level labels.
repo_id = "github";

Copilot uses AI. Check for mistakes.
baseline_scope = "github".to_string();
integrity = writer_integrity("github", ctx);
}

// === Star/unstar operations (public metadata) ===
"star_repository" | "unstar_repository" => {
// Starring is a public action; response is minimal metadata.
Expand All @@ -679,6 +693,33 @@ pub fn apply_tool_labels(
integrity = project_github_label(ctx);
}

// === Issue/PR comment editing/deletion (pre-emptive) ===
"update_issue_comment" | "delete_issue_comment" => {
// Editing or deleting an issue/PR comment is a repo-scoped write.
// S = S(repo); I = writer
secrecy = apply_repo_visibility_secrecy(&owner, &repo, repo_id, secrecy, ctx);
integrity = writer_integrity(repo_id, ctx);
}

// === Release management (pre-emptive) ===
"create_release" | "edit_release" | "delete_release" => {
// Release operations are repo-scoped writes.
// S = S(repo); I = writer
secrecy = apply_repo_visibility_secrecy(&owner, &repo, repo_id, secrecy, ctx);
integrity = writer_integrity(repo_id, ctx);
}

// === Gist deletion (pre-emptive) ===
"delete_gist" => {
// Gist deletion is a write on user-scoped content.
// Conservatively treat gists as private/user-scoped, consistent with
// other gist operations that may target secret gists.
// S = private_user; I = writer(user)
secrecy = private_user_label();
baseline_scope = "user".to_string();
integrity = writer_integrity("user", ctx);
}
Comment on lines +696 to +721
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

New apply_tool_labels match arms were added for update_issue_comment/delete_issue_comment, release management, and delete_gist, but there are no corresponding unit tests in labels/mod.rs validating the expected secrecy/integrity outputs. Given these are write operations that impact DIFC enforcement, please add tests asserting S(repo); I=writer for repo-scoped tools and S=private:user; I=writer(user) for delete_gist.

Copilot uses AI. Check for mistakes.

_ => {
// Default: inherit provided labels
}
Expand Down
58 changes: 58 additions & 0 deletions guards/github-guard/rust-guard/src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,20 @@ pub const WRITE_OPERATIONS: &[&str] = &[
// Pre-emptive: gh repo deploy-key add/delete — SSH key with optional write access
"add_deploy_key",
"delete_deploy_key",
// Deprecated alias coverage (guard sees alias name before backend resolves it)
"run_workflow", // deprecated alias for actions_run_trigger (POST workflow dispatch)
"delete_workflow_run_logs", // deprecated alias for actions_run_trigger (DELETE run logs)
"add_project_item", // deprecated alias for projects_write (addProjectV2ItemById)
"delete_project_item", // deprecated alias for projects_write (deleteProjectV2Item)
// Pre-emptive: issue/PR comment editing/deletion (gh issue/pr comment --edit/--delete)
"update_issue_comment", // PATCH /repos/.../issues/comments/{id}
"delete_issue_comment", // DELETE /repos/.../issues/comments/{id}
// Pre-emptive: release management (gh release create/edit/delete)
"create_release", // POST /repos/.../releases
"edit_release", // PATCH /repos/.../releases/{id}
"delete_release", // DELETE /repos/.../releases/{id}
// Pre-emptive: gist deletion (gh gist delete)
"delete_gist", // DELETE /gists/{gist_id}
];

/// Read-write operations that both read and modify data
Expand All @@ -73,6 +87,8 @@ pub const READ_WRITE_OPERATIONS: &[&str] = &[
// Pre-emptive entries for anticipated future MCP tools (no equivalent tool today)
// gh agent-task create — creates a Copilot coding-agent job (branch + PR); blocked as unsupported
"create_agent_task",
// Deprecated alias coverage
"update_project_item", // deprecated alias for projects_write (updateProjectV2ItemFieldValue)
];

/// Check if a tool is a write operation
Expand Down Expand Up @@ -249,4 +265,46 @@ mod tests {
"create_agent_task should not be in WRITE_OPERATIONS (it is in READ_WRITE_OPERATIONS)"
);
}

#[test]
fn test_deprecated_alias_write_operations() {
for op in &[
"run_workflow",
"delete_workflow_run_logs",
"add_project_item",
"delete_project_item",
] {
assert!(
is_write_operation(op),
"{} (deprecated alias) must be classified as a write operation",
op
);
}
}

#[test]
fn test_deprecated_alias_read_write_operations() {
assert!(
is_read_write_operation("update_project_item"),
"update_project_item (deprecated alias) must be classified as a read-write operation"
);
}

#[test]
fn test_preemptive_cli_write_operations() {
for op in &[
"update_issue_comment",
"delete_issue_comment",
"create_release",
"edit_release",
"delete_release",
"delete_gist",
] {
assert!(
is_write_operation(op),
"{} (pre-emptive CLI) must be classified as a write operation",
op
);
}
}
}
Loading