import { DesktopCommandBroker } from './sandbox.js'; import { readWorkspaceTextFile } from './workspace.js'; import type { DesktopToolEvent, DesktopToolSummary } from '../shared.js'; export interface ToolCallRequest { readonly name: string; readonly input: Record; } const LEDGER_API = 'https://sovereign-email-agent.snapkittywest.workers.dev/api/commit'; export const SOVEREIGN_TOOLS: ReadonlyArray = [ { name: 'workspace.search', description: 'Search this repo with ripgrep. Read-only.', inputSchema: { pattern: 'string', path: 'optional string path inside repo' }, }, { name: 'workspace.read_file', description: 'Read a text file from inside this repo. Read-only.', inputSchema: { path: 'string path inside repo' }, }, { name: 'repo.status', description: 'Run git status -sb. Read-only.', inputSchema: {}, }, { name: 'repo.diff_stat', description: 'Run git diff --stat. Read-only.', inputSchema: {}, }, { name: 'repo.test', description: 'Run npm test through the governed command broker.', inputSchema: {}, }, { name: 'repo.build', description: 'Run npm run build through the governed command broker.', inputSchema: {}, }, { name: 'repo.verify', description: 'Run npm run verify through the governed command broker.', inputSchema: {}, }, { name: 'ledger.seal', description: 'Seal the current file to the Fibonacci WORM ledger and commit to GitHub. Artifact hash (SHA-512 → 0xFFFFFFFF → SHA-512) recorded on-chain.', inputSchema: { path: 'string — owner/repo/file/path.ext', code: 'string — file content', branch: 'optional string — default main' }, }, ]; const TOOL_NAMES = new Set(SOVEREIGN_TOOLS.map(tool => tool.name)); export class SovereignToolBroker { constructor( private readonly repoRoot: string, private readonly commands: DesktopCommandBroker, ) {} list(): ReadonlyArray { return SOVEREIGN_TOOLS; } async execute(call: ToolCallRequest): Promise { try { if (!TOOL_NAMES.has(call.name)) { return { name: call.name, ok: false, output: `Unknown tool: ${call.name}` }; } switch (call.name) { case 'workspace.search': return this.runCommandTool(call.name, buildSearchCommand(call.input)); case 'workspace.read_file': { const path = requireString(call.input, 'path'); const file = await readWorkspaceTextFile(this.repoRoot, path); return { name: call.name, ok: true, output: truncateToolOutput(`FILE ${file.relativePath}\n\n${file.content}`), }; } case 'repo.status': return this.runCommandTool(call.name, 'git status -sb'); case 'repo.diff_stat': return this.runCommandTool(call.name, 'git diff --stat'); case 'repo.test': return this.runCommandTool(call.name, 'npm test'); case 'repo.build': return this.runCommandTool(call.name, 'npm run build'); case 'repo.verify': return this.runCommandTool(call.name, 'npm run verify'); case 'ledger.seal': return this.sealToLedger(call); default: return { name: call.name, ok: false, output: `Unhandled tool: ${call.name}` }; } } catch (error) { return { name: call.name, ok: false, output: error instanceof Error ? error.message : String(error), }; } } private async sealToLedger(call: ToolCallRequest): Promise { const path = requireString(call.input, 'path'); const code = requireString(call.input, 'code'); const branch = typeof call.input.branch === 'string' ? call.input.branch : 'main'; const res = await fetch(LEDGER_API, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path, code, branch, author: 'bob-desktop-ide' }), }); if (!res.ok) { const err = await res.text(); return { name: call.name, ok: false, output: `Ledger API error ${res.status}: ${err}` }; } const data = await res.json() as Record; const ledger = data.ledger as Record; const output = [ `SOVEREIGN COMMIT SEALED`, `══════════════════════════════`, ` File: ${data.file}`, ` Branch: ${data.branch}`, ` Commit: ${data.commit}`, ` URL: ${data.url}`, ``, `FIBONACCI LEDGER`, ` Block: ${ledger.block}`, ` F(${ledger.block}): ${ledger.fib}`, ` Hash: ${data.artifact_hash}`, ` Pipeline: ${data.pipeline}`, `══════════════════════════════`, ].join('\n'); return { name: call.name, ok: true, output }; } private async runCommandTool(name: string, command: string): Promise { const result = await this.commands.run({ command }, () => undefined); const output = [ result.reason ? `reason: ${result.reason}` : '', result.stdout, result.stderr, ].filter(Boolean).join('\n'); return { name, ok: result.allowed && result.exitCode === 0, output: truncateToolOutput(output || `exit=${result.exitCode}`), }; } } export function parseToolCall(content: string): ToolCallRequest | undefined { const candidates = [ ...extractJsonCodeBlocks(content), content.trim(), ]; for (const candidate of candidates) { try { const parsed = JSON.parse(candidate) as unknown; const toolCall = typeof parsed === 'object' && parsed !== null && 'tool_call' in parsed ? (parsed as { tool_call?: unknown }).tool_call : parsed; if (typeof toolCall !== 'object' || toolCall === null) continue; const record = toolCall as Record; if (typeof record.name !== 'string') continue; const input = typeof record.input === 'object' && record.input !== null ? record.input as Record : {}; return { name: record.name, input }; } catch { continue; } } return undefined; } export function removeToolCallJson(content: string): string { return content .replace(/```(?:json)?\s*{[\s\S]*?"tool_call"[\s\S]*?}\s*```/gi, '') .replace(/^\s*{[\s\S]*?"tool_call"[\s\S]*?}\s*$/i, '') .trim(); } export function formatToolResultForModel(call: ToolCallRequest, event: DesktopToolEvent): string { return JSON.stringify({ tool_result: { name: call.name, ok: event.ok, output: event.output, }, }, null, 2); } function buildSearchCommand(input: Record): string { const pattern = requireString(input, 'pattern'); const path = typeof input.path === 'string' && input.path.trim() ? input.path.trim() : '.'; return `rg ${quoteArg(pattern)} ${quoteArg(path)}`; } function requireString(input: Record, key: string): string { const value = input[key]; if (typeof value !== 'string' || !value.trim()) { throw new Error(`Tool input field "${key}" must be a non-empty string.`); } return value.trim(); } function quoteArg(value: string): string { return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`; } function extractJsonCodeBlocks(content: string): string[] { const matches = [...content.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi)]; return matches.map(match => match[1]?.trim() ?? '').filter(Boolean); } function truncateToolOutput(output: string): string { const limit = 12_000; if (output.length <= limit) return output; return `${output.slice(0, limit)}\n\n[tool output truncated at ${limit} chars]`; }