__('Llamly', 'llamly'), 'description' => __('Reversible WordPress tools from Llamly. Every change is journaled and can be undone in Llamly › Activity.', 'llamly'), ]); } public static function registerAbilities(): void { foreach (ToolRegistry::all() as $name => $definition) { if (in_array($name, ['wp_batch_execute'], true)) { continue; } $schema = $definition['inputSchema']; if (isset($schema['properties']) && $schema['properties'] === []) { $schema['properties'] = (object) []; } $write = ($definition['access'] ?? '') === 'write'; wp_register_ability('llamly/' . str_replace('_', '-', $name), [ 'label' => ucfirst(str_replace(['wp_', '_'], ['', ' '], $name)), 'description' => (string) ($definition['description'] ?? $name), 'category' => self::CATEGORY, 'input_schema' => $schema, 'execute_callback' => static fn($input = null) => self::execute($name, $input), 'permission_callback' => static fn($input = null) => self::permitted($name), 'meta' => [ 'annotations' => ['readonly' => !$write, 'destructive' => $write, 'idempotent' => !$write], 'public' => true, 'show_in_rest' => true, ], ]); } } /** The current WordPress user acts with their own content capabilities; the key keeps their changes apart in the journal. */ private static function context(): array { $userId = get_current_user_id(); return [ 'kind' => 'ability', 'user_id' => $userId, 'credential_id' => null, 'credential_key' => 'ability:' . $userId, 'scope_mode' => 'full', 'allowed_tools' => [], ]; } private static function permitted(string $name): bool { $definition = ToolRegistry::get($name); if ($definition === null || get_current_user_id() < 1) { return false; } $context = self::context(); return CapabilityCeiling::run($context, static fn(): bool => Authorization::canUseTool($context, $definition)); } private static function execute(string $name, mixed $input): mixed { if (get_current_user_id() < 1) { return new \WP_Error('forbidden', __('Sign in to use Llamly abilities.', 'llamly'), ['status' => 401]); } $arguments = is_array($input) || is_object($input) ? $input : []; $context = self::context(); return CapabilityCeiling::run($context, static fn(): mixed => Dispatcher::run(['tool' => $name, 'arguments' => $arguments], $context)); } }