From 7b1fbbe2beac5be59af78818e053e7bdb6f42e8e Mon Sep 17 00:00:00 2001 From: TIANYOU CHEN <42710806+CTY-git@users.noreply.github.com> Date: Wed, 16 Apr 2025 12:21:47 +0800 Subject: [PATCH 01/10] add git tool --- patchwork/common/tools/git_tool.py | 44 ++++++++++++++++++++++ patchwork/common/tools/github_tool.py | 2 +- patchwork/steps/GitHubAgent/GitHubAgent.py | 6 ++- 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 patchwork/common/tools/git_tool.py diff --git a/patchwork/common/tools/git_tool.py b/patchwork/common/tools/git_tool.py new file mode 100644 index 000000000..4b32765aa --- /dev/null +++ b/patchwork/common/tools/git_tool.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +import os +import subprocess + +from patchwork.common.tools.tool import Tool + + +class GitTool(Tool, tool_name="git_tool", abc_register=False): + def __init__(self, path: str): + super().__init__() + self.path = path + + @property + def json_schema(self) -> dict: + return { + "name": "git_tool", + "description": """\ +Access to the Git CLI, the command is also `git` all args provided are used as is +""", + "input_schema": { + "type": "object", + "properties": { + "args": { + "type": "array", + "items": {"type": "string"}, + "description": "The args to run `git` command with.", + } + }, + "required": ["args"], + }, + } + + def execute(self, args: list[str]) -> str: + env = os.environ.copy() + p = subprocess.run( + ["gh", *args], + env=env, + cwd=self.path, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + return p.stdout diff --git a/patchwork/common/tools/github_tool.py b/patchwork/common/tools/github_tool.py index aa5d5effe..16e418c08 100644 --- a/patchwork/common/tools/github_tool.py +++ b/patchwork/common/tools/github_tool.py @@ -6,7 +6,7 @@ from patchwork.common.tools.tool import Tool -class GitHubTool(Tool, tool_name="github_tool"): +class GitHubTool(Tool, tool_name="github_tool", abc_register=False): def __init__(self, path: str, gh_token: str): super().__init__() self.path = path diff --git a/patchwork/steps/GitHubAgent/GitHubAgent.py b/patchwork/steps/GitHubAgent/GitHubAgent.py index bc8d319c1..0ac014538 100644 --- a/patchwork/steps/GitHubAgent/GitHubAgent.py +++ b/patchwork/steps/GitHubAgent/GitHubAgent.py @@ -5,6 +5,7 @@ AgentConfig, AgenticStrategyV2, ) +from patchwork.common.tools.git_tool import GitTool from patchwork.common.tools.github_tool import GitHubTool from patchwork.common.utils.utils import mustache_render from patchwork.step import Step @@ -34,7 +35,10 @@ def __init__(self, inputs): AgentConfig( name="Assistant", model="gemini-2.0-flash", - tool_set=dict(github_tool=GitHubTool(base_path, inputs["github_api_key"])), + tool_set=dict( + github_tool=GitHubTool(base_path, inputs["github_api_key"]), + git_tool=GitTool(base_path), + ), system_prompt="""\ You are a senior software developer helping the program manager to obtain some data from GitHub. You can access github through the `gh` CLI app. From 5bafb638391958f89f114d7d0a7973200d30a9d4 Mon Sep 17 00:00:00 2001 From: TIANYOU CHEN <42710806+CTY-git@users.noreply.github.com> Date: Wed, 16 Apr 2025 12:25:11 +0800 Subject: [PATCH 02/10] bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ab89dee54..d136584c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "patchwork-cli" -version = "0.0.123" +version = "0.0.124" description = "" authors = ["patched.codes"] license = "AGPL" From c953072cb18039a0d17b007a95f14aafc65414e4 Mon Sep 17 00:00:00 2001 From: TIANYOU CHEN <42710806+CTY-git@users.noreply.github.com> Date: Wed, 16 Apr 2025 13:33:43 +0800 Subject: [PATCH 03/10] fixes --- patchwork/common/tools/git_tool.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/patchwork/common/tools/git_tool.py b/patchwork/common/tools/git_tool.py index 4b32765aa..d5acbfa05 100644 --- a/patchwork/common/tools/git_tool.py +++ b/patchwork/common/tools/git_tool.py @@ -16,7 +16,7 @@ def json_schema(self) -> dict: return { "name": "git_tool", "description": """\ -Access to the Git CLI, the command is also `git` all args provided are used as is +Access to the Git CLI, the command is also `git` all args provided are used as is. """, "input_schema": { "type": "object", @@ -24,7 +24,12 @@ def json_schema(self) -> dict: "args": { "type": "array", "items": {"type": "string"}, - "description": "The args to run `git` command with.", + "description": """ +The args to run `git` command with. +E.g. +[\"commit\", \"-m\", \"A commit message\"] to commit changes with a commit message. +[\"add\", \".\"] to stage all changed files. +""", } }, "required": ["args"], @@ -34,7 +39,7 @@ def json_schema(self) -> dict: def execute(self, args: list[str]) -> str: env = os.environ.copy() p = subprocess.run( - ["gh", *args], + ["git", *args], env=env, cwd=self.path, text=True, From 74dd5681a14b492b4ca2cb65acf4dd6d3ad48db4 Mon Sep 17 00:00:00 2001 From: Arpit Roopchandani <17565234+whoisarpit@users.noreply.github.com> Date: Wed, 16 Apr 2025 13:54:33 +0800 Subject: [PATCH 04/10] Add additional system prompt for better usage --- patchwork/steps/GitHubAgent/GitHubAgent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patchwork/steps/GitHubAgent/GitHubAgent.py b/patchwork/steps/GitHubAgent/GitHubAgent.py index 0ac014538..ede41c109 100644 --- a/patchwork/steps/GitHubAgent/GitHubAgent.py +++ b/patchwork/steps/GitHubAgent/GitHubAgent.py @@ -41,7 +41,7 @@ def __init__(self, inputs): ), system_prompt="""\ You are a senior software developer helping the program manager to obtain some data from GitHub. -You can access github through the `gh` CLI app. +You can access github through the `gh` CLI app through the `github_tool`, and `git` through the `git_tool`. Your `gh` app has already been authenticated. """, ) From aa08a5a1e5bbf2a147a4b30f68439908d2cf7d27 Mon Sep 17 00:00:00 2001 From: "patched.codes[bot]" <298395+patched.codes[bot]@users.noreply.github.com> Date: Wed, 16 Apr 2025 05:56:32 +0000 Subject: [PATCH 05/10] Patched patchwork/common/tools/bash_tool.py --- patchwork/common/tools/bash_tool.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/patchwork/common/tools/bash_tool.py b/patchwork/common/tools/bash_tool.py index 8440f179a..1e802e781 100644 --- a/patchwork/common/tools/bash_tool.py +++ b/patchwork/common/tools/bash_tool.py @@ -45,10 +45,11 @@ def execute( try: result = subprocess.run( - command, shell=True, cwd=self.path, capture_output=True, text=True, timeout=60 # Add timeout for safety + command.split(), shell=False, cwd=self.path, capture_output=True, text=True, timeout=60 ) return result.stdout if result.returncode == 0 else f"Error: {result.stderr}" except subprocess.TimeoutExpired: return "Error: Command timed out after 60 seconds" except Exception as e: return f"Error: {str(e)}" + From b96b237fd45dcf26cfc29affcb0220da6addc4c2 Mon Sep 17 00:00:00 2001 From: "patched.codes[bot]" <298395+patched.codes[bot]@users.noreply.github.com> Date: Wed, 16 Apr 2025 05:56:32 +0000 Subject: [PATCH 06/10] Patched patchwork/steps/CallShell/CallShell.py --- patchwork/steps/CallShell/CallShell.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/patchwork/steps/CallShell/CallShell.py b/patchwork/steps/CallShell/CallShell.py index 98ee55a74..720328b33 100644 --- a/patchwork/steps/CallShell/CallShell.py +++ b/patchwork/steps/CallShell/CallShell.py @@ -46,7 +46,8 @@ def __parse_env_text(env_text: str) -> dict[str, str]: return env def run(self) -> dict: - p = subprocess.run(self.script, shell=True, capture_output=True, text=True, cwd=self.working_dir, env=self.env) + command_args = shlex.split(self.script) + p = subprocess.run(command_args, shell=False, capture_output=True, text=True, cwd=self.working_dir, env=self.env) try: p.check_returncode() except subprocess.CalledProcessError as e: From 1e38555cdf34de2f0d75e4907eb3c9d7dd0d76ad Mon Sep 17 00:00:00 2001 From: "patched.codes[bot]" <298395+patched.codes[bot]@users.noreply.github.com> Date: Wed, 16 Apr 2025 05:56:32 +0000 Subject: [PATCH 07/10] Patched patchwork/common/utils/step_typing.py --- patchwork/common/utils/step_typing.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/patchwork/common/utils/step_typing.py b/patchwork/common/utils/step_typing.py index d349f7fc1..0c94790e7 100644 --- a/patchwork/common/utils/step_typing.py +++ b/patchwork/common/utils/step_typing.py @@ -106,9 +106,15 @@ def validate_step_type_config_with_inputs( def validate_step_with_inputs(input_keys: Set[str], step: Type[Step]) -> Tuple[Set[str], Dict[str, str]]: + allowed_modules = {"module_1.typed", "module_2.typed"} # Example whitelist module_path, _, _ = step.__module__.rpartition(".") step_name = step.__name__ - type_module = importlib.import_module(f"{module_path}.typed") + type_module_path = f"{module_path}.typed" + + if type_module_path not in allowed_modules: + raise ValueError(f"Importing from {type_module_path} is not allowed") + + type_module = importlib.import_module(type_module_path) step_input_model = getattr(type_module, f"{step_name}Inputs", __NOT_GIVEN) step_output_model = getattr(type_module, f"{step_name}Outputs", __NOT_GIVEN) if step_input_model is __NOT_GIVEN: From 2cc98b5b051fc78dde16e2c9689575429f3a8871 Mon Sep 17 00:00:00 2001 From: "patched.codes[bot]" <298395+patched.codes[bot]@users.noreply.github.com> Date: Wed, 16 Apr 2025 05:56:32 +0000 Subject: [PATCH 08/10] Patched patchwork/app.py --- patchwork/app.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/patchwork/app.py b/patchwork/app.py index 4149959a6..abd1a01bd 100644 --- a/patchwork/app.py +++ b/patchwork/app.py @@ -59,6 +59,9 @@ def list_option_callback(ctx: click.Context, param: click.Parameter, value: str def find_patchflow(possible_module_paths: Iterable[str], patchflow: str) -> Any | None: + # Define a whitelist of allowed module paths + allowed_modules = {'allowed_module_1', 'allowed_module_2'} + for module_path in possible_module_paths: try: spec = importlib.util.spec_from_file_location("custom_module", module_path) @@ -71,14 +74,18 @@ def find_patchflow(possible_module_paths: Iterable[str], patchflow: str) -> Any except Exception: logger.debug(f"Patchflow {patchflow} not found as a file/directory in {module_path}") - try: - module = importlib.import_module(module_path) - logger.info(f"Patchflow {patchflow} loaded from {module_path}") - return getattr(module, patchflow) - except ModuleNotFoundError: - logger.debug(f"Patchflow {patchflow} not found as a module in {module_path}") - except AttributeError: - logger.debug(f"Patchflow {patchflow} not found in {module_path}") + # Check if the module is in the whitelist before importing + if module_path in allowed_modules: + try: + module = importlib.import_module(module_path) + logger.info(f"Patchflow {patchflow} loaded from {module_path}") + return getattr(module, patchflow) + except ModuleNotFoundError: + logger.debug(f"Patchflow {patchflow} not found as a module in {module_path}") + except AttributeError: + logger.debug(f"Patchflow {patchflow} not found in {module_path}") + else: + logger.warning(f"Module path {module_path} is not in the whitelist.") return None From f971e58daf0284e34cc2c67355e0c8f6df698b59 Mon Sep 17 00:00:00 2001 From: "patched.codes[bot]" <298395+patched.codes[bot]@users.noreply.github.com> Date: Wed, 16 Apr 2025 05:56:32 +0000 Subject: [PATCH 09/10] Patched patchwork/common/utils/dependency.py --- patchwork/common/utils/dependency.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/patchwork/common/utils/dependency.py b/patchwork/common/utils/dependency.py index 27b89bfed..6e62f0f92 100644 --- a/patchwork/common/utils/dependency.py +++ b/patchwork/common/utils/dependency.py @@ -6,9 +6,13 @@ "notification": ["slack_sdk"], } +__ALLOWED_MODULES = {module for modules in __DEPENDENCY_GROUPS.values() for module in modules} @lru_cache(maxsize=None) def import_with_dependency_group(name): + if name not in __ALLOWED_MODULES: + raise ImportError(f"Import of untrusted module '{name}' is not allowed.") + try: return importlib.import_module(name) except ImportError: @@ -20,6 +24,5 @@ def import_with_dependency_group(name): error_msg = f"Please `pip install patchwork-cli[{dependency_group}]` to use this step" raise ImportError(error_msg) - def slack_sdk(): return import_with_dependency_group("slack_sdk") From 68c86037b688f4ca164eb035cc04bc0b0e349a29 Mon Sep 17 00:00:00 2001 From: "patched.codes[bot]" <298395+patched.codes[bot]@users.noreply.github.com> Date: Wed, 16 Apr 2025 05:56:32 +0000 Subject: [PATCH 10/10] Patched patchwork/common/tools/csvkit_tool.py --- patchwork/common/tools/csvkit_tool.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/patchwork/common/tools/csvkit_tool.py b/patchwork/common/tools/csvkit_tool.py index a1ef8dc59..9c88a88d9 100644 --- a/patchwork/common/tools/csvkit_tool.py +++ b/patchwork/common/tools/csvkit_tool.py @@ -117,11 +117,12 @@ def execute(self, files: list[str], query: str) -> str: files_to_insert = [] if db_path.is_file(): with sqlite3.connect(str(db_path)) as conn: + cursor = conn.cursor() for file in files: - res = conn.execute( - f"SELECT 1 from {file.removesuffix('.csv')}", - ) - if res.fetchone() is None: + table_name = file.removesuffix('.csv') + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,)) + res = cursor.fetchone() + if res is None: files_to_insert.append(file) else: files_to_insert = files