PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.1
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.1
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.1.1, at includes/agents/rest.php

522 lines 15.1 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 // The three capability gates themselves (`openstation_agents_user_can_read`
191 // / `_manage` / `_invoke`) live in bootstrap.php: the WP Explorer
192 // integration loads while the feature flag is off, and this file does
193 // not.
194 // ---------------------------------------------------------------------------
195
196 /**
197 * Read-route permission callback.
198 *
199 * @return bool|WP_Error
200 */
201 function openstation_agents_rest_read_permission() {
202 if ( ! is_user_logged_in() || ! openstation_agents_user_can_read() ) {
203 return new WP_Error(
204 'openstation_agents_forbidden',
205 __( 'You do not have permission to read OpenStation agents.', 'desktop-mode' ),
206 array( 'status' => rest_authorization_required_code() )
207 );
208 }
209 return true;
210 }
211
212 /**
213 * Write-route permission callback.
214 *
215 * @return bool|WP_Error
216 */
217 function openstation_agents_rest_write_permission() {
218 if ( ! is_user_logged_in() || ! openstation_agents_user_can_manage() ) {
219 return new WP_Error(
220 'openstation_agents_forbidden',
221 __( 'You do not have permission to manage OpenStation agents.', 'desktop-mode' ),
222 array( 'status' => rest_authorization_required_code() )
223 );
224 }
225 return true;
226 }
227
228 /**
229 * Invoke-route permission callback.
230 *
231 * @return bool|WP_Error
232 */
233 function openstation_agents_rest_invoke_permission() {
234 if ( ! is_user_logged_in() || ! openstation_agents_user_can_invoke() ) {
235 return new WP_Error(
236 'openstation_agents_forbidden',
237 __( 'You do not have permission to invoke OpenStation agents.', 'desktop-mode' ),
238 array( 'status' => rest_authorization_required_code() )
239 );
240 }
241 return true;
242 }
243
244 // ---------------------------------------------------------------------------
245 // Handlers
246 // ---------------------------------------------------------------------------
247
248 /**
249 * GET /agents — list every agent on the site.
250 *
251 * @return WP_REST_Response
252 */
253 function openstation_agents_rest_list() {
254 $out = array();
255 foreach ( openstation_agent_get_agents() as $user ) {
256 $shape = openstation_agents_rest_shape_user( $user );
257 if ( $shape ) {
258 $out[] = $shape;
259 }
260 }
261 $response = rest_ensure_response( $out );
262 // Standard collection headers — WP Explorer's root grid derives
263 // its folder counts from `X-WP-Total`.
264 $response->header( 'X-WP-Total', (string) count( $out ) );
265 $response->header( 'X-WP-TotalPages', '1' );
266 return $response;
267 }
268
269 /**
270 * GET /agents/:id — fetch a single agent.
271 *
272 * @param WP_REST_Request $request REST request.
273 * @return WP_REST_Response|WP_Error
274 */
275 function openstation_agents_rest_get( WP_REST_Request $request ) {
276 $user = get_userdata( (int) $request['id'] );
277 if ( ! $user || ! openstation_agent_is_agent( $user ) ) {
278 return new WP_Error(
279 'openstation_agents_not_found',
280 __( 'Agent not found.', 'desktop-mode' ),
281 array( 'status' => 404 )
282 );
283 }
284 return rest_ensure_response( openstation_agents_rest_shape_user( $user ) );
285 }
286
287 /**
288 * POST /agents — create.
289 *
290 * @param WP_REST_Request $request REST request.
291 * @return WP_REST_Response|WP_Error
292 */
293 function openstation_agents_rest_create( WP_REST_Request $request ) {
294 $user = openstation_agent_create(
295 array(
296 'name' => (string) $request['name'],
297 'role' => (string) $request['role'],
298 'description' => (string) $request['description'],
299 'instructions' => (string) $request['instructions'],
300 'abilities' => (array) $request['abilities'],
301 )
302 );
303 if ( is_wp_error( $user ) ) {
304 $data = $user->get_error_data();
305 if ( ! is_array( $data ) || ! isset( $data['status'] ) ) {
306 $user->add_data( array( 'status' => 400 ) );
307 }
308 return $user;
309 }
310
311 $response = rest_ensure_response( openstation_agents_rest_shape_user( $user ) );
312 $response->set_status( 201 );
313 return $response;
314 }
315
316 /**
317 * POST /agents/:id — patch any subset of the definition fields.
318 *
319 * @param WP_REST_Request $request REST request.
320 * @return WP_REST_Response|WP_Error
321 */
322 function openstation_agents_rest_patch( WP_REST_Request $request ) {
323 $user = get_userdata( (int) $request['id'] );
324 if ( ! $user || ! openstation_agent_is_agent( $user ) ) {
325 return new WP_Error(
326 'openstation_agents_not_found',
327 __( 'Agent not found.', 'desktop-mode' ),
328 array( 'status' => 404 )
329 );
330 }
331
332 $body = $request->get_json_params();
333 if ( ! is_array( $body ) ) {
334 $body = $request->get_body_params();
335 }
336 if ( ! is_array( $body ) ) {
337 $body = array();
338 }
339
340 $fields = array();
341 $allowed = array( 'name', 'role', 'description', 'instructions', 'abilities', 'triggers', 'model', 'rateLimit' );
342 foreach ( $allowed as $field ) {
343 if ( array_key_exists( $field, $body ) ) {
344 $fields[ $field ] = $body[ $field ];
345 }
346 }
347
348 $updated = openstation_agent_update( (int) $user->ID, $fields );
349 if ( is_wp_error( $updated ) ) {
350 $updated->add_data( array( 'status' => 400 ) );
351 return $updated;
352 }
353
354 return rest_ensure_response(
355 openstation_agents_rest_shape_user( get_userdata( (int) $user->ID ) )
356 );
357 }
358
359 /**
360 * DELETE /agents/:id.
361 *
362 * @param WP_REST_Request $request REST request.
363 * @return WP_REST_Response|WP_Error
364 */
365 function openstation_agents_rest_delete( WP_REST_Request $request ) {
366 $user_id = (int) $request['id'];
367 $user = get_userdata( $user_id );
368 if ( ! $user || ! openstation_agent_is_agent( $user ) ) {
369 return new WP_Error(
370 'openstation_agents_not_found',
371 __( 'Agent not found.', 'desktop-mode' ),
372 array( 'status' => 404 )
373 );
374 }
375
376 $result = openstation_agent_delete( $user_id );
377 if ( is_wp_error( $result ) ) {
378 $result->add_data( array( 'status' => 500 ) );
379 return $result;
380 }
381
382 return rest_ensure_response(
383 array(
384 'deleted' => true,
385 'id' => $user_id,
386 )
387 );
388 }
389
390 /**
391 * POST /agents/:id/invoke — run the agent with the supplied message.
392 *
393 * @param WP_REST_Request $request REST request.
394 * @return WP_REST_Response|WP_Error
395 */
396 function openstation_agents_rest_invoke( WP_REST_Request $request ) {
397 $user = get_userdata( (int) $request['id'] );
398 if ( ! $user || ! openstation_agent_is_agent( $user ) ) {
399 return new WP_Error(
400 'openstation_agents_not_found',
401 __( 'Agent not found.', 'desktop-mode' ),
402 array( 'status' => 404 )
403 );
404 }
405
406 $source = (string) $request['source'];
407
408 // Per-agent gate. The route's `permission_callback` cannot run this
409 // one: it has no access to the resolved agent, and the capability an
410 // agent requires is a property of that agent's trigger config.
411 if ( ! openstation_agent_user_can_invoke_agent( (int) $user->ID, $source ) ) {
412 return new WP_Error(
413 'openstation_agents_forbidden',
414 __( 'You do not have permission to invoke this agent.', 'desktop-mode' ),
415 array( 'status' => rest_authorization_required_code() )
416 );
417 }
418
419 $result = openstation_agent_invoke(
420 (int) $user->ID,
421 (string) $request['message'],
422 array(
423 'source' => $source,
424 'invoker' => get_current_user_id(),
425 'history' => (array) $request['history'],
426 )
427 );
428 if ( is_wp_error( $result ) ) {
429 $data = $result->get_error_data();
430 if ( ! is_array( $data ) || ! isset( $data['status'] ) ) {
431 $result->add_data( array( 'status' => 500 ) );
432 }
433 return $result;
434 }
435 return rest_ensure_response( $result );
436 }
437
438 /**
439 * GET /agents/abilities — the abilities catalogue for the picker.
440 *
441 * @return WP_REST_Response
442 */
443 function openstation_agents_rest_abilities_catalogue() {
444 return rest_ensure_response( openstation_agents_abilities_catalogue() );
445 }
446
447 /**
448 * GET /agents/trigger-kinds — the trigger-kinds catalogue.
449 *
450 * @return WP_REST_Response
451 */
452 function openstation_agents_rest_trigger_kinds() {
453 return rest_ensure_response( openstation_agent_trigger_kinds() );
454 }
455
456 /**
457 * GET /agents/hooks-catalogue — the curated WP hooks catalogue.
458 *
459 * @return WP_REST_Response
460 */
461 function openstation_agents_rest_hooks_catalogue() {
462 return rest_ensure_response( openstation_agent_hooks_catalogue() );
463 }
464
465 /**
466 * GET /agents/roles — roles the current user may assign to an agent.
467 *
468 * @return WP_REST_Response
469 */
470 function openstation_agents_rest_roles() {
471 $names = wp_roles()->get_names();
472 $out = array();
473 foreach ( openstation_agent_allowed_roles() as $slug ) {
474 $out[] = array(
475 'slug' => $slug,
476 'label' => isset( $names[ $slug ] ) ? translate_user_role( $names[ $slug ] ) : $slug,
477 );
478 }
479 return rest_ensure_response( $out );
480 }
481
482 /**
483 * Build the canonical REST shape for one agent.
484 *
485 * @param WP_User|null $user Agent user.
486 * @return array|null Null when the user is not an agent.
487 */
488 function openstation_agents_rest_shape_user( $user ) {
489 if ( ! $user instanceof WP_User || ! openstation_agent_is_agent( $user ) ) {
490 return null;
491 }
492
493 $slug = (string) $user->user_login;
494 if ( 0 === strpos( $slug, 'agent-' ) ) {
495 $slug = substr( $slug, strlen( 'agent-' ) );
496 }
497
498 $role = '';
499 if ( is_array( $user->roles ) && ! empty( $user->roles ) ) {
500 $role = (string) reset( $user->roles );
501 }
502
503 $avatar = get_avatar_url( $user->ID, array( 'size' => 96 ) );
504 if ( ! is_string( $avatar ) || '' === $avatar ) {
505 $avatar = openstation_agent_avatar_url();
506 }
507
508 return array(
509 'id' => (int) $user->ID,
510 'slug' => $slug,
511 'name' => (string) $user->display_name,
512 'description' => openstation_agent_get_description( (int) $user->ID ),
513 'instructions' => openstation_agent_get_instructions( (int) $user->ID ),
514 'role' => $role,
515 'abilities' => openstation_agent_get_abilities( (int) $user->ID ),
516 'triggers' => openstation_agent_get_triggers( (int) $user->ID ),
517 'model' => openstation_agent_get_model( (int) $user->ID ),
518 'rateLimit' => openstation_agent_get_rate_limit( (int) $user->ID ),
519 'avatarUrl' => $avatar,
520 );
521 }
522