From 44ae901b83a3ca88e6a29dc5fada53ab8732c2d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 11:21:32 +0000 Subject: [PATCH 1/2] Initial plan From d022f22ceb1a6817f5aafb08e636edcaa3bf6382 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 11:32:53 +0000 Subject: [PATCH 2/2] Fix TRY003 violations and remove from suppression list Agent-Logs-Url: https://github.com/GitHubSecurityLab/seclab-taskflow-agent/sessions/93ebfeff-7922-41fa-b6a6-fe1abafe4e1c Co-authored-by: kevinbackhouse <4358136+kevinbackhouse@users.noreply.github.com> --- pyproject.toml | 2 +- src/seclab_taskflow_agent/agent.py | 3 +- src/seclab_taskflow_agent/available_tools.py | 40 ++++++++----------- src/seclab_taskflow_agent/capi.py | 3 +- src/seclab_taskflow_agent/cli.py | 3 +- src/seclab_taskflow_agent/mcp_lifecycle.py | 3 +- .../mcp_servers/codeql/client.py | 24 +++++++---- .../mcp_servers/codeql/mcp_server.py | 9 +++-- src/seclab_taskflow_agent/mcp_transport.py | 15 ++++--- src/seclab_taskflow_agent/mcp_utils.py | 6 ++- src/seclab_taskflow_agent/models.py | 8 ++-- src/seclab_taskflow_agent/runner.py | 38 +++++++++++------- src/seclab_taskflow_agent/session.py | 3 +- src/seclab_taskflow_agent/shell_utils.py | 3 +- src/seclab_taskflow_agent/template_utils.py | 3 +- 15 files changed, 95 insertions(+), 68 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f6de0e4..7f62022 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -174,7 +174,6 @@ ignore = [ "EM102", # Exception f-strings "G004", # Logging f-strings "T201", # print() used for user output - "TRY003", # Raise with inline message strings # Backwards-compatibility suppressions for existing code "A001", # Variable shadows built-in @@ -253,3 +252,4 @@ ignore = [ [tool.ruff.lint.per-file-ignores] "tests/*" = ["S101", "PLR2004"] +"src/seclab_taskflow_agent/mcp_servers/codeql/jsonrpyc/*" = ["TRY003"] diff --git a/src/seclab_taskflow_agent/agent.py b/src/seclab_taskflow_agent/agent.py index a26222a..b118535 100644 --- a/src/seclab_taskflow_agent/agent.py +++ b/src/seclab_taskflow_agent/agent.py @@ -172,7 +172,8 @@ def __init__( if token: resolved_token = os.getenv(token, "") if not resolved_token: - raise RuntimeError(f"Token env var {token!r} is not set") + msg = f"Token env var {token!r} is not set" + raise RuntimeError(msg) else: resolved_token = get_AI_token() diff --git a/src/seclab_taskflow_agent/available_tools.py b/src/seclab_taskflow_agent/available_tools.py index 577ae06..1967efd 100644 --- a/src/seclab_taskflow_agent/available_tools.py +++ b/src/seclab_taskflow_agent/available_tools.py @@ -108,18 +108,15 @@ def _load(self, tooltype: AvailableToolType, toolname: str) -> DocumentModel: # Resolve package and filename from dotted path components = toolname.rsplit(".", 1) if len(components) != 2: - raise BadToolNameError( - f'Not a valid toolname: "{toolname}". ' - f'Expected format: "packagename.filename"' - ) + msg = f'Not a valid toolname: "{toolname}". Expected format: "packagename.filename"' + raise BadToolNameError(msg) package, filename = components try: pkg_dir = importlib.resources.files(package) if not pkg_dir.is_dir(): - raise BadToolNameError( - f"Cannot load {toolname} because {pkg_dir} is not a valid directory." - ) + msg = f"Cannot load {toolname} because {pkg_dir} is not a valid directory." + raise BadToolNameError(msg) filepath = pkg_dir.joinpath(filename + ".yaml") with filepath.open() as fh: raw = yaml.safe_load(fh) @@ -128,17 +125,14 @@ def _load(self, tooltype: AvailableToolType, toolname: str) -> DocumentModel: header = raw.get("seclab-taskflow-agent", {}) filetype = header.get("filetype", "") if filetype != tooltype.value: - raise FileTypeException( - f"Error in {filepath}: expected filetype {tooltype.value!r}, " - f"got {filetype!r}." - ) + msg = f"Error in {filepath}: expected filetype {tooltype.value!r}, got {filetype!r}." + raise FileTypeException(msg) # Parse into the appropriate Pydantic model model_cls = DOCUMENT_MODELS.get(filetype) if model_cls is None: - raise BadToolNameError( - f"Unknown filetype {filetype!r} in {toolname}" - ) + msg = f"Unknown filetype {filetype!r} in {toolname}" + raise BadToolNameError(msg) try: doc = model_cls(**raw) @@ -147,21 +141,19 @@ def _load(self, tooltype: AvailableToolType, toolname: str) -> DocumentModel: for err in exc.errors(): if "Unsupported version" in str(err.get("msg", "")): raise VersionException(str(err["msg"])) from exc - raise BadToolNameError( - f"Validation error loading {toolname}: {exc}" - ) from exc - - # Cache and return + msg = f"Validation error loading {toolname}: {exc}" + raise BadToolNameError(msg) from exc if tooltype not in self._cache: self._cache[tooltype] = {} self._cache[tooltype][toolname] = doc return doc except ModuleNotFoundError as exc: - raise BadToolNameError(f"Cannot load {toolname}: {exc}") from exc + msg = f"Cannot load {toolname}: {exc}" + raise BadToolNameError(msg) from exc except FileNotFoundError: - raise BadToolNameError( - f"Cannot load {toolname} because {filepath} is not a valid file." - ) + msg = f"Cannot load {toolname} because {filepath} is not a valid file." + raise BadToolNameError(msg) except ValueError as exc: - raise BadToolNameError(f"Cannot load {toolname}: {exc}") from exc + msg = f"Cannot load {toolname}: {exc}" + raise BadToolNameError(msg) from exc diff --git a/src/seclab_taskflow_agent/capi.py b/src/seclab_taskflow_agent/capi.py index 8bb43cc..967fd26 100644 --- a/src/seclab_taskflow_agent/capi.py +++ b/src/seclab_taskflow_agent/capi.py @@ -167,7 +167,8 @@ def get_AI_token() -> str: token = os.getenv("COPILOT_TOKEN") if token: return token - raise RuntimeError("AI_API_TOKEN environment variable is not set.") + msg = "AI_API_TOKEN environment variable is not set." + raise RuntimeError(msg) # --------------------------------------------------------------------------- diff --git a/src/seclab_taskflow_agent/cli.py b/src/seclab_taskflow_agent/cli.py index 7569431..a67459c 100644 --- a/src/seclab_taskflow_agent/cli.py +++ b/src/seclab_taskflow_agent/cli.py @@ -36,7 +36,8 @@ def _parse_global(value: str) -> tuple[str, str]: """Parse a ``KEY=VALUE`` string into a (key, value) pair.""" if "=" not in value: - raise typer.BadParameter(f"Invalid global variable format: {value!r}. Expected KEY=VALUE.") + msg = f"Invalid global variable format: {value!r}. Expected KEY=VALUE." + raise typer.BadParameter(msg) key, _, val = value.partition("=") return key.strip(), val.strip() diff --git a/src/seclab_taskflow_agent/mcp_lifecycle.py b/src/seclab_taskflow_agent/mcp_lifecycle.py index 117f52a..0453436 100644 --- a/src/seclab_taskflow_agent/mcp_lifecycle.py +++ b/src/seclab_taskflow_agent/mcp_lifecycle.py @@ -116,7 +116,8 @@ def _print_err(line: str) -> None: client_session_timeout_seconds=client_session_timeout, ) case _: - raise ValueError(f"Unsupported MCP transport: {params['kind']}") + msg = f"Unsupported MCP transport: {params['kind']}" + raise ValueError(msg) entries.append(MCPServerEntry(MCPNamespaceWrap(confirms, mcp_server), server_proc, name=tb)) diff --git a/src/seclab_taskflow_agent/mcp_servers/codeql/client.py b/src/seclab_taskflow_agent/mcp_servers/codeql/client.py index af7d03d..b0d335f 100644 --- a/src/seclab_taskflow_agent/mcp_servers/codeql/client.py +++ b/src/seclab_taskflow_agent/mcp_servers/codeql/client.py @@ -194,10 +194,12 @@ def _server_request_run( template_values: dict | None = None, ): if not self.active_database: - raise RuntimeError("No Active Database") + msg = "No Active Database" + raise RuntimeError(msg) if not self.active_connection: - raise RuntimeError("No Active Connection") + msg = "No Active Connection" + raise RuntimeError(msg) if isinstance(quick_eval_pos, dict): # A quick eval position contains: @@ -302,7 +304,8 @@ def _format(self, query): def _resolve_query_server(self): help_msg = shell_command_to_string(self.codeql_cli + ["excute", "--help"]) if not re.search("query-server2", help_msg): - raise RuntimeError("Legacy server not supported!") + msg = "Legacy server not supported!" + raise RuntimeError(msg) return "query-server2" def _resolve_library_paths(self, query_path): @@ -463,11 +466,13 @@ def _file_uri_to_path(uri): # internally the codeql client will resolve both relative and full paths # regardless of root directory differences if not uri.startswith("file:///"): - raise ValueError("URI path should be formatted as absolute") + msg = "URI path should be formatted as absolute" + raise ValueError(msg) # note: don't try to parse paths like "file://a/b" because that returns "/b", should be "file:///a/b" parsed = urlparse(uri) if parsed.scheme != "file": - raise ValueError(f"Not a file:// uri: {uri}") + msg = f"Not a file:// uri: {uri}" + raise ValueError(msg) path = unquote(parsed.path) region = None if ":" in path: @@ -605,7 +610,8 @@ def run_query( if target: target_pos = get_query_position(query_path, target) if not target_pos: - raise ValueError(f"Could not resolve quick eval target for {target}") + msg = f"Could not resolve quick eval target for {target}" + raise ValueError(msg) try: with ( QueryServer(database, keep_alive=keep_alive, log_stderr=log_stderr) as server, @@ -633,7 +639,9 @@ def run_query( case "sarif": result = server._bqrs_to_sarif(bqrs_path, server._query_info(query_path)) case _: - raise ValueError("Unsupported output format {fmt}") + msg = f"Unsupported output format {fmt}" + raise ValueError(msg) except Exception as e: - raise RuntimeError(f"Error in run_query: {e}") from e + msg = f"Error in run_query: {e}" + raise RuntimeError(msg) from e return result diff --git a/src/seclab_taskflow_agent/mcp_servers/codeql/mcp_server.py b/src/seclab_taskflow_agent/mcp_servers/codeql/mcp_server.py index d245666..50e0767 100644 --- a/src/seclab_taskflow_agent/mcp_servers/codeql/mcp_server.py +++ b/src/seclab_taskflow_agent/mcp_servers/codeql/mcp_server.py @@ -53,10 +53,12 @@ def _resolve_query_path(language: str, query: str) -> Path: global TEMPLATED_QUERY_PATHS if language not in TEMPLATED_QUERY_PATHS: - raise RuntimeError(f"Error: Language `{language}` not supported!") + msg = f"Error: Language `{language}` not supported!" + raise RuntimeError(msg) query_path = TEMPLATED_QUERY_PATHS[language].get(query) if not query_path: - raise RuntimeError(f"Error: query `{query}` not supported for `{language}`!") + msg = f"Error: query `{query}` not supported for `{language}`!" + raise RuntimeError(msg) return Path(query_path) @@ -69,7 +71,8 @@ def _resolve_db_path(relative_db_path: str | Path): absolute_path = CODEQL_DBS_BASE_PATH / relative_db_path if not absolute_path.is_dir(): _debug_log(f"Database path not found: {absolute_path}") - raise RuntimeError(f"Error: Database not found at {absolute_path}!") + msg = f"Error: Database not found at {absolute_path}!" + raise RuntimeError(msg) return absolute_path diff --git a/src/seclab_taskflow_agent/mcp_transport.py b/src/seclab_taskflow_agent/mcp_transport.py index 8632fd8..4718f7d 100644 --- a/src/seclab_taskflow_agent/mcp_transport.py +++ b/src/seclab_taskflow_agent/mcp_transport.py @@ -109,7 +109,8 @@ async def async_wait_for_connection( host = parsed.hostname port = parsed.port if host is None or port is None: - raise ValueError(f"URL must include a host and port: {self.url}") + msg = f"URL must include a host and port: {self.url}" + raise ValueError(msg) deadline = asyncio.get_event_loop().time() + timeout while True: try: @@ -119,7 +120,8 @@ async def async_wait_for_connection( return except (OSError, ConnectionRefusedError): if asyncio.get_event_loop().time() > deadline: - raise TimeoutError(f"Could not connect to {host}:{port} after {timeout} seconds") + msg = f"Could not connect to {host}:{port} after {timeout} seconds" + raise TimeoutError(msg) await asyncio.sleep(poll_interval) def wait_for_connection( @@ -139,7 +141,8 @@ def wait_for_connection( host = parsed.hostname port = parsed.port if host is None or port is None: - raise ValueError(f"URL must include a host and port: {self.url}") + msg = f"URL must include a host and port: {self.url}" + raise ValueError(msg) deadline = time.time() + timeout while True: try: @@ -147,7 +150,8 @@ def wait_for_connection( return except OSError: if time.time() > deadline: - raise TimeoutError(f"Could not connect to {host}:{port} after {timeout} seconds") + msg = f"Could not connect to {host}:{port} after {timeout} seconds" + raise TimeoutError(msg) time.sleep(poll_interval) def run(self) -> None: @@ -216,7 +220,8 @@ def join_and_raise(self, timeout: float | None = None) -> None: """ self.join(timeout) if self.is_alive(): - raise RuntimeError("Process thread did not exit within timeout.") + msg = "Process thread did not exit within timeout." + raise RuntimeError(msg) if self.exception is not None: raise self.exception diff --git a/src/seclab_taskflow_agent/mcp_utils.py b/src/seclab_taskflow_agent/mcp_utils.py index a186bee..5a9384a 100644 --- a/src/seclab_taskflow_agent/mcp_utils.py +++ b/src/seclab_taskflow_agent/mcp_utils.py @@ -202,7 +202,8 @@ def mcp_client_params( logging.debug(f"Initializing streamable toolbox: {tb}\nargs:\n{args}\nenv:\n{env}\n") exe = shutil.which(sp.command) if exe is None: - raise FileNotFoundError(f"Could not resolve path to {sp.command}") + msg = f"Could not resolve path to {sp.command}" + raise FileNotFoundError(msg) start_cmd = [exe] if args: for i, v in enumerate(args): @@ -220,7 +221,8 @@ def mcp_client_params( server_params["env"] = env case _: - raise ValueError(f"Unsupported MCP transport {kind}") + msg = f"Unsupported MCP transport {kind}" + raise ValueError(msg) client_params[tb] = ( server_params, diff --git a/src/seclab_taskflow_agent/models.py b/src/seclab_taskflow_agent/models.py index 6445b64..a2804cc 100644 --- a/src/seclab_taskflow_agent/models.py +++ b/src/seclab_taskflow_agent/models.py @@ -62,9 +62,8 @@ def _normalise_version(cls, v: Any) -> str: @classmethod def _validate_version(cls, v: str) -> str: if v != SUPPORTED_VERSION: - raise ValueError( - f"Unsupported version: {v}. Only version {SUPPORTED_VERSION} is supported." - ) + msg = f"Unsupported version: {v}. Only version {SUPPORTED_VERSION} is supported." + raise ValueError(msg) return v @@ -106,7 +105,8 @@ class TaskDefinition(BaseModel): @model_validator(mode="after") def _run_xor_prompt(self) -> TaskDefinition: if self.run and self.user_prompt: - raise ValueError("shell task ('run') and prompt task ('user_prompt') are mutually exclusive") + msg = "shell task ('run') and prompt task ('user_prompt') are mutually exclusive" + raise ValueError(msg) return self diff --git a/src/seclab_taskflow_agent/runner.py b/src/seclab_taskflow_agent/runner.py index 5869385..f879618 100644 --- a/src/seclab_taskflow_agent/runner.py +++ b/src/seclab_taskflow_agent/runner.py @@ -78,9 +78,8 @@ def _resolve_model_config( models_params: dict[str, dict[str, Any]] = m_config.model_settings or {} unknown = set(models_params) - set(model_keys) if unknown: - raise ValueError( - f"Settings section of model_config file {model_config_ref} contains models not in the model section: {unknown}" - ) + msg = f"Settings section of model_config file {model_config_ref} contains models not in the model section: {unknown}" + raise ValueError(msg) return model_keys, model_dict, models_params, m_config.api_type @@ -103,9 +102,11 @@ def _merge_reusable_task( """ reusable_doc = available_tools.get_taskflow(task.uses) if reusable_doc is None: - raise ValueError(f"No such reusable taskflow: {task.uses}") + msg = f"No such reusable taskflow: {task.uses}" + raise ValueError(msg) if len(reusable_doc.taskflow) > 1: - raise ValueError("Reusable taskflows can only contain 1 task") + msg = "Reusable taskflows can only contain 1 task" + raise ValueError(msg) parent_task = reusable_doc.taskflow[0].task merged: dict[str, Any] = parent_task.model_dump(by_alias=True, exclude_defaults=True) current: dict[str, Any] = task.model_dump(by_alias=True, exclude_defaults=True) @@ -147,7 +148,8 @@ def _resolve_task_model( task_model_settings: dict[str, Any] | Any = task.model_settings or {} if not isinstance(task_model_settings, dict): - raise ValueError(f"model_settings in task {task.name or ''} needs to be a dictionary") + msg = f"model_settings in task {task.name or ''} needs to be a dictionary" + raise ValueError(msg) # Task-level overrides can also set engine keys task_settings = dict(task_model_settings) @@ -198,14 +200,16 @@ async def _build_prompts_to_run( raise except json.JSONDecodeError as exc: logging.critical(f"Could not parse tool result as JSON: {last_mcp_tool_results[-1][:200]}") - raise ValueError("Tool result is not valid JSON") from exc + msg = "Tool result is not valid JSON" + raise ValueError(msg) from exc text = last_result.get("text", "") try: iterable_result = json.loads(text) except json.JSONDecodeError as exc: logging.critical(f"Could not parse result text: {text}") - raise ValueError("Result text is not valid JSON") from exc + msg = "Result text is not valid JSON" + raise ValueError(msg) from exc try: iter(iterable_result) except TypeError: @@ -228,7 +232,8 @@ async def _build_prompts_to_run( prompts_to_run.append(rendered_prompt) except jinja2.TemplateError as e: logging.error(f"Error rendering template for result {value}: {e}") - raise ValueError(f"Template rendering failed: {e}") + msg = f"Template rendering failed: {e}" + raise ValueError(msg) # Consume only after all prompts rendered successfully so that # the result remains available for retry/resume on failure. @@ -403,7 +408,8 @@ async def _run_streamed() -> None: max_retry -= 1 except RateLimitError: if rate_limit_backoff == MAX_RATE_LIMIT_BACKOFF: - raise APITimeoutError("Max rate limit backoff reached") + msg = "Max rate limit backoff reached" + raise APITimeoutError(msg) if rate_limit_backoff > MAX_RATE_LIMIT_BACKOFF: rate_limit_backoff = MAX_RATE_LIMIT_BACKOFF else: @@ -556,7 +562,8 @@ async def on_handoff_hook(context: RunContextWrapper[TContext], agent: Agent[TCo inputs = task.inputs or {} task_prompt = task.user_prompt or "" if run and task_prompt: - raise ValueError("shell task and prompt task are mutually exclusive!") + msg = "shell task and prompt task are mutually exclusive!" + raise ValueError(msg) must_complete = task.must_complete max_turns = task.max_steps or DEFAULT_MAX_TURNS toolboxes_override = task.toolboxes or [] @@ -577,7 +584,8 @@ async def on_handoff_hook(context: RunContextWrapper[TContext], agent: Agent[TCo ) except jinja2.TemplateError as e: logging.error(f"Template rendering error: {e}") - raise ValueError(f"Failed to render prompt template: {e}") from e + msg = f"Failed to render prompt template: {e}" + raise ValueError(msg) from e with TmpEnv(env): prompts_to_run: list[str] = await _build_prompts_to_run( @@ -611,14 +619,16 @@ async def run_prompts(async_task: bool = False, max_concurrent_tasks: int = 5) - for agent_name in current_agents: personality = available_tools.get_personality(agent_name) if personality is None: - raise ValueError(f"No such personality: {agent_name}") + msg = f"No such personality: {agent_name}" + raise ValueError(msg) resolved_agents[agent_name] = personality if not resolved_agents: - raise ValueError( + msg = ( "No agents resolved for this task. " "Specify a personality with -p or provide an agents list." ) + raise ValueError(msg) async def _deploy(ra: dict, pp: str) -> bool: async with semaphore: diff --git a/src/seclab_taskflow_agent/session.py b/src/seclab_taskflow_agent/session.py index 9b77151..d791b75 100644 --- a/src/seclab_taskflow_agent/session.py +++ b/src/seclab_taskflow_agent/session.py @@ -121,7 +121,8 @@ def load(cls, session_id: str) -> TaskflowSession: """ path = session_dir() / f"{session_id}.json" if not path.exists(): - raise FileNotFoundError(f"No session checkpoint found: {session_id}") + msg = f"No session checkpoint found: {session_id}" + raise FileNotFoundError(msg) return cls.model_validate_json(path.read_text()) @classmethod diff --git a/src/seclab_taskflow_agent/shell_utils.py b/src/seclab_taskflow_agent/shell_utils.py index 75175ec..ca8194a 100644 --- a/src/seclab_taskflow_agent/shell_utils.py +++ b/src/seclab_taskflow_agent/shell_utils.py @@ -23,7 +23,8 @@ def shell_command_to_string(cmd: list[str]) -> str: stdout, stderr = p.communicate() p.wait() if p.returncode: - raise RuntimeError(f"Command {cmd} failed: {stderr}") + msg = f"Command {cmd} failed: {stderr}" + raise RuntimeError(msg) return stdout diff --git a/src/seclab_taskflow_agent/template_utils.py b/src/seclab_taskflow_agent/template_utils.py index 2f21d4a..61b89b5 100644 --- a/src/seclab_taskflow_agent/template_utils.py +++ b/src/seclab_taskflow_agent/template_utils.py @@ -77,7 +77,8 @@ def env_function(var_name: str, default: Optional[str] = None, required: bool = """ value = os.getenv(var_name, default) if value is None and required: - raise LookupError(f"Required environment variable {var_name} not found!") + msg = f"Required environment variable {var_name} not found!" + raise LookupError(msg) return value or ""