(args: z.infer<typeof ArgsSchema>, ctx: ToolContext)
| 19 | argsSchema = ArgsSchema; |
| 20 | |
| 21 | async execute(args: z.infer<typeof ArgsSchema>, ctx: ToolContext): Promise<ToolResult> { |
| 22 | const cmd = args.command.trim(); |
| 23 | if (!cmd) return { content: '[ERROR] Empty command', isError: true }; |
| 24 | |
| 25 | // Permission check |
| 26 | const permReq = { tool: 'shell', operation: cmd, description: args.description }; |
| 27 | const decision = ctx.permissions.evaluate(permReq); |
| 28 | if (decision === 'deny') { |
| 29 | return { content: `[PERMISSION_DENIED] Command blocked by policy: ${cmd}\nIf you really need this, ask the user to add an allow rule.`, isError: true }; |
| 30 | } |
| 31 | if (decision === 'ask') { |
| 32 | ctx.emit({ type: 'permission-request', tool: 'shell', operation: cmd, description: args.description }); |
| 33 | const answer = await ctx.askUser( |
| 34 | `Run: ${cmd}${args.description ? `\n (${args.description})` : ''}`, |
| 35 | ['yes', 'no', 'always'], |
| 36 | ); |
| 37 | if (answer === 'no') { |
| 38 | return { content: `[USER_REJECTED] User declined to run: ${cmd}`, isError: true }; |
| 39 | } |
| 40 | if (answer === 'always') { |
| 41 | ctx.permissions.rememberDecision(permReq, 'allow', 'pattern'); |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | const timeoutMs = (args.timeout_seconds ?? 120) * 1000; |
| 46 | |
| 47 | // Auto-snapshot: if the wiring is present and this command pattern is destructive, |
| 48 | // take a git stash first so /undo can roll back. Best-effort — never blocks on |
| 49 | // failure, but the failure IS surfaced in the result so the user knows /undo |
| 50 | // is unavailable for this command. |
| 51 | let snapshotWarning: string | null = null; |
| 52 | if (ctx.snapshotService) { |
| 53 | const snapshot = await import('../../safety/snapshot.js'); |
| 54 | const check = snapshot.isDestructiveBash(cmd); |
| 55 | if (check.destructive) { |
| 56 | try { |
| 57 | ctx.snapshotService.takeSnapshot( |
| 58 | `before bash: ${check.label} (${cmd.slice(0, 80)})`, |
| 59 | ctx.currentTurn ?? 0, |
| 60 | ); |
| 61 | } catch (e: any) { |
| 62 | // Snapshot failure is non-fatal — log, surface to the user, and proceed. |
| 63 | logger.warn('Auto-snapshot before bash failed (continuing)', { err: e?.message }); |
| 64 | snapshotWarning = '⚠ snapshot failed — /undo unavailable for this command'; |
| 65 | } |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | const result = await this.runCommand(cmd, ctx, timeoutMs); |
| 70 | if (snapshotWarning) { |
| 71 | return { ...result, content: `${snapshotWarning}\n${result.content}` }; |
| 72 | } |
| 73 | return result; |
| 74 | } |
| 75 | |
| 76 | private runCommand(cmd: string, ctx: ToolContext, timeoutMs: number): Promise<ToolResult> { |
| 77 | return new Promise(resolve => { |
nothing calls this directly
no test coverage detected