PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.12
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.12
1.1.12 1.1.11 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 All 36 releases
← All changes | includes/agents/store.php +451 -130 0.9.8 → 1.1.12 View file →
@@ -1,10 +1,10 @@
1 1 <?php
2 2 /**
3 - * Desktop Mode — Agents: definition store (user meta on the agent row).
3 + * OpenStation — Agents: definition store (user meta on the agent row).
4 4 *
5 5 * Everything that defines an agent beyond its `wp_users` row lives as
6 - * user meta on that row, in one `_desktop_mode_agent_*` key family:
6 + * user meta on that row, in one `_openstation_agent_*` key family:
7 7 *
8 8 * - `_desktop_mode_agent` marker ('1') — the existence test
9 9 * - `_desktop_mode_agent_description` "when to use" short text
10 10 * - `_desktop_mode_agent_instructions` system prompt (markdown)
@@ -16,9 +16,9 @@
16 16 * - `_desktop_mode_agent_rate_limit` invocations/hour, 0 = default
17 17 * - `_desktop_mode_agent_created_by` creating user id (audit aid)
18 18 *
19 19 * User meta has no revisions — the audit trail for definition changes
20 - * is the `desktop_mode_agent_{created,updated,deleted}` actions fired
20 + * is the `openstation_agent_{created,updated,deleted}` actions fired
21 21 * from this module's orchestrators, each carrying before/after values
22 22 * so logging plugins can persist a history.
23 23 *
24 24 * This module owns every key: constants, `register_meta()` calls,
@@ -24,47 +24,130 @@
24 24 * This module owns every key: constants, `register_meta()` calls,
25 25 * sanitization, getters/setters, and the create/update orchestrators
26 26 * the REST surface calls. `identity.php` owns the user row itself.
27 27 *
28 - * @package WPDesktopMode
28 + * @package OpenStation
29 29 */
30 30
31 31 defined( 'ABSPATH' ) || exit;
32 32
33 -require_once DESKTOP_MODE_DIR . 'includes/agents/guard.php';
33 +require_once OPENSTATION_DIR . 'includes/agents/guard.php';
34 34
35 35 /**
36 36 * Meta keys owned by the agents store. Constants so the other layer
37 37 * files reuse them instead of typing the literals.
38 38 *
39 - * `DESKTOP_MODE_AGENT_USER_MARKER_META` is the exception — it lives in
39 + * `OPENSTATION_AGENT_USER_MARKER_META` is the exception — it lives in
40 40 * guard.php, which loads unconditionally, because the agent test has to
41 41 * resolve even when this module does not load.
42 + *
43 + * The VALUE keeps its pre-rebrand spelling on purpose: it is a
44 + * persisted or externally-visible identifier, so renaming it would
45 + * orphan data already written by live installs (or break a live
46 + * URL). The mismatch between this constant's name and its value is
47 + * deliberate — it is NOT a half-finished rename.
42 48 */
43 -const DESKTOP_MODE_AGENT_DESCRIPTION_META = '_desktop_mode_agent_description';
44 -const DESKTOP_MODE_AGENT_INSTRUCTIONS_META = '_desktop_mode_agent_instructions';
45 -const DESKTOP_MODE_AGENT_ABILITIES_META = '_desktop_mode_agent_abilities';
46 -const DESKTOP_MODE_AGENT_TRIGGERS_META = '_desktop_mode_agent_triggers';
47 -const DESKTOP_MODE_AGENT_MODEL_META = '_desktop_mode_agent_model';
48 -const DESKTOP_MODE_AGENT_RATE_LIMIT_META = '_desktop_mode_agent_rate_limit';
49 -const DESKTOP_MODE_AGENT_CREATED_BY_META = '_desktop_mode_agent_created_by';
49 +const OPENSTATION_AGENT_DESCRIPTION_META = '_desktop_mode_agent_description';
50 +/**
51 + * The VALUE keeps its pre-rebrand spelling on purpose: it is a
52 + * persisted or externally-visible identifier, so renaming it would
53 + * orphan data already written by live installs (or break a live
54 + * URL). The mismatch between this constant's name and its value is
55 + * deliberate — it is NOT a half-finished rename.
56 + */
57 +const OPENSTATION_AGENT_INSTRUCTIONS_META = '_desktop_mode_agent_instructions';
58 +/**
59 + * The VALUE keeps its pre-rebrand spelling on purpose: it is a
60 + * persisted or externally-visible identifier, so renaming it would
61 + * orphan data already written by live installs (or break a live
62 + * URL). The mismatch between this constant's name and its value is
63 + * deliberate — it is NOT a half-finished rename.
64 + */
65 +const OPENSTATION_AGENT_ABILITIES_META = '_desktop_mode_agent_abilities';
66 +/**
67 + * The VALUE keeps its pre-rebrand spelling on purpose: it is a
68 + * persisted or externally-visible identifier, so renaming it would
69 + * orphan data already written by live installs (or break a live
70 + * URL). The mismatch between this constant's name and its value is
71 + * deliberate — it is NOT a half-finished rename.
72 + */
73 +const OPENSTATION_AGENT_TRIGGERS_META = '_desktop_mode_agent_triggers';
74 +/**
75 + * The VALUE keeps its pre-rebrand spelling on purpose: it is a
76 + * persisted or externally-visible identifier, so renaming it would
77 + * orphan data already written by live installs (or break a live
78 + * URL). The mismatch between this constant's name and its value is
79 + * deliberate — it is NOT a half-finished rename.
80 + */
81 +const OPENSTATION_AGENT_MODEL_META = '_desktop_mode_agent_model';
82 +/**
83 + * The VALUE keeps its pre-rebrand spelling on purpose: it is a
84 + * persisted or externally-visible identifier, so renaming it would
85 + * orphan data already written by live installs (or break a live
86 + * URL). The mismatch between this constant's name and its value is
87 + * deliberate — it is NOT a half-finished rename.
88 + */
89 +const OPENSTATION_AGENT_RATE_LIMIT_META = '_desktop_mode_agent_rate_limit';
90 +/**
91 + * The VALUE keeps its pre-rebrand spelling on purpose: it is a
92 + * persisted or externally-visible identifier, so renaming it would
93 + * orphan data already written by live installs (or break a live
94 + * URL). The mismatch between this constant's name and its value is
95 + * deliberate — it is NOT a half-finished rename.
96 + */
97 +const OPENSTATION_AGENT_CREATED_BY_META = '_desktop_mode_agent_created_by';
98 +/**
99 + * The VALUE keeps its pre-rebrand spelling on purpose: it is a
100 + * persisted or externally-visible identifier, so renaming it would
101 + * orphan data already written by live installs (or break a live
102 + * URL). The mismatch between this constant's name and its value is
103 + * deliberate — it is NOT a half-finished rename.
104 + */
105 +const OPENSTATION_AGENT_VIBES_META = '_desktop_mode_agent_vibes';
50 106
51 107 /**
108 + * Longest voice line an agent may carry.
109 + *
110 + * Short enough that it stays a voice rather than becoming a second
111 + * instruction block by volume.
112 + */
113 +const OPENSTATION_AGENT_VIBES_MAX_LENGTH = 120;
114 +/**
115 + * The VALUE keeps its pre-rebrand spelling on purpose: it is a
116 + * persisted or externally-visible identifier, so renaming it would
117 + * orphan data already written by live installs (or break a live
118 + * URL). The mismatch between this constant's name and its value is
119 + * deliberate — it is NOT a half-finished rename.
120 + */
121 +const OPENSTATION_AGENT_FACE_META = '_desktop_mode_agent_face';
122 +/**
123 + * The VALUE keeps its pre-rebrand spelling on purpose: it is a
124 + * persisted or externally-visible identifier, so renaming it would
125 + * orphan data already written by live installs (or break a live
126 + * URL). The mismatch between this constant's name and its value is
127 + * deliberate — it is NOT a half-finished rename.
128 + */
129 +const OPENSTATION_AGENT_FACE_SEED_META = '_desktop_mode_agent_face_seed';
130 +
131 +/**
52 132 * Every meta key the store writes — the privacy eraser and any future
53 133 * cleanup path iterate this list instead of re-typing the constants.
54 134 *
55 135 * @return string[]
56 136 */
57 -function desktop_mode_agent_meta_keys() {
137 +function openstation_agent_meta_keys() {
58 138 return array(
59 - DESKTOP_MODE_AGENT_USER_MARKER_META,
60 - DESKTOP_MODE_AGENT_DESCRIPTION_META,
61 - DESKTOP_MODE_AGENT_INSTRUCTIONS_META,
62 - DESKTOP_MODE_AGENT_ABILITIES_META,
63 - DESKTOP_MODE_AGENT_TRIGGERS_META,
64 - DESKTOP_MODE_AGENT_MODEL_META,
65 - DESKTOP_MODE_AGENT_RATE_LIMIT_META,
66 - DESKTOP_MODE_AGENT_CREATED_BY_META,
139 + OPENSTATION_AGENT_USER_MARKER_META,
140 + OPENSTATION_AGENT_DESCRIPTION_META,
141 + OPENSTATION_AGENT_INSTRUCTIONS_META,
142 + OPENSTATION_AGENT_ABILITIES_META,
143 + OPENSTATION_AGENT_TRIGGERS_META,
144 + OPENSTATION_AGENT_MODEL_META,
145 + OPENSTATION_AGENT_RATE_LIMIT_META,
146 + OPENSTATION_AGENT_CREATED_BY_META,
147 + OPENSTATION_AGENT_VIBES_META,
148 + OPENSTATION_AGENT_FACE_META,
149 + OPENSTATION_AGENT_FACE_SEED_META,
67 150 );
68 151 }
69 152
70 153 /**
@@ -75,9 +158,9 @@
75 158 * never exposes agent definitions.
76 159 *
77 160 * @return void
78 161 */
79 -function desktop_mode_agents_register_user_meta() {
162 +function openstation_agents_register_user_meta() {
80 163 $auth = static function () {
81 164 return current_user_can( 'edit_users' );
82 165 };
83 166
@@ -82,9 +165,9 @@
82 165 };
83 166
84 167 register_meta(
85 168 'user',
86 - DESKTOP_MODE_AGENT_DESCRIPTION_META,
169 + OPENSTATION_AGENT_DESCRIPTION_META,
87 170 array(
88 171 'type' => 'string',
89 172 'single' => true,
90 173 'default' => '',
@@ -94,9 +177,9 @@
94 177 )
95 178 );
96 179 register_meta(
97 180 'user',
98 - DESKTOP_MODE_AGENT_INSTRUCTIONS_META,
181 + OPENSTATION_AGENT_INSTRUCTIONS_META,
99 182 array(
100 183 'type' => 'string',
101 184 'single' => true,
102 185 'default' => '',
@@ -106,33 +189,33 @@
106 189 )
107 190 );
108 191 register_meta(
109 192 'user',
110 - DESKTOP_MODE_AGENT_ABILITIES_META,
193 + OPENSTATION_AGENT_ABILITIES_META,
111 194 array(
112 195 'type' => 'string',
113 196 'single' => true,
114 197 'default' => '',
115 198 'show_in_rest' => false,
116 - 'sanitize_callback' => 'desktop_mode_agent_sanitize_abilities_json',
199 + 'sanitize_callback' => 'openstation_agent_sanitize_abilities_json',
117 200 'auth_callback' => $auth,
118 201 )
119 202 );
120 203 register_meta(
121 204 'user',
122 - DESKTOP_MODE_AGENT_TRIGGERS_META,
205 + OPENSTATION_AGENT_TRIGGERS_META,
123 206 array(
124 207 'type' => 'string',
125 208 'single' => true,
126 209 'default' => '',
127 210 'show_in_rest' => false,
128 - 'sanitize_callback' => 'desktop_mode_agent_sanitize_triggers_json',
211 + 'sanitize_callback' => 'openstation_agent_sanitize_triggers_json',
129 212 'auth_callback' => $auth,
130 213 )
131 214 );
132 215 register_meta(
133 216 'user',
134 - DESKTOP_MODE_AGENT_MODEL_META,
217 + OPENSTATION_AGENT_MODEL_META,
135 218 array(
136 219 'type' => 'string',
137 220 'single' => true,
138 221 'default' => '',
@@ -142,9 +225,9 @@
142 225 )
143 226 );
144 227 register_meta(
145 228 'user',
146 - DESKTOP_MODE_AGENT_RATE_LIMIT_META,
229 + OPENSTATION_AGENT_RATE_LIMIT_META,
147 230 array(
148 231 'type' => 'integer',
149 232 'single' => true,
150 233 'default' => 0,
@@ -152,10 +235,46 @@
152 235 'sanitize_callback' => 'absint',
153 236 'auth_callback' => $auth,
154 237 )
155 238 );
239 + register_meta(
240 + 'user',
241 + OPENSTATION_AGENT_VIBES_META,
242 + array(
243 + 'type' => 'string',
244 + 'single' => true,
245 + 'default' => '',
246 + 'show_in_rest' => false,
247 + 'sanitize_callback' => 'openstation_agent_sanitize_vibes',
248 + 'auth_callback' => $auth,
249 + )
250 + );
251 + register_meta(
252 + 'user',
253 + OPENSTATION_AGENT_FACE_META,
254 + array(
255 + 'type' => 'string',
256 + 'single' => true,
257 + 'default' => '',
258 + 'show_in_rest' => false,
259 + 'sanitize_callback' => 'openstation_agent_sanitize_face_json',
260 + 'auth_callback' => $auth,
261 + )
262 + );
263 + register_meta(
264 + 'user',
265 + OPENSTATION_AGENT_FACE_SEED_META,
266 + array(
267 + 'type' => 'integer',
268 + 'single' => true,
269 + 'default' => 0,
270 + 'show_in_rest' => false,
271 + 'sanitize_callback' => 'absint',
272 + 'auth_callback' => $auth,
273 + )
274 + );
156 275 }
157 -add_action( 'init', 'desktop_mode_agents_register_user_meta' );
276 +add_action( 'init', 'openstation_agents_register_user_meta' );
158 277
159 278 // ---------------------------------------------------------------------------
160 279 // Sanitizers
161 280 // ---------------------------------------------------------------------------
@@ -165,9 +284,9 @@
165 284 *
166 285 * @param mixed $value Incoming list.
167 286 * @return string[]
168 287 */
169 -function desktop_mode_agents_sanitize_ability_slugs( $value ) {
288 +function openstation_agents_sanitize_ability_slugs( $value ) {
170 289 if ( is_string( $value ) ) {
171 290 $decoded = json_decode( $value, true );
172 291 $value = is_array( $decoded ) ? $decoded : array();
173 292 }
@@ -194,13 +313,118 @@
194 313 *
195 314 * @param mixed $value Incoming value (array or JSON string).
196 315 * @return string JSON-encoded slug list.
197 316 */
198 -function desktop_mode_agent_sanitize_abilities_json( $value ) {
199 - return (string) wp_json_encode( desktop_mode_agents_sanitize_ability_slugs( $value ) );
317 +function openstation_agent_sanitize_abilities_json( $value ) {
318 + return (string) wp_json_encode( openstation_agents_sanitize_ability_slugs( $value ) );
200 319 }
201 320
202 321 /**
322 + * Sanitize an agent's voice line.
323 + *
324 + * One short line of character: "blunt, precise, no sugarcoating". It
325 + * is appended to the agent's instructions at run time, so it reaches a
326 + * language model.
327 + *
328 + * **That is not a new privilege boundary.** Writing it needs
329 + * `edit_users`, the same capability that already lets you write
330 + * `instructions`, which is the entire system prompt. A 120-character
331 + * tone line is strictly less reach than that, so it deliberately sits
332 + * behind the same gate rather than a stricter one, and this note
333 + * exists so nobody "hardens" it later into a confusing split.
334 + *
335 + * Two structural guards it does need. `sanitize_text_field()` strips
336 + * line breaks, which is load-bearing: the runner marks operator turns
337 + * in the composed prompt, and a multi-line voice line could otherwise
338 + * fake a turn boundary. And the length cap keeps a "voice" from
339 + * becoming a second instruction block by volume.
340 + *
341 + * @param mixed $value Incoming line.
342 + * @return string
343 + */
344 +function openstation_agent_sanitize_vibes( $value ) {
345 + if ( ! is_scalar( $value ) ) {
346 + return '';
347 + }
348 + $clean = sanitize_text_field( (string) $value );
349 + return mb_substr( $clean, 0, OPENSTATION_AGENT_VIBES_MAX_LENGTH );
350 +}
351 +
352 +/**
353 + * Sanitize an agent's face for storage.
354 + *
355 + * The narrowing itself is `openstation_mio_narrow_look()`, which the
356 + * WP Explorer config also calls to preview the shipped cast while the
357 + * feature flag is off. Shared on purpose: two copies of "clamp, then
358 + * keep what was carried" is how a preview starts drawing a face the
359 + * seeder would never store. What this adds on top is the storage
360 + * shape — a JSON string, and an empty one when nothing was set, so an
361 + * agent with no opinion keeps no row at all rather than an empty blob.
362 + *
363 + * @param mixed $value Incoming look (array or JSON string).
364 + * @return string JSON, or an empty string when nothing was set.
365 + */
366 +function openstation_agent_sanitize_face_json( $value ) {
367 + if ( is_string( $value ) ) {
368 + $decoded = json_decode( $value, true );
369 + $value = is_array( $decoded ) ? $decoded : array();
370 + }
371 + $out = openstation_mio_narrow_look( $value );
372 + if ( empty( $out['appearance'] ) && empty( $out['physics'] ) ) {
373 + return '';
374 + }
375 + return (string) wp_json_encode( $out );
376 +}
377 +
378 +/**
379 + * Read an agent's voice line.
380 + *
381 + * @param int $user_id Agent user id.
382 + * @return string
383 + */
384 +function openstation_agent_get_vibes( $user_id ) {
385 + return (string) get_user_meta( (int) $user_id, OPENSTATION_AGENT_VIBES_META, true );
386 +}
387 +
388 +/**
389 + * Read an agent's stored face.
390 + *
391 + * A partial look: only what was overridden. Empty means "no face
392 + * chosen", which the avatar resolver reads as the shipped robot.
393 + *
394 + * @param int $user_id Agent user id.
395 + * @return array {
396 + * @type array $appearance Partial appearance overrides.
397 + * @type array $physics Partial silhouette overrides.
398 + * }
399 + */
400 +function openstation_agent_get_face( $user_id ) {
401 + $raw = (string) get_user_meta( (int) $user_id, OPENSTATION_AGENT_FACE_META, true );
402 + if ( '' === $raw ) {
403 + return array(
404 + 'appearance' => array(),
405 + 'physics' => array(),
406 + );
407 + }
408 + return openstation_sanitize_mio_look( json_decode( $raw, true ) );
409 +}
410 +
411 +/**
412 + * Read the seed an agent's face was rolled from.
413 + *
414 + * Kept alongside the face rather than instead of it. The face is what
415 + * gets drawn; the seed is provenance, and it is what lets a future
416 + * change to the randomizer's ranges re-roll every agent in one
417 + * migration instead of stranding them on an old palette.
418 + *
419 + * @param int $user_id Agent user id.
420 + * @return int Seed, or 0 when none was recorded.
421 + */
422 +function openstation_agent_get_face_seed( $user_id ) {
423 + return (int) get_user_meta( (int) $user_id, OPENSTATION_AGENT_FACE_SEED_META, true );
424 +}
425 +
426 +/**
203 427 * Sanitize the triggers array.
204 428 *
205 429 * Validates each row against the kind catalogue. Drops any row that
206 430 * doesn't match a known kind — one bad row never rejects the whole
@@ -208,9 +432,9 @@
208 432 *
209 433 * @param mixed $value Incoming triggers array (or JSON string).
210 434 * @return array
211 435 */
212 -function desktop_mode_agent_sanitize_triggers( $value ) {
436 +function openstation_agent_sanitize_triggers( $value ) {
213 437 if ( is_string( $value ) ) {
214 438 $decoded = json_decode( $value, true );
215 439 $value = is_array( $decoded ) ? $decoded : array();
216 440 }
@@ -218,9 +442,9 @@
218 442 return array();
219 443 }
220 444
221 445 $known_kinds = array();
222 - foreach ( desktop_mode_agent_trigger_kinds() as $kind ) {
446 + foreach ( openstation_agent_trigger_kinds() as $kind ) {
223 447 $known_kinds[ $kind['slug'] ] = $kind;
224 448 }
225 449
226 450 $out = array();
@@ -233,9 +457,9 @@
233 457 continue;
234 458 }
235 459
236 460 $config = isset( $row['config'] ) && is_array( $row['config'] ) ? $row['config'] : array();
237 - $config = desktop_mode_agent_sanitize_trigger_config_deep( $config );
461 + $config = openstation_agent_sanitize_trigger_config_deep( $config );
238 462
239 463 $out[] = array(
240 464 'kind' => $kind,
241 465 'config' => $config,
@@ -250,10 +474,10 @@
250 474 *
251 475 * @param mixed $value Incoming value.
252 476 * @return string JSON-encoded triggers list.
253 477 */
254 -function desktop_mode_agent_sanitize_triggers_json( $value ) {
255 - return (string) wp_json_encode( desktop_mode_agent_sanitize_triggers( $value ) );
478 +function openstation_agent_sanitize_triggers_json( $value ) {
479 + return (string) wp_json_encode( openstation_agent_sanitize_triggers( $value ) );
256 480 }
257 481
258 482 /**
259 483 * Recursively coerce trigger-config values into safe primitives.
@@ -266,9 +490,9 @@
266 490 *
267 491 * @param mixed $value Arbitrary input.
268 492 * @return mixed
269 493 */
270 -function desktop_mode_agent_sanitize_trigger_config_deep( $value ) {
494 +function openstation_agent_sanitize_trigger_config_deep( $value ) {
271 495 if ( is_array( $value ) ) {
272 496 $out = array();
273 497 foreach ( $value as $k => $v ) {
274 498 if ( is_string( $k ) ) {
@@ -278,9 +502,9 @@
278 502 }
279 503 } else {
280 504 $key = (int) $k;
281 505 }
282 - $out[ $key ] = desktop_mode_agent_sanitize_trigger_config_deep( $v );
506 + $out[ $key ] = openstation_agent_sanitize_trigger_config_deep( $v );
283 507 }
284 508 return $out;
285 509 }
286 510 if ( is_bool( $value ) || is_int( $value ) ) {
@@ -305,15 +529,15 @@
305 529 * `chat`, `send-to`, and `drag` are wired; the other kinds are declared so the
306 530 * Triggers pane can already store configuration for them, and later
307 531 * phases add the intake plumbing without a storage migration.
308 532 *
309 - * Plugins can extend the list via the `desktop_mode_agent_trigger_kinds`
533 + * Plugins can extend the list via the `openstation_agent_trigger_kinds`
310 534 * filter — each entry must declare a `slug`, `label`, and a JSON-Schema
311 535 * `config_schema` describing the shape of `trigger.config`.
312 536 *
313 537 * @return array<int, array{slug:string,wired:bool,label:string,description:string,icon:string,config_schema:array}>
314 538 */
315 -function desktop_mode_agent_trigger_kinds() {
539 +function openstation_agent_trigger_kinds() {
316 540 $kinds = array(
317 541 array(
318 542 'slug' => 'chat',
319 543 'wired' => true,
@@ -420,9 +644,9 @@
420 644 * Filter the trigger kinds available to agents.
421 645 *
422 646 * @param array $kinds Default trigger kinds.
423 647 */
424 - $filtered = apply_filters( 'desktop_mode_agent_trigger_kinds', $kinds );
648 + $filtered = apply_filters( 'openstation_agent_trigger_kinds', $kinds );
425 649 if ( ! is_array( $filtered ) ) {
426 650 return $kinds;
427 651 }
428 652 return array_values( $filtered );
@@ -436,9 +660,9 @@
436 660 * hook name.
437 661 *
438 662 * @return array<int, array{hook:string, when:string}>
439 663 */
440 -function desktop_mode_agent_hooks_catalogue() {
664 +function openstation_agent_hooks_catalogue() {
441 665 $hooks = array(
442 666 array(
443 667 'hook' => 'save_post',
444 668 'when' => __( 'Every time a post is saved.', 'desktop-mode' ),
@@ -478,9 +702,9 @@
478 702 * trigger configurator.
479 703 *
480 704 * @param array $hooks Default catalogue.
481 705 */
482 - $filtered = apply_filters( 'desktop_mode_agent_hooks_catalogue', $hooks );
706 + $filtered = apply_filters( 'openstation_agent_hooks_catalogue', $hooks );
483 707 return is_array( $filtered ) ? array_values( $filtered ) : $hooks;
484 708 }
485 709
486 710 /**
@@ -507,9 +731,9 @@
507 731 *
508 732 * @param string $role Role slug being assigned.
509 733 * @return bool
510 734 */
511 -function desktop_mode_agent_actor_can_assign_role( $role ) {
735 +function openstation_agent_actor_can_assign_role( $role ) {
512 736 $role = sanitize_key( (string) $role );
513 737 $can = current_user_can( 'promote_users' );
514 738
515 739 if ( $can && 'administrator' === $role ) {
@@ -533,9 +757,9 @@
533 757 * @param string $role Role slug being assigned.
534 758 * @param int $user_id Acting user id (0 when there is none).
535 759 */
536 760 return (bool) apply_filters(
537 - 'desktop_mode_agent_actor_can_assign_role',
761 + 'openstation_agent_actor_can_assign_role',
538 762 $can,
539 763 $role,
540 764 get_current_user_id()
541 765 );
@@ -546,14 +770,14 @@
546 770 * can actually hand out.
547 771 *
548 772 * The whitelist keeps agents in the standard content-role band; each
549 773 * survivor is then run through
550 - * {@see desktop_mode_agent_actor_can_assign_role()}, which is where the
774 + * {@see openstation_agent_actor_can_assign_role()}, which is where the
551 775 * real gating lives.
552 776 *
553 777 * @return string[] Role slugs.
554 778 */
555 -function desktop_mode_agent_allowed_roles() {
779 +function openstation_agent_allowed_roles() {
556 780 $whitelist = array( 'administrator', 'editor', 'author', 'contributor' );
557 781
558 782 /**
559 783 * Filter the roles an agent may be assigned.
@@ -564,9 +788,9 @@
564 788 * to clear both constraints.
565 789 *
566 790 * @param string[] $whitelist Default role slugs.
567 791 */
568 - $whitelist = apply_filters( 'desktop_mode_agent_allowed_roles', $whitelist );
792 + $whitelist = apply_filters( 'openstation_agent_allowed_roles', $whitelist );
569 793 if ( ! is_array( $whitelist ) ) {
570 794 return array();
571 795 }
572 796
@@ -578,9 +802,9 @@
578 802 $candidates = array_intersect( array_map( 'strval', $whitelist ), $editable );
579 803
580 804 $allowed = array();
581 805 foreach ( $candidates as $role ) {
582 - if ( desktop_mode_agent_actor_can_assign_role( $role ) ) {
806 + if ( openstation_agent_actor_can_assign_role( $role ) ) {
583 807 $allowed[] = $role;
584 808 }
585 809 }
586 810
@@ -596,10 +820,10 @@
596 820 *
597 821 * @param int $user_id Agent user id.
598 822 * @return string
599 823 */
600 -function desktop_mode_agent_get_description( $user_id ) {
601 - return (string) get_user_meta( (int) $user_id, DESKTOP_MODE_AGENT_DESCRIPTION_META, true );
824 +function openstation_agent_get_description( $user_id ) {
825 + return (string) get_user_meta( (int) $user_id, OPENSTATION_AGENT_DESCRIPTION_META, true );
602 826 }
603 827
604 828 /**
605 829 * Read the system prompt.
@@ -606,10 +830,10 @@
606 830 *
607 831 * @param int $user_id Agent user id.
608 832 * @return string
609 833 */
610 -function desktop_mode_agent_get_instructions( $user_id ) {
611 - return (string) get_user_meta( (int) $user_id, DESKTOP_MODE_AGENT_INSTRUCTIONS_META, true );
834 +function openstation_agent_get_instructions( $user_id ) {
835 + return (string) get_user_meta( (int) $user_id, OPENSTATION_AGENT_INSTRUCTIONS_META, true );
612 836 }
613 837
614 838 /**
615 839 * Read the ability allowlist.
@@ -616,14 +840,14 @@
616 840 *
617 841 * @param int $user_id Agent user id.
618 842 * @return string[]
619 843 */
620 -function desktop_mode_agent_get_abilities( $user_id ) {
621 - $raw = get_user_meta( (int) $user_id, DESKTOP_MODE_AGENT_ABILITIES_META, true );
844 +function openstation_agent_get_abilities( $user_id ) {
845 + $raw = get_user_meta( (int) $user_id, OPENSTATION_AGENT_ABILITIES_META, true );
622 846 if ( '' === $raw || null === $raw ) {
623 847 return array();
624 848 }
625 - return desktop_mode_agents_sanitize_ability_slugs( $raw );
849 + return openstation_agents_sanitize_ability_slugs( $raw );
626 850 }
627 851
628 852 /**
629 853 * Read triggers.
@@ -630,14 +854,14 @@
630 854 *
631 855 * @param int $user_id Agent user id.
632 856 * @return array
633 857 */
634 -function desktop_mode_agent_get_triggers( $user_id ) {
635 - $raw = get_user_meta( (int) $user_id, DESKTOP_MODE_AGENT_TRIGGERS_META, true );
858 +function openstation_agent_get_triggers( $user_id ) {
859 + $raw = get_user_meta( (int) $user_id, OPENSTATION_AGENT_TRIGGERS_META, true );
636 860 if ( '' === $raw || null === $raw ) {
637 861 return array();
638 862 }
639 - return desktop_mode_agent_sanitize_triggers( $raw );
863 + return openstation_agent_sanitize_triggers( $raw );
640 864 }
641 865
642 866 /**
643 867 * Read the model override.
@@ -644,10 +868,10 @@
644 868 *
645 869 * @param int $user_id Agent user id.
646 870 * @return string Empty string if not set.
647 871 */
648 -function desktop_mode_agent_get_model( $user_id ) {
649 - return (string) get_user_meta( (int) $user_id, DESKTOP_MODE_AGENT_MODEL_META, true );
872 +function openstation_agent_get_model( $user_id ) {
873 + return (string) get_user_meta( (int) $user_id, OPENSTATION_AGENT_MODEL_META, true );
650 874 }
651 875
652 876 /**
653 877 * Read the rate limit (invocations per hour).
@@ -654,10 +878,10 @@
654 878 *
655 879 * @param int $user_id Agent user id.
656 880 * @return int Zero when no per-agent override is set.
657 881 */
658 -function desktop_mode_agent_get_rate_limit( $user_id ) {
659 - return (int) get_user_meta( (int) $user_id, DESKTOP_MODE_AGENT_RATE_LIMIT_META, true );
882 +function openstation_agent_get_rate_limit( $user_id ) {
883 + return (int) get_user_meta( (int) $user_id, OPENSTATION_AGENT_RATE_LIMIT_META, true );
660 884 }
661 885
662 886 // ---------------------------------------------------------------------------
663 887 // Per-agent invocation gate
@@ -666,9 +890,12 @@
666 890 /**
667 891 * The agent's trigger row for a given invocation source, if any.
668 892 *
669 893 * Source slugs on the invoke route map 1:1 onto trigger kinds
670 - * (`chat`, `drag`, `send-to`).
894 + * (`chat`, `drag`, `send-to`). The row is context for the invocation
895 + * filter; it does not decide which capabilities the invocation gate
896 + * requires, which is every capability on every trigger (see
897 + * `openstation_agent_user_can_invoke_agent()`).
671 898 *
672 899 * @param int $agent_user_id Agent user id.
673 900 * @param string $source Invocation source slug.
674 901 * @return array|null Trigger row, or null when the agent declares none
@@ -673,11 +900,11 @@
673 900 * @param string $source Invocation source slug.
674 901 * @return array|null Trigger row, or null when the agent declares none
675 902 * for this source.
676 903 */
677 -function desktop_mode_agent_trigger_for_source( $agent_user_id, $source ) {
904 +function openstation_agent_trigger_for_source( $agent_user_id, $source ) {
678 905 $source = sanitize_key( (string) $source );
679 - foreach ( desktop_mode_agent_get_triggers( (int) $agent_user_id ) as $trigger ) {
906 + foreach ( openstation_agent_get_triggers( (int) $agent_user_id ) as $trigger ) {
680 907 if ( isset( $trigger['kind'] ) && $source === $trigger['kind'] ) {
681 908 return $trigger;
682 909 }
683 910 }
@@ -684,50 +911,72 @@
684 911 return null;
685 912 }
686 913
687 914 /**
688 - * Whether the current user may invoke THIS agent through THIS source.
915 + * Whether the current user may invoke THIS agent.
689 916 *
690 - * The route-level `desktop_mode_agents_user_can_invoke()` check is
917 + * The route-level `openstation_agents_user_can_invoke()` check is
691 918 * site-wide — it answers "may this user invoke agents at all". This is
692 919 * the per-agent half: a trigger may declare a `capability` in its
693 - * config, and until it is enforced here the field is decorative. The
694 - * Triggers pane collects it and the store persists it, so an
695 - * administrator restricting an agent to `manage_options` has every
696 - * reason to believe it took effect.
920 + * config, and the Triggers pane collects it and the store persists it,
921 + * so an administrator restricting an agent to `manage_options` has
922 + * every reason to believe it took effect.
697 923 *
698 - * An agent with no trigger for the source, or a trigger that declares
699 - * no capability, is left to the route-level check — requiring a
924 + * The caller must hold EVERY capability declared on ANY of the agent's
925 + * triggers, whichever source the request names. The source is supplied
926 + * by the client (the invoke route takes it as a request parameter), so
927 + * it describes how the request says it arrived, not what it is allowed
928 + * to reach: a capability scoped to one trigger kind would be satisfied
929 + * by naming another. A capability configured on an agent is therefore
930 + * a property of the agent.
931 + *
932 + * An agent whose triggers declare no capability (including one with no
933 + * triggers at all) is left to the route-level check — requiring a
700 934 * configured trigger would lock out every agent created before triggers
701 935 * were set up, which is all of them by default.
702 936 *
703 937 * @param int $agent_user_id Agent user id.
704 - * @param string $source Invocation source slug.
938 + * @param string $source Invocation source slug the request names
939 + * (`chat`, `drag`, `send-to`). Context for
940 + * the filter only; it does not select which
941 + * capabilities apply.
705 942 * @return bool
706 943 */
707 -function desktop_mode_agent_user_can_invoke_agent( $agent_user_id, $source = 'chat' ) {
708 - $trigger = desktop_mode_agent_trigger_for_source( $agent_user_id, $source );
709 - $capability = '';
710 - if ( is_array( $trigger ) && isset( $trigger['config']['capability'] ) ) {
711 - $capability = trim( (string) $trigger['config']['capability'] );
944 +function openstation_agent_user_can_invoke_agent( $agent_user_id, $source = 'chat' ) {
945 + $can = true;
946 + foreach ( openstation_agent_get_triggers( (int) $agent_user_id ) as $row ) {
947 + if ( ! isset( $row['config']['capability'] ) || ! is_scalar( $row['config']['capability'] ) ) {
948 + continue;
949 + }
950 + $capability = trim( (string) $row['config']['capability'] );
951 + if ( '' !== $capability && ! current_user_can( $capability ) ) {
952 + $can = false;
953 + break;
954 + }
712 955 }
713 956
714 - $can = '' === $capability || current_user_can( $capability );
715 -
716 957 /**
717 958 * Filter whether the current user may invoke a specific agent.
718 959 *
719 - * @param bool $can Whether invocation is allowed.
960 + * @param bool $can Whether invocation is allowed: the
961 + * caller holds every capability
962 + * declared on any of the agent's
963 + * triggers.
720 964 * @param int $agent_user_id Agent user id.
721 - * @param string $source Invocation source slug.
722 - * @param array|null $trigger The matching trigger row, if any.
965 + * @param string $source Invocation source slug the request
966 + * names. Client-supplied on the invoke
967 + * route, so context rather than proof
968 + * of how the request arrived.
969 + * @param array|null $trigger The trigger row whose kind matches
970 + * `$source`, if any. Context only: it
971 + * is not what decided `$can`.
723 972 */
724 973 return (bool) apply_filters(
725 - 'desktop_mode_agent_user_can_invoke_agent',
974 + 'openstation_agent_user_can_invoke_agent',
726 975 $can,
727 976 (int) $agent_user_id,
728 977 (string) $source,
729 - $trigger
978 + openstation_agent_trigger_for_source( $agent_user_id, $source )
730 979 );
731 980 }
732 981
733 982 // ---------------------------------------------------------------------------
@@ -739,11 +988,11 @@
739 988 *
740 989 * @param array $args Optional overrides merged into the `get_users()` query.
741 990 * @return WP_User[]
742 991 */
743 -function desktop_mode_agent_get_agents( $args = array() ) {
992 +function openstation_agent_get_agents( $args = array() ) {
744 993 $defaults = array(
745 - 'meta_key' => DESKTOP_MODE_AGENT_USER_MARKER_META, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
994 + 'meta_key' => OPENSTATION_AGENT_USER_MARKER_META, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
746 995 'meta_value' => '1', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
747 996 'orderby' => 'display_name',
748 997 'order' => 'ASC',
749 998 'number' => 200,
@@ -757,22 +1006,22 @@
757 1006
758 1007 /**
759 1008 * Create an agent: synthetic user row + definition meta.
760 1009 *
761 - * @param array{name:string, role:string, slug?:string, description?:string, instructions?:string, abilities?:array} $args Creation args.
1010 + * @param array{name:string, role:string, slug?:string, description?:string, instructions?:string, abilities?:array, triggers?:array, vibes?:string, face?:array|string, faceSeed?:int} $args Creation args.
762 1011 * @return WP_User|WP_Error
763 1012 */
764 -function desktop_mode_agent_create( $args ) {
1013 +function openstation_agent_create( $args ) {
765 1014 $role = isset( $args['role'] ) ? sanitize_key( (string) $args['role'] ) : '';
766 - $allowed = desktop_mode_agent_allowed_roles();
1015 + $allowed = openstation_agent_allowed_roles();
767 1016 if ( '' === $role || ! in_array( $role, $allowed, true ) ) {
768 1017 return new WP_Error(
769 - 'desktop_mode_agent_invalid_role',
1018 + 'openstation_agent_invalid_role',
770 1019 __( 'Pick a role you are allowed to assign to an agent.', 'desktop-mode' )
771 1020 );
772 1021 }
773 1022
774 - $user = desktop_mode_agent_create_user( $args );
1023 + $user = openstation_agent_create_user( $args );
775 1024 if ( is_wp_error( $user ) ) {
776 1025 return $user;
777 1026 }
778 1027
@@ -777,20 +1026,41 @@
777 1026 }
778 1027
779 1028 $description = isset( $args['description'] ) ? sanitize_text_field( (string) $args['description'] ) : '';
780 1029 $instructions = isset( $args['instructions'] ) ? wp_kses_post( (string) $args['instructions'] ) : '';
781 - $abilities = isset( $args['abilities'] ) ? desktop_mode_agents_sanitize_ability_slugs( $args['abilities'] ) : array();
1030 + $abilities = isset( $args['abilities'] ) ? openstation_agents_sanitize_ability_slugs( $args['abilities'] ) : array();
1031 + $triggers = isset( $args['triggers'] ) ? openstation_agent_sanitize_triggers( $args['triggers'] ) : array();
1032 + $vibes = isset( $args['vibes'] ) ? openstation_agent_sanitize_vibes( $args['vibes'] ) : '';
1033 + $face = isset( $args['face'] ) ? openstation_agent_sanitize_face_json( $args['face'] ) : '';
1034 + // Every agent gets a seed even when nobody chose a face, so the
1035 + // backfill has something deterministic to roll from and two admins
1036 + // racing it land on the same portrait. `crc32` of the login is
1037 + // stable, cheap, and already unique per agent.
1038 + $seed = isset( $args['faceSeed'] ) ? absint( $args['faceSeed'] ) : 0;
1039 + if ( 0 === $seed ) {
1040 + $seed = crc32( (string) $user->user_login );
1041 + }
782 1042
783 1043 if ( '' !== $description ) {
784 - update_user_meta( $user->ID, DESKTOP_MODE_AGENT_DESCRIPTION_META, $description );
1044 + update_user_meta( $user->ID, OPENSTATION_AGENT_DESCRIPTION_META, $description );
785 1045 }
786 1046 if ( '' !== $instructions ) {
787 - update_user_meta( $user->ID, DESKTOP_MODE_AGENT_INSTRUCTIONS_META, $instructions );
1047 + update_user_meta( $user->ID, OPENSTATION_AGENT_INSTRUCTIONS_META, $instructions );
788 1048 }
789 1049 if ( ! empty( $abilities ) ) {
790 - update_user_meta( $user->ID, DESKTOP_MODE_AGENT_ABILITIES_META, wp_json_encode( $abilities ) );
1050 + update_user_meta( $user->ID, OPENSTATION_AGENT_ABILITIES_META, wp_json_encode( $abilities ) );
791 1051 }
792 - update_user_meta( $user->ID, DESKTOP_MODE_AGENT_CREATED_BY_META, get_current_user_id() );
1052 + if ( ! empty( $triggers ) ) {
1053 + update_user_meta( $user->ID, OPENSTATION_AGENT_TRIGGERS_META, wp_json_encode( $triggers ) );
1054 + }
1055 + if ( '' !== $vibes ) {
1056 + update_user_meta( $user->ID, OPENSTATION_AGENT_VIBES_META, $vibes );
1057 + }
1058 + if ( '' !== $face ) {
1059 + update_user_meta( $user->ID, OPENSTATION_AGENT_FACE_META, $face );
1060 + }
1061 + update_user_meta( $user->ID, OPENSTATION_AGENT_FACE_SEED_META, $seed );
1062 + update_user_meta( $user->ID, OPENSTATION_AGENT_CREATED_BY_META, get_current_user_id() );
793 1063
794 1064 /**
795 1065 * Fires after an agent is created.
796 1066 *
@@ -799,9 +1069,9 @@
799 1069 * description, instructions, abilities).
800 1070 * @param int $actor_id User who created the agent.
801 1071 */
802 1072 do_action(
803 - 'desktop_mode_agent_created',
1073 + 'openstation_agent_created',
804 1074 (int) $user->ID,
805 1075 array(
806 1076 'name' => (string) $user->display_name,
807 1077 'role' => $role,
@@ -807,8 +1077,11 @@
807 1077 'role' => $role,
808 1078 'description' => $description,
809 1079 'instructions' => $instructions,
810 1080 'abilities' => $abilities,
1081 + 'vibes' => $vibes,
1082 + 'face' => $face,
1083 + 'faceSeed' => $seed,
811 1084 ),
812 1085 get_current_user_id()
813 1086 );
814 1087
@@ -816,23 +1089,24 @@
816 1089 }
817 1090
818 1091 /**
819 1092 * Update an agent's definition. Accepts any subset of the recognized
820 - * fields, applies the valid ones, and fires `desktop_mode_agent_updated`
1093 + * fields, applies the valid ones, and fires `openstation_agent_updated`
821 1094 * once with a before/after map of everything that changed.
822 1095 *
823 1096 * Recognized fields: `name`, `role`, `description`, `instructions`,
824 - * `abilities`, `triggers`, `model`, `rateLimit`.
1097 + * `abilities`, `triggers`, `model`, `rateLimit`, `vibes`, `face`,
1098 + * `faceSeed`.
825 1099 *
826 1100 * @param int $user_id Agent user id.
827 1101 * @param array $fields Field map.
828 1102 * @return true|WP_Error
829 1103 */
830 -function desktop_mode_agent_update( $user_id, array $fields ) {
1104 +function openstation_agent_update( $user_id, array $fields ) {
831 1105 $user = get_userdata( (int) $user_id );
832 - if ( ! $user || ! desktop_mode_agent_is_agent( $user ) ) {
1106 + if ( ! $user || ! openstation_agent_is_agent( $user ) ) {
833 1107 return new WP_Error(
834 - 'desktop_mode_agent_not_found',
1108 + 'openstation_agent_not_found',
835 1109 __( 'Agent not found.', 'desktop-mode' )
836 1110 );
837 1111 }
838 1112
@@ -841,13 +1115,16 @@
841 1115 if ( isset( $fields['name'] ) ) {
842 1116 $name = sanitize_text_field( (string) $fields['name'] );
843 1117 if ( '' === $name ) {
844 1118 return new WP_Error(
845 - 'desktop_mode_agent_invalid_name',
1119 + 'openstation_agent_invalid_name',
846 1120 __( 'Agent name cannot be empty.', 'desktop-mode' )
847 1121 );
848 1122 }
849 - if ( $name !== (string) $user->display_name ) {
1123 + // Compared as plain text: the stored name carries entities
1124 + // (`&amp;`), and the client sends back the decoded one it was
1125 + // given on every save. Raw, that reads as a rename.
1126 + if ( openstation_plain_text_title( $name ) !== openstation_plain_text_title( $user->display_name ) ) {
850 1127 $changed['name'] = array(
851 1128 'from' => (string) $user->display_name,
852 1129 'to' => $name,
853 1130 );
@@ -862,11 +1139,11 @@
862 1139 }
863 1140
864 1141 if ( isset( $fields['role'] ) ) {
865 1142 $role = sanitize_key( (string) $fields['role'] );
866 - if ( ! in_array( $role, desktop_mode_agent_allowed_roles(), true ) ) {
1143 + if ( ! in_array( $role, openstation_agent_allowed_roles(), true ) ) {
867 1144 return new WP_Error(
868 - 'desktop_mode_agent_invalid_role',
1145 + 'openstation_agent_invalid_role',
869 1146 __( 'Pick a role you are allowed to assign to an agent.', 'desktop-mode' )
870 1147 );
871 1148 }
872 1149 $current_role = is_array( $user->roles ) && ! empty( $user->roles ) ? (string) reset( $user->roles ) : '';
@@ -880,57 +1157,57 @@
880 1157 }
881 1158
882 1159 if ( isset( $fields['description'] ) ) {
883 1160 $description = sanitize_text_field( (string) $fields['description'] );
884 - $before = desktop_mode_agent_get_description( $user->ID );
1161 + $before = openstation_agent_get_description( $user->ID );
885 1162 if ( $description !== $before ) {
886 1163 $changed['description'] = array(
887 1164 'from' => $before,
888 1165 'to' => $description,
889 1166 );
890 - update_user_meta( $user->ID, DESKTOP_MODE_AGENT_DESCRIPTION_META, $description );
1167 + update_user_meta( $user->ID, OPENSTATION_AGENT_DESCRIPTION_META, $description );
891 1168 }
892 1169 }
893 1170
894 1171 if ( isset( $fields['instructions'] ) ) {
895 1172 $instructions = wp_kses_post( (string) $fields['instructions'] );
896 - $before = desktop_mode_agent_get_instructions( $user->ID );
1173 + $before = openstation_agent_get_instructions( $user->ID );
897 1174 if ( $instructions !== $before ) {
898 1175 $changed['instructions'] = array(
899 1176 'from' => $before,
900 1177 'to' => $instructions,
901 1178 );
902 - update_user_meta( $user->ID, DESKTOP_MODE_AGENT_INSTRUCTIONS_META, $instructions );
1179 + update_user_meta( $user->ID, OPENSTATION_AGENT_INSTRUCTIONS_META, $instructions );
903 1180 }
904 1181 }
905 1182
906 1183 if ( isset( $fields['abilities'] ) ) {
907 - $abilities = desktop_mode_agents_sanitize_ability_slugs( $fields['abilities'] );
908 - $before = desktop_mode_agent_get_abilities( $user->ID );
1184 + $abilities = openstation_agents_sanitize_ability_slugs( $fields['abilities'] );
1185 + $before = openstation_agent_get_abilities( $user->ID );
909 1186 if ( $abilities !== $before ) {
910 1187 $changed['abilities'] = array(
911 1188 'from' => $before,
912 1189 'to' => $abilities,
913 1190 );
914 - update_user_meta( $user->ID, DESKTOP_MODE_AGENT_ABILITIES_META, wp_json_encode( $abilities ) );
1191 + update_user_meta( $user->ID, OPENSTATION_AGENT_ABILITIES_META, wp_json_encode( $abilities ) );
915 1192 }
916 1193 }
917 1194
918 1195 if ( isset( $fields['triggers'] ) ) {
919 - $triggers = desktop_mode_agent_sanitize_triggers( $fields['triggers'] );
920 - $before = desktop_mode_agent_get_triggers( $user->ID );
1196 + $triggers = openstation_agent_sanitize_triggers( $fields['triggers'] );
1197 + $before = openstation_agent_get_triggers( $user->ID );
921 1198 if ( $triggers !== $before ) {
922 1199 $changed['triggers'] = array(
923 1200 'from' => $before,
924 1201 'to' => $triggers,
925 1202 );
926 - update_user_meta( $user->ID, DESKTOP_MODE_AGENT_TRIGGERS_META, wp_json_encode( $triggers ) );
1203 + update_user_meta( $user->ID, OPENSTATION_AGENT_TRIGGERS_META, wp_json_encode( $triggers ) );
927 1204 }
928 1205 }
929 1206
930 1207 if ( isset( $fields['model'] ) ) {
931 1208 $model = sanitize_text_field( (string) $fields['model'] );
932 - $before = desktop_mode_agent_get_model( $user->ID );
1209 + $before = openstation_agent_get_model( $user->ID );
933 1210 if ( $model !== $before ) {
934 1211 $changed['model'] = array(
935 1212 'from' => $before,
936 1213 'to' => $model,
@@ -935,11 +1212,11 @@
935 1212 'from' => $before,
936 1213 'to' => $model,
937 1214 );
938 1215 if ( '' === $model ) {
939 - delete_user_meta( $user->ID, DESKTOP_MODE_AGENT_MODEL_META );
1216 + delete_user_meta( $user->ID, OPENSTATION_AGENT_MODEL_META );
940 1217 } else {
941 - update_user_meta( $user->ID, DESKTOP_MODE_AGENT_MODEL_META, $model );
1218 + update_user_meta( $user->ID, OPENSTATION_AGENT_MODEL_META, $model );
942 1219 }
943 1220 }
944 1221 }
945 1222
@@ -944,9 +1221,9 @@
944 1221 }
945 1222
946 1223 if ( isset( $fields['rateLimit'] ) ) {
947 1224 $rate = max( 0, (int) $fields['rateLimit'] );
948 - $before = desktop_mode_agent_get_rate_limit( $user->ID );
1225 + $before = openstation_agent_get_rate_limit( $user->ID );
949 1226 if ( $rate !== $before ) {
950 1227 $changed['rateLimit'] = array(
951 1228 'from' => $before,
952 1229 'to' => $rate,
@@ -951,15 +1228,59 @@
951 1228 'from' => $before,
952 1229 'to' => $rate,
953 1230 );
954 1231 if ( 0 === $rate ) {
955 - delete_user_meta( $user->ID, DESKTOP_MODE_AGENT_RATE_LIMIT_META );
1232 + delete_user_meta( $user->ID, OPENSTATION_AGENT_RATE_LIMIT_META );
956 1233 } else {
957 - update_user_meta( $user->ID, DESKTOP_MODE_AGENT_RATE_LIMIT_META, $rate );
1234 + update_user_meta( $user->ID, OPENSTATION_AGENT_RATE_LIMIT_META, $rate );
958 1235 }
959 1236 }
960 1237 }
961 1238
1239 + if ( isset( $fields['vibes'] ) ) {
1240 + $vibes = openstation_agent_sanitize_vibes( $fields['vibes'] );
1241 + $before = openstation_agent_get_vibes( $user->ID );
1242 + if ( $vibes !== $before ) {
1243 + $changed['vibes'] = array(
1244 + 'from' => $before,
1245 + 'to' => $vibes,
1246 + );
1247 + if ( '' === $vibes ) {
1248 + delete_user_meta( $user->ID, OPENSTATION_AGENT_VIBES_META );
1249 + } else {
1250 + update_user_meta( $user->ID, OPENSTATION_AGENT_VIBES_META, $vibes );
1251 + }
1252 + }
1253 + }
1254 +
1255 + if ( isset( $fields['face'] ) ) {
1256 + $face = openstation_agent_sanitize_face_json( $fields['face'] );
1257 + $before = (string) get_user_meta( $user->ID, OPENSTATION_AGENT_FACE_META, true );
1258 + if ( $face !== $before ) {
1259 + $changed['face'] = array(
1260 + 'from' => $before,
1261 + 'to' => $face,
1262 + );
1263 + if ( '' === $face ) {
1264 + delete_user_meta( $user->ID, OPENSTATION_AGENT_FACE_META );
1265 + } else {
1266 + update_user_meta( $user->ID, OPENSTATION_AGENT_FACE_META, $face );
1267 + }
1268 + }
1269 + }
1270 +
1271 + if ( isset( $fields['faceSeed'] ) ) {
1272 + $seed = absint( $fields['faceSeed'] );
1273 + $before = openstation_agent_get_face_seed( $user->ID );
1274 + if ( $seed !== $before ) {
1275 + $changed['faceSeed'] = array(
1276 + 'from' => $before,
1277 + 'to' => $seed,
1278 + );
1279 + update_user_meta( $user->ID, OPENSTATION_AGENT_FACE_SEED_META, $seed );
1280 + }
1281 + }
1282 +
962 1283 if ( ! empty( $changed ) ) {
963 1284 /**
964 1285 * Fires after an agent's definition changed.
965 1286 *
@@ -969,9 +1290,9 @@
969 1290 * @param int $user_id Agent user id.
970 1291 * @param array $changed Map of field => { from, to }.
971 1292 * @param int $actor_id User who made the change.
972 1293 */
973 - do_action( 'desktop_mode_agent_updated', (int) $user->ID, $changed, get_current_user_id() );
1294 + do_action( 'openstation_agent_updated', (int) $user->ID, $changed, get_current_user_id() );
974 1295 }
975 1296
976 1297 return true;
977 1298 }