PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.0.0
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.0.0
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
desktop-mode / includes / agents / rest.php

rest.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.0.0, at includes/agents/rest.php

559 lines 15.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — Agents: REST surface at /desktop-mode/v1/agents.
4 *
5 * One CRUD surface over the two layers (user row + definition meta) so
6 * the bundle never coordinates `/wp/v2/users` and raw meta from JS.
7 *
8 * Routes:
9 *
10 * GET /desktop-mode/v1/agents list
11 * POST /desktop-mode/v1/agents create
12 * GET /desktop-mode/v1/agents/(?P<id>\d+) get
13 * POST /desktop-mode/v1/agents/(?P<id>\d+) patch
14 * DELETE /desktop-mode/v1/agents/(?P<id>\d+) delete
15 * POST /desktop-mode/v1/agents/(?P<id>\d+)/invoke run (chat trigger)
16 * GET /desktop-mode/v1/agents/abilities abilities catalogue
17 * GET /desktop-mode/v1/agents/trigger-kinds trigger kinds catalogue
18 * GET /desktop-mode/v1/agents/hooks-catalogue hook autocomplete
19 * GET /desktop-mode/v1/agents/roles assignable roles (writers only)
20 *
21 * Permissions: reads and invokes default to `edit_posts` (the same
22 * audience as the WP Explorer window hosting the UI); writes require
23 * `edit_users` (agents are real users — managing them is user
24 * management). All three are filterable.
25 *
26 * @package OpenStation
27 */
28
29 defined( 'ABSPATH' ) || exit;
30
31 /**
32 * Register REST routes on rest_api_init.
33 *
34 * @return void
35 */
36 function openstation_agents_register_rest_routes() {
37 $namespace = 'desktop-mode/v1';
38
39 register_rest_route(
40 $namespace,
41 '/agents',
42 array(
43 array(
44 'methods' => WP_REST_Server::READABLE,
45 'permission_callback' => 'openstation_agents_rest_read_permission',
46 'callback' => 'openstation_agents_rest_list',
47 ),
48 array(
49 'methods' => WP_REST_Server::CREATABLE,
50 'permission_callback' => 'openstation_agents_rest_write_permission',
51 'callback' => 'openstation_agents_rest_create',
52 'args' => array(
53 'name' => array(
54 'type' => 'string',
55 'required' => true,
56 'sanitize_callback' => 'sanitize_text_field',
57 ),
58 'role' => array(
59 'type' => 'string',
60 'required' => true,
61 'sanitize_callback' => 'sanitize_key',
62 ),
63 'description' => array(
64 'type' => 'string',
65 'default' => '',
66 'sanitize_callback' => 'sanitize_text_field',
67 ),
68 'instructions' => array(
69 'type' => 'string',
70 'default' => '',
71 ),
72 'abilities' => array(
73 'type' => 'array',
74 'default' => array(),
75 'items' => array( 'type' => 'string' ),
76 ),
77 ),
78 ),
79 )
80 );
81
82 register_rest_route(
83 $namespace,
84 '/agents/abilities',
85 array(
86 'methods' => WP_REST_Server::READABLE,
87 'permission_callback' => 'openstation_agents_rest_read_permission',
88 'callback' => 'openstation_agents_rest_abilities_catalogue',
89 )
90 );
91
92 register_rest_route(
93 $namespace,
94 '/agents/trigger-kinds',
95 array(
96 'methods' => WP_REST_Server::READABLE,
97 'permission_callback' => 'openstation_agents_rest_read_permission',
98 'callback' => 'openstation_agents_rest_trigger_kinds',
99 )
100 );
101
102 register_rest_route(
103 $namespace,
104 '/agents/hooks-catalogue',
105 array(
106 'methods' => WP_REST_Server::READABLE,
107 'permission_callback' => 'openstation_agents_rest_read_permission',
108 'callback' => 'openstation_agents_rest_hooks_catalogue',
109 )
110 );
111
112 register_rest_route(
113 $namespace,
114 '/agents/roles',
115 array(
116 'methods' => WP_REST_Server::READABLE,
117 'permission_callback' => 'openstation_agents_rest_write_permission',
118 'callback' => 'openstation_agents_rest_roles',
119 )
120 );
121
122 register_rest_route(
123 $namespace,
124 '/agents/(?P<id>\d+)',
125 array(
126 array(
127 'methods' => WP_REST_Server::READABLE,
128 'permission_callback' => 'openstation_agents_rest_read_permission',
129 'callback' => 'openstation_agents_rest_get',
130 ),
131 array(
132 'methods' => WP_REST_Server::CREATABLE,
133 'permission_callback' => 'openstation_agents_rest_write_permission',
134 'callback' => 'openstation_agents_rest_patch',
135 ),
136 array(
137 'methods' => WP_REST_Server::DELETABLE,
138 'permission_callback' => 'openstation_agents_rest_write_permission',
139 'callback' => 'openstation_agents_rest_delete',
140 ),
141 )
142 );
143
144 register_rest_route(
145 $namespace,
146 '/agents/(?P<id>\d+)/invoke',
147 array(
148 'methods' => WP_REST_Server::CREATABLE,
149 'permission_callback' => 'openstation_agents_rest_invoke_permission',
150 'callback' => 'openstation_agents_rest_invoke',
151 'args' => array(
152 'message' => array(
153 'type' => 'string',
154 'required' => true,
155 'sanitize_callback' => 'sanitize_textarea_field',
156 ),
157 'source' => array(
158 'type' => 'string',
159 'default' => 'chat',
160 'enum' => array( 'chat', 'drag', 'send-to' ),
161 'sanitize_callback' => 'sanitize_key',
162 ),
163 // Prior conversation turns, oldest first. Without these
164 // every message is a contextless run — a follow-up like
165 // "yes, do it" would be resolved against nothing and the
166 // agent could act on the wrong entity entirely.
167 'history' => array(
168 'type' => 'array',
169 'default' => array(),
170 'items' => array(
171 'type' => 'object',
172 'properties' => array(
173 'role' => array(
174 'type' => 'string',
175 'enum' => array( 'user', 'agent' ),
176 ),
177 'text' => array( 'type' => 'string' ),
178 ),
179 ),
180 ),
181 ),
182 )
183 );
184 }
185 add_action( 'rest_api_init', 'openstation_agents_register_rest_routes' );
186
187 // ---------------------------------------------------------------------------
188 // Permissions
189 // ---------------------------------------------------------------------------
190
191 /**
192 * Whether the current user can see agents.
193 *
194 * @return bool
195 */
196 function openstation_agents_user_can_read() {
197 /**
198 * Filter whether the current user can read OpenStation agents.
199 *
200 * @param bool $can Default: `edit_posts` capability.
201 */
202 return (bool) apply_filters( 'openstation_agents_user_can_read', current_user_can( 'edit_posts' ) );
203 }
204
205 /**
206 * Whether the current user can create / edit / delete agents.
207 *
208 * @return bool
209 */
210 function openstation_agents_user_can_manage() {
211 /**
212 * Filter whether the current user can manage OpenStation agents.
213 *
214 * @param bool $can Default: `edit_users` capability.
215 */
216 return (bool) apply_filters( 'openstation_agents_user_can_manage', current_user_can( 'edit_users' ) );
217 }
218
219 /**
220 * Whether the current user can invoke agents.
221 *
222 * @return bool
223 */
224 function openstation_agents_user_can_invoke() {
225 /**
226 * Filter whether the current user can invoke OpenStation agents.
227 *
228 * @param bool $can Default: `edit_posts` capability.
229 */
230 return (bool) apply_filters( 'openstation_agents_user_can_invoke', current_user_can( 'edit_posts' ) );
231 }
232
233 /**
234 * Read-route permission callback.
235 *
236 * @return bool|WP_Error
237 */
238 function openstation_agents_rest_read_permission() {
239 if ( ! is_user_logged_in() || ! openstation_agents_user_can_read() ) {
240 return new WP_Error(
241 'openstation_agents_forbidden',
242 __( 'You do not have permission to read OpenStation agents.', 'desktop-mode' ),
243 array( 'status' => rest_authorization_required_code() )
244 );
245 }
246 return true;
247 }
248
249 /**
250 * Write-route permission callback.
251 *
252 * @return bool|WP_Error
253 */
254 function openstation_agents_rest_write_permission() {
255 if ( ! is_user_logged_in() || ! openstation_agents_user_can_manage() ) {
256 return new WP_Error(
257 'openstation_agents_forbidden',
258 __( 'You do not have permission to manage OpenStation agents.', 'desktop-mode' ),
259 array( 'status' => rest_authorization_required_code() )
260 );
261 }
262 return true;
263 }
264
265 /**
266 * Invoke-route permission callback.
267 *
268 * @return bool|WP_Error
269 */
270 function openstation_agents_rest_invoke_permission() {
271 if ( ! is_user_logged_in() || ! openstation_agents_user_can_invoke() ) {
272 return new WP_Error(
273 'openstation_agents_forbidden',
274 __( 'You do not have permission to invoke OpenStation agents.', 'desktop-mode' ),
275 array( 'status' => rest_authorization_required_code() )
276 );
277 }
278 return true;
279 }
280
281 // ---------------------------------------------------------------------------
282 // Handlers
283 // ---------------------------------------------------------------------------
284
285 /**
286 * GET /agents — list every agent on the site.
287 *
288 * @return WP_REST_Response
289 */
290 function openstation_agents_rest_list() {
291 $out = array();
292 foreach ( openstation_agent_get_agents() as $user ) {
293 $shape = openstation_agents_rest_shape_user( $user );
294 if ( $shape ) {
295 $out[] = $shape;
296 }
297 }
298 $response = rest_ensure_response( $out );
299 // Standard collection headers — WP Explorer's root grid derives
300 // its folder counts from `X-WP-Total`.
301 $response->header( 'X-WP-Total', (string) count( $out ) );
302 $response->header( 'X-WP-TotalPages', '1' );
303 return $response;
304 }
305
306 /**
307 * GET /agents/:id — fetch a single agent.
308 *
309 * @param WP_REST_Request $request REST request.
310 * @return WP_REST_Response|WP_Error
311 */
312 function openstation_agents_rest_get( WP_REST_Request $request ) {
313 $user = get_userdata( (int) $request['id'] );
314 if ( ! $user || ! openstation_agent_is_agent( $user ) ) {
315 return new WP_Error(
316 'openstation_agents_not_found',
317 __( 'Agent not found.', 'desktop-mode' ),
318 array( 'status' => 404 )
319 );
320 }
321 return rest_ensure_response( openstation_agents_rest_shape_user( $user ) );
322 }
323
324 /**
325 * POST /agents — create.
326 *
327 * @param WP_REST_Request $request REST request.
328 * @return WP_REST_Response|WP_Error
329 */
330 function openstation_agents_rest_create( WP_REST_Request $request ) {
331 $user = openstation_agent_create(
332 array(
333 'name' => (string) $request['name'],
334 'role' => (string) $request['role'],
335 'description' => (string) $request['description'],
336 'instructions' => (string) $request['instructions'],
337 'abilities' => (array) $request['abilities'],
338 )
339 );
340 if ( is_wp_error( $user ) ) {
341 $data = $user->get_error_data();
342 if ( ! is_array( $data ) || ! isset( $data['status'] ) ) {
343 $user->add_data( array( 'status' => 400 ) );
344 }
345 return $user;
346 }
347
348 $response = rest_ensure_response( openstation_agents_rest_shape_user( $user ) );
349 $response->set_status( 201 );
350 return $response;
351 }
352
353 /**
354 * POST /agents/:id — patch any subset of the definition fields.
355 *
356 * @param WP_REST_Request $request REST request.
357 * @return WP_REST_Response|WP_Error
358 */
359 function openstation_agents_rest_patch( WP_REST_Request $request ) {
360 $user = get_userdata( (int) $request['id'] );
361 if ( ! $user || ! openstation_agent_is_agent( $user ) ) {
362 return new WP_Error(
363 'openstation_agents_not_found',
364 __( 'Agent not found.', 'desktop-mode' ),
365 array( 'status' => 404 )
366 );
367 }
368
369 $body = $request->get_json_params();
370 if ( ! is_array( $body ) ) {
371 $body = $request->get_body_params();
372 }
373 if ( ! is_array( $body ) ) {
374 $body = array();
375 }
376
377 $fields = array();
378 $allowed = array( 'name', 'role', 'description', 'instructions', 'abilities', 'triggers', 'model', 'rateLimit' );
379 foreach ( $allowed as $field ) {
380 if ( array_key_exists( $field, $body ) ) {
381 $fields[ $field ] = $body[ $field ];
382 }
383 }
384
385 $updated = openstation_agent_update( (int) $user->ID, $fields );
386 if ( is_wp_error( $updated ) ) {
387 $updated->add_data( array( 'status' => 400 ) );
388 return $updated;
389 }
390
391 return rest_ensure_response(
392 openstation_agents_rest_shape_user( get_userdata( (int) $user->ID ) )
393 );
394 }
395
396 /**
397 * DELETE /agents/:id.
398 *
399 * @param WP_REST_Request $request REST request.
400 * @return WP_REST_Response|WP_Error
401 */
402 function openstation_agents_rest_delete( WP_REST_Request $request ) {
403 $user_id = (int) $request['id'];
404 $user = get_userdata( $user_id );
405 if ( ! $user || ! openstation_agent_is_agent( $user ) ) {
406 return new WP_Error(
407 'openstation_agents_not_found',
408 __( 'Agent not found.', 'desktop-mode' ),
409 array( 'status' => 404 )
410 );
411 }
412
413 $result = openstation_agent_delete( $user_id );
414 if ( is_wp_error( $result ) ) {
415 $result->add_data( array( 'status' => 500 ) );
416 return $result;
417 }
418
419 return rest_ensure_response(
420 array(
421 'deleted' => true,
422 'id' => $user_id,
423 )
424 );
425 }
426
427 /**
428 * POST /agents/:id/invoke — run the agent with the supplied message.
429 *
430 * @param WP_REST_Request $request REST request.
431 * @return WP_REST_Response|WP_Error
432 */
433 function openstation_agents_rest_invoke( WP_REST_Request $request ) {
434 $user = get_userdata( (int) $request['id'] );
435 if ( ! $user || ! openstation_agent_is_agent( $user ) ) {
436 return new WP_Error(
437 'openstation_agents_not_found',
438 __( 'Agent not found.', 'desktop-mode' ),
439 array( 'status' => 404 )
440 );
441 }
442
443 $source = (string) $request['source'];
444
445 // Per-agent gate. The route's `permission_callback` cannot run this
446 // one: it has no access to the resolved agent, and the capability an
447 // agent requires is a property of that agent's trigger config.
448 if ( ! openstation_agent_user_can_invoke_agent( (int) $user->ID, $source ) ) {
449 return new WP_Error(
450 'openstation_agents_forbidden',
451 __( 'You do not have permission to invoke this agent.', 'desktop-mode' ),
452 array( 'status' => rest_authorization_required_code() )
453 );
454 }
455
456 $result = openstation_agent_invoke(
457 (int) $user->ID,
458 (string) $request['message'],
459 array(
460 'source' => $source,
461 'invoker' => get_current_user_id(),
462 'history' => (array) $request['history'],
463 )
464 );
465 if ( is_wp_error( $result ) ) {
466 $data = $result->get_error_data();
467 if ( ! is_array( $data ) || ! isset( $data['status'] ) ) {
468 $result->add_data( array( 'status' => 500 ) );
469 }
470 return $result;
471 }
472 return rest_ensure_response( $result );
473 }
474
475 /**
476 * GET /agents/abilities — the abilities catalogue for the picker.
477 *
478 * @return WP_REST_Response
479 */
480 function openstation_agents_rest_abilities_catalogue() {
481 return rest_ensure_response( openstation_agents_abilities_catalogue() );
482 }
483
484 /**
485 * GET /agents/trigger-kinds — the trigger-kinds catalogue.
486 *
487 * @return WP_REST_Response
488 */
489 function openstation_agents_rest_trigger_kinds() {
490 return rest_ensure_response( openstation_agent_trigger_kinds() );
491 }
492
493 /**
494 * GET /agents/hooks-catalogue — the curated WP hooks catalogue.
495 *
496 * @return WP_REST_Response
497 */
498 function openstation_agents_rest_hooks_catalogue() {
499 return rest_ensure_response( openstation_agent_hooks_catalogue() );
500 }
501
502 /**
503 * GET /agents/roles — roles the current user may assign to an agent.
504 *
505 * @return WP_REST_Response
506 */
507 function openstation_agents_rest_roles() {
508 $names = wp_roles()->get_names();
509 $out = array();
510 foreach ( openstation_agent_allowed_roles() as $slug ) {
511 $out[] = array(
512 'slug' => $slug,
513 'label' => isset( $names[ $slug ] ) ? translate_user_role( $names[ $slug ] ) : $slug,
514 );
515 }
516 return rest_ensure_response( $out );
517 }
518
519 /**
520 * Build the canonical REST shape for one agent.
521 *
522 * @param WP_User|null $user Agent user.
523 * @return array|null Null when the user is not an agent.
524 */
525 function openstation_agents_rest_shape_user( $user ) {
526 if ( ! $user instanceof WP_User || ! openstation_agent_is_agent( $user ) ) {
527 return null;
528 }
529
530 $slug = (string) $user->user_login;
531 if ( 0 === strpos( $slug, 'agent-' ) ) {
532 $slug = substr( $slug, strlen( 'agent-' ) );
533 }
534
535 $role = '';
536 if ( is_array( $user->roles ) && ! empty( $user->roles ) ) {
537 $role = (string) reset( $user->roles );
538 }
539
540 $avatar = get_avatar_url( $user->ID, array( 'size' => 96 ) );
541 if ( ! is_string( $avatar ) || '' === $avatar ) {
542 $avatar = openstation_agent_avatar_url();
543 }
544
545 return array(
546 'id' => (int) $user->ID,
547 'slug' => $slug,
548 'name' => (string) $user->display_name,
549 'description' => openstation_agent_get_description( (int) $user->ID ),
550 'instructions' => openstation_agent_get_instructions( (int) $user->ID ),
551 'role' => $role,
552 'abilities' => openstation_agent_get_abilities( (int) $user->ID ),
553 'triggers' => openstation_agent_get_triggers( (int) $user->ID ),
554 'model' => openstation_agent_get_model( (int) $user->ID ),
555 'rateLimit' => openstation_agent_get_rate_limit( (int) $user->ID ),
556 'avatarUrl' => $avatar,
557 );
558 }
559