spb/zyquo-agent Public MIT
The autonomous agent that actually operates your Mac — plans, runs real commands, verifies its own work.
Swift 94.7%
Shell 4.1%
Python 0.7%
Makefile 0.5%
1//2// ShellTool.swift3// Zyquo Agent4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// The agent's `bash` tool: runs a shell command inside the task workspace.9// Every call is (1) cleared by the PolicyEngine — never bypassed, in any10// mode — (2) executed by ExecutionService with live line streaming and a11// timeout, and (3) appended to the AuditLog with its exit code and a12// truncated output excerpt.13//1415import Foundation1617struct ShellTool: Tool {18 private let executor: ExecutionService1920 init(executor: ExecutionService) {21 self.executor = executor22 }2324 let name = "bash"2526 let description = """27 Run a shell command with /bin/bash inside the task workspace (the \28 working directory). Use it for anything a terminal can do: creating \29 files and folders, running scripts, installing project dependencies, \30 inspecting the system. stdout and stderr stream live to the user and \31 are returned with the exit code. Commands may be blocked or held for \32 user approval by the safety policy — if a command is denied, explain \33 the situation to the user or find a safer approach instead of \34 retrying the same command. Prefer the dedicated file tools \35 (read_file, write_file, edit_file, list_dir, search_files) for file \36 content work; prefer relative paths, which resolve inside the \37 workspace. Long or interactive commands will hit the timeout — pass \38 timeout_seconds for legitimately slow commands.39 """4041 var parametersSchema: JSONValue {42 .object([43 "type": .string("object"),44 "properties": .object([45 "command": .object([46 "type": .string("string"),47 "description": .string("The exact bash command to execute."),48 ]),49 "timeout_seconds": .object([50 "type": .string("integer"),51 "description": .string("Optional timeout in seconds for this command (default 120)."),52 ]),53 "explanation": .object([54 "type": .string("string"),55 "description": .string("One short sentence explaining what this command does and why — shown to the user on approval cards."),56 ]),57 ]),58 "required": .array([.string("command")]),59 ])60 }6162 func execute(arguments: JSONValue, context: ToolExecutionContext) async throws -> ToolExecutionResult {63 guard case .object(let object) = arguments,64 case .string(let command)? = object["command"],65 !command.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {66 return .failure("bash: missing required parameter `command`.")67 }68 var timeout: TimeInterval?69 if case .number(let seconds)? = object["timeout_seconds"], seconds > 0 {70 timeout = seconds71 }72 var explanation: String?73 if case .string(let text)? = object["explanation"] {74 explanation = text75 }7677 // ---- Safety gate (never skipped) --------------------------------78 let cleared: ClearedAction79 do {80 cleared = try await context.policy.clear(ActionRequest(81 kind: .shellCommand,82 payload: command,83 cwd: context.workspaceURL,84 explanation: explanation85 ))86 } catch let denial as PolicyDenied {87 await context.audit.append(AuditEntry(88 actionKind: name,89 payload: command,90 cwd: context.workspaceURL.path,91 ruling: PolicyDecisionRecord.Ruling.denied.rawValue,92 exitCode: nil,93 outputExcerpt: denial.reason94 ))95 return .failure("Command not run — denied by the safety policy: \(denial.reason)")96 }9798 // ---- Execute ------------------------------------------------------99 let result: ExecutionResult100 do {101 result = try await executor.runBash(102 command: cleared.payload,103 cwd: context.workspaceURL,104 timeout: timeout,105 onOutput: context.onOutput106 )107 } catch let launchError as ExecutionLaunchError {108 await context.audit.append(AuditEntry(109 actionKind: name,110 payload: cleared.payload,111 cwd: context.workspaceURL.path,112 ruling: cleared.decision.ruling.rawValue,113 exitCode: nil,114 outputExcerpt: launchError.reason115 ))116 return .failure("bash failed to launch: \(launchError.reason)")117 }118119 // ---- Audit ---------------------------------------------------------120 await context.audit.append(AuditEntry(121 actionKind: name,122 payload: cleared.payload,123 cwd: context.workspaceURL.path,124 ruling: cleared.decision.ruling.rawValue,125 exitCode: result.exitCode,126 outputExcerpt: result.combinedOutput127 ))128129 // ---- Result for the model -------------------------------------------130 var content = result.combinedOutput131 if result.isTimeout {132 content += "\n[timed out after \(Int(timeout ?? 120))s — process was killed]"133 }134 if result.isCancelled {135 content += "\n[cancelled by the user]"136 }137 content += "\n[exit code: \(result.exitCode.map(String.init) ?? "none")]"138139 let failed = result.isTimeout || result.isCancelled || (result.exitCode ?? 1) != 0140 return ToolExecutionResult(content: content, isError: failed)141 }142}143