withTools([new \Nibiru\Module\Ai\Plugins\Tools\PdoQuery()]) * ->run('How many users registered last week?'); * * Safety: rejects anything that looks like INSERT/UPDATE/DELETE/DROP/TRUNCATE/ALTER. * If you need write access, write a more privileged subclass with an audit trail. */ class PdoQuery extends Tool { public function name(): string { return 'pdo_query'; } public function description(): string { return 'Run a single read-only SQL SELECT against the application database. ' . 'Use for counts, aggregates, lookups. Returns rows as JSON.'; } public function schema(): array { return [ 'sql' => [ 'type' => 'string', 'description' => 'A single SELECT statement. Use placeholders (:name) for dynamic values.', 'required' => true, ], 'params' => [ 'type' => 'object', 'description' => 'Optional parameter bindings, e.g. {":id": 42}.', 'required' => false, ], ]; } public function execute(array $args): mixed { $sql = trim((string) ($args['sql'] ?? '')); if ($sql === '') return 'ERROR: empty SQL'; if (!preg_match('/^\s*SELECT\s/i', $sql)) { return 'ERROR: only SELECT is permitted by pdo_query'; } if (preg_match('/;\s*\S/', $sql)) { return 'ERROR: only a single statement is permitted'; } if (preg_match('/\b(INSERT|UPDATE|DELETE|DROP|TRUNCATE|ALTER|CREATE|GRANT|REVOKE)\b/i', $sql)) { return 'ERROR: write/DDL operations are blocked'; } try { $params = is_array($args['params'] ?? null) ? $args['params'] : []; $rows = Pdo::fetchAll($sql, $params); // Cap the response so the agent doesn't choke on huge results. $rows = array_slice($rows, 0, 50); return json_encode($rows, JSON_UNESCAPED_UNICODE); } catch (\Throwable $e) { return 'ERROR: ' . $e->getMessage(); } } }