| 146 | ) |
| 147 | |
| 148 | async def run_async( |
| 149 | self, *, args: dict[str, Any], tool_context: ToolContext |
| 150 | ) -> Any: |
| 151 | command = args.get("command") |
| 152 | if not command: |
| 153 | return {"error": "Command is required."} |
| 154 | |
| 155 | # Static validation. |
| 156 | error = _validate_command(command, self._policy) |
| 157 | if error: |
| 158 | return {"error": error} |
| 159 | |
| 160 | # Always request user confirmation. |
| 161 | if not tool_context.tool_confirmation: |
| 162 | tool_context.request_confirmation( |
| 163 | hint=f"Please approve or reject the bash command: {command}", |
| 164 | ) |
| 165 | tool_context.actions.skip_summarization = True |
| 166 | return { |
| 167 | "error": ( |
| 168 | "This tool call requires confirmation, please approve or reject." |
| 169 | ) |
| 170 | } |
| 171 | elif not tool_context.tool_confirmation.confirmed: |
| 172 | return {"error": "This tool call is rejected."} |
| 173 | |
| 174 | stdout = None |
| 175 | stderr = None |
| 176 | try: |
| 177 | process = await asyncio.create_subprocess_exec( |
| 178 | *shlex.split(command), |
| 179 | cwd=str(self._workspace), |
| 180 | stdout=asyncio.subprocess.PIPE, |
| 181 | stderr=asyncio.subprocess.PIPE, |
| 182 | start_new_session=True, |
| 183 | preexec_fn=lambda: _set_resource_limits(self._policy), |
| 184 | ) |
| 185 | |
| 186 | try: |
| 187 | stdout, stderr = await asyncio.wait_for( |
| 188 | process.communicate(), timeout=self._policy.timeout_seconds |
| 189 | ) |
| 190 | except asyncio.TimeoutError: |
| 191 | try: |
| 192 | if process.pid: |
| 193 | os.killpg(process.pid, signal.SIGKILL) |
| 194 | except ProcessLookupError: |
| 195 | pass |
| 196 | stdout, stderr = await process.communicate() |
| 197 | return { |
| 198 | "error": ( |
| 199 | f"Command timed out after {self._policy.timeout_seconds}" |
| 200 | " seconds." |
| 201 | ), |
| 202 | "stdout": ( |
| 203 | stdout.decode(errors="replace") |
| 204 | if stdout |
| 205 | else "<no stdout captured>" |