From 653801700eb0e52393f8ae36455815fc0ad613c3 Mon Sep 17 00:00:00 2001 From: Guille Date: Wed, 22 Apr 2026 09:54:42 +0200 Subject: [PATCH] feat: add fork action to github tool (#2139) feat: add fork_repo action to GitHub WASM tool Adds fork_repo action with full input validation, optional organization/name/default_branch_only params. CI failures are pre-existing (RUSTSEC-2026-0098 in rustls-webpki transitive dep, unrelated to this PR). --- tools-src/github/Cargo.toml | 2 +- tools-src/github/README.md | 16 ++++ .../github/github-tool.capabilities.json | 4 +- tools-src/github/src/lib.rs | 76 +++++++++++++++++++ 4 files changed, 95 insertions(+), 3 deletions(-) diff --git a/tools-src/github/Cargo.toml b/tools-src/github/Cargo.toml index 4c8ae6ecc9..8d0086d9bb 100644 --- a/tools-src/github/Cargo.toml +++ b/tools-src/github/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "github-tool" -version = "0.2.2" +version = "0.2.3" edition = "2021" description = "GitHub integration tool for IronClaw (WASM component)" license = "MIT OR Apache-2.0" diff --git a/tools-src/github/README.md b/tools-src/github/README.md index 2e8ca8a98d..988cc7521c 100644 --- a/tools-src/github/README.md +++ b/tools-src/github/README.md @@ -8,6 +8,7 @@ search, branches, file reads and writes, releases, and workflows. - **Repositories** - Get repo details, list user repos, create repositories - **Search** - Search repositories, code, and issues/PRs - **Branches** - List branches and create new branches from an existing ref +- **Fork** - Fork repositories - **Issues** - List/create/get issues, list/add issue comments - **Pull Requests** - List/create/get PRs, review files, create reviews, list/reply review comments, merge PRs - **File Content** - Read files and create/update/delete repository files @@ -286,6 +287,21 @@ When updating an existing file, include the current blob `sha`. } ``` +### Fork Repository + +```json +{ + "action": "fork_repo", + "owner": "nearai", + "repo": "ironclaw", + "organization": "my-org", + "name": "ironclaw-fork", + "default_branch_only": true +} +``` + +`organization`, `name`, and `default_branch_only` are optional. Omit `organization` to fork into the authenticated user's account. + ### Create Branch ```json diff --git a/tools-src/github/github-tool.capabilities.json b/tools-src/github/github-tool.capabilities.json index f9c0215670..036f7e9dad 100644 --- a/tools-src/github/github-tool.capabilities.json +++ b/tools-src/github/github-tool.capabilities.json @@ -1,5 +1,5 @@ { - "version": "0.2.2", + "version": "0.2.3", "wit_version": "0.3.0", "description": "Manage GitHub repositories, issues, pull requests, search, branches, file reads and writes, releases, and workflows.", "discovery_summary": { @@ -16,7 +16,7 @@ "notes": [ "Use `tool_info(name: \"github\", detail: \"schema\")` for the full action schema before guessing fields.", "Supported families: repositories, issues, pull requests, reviews/comments, search, branches, code reads, file writes, releases, workflow dispatch/runs, and webhook normalization.", - "Not supported yet: forks, labels, milestones, projects, org/team admin, GraphQL, release asset uploads, and repository deletion." + "Not supported yet: labels, milestones, projects, org/team admin, GraphQL, release asset uploads, and repository deletion." ], "examples": [ { diff --git a/tools-src/github/src/lib.rs b/tools-src/github/src/lib.rs index 63a2f73f83..15d6232f0b 100644 --- a/tools-src/github/src/lib.rs +++ b/tools-src/github/src/lib.rs @@ -430,6 +430,14 @@ enum GitHubAction { page: Option, limit: Option, }, + #[serde(rename = "fork_repo")] + ForkRepo { + owner: String, + repo: String, + organization: Option, + name: Option, + default_branch_only: Option, + }, #[serde(rename = "handle_webhook")] HandleWebhook { webhook: GitHubWebhookRequest }, } @@ -752,6 +760,19 @@ fn execute_inner(params: &str) -> Result { page, limit, } => get_workflow_runs(&owner, &repo, workflow_id.as_deref(), page, limit), + GitHubAction::ForkRepo { + owner, + repo, + organization, + name, + default_branch_only, + } => fork_repo( + &owner, + &repo, + organization.as_deref(), + name.as_deref(), + default_branch_only, + ), GitHubAction::HandleWebhook { webhook } => handle_webhook(webhook), } } @@ -926,6 +947,49 @@ fn create_repo( github_request("POST", &path, Some(req_body.to_string())) } +fn fork_repo( + owner: &str, + repo: &str, + organization: Option<&str>, + name: Option<&str>, + default_branch_only: Option, +) -> Result { + if !validate_path_segment(owner) || !validate_path_segment(repo) { + return Err("Invalid owner or repo name".into()); + } + validate_input_length(owner, "owner")?; + validate_input_length(repo, "repo")?; + if let Some(org) = organization { + validate_input_length(org, "organization")?; + if !validate_path_segment(org) { + return Err("Invalid org name".into()); + } + } + if let Some(n) = name { + validate_input_length(n, "name")?; + if !validate_path_segment(n) { + return Err("Invalid fork name".into()); + } + } + + let encoded_owner = url_encode_path(owner); + let encoded_repo = url_encode_path(repo); + let path = format!("/repos/{}/{}/forks", encoded_owner, encoded_repo); + + let mut req_body = serde_json::json!({}); + if let Some(org) = organization { + req_body["organization"] = serde_json::json!(org); + } + if let Some(n) = name { + req_body["name"] = serde_json::json!(n); + } + if let Some(only) = default_branch_only { + req_body["default_branch_only"] = serde_json::json!(only); + } + + github_request("POST", &path, Some(req_body.to_string())) +} + fn list_issues( owner: &str, repo: &str, @@ -2300,6 +2364,17 @@ const SCHEMA: &str = r#"{ }, "required": ["action", "owner", "repo"] }, + { + "properties": { + "action": { "const": "fork_repo" }, + "owner": { "type": "string", "description": "Repository owner (user or org) to fork from" }, + "repo": { "type": "string", "description": "Repository name to fork" }, + "organization": { "type": "string", "description": "Optional organization to fork into; omit to fork into the authenticated user's account" }, + "name": { "type": "string", "description": "Optional name for the fork; defaults to the original repo name" }, + "default_branch_only": { "type": "boolean", "default": false, "description": "When true, only the default branch is copied into the fork" } + }, + "required": ["action", "owner", "repo"] + }, { "properties": { "action": { "const": "handle_webhook" }, @@ -2377,6 +2452,7 @@ mod tests { "create_release", "trigger_workflow", "get_workflow_runs", + "fork_repo", "handle_webhook", ] .into_iter()