PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
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
← All changes | includes/agents/store.php +247 -2 1.0.01.1.10 View file →
@@ -94,10 +94,42 @@
94 94 * URL). The mismatch between this constant's name and its value is
95 95 * deliberate — it is NOT a half-finished rename.
96 96 */
97 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';
98 106
99 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 +/**
100 132 * Every meta key the store writes — the privacy eraser and any future
101 133 * cleanup path iterate this list instead of re-typing the constants.
102 134 *
103 135 * @return string[]
@@ -111,8 +143,11 @@
111 143 OPENSTATION_AGENT_TRIGGERS_META,
112 144 OPENSTATION_AGENT_MODEL_META,
113 145 OPENSTATION_AGENT_RATE_LIMIT_META,
114 146 OPENSTATION_AGENT_CREATED_BY_META,
147 + OPENSTATION_AGENT_VIBES_META,
148 + OPENSTATION_AGENT_FACE_META,
149 + OPENSTATION_AGENT_FACE_SEED_META,
115 150 );
116 151 }
117 152
118 153 /**
@@ -200,8 +235,44 @@
200 235 'sanitize_callback' => 'absint',
201 236 'auth_callback' => $auth,
202 237 )
203 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 + );
204 275 }
205 276 add_action( 'init', 'openstation_agents_register_user_meta' );
206 277
207 278 // ---------------------------------------------------------------------------
@@ -247,8 +318,113 @@
247 318 return (string) wp_json_encode( openstation_agents_sanitize_ability_slugs( $value ) );
248 319 }
249 320
250 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 +/**
251 427 * Sanitize the triggers array.
252 428 *
253 429 * Validates each row against the kind catalogue. Drops any row that
254 430 * doesn't match a known kind — one bad row never rejects the whole
@@ -805,9 +981,9 @@
805 981
806 982 /**
807 983 * Create an agent: synthetic user row + definition meta.
808 984 *
809 - * @param array{name:string, role:string, slug?:string, description?:string, instructions?:string, abilities?:array} $args Creation args.
985 + * @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.
810 986 * @return WP_User|WP_Error
811 987 */
812 988 function openstation_agent_create( $args ) {
813 989 $role = isset( $args['role'] ) ? sanitize_key( (string) $args['role'] ) : '';
@@ -826,8 +1002,19 @@
826 1002
827 1003 $description = isset( $args['description'] ) ? sanitize_text_field( (string) $args['description'] ) : '';
828 1004 $instructions = isset( $args['instructions'] ) ? wp_kses_post( (string) $args['instructions'] ) : '';
829 1005 $abilities = isset( $args['abilities'] ) ? openstation_agents_sanitize_ability_slugs( $args['abilities'] ) : array();
1006 + $triggers = isset( $args['triggers'] ) ? openstation_agent_sanitize_triggers( $args['triggers'] ) : array();
1007 + $vibes = isset( $args['vibes'] ) ? openstation_agent_sanitize_vibes( $args['vibes'] ) : '';
1008 + $face = isset( $args['face'] ) ? openstation_agent_sanitize_face_json( $args['face'] ) : '';
1009 + // Every agent gets a seed even when nobody chose a face, so the
1010 + // backfill has something deterministic to roll from and two admins
1011 + // racing it land on the same portrait. `crc32` of the login is
1012 + // stable, cheap, and already unique per agent.
1013 + $seed = isset( $args['faceSeed'] ) ? absint( $args['faceSeed'] ) : 0;
1014 + if ( 0 === $seed ) {
1015 + $seed = crc32( (string) $user->user_login );
1016 + }
830 1017
831 1018 if ( '' !== $description ) {
832 1019 update_user_meta( $user->ID, OPENSTATION_AGENT_DESCRIPTION_META, $description );
833 1020 }
@@ -836,8 +1023,18 @@
836 1023 }
837 1024 if ( ! empty( $abilities ) ) {
838 1025 update_user_meta( $user->ID, OPENSTATION_AGENT_ABILITIES_META, wp_json_encode( $abilities ) );
839 1026 }
1027 + if ( ! empty( $triggers ) ) {
1028 + update_user_meta( $user->ID, OPENSTATION_AGENT_TRIGGERS_META, wp_json_encode( $triggers ) );
1029 + }
1030 + if ( '' !== $vibes ) {
1031 + update_user_meta( $user->ID, OPENSTATION_AGENT_VIBES_META, $vibes );
1032 + }
1033 + if ( '' !== $face ) {
1034 + update_user_meta( $user->ID, OPENSTATION_AGENT_FACE_META, $face );
1035 + }
1036 + update_user_meta( $user->ID, OPENSTATION_AGENT_FACE_SEED_META, $seed );
840 1037 update_user_meta( $user->ID, OPENSTATION_AGENT_CREATED_BY_META, get_current_user_id() );
841 1038
842 1039 /**
843 1040 * Fires after an agent is created.
@@ -855,8 +1052,11 @@
855 1052 'role' => $role,
856 1053 'description' => $description,
857 1054 'instructions' => $instructions,
858 1055 'abilities' => $abilities,
1056 + 'vibes' => $vibes,
1057 + 'face' => $face,
1058 + 'faceSeed' => $seed,
859 1059 ),
860 1060 get_current_user_id()
861 1061 );
862 1062
@@ -868,9 +1068,10 @@
868 1068 * fields, applies the valid ones, and fires `openstation_agent_updated`
869 1069 * once with a before/after map of everything that changed.
870 1070 *
871 1071 * Recognized fields: `name`, `role`, `description`, `instructions`,
872 - * `abilities`, `triggers`, `model`, `rateLimit`.
1072 + * `abilities`, `triggers`, `model`, `rateLimit`, `vibes`, `face`,
1073 + * `faceSeed`.
873 1074 *
874 1075 * @param int $user_id Agent user id.
875 1076 * @param array $fields Field map.
876 1077 * @return true|WP_Error
@@ -1003,8 +1204,52 @@
1003 1204 delete_user_meta( $user->ID, OPENSTATION_AGENT_RATE_LIMIT_META );
1004 1205 } else {
1005 1206 update_user_meta( $user->ID, OPENSTATION_AGENT_RATE_LIMIT_META, $rate );
1006 1207 }
1208 + }
1209 + }
1210 +
1211 + if ( isset( $fields['vibes'] ) ) {
1212 + $vibes = openstation_agent_sanitize_vibes( $fields['vibes'] );
1213 + $before = openstation_agent_get_vibes( $user->ID );
1214 + if ( $vibes !== $before ) {
1215 + $changed['vibes'] = array(
1216 + 'from' => $before,
1217 + 'to' => $vibes,
1218 + );
1219 + if ( '' === $vibes ) {
1220 + delete_user_meta( $user->ID, OPENSTATION_AGENT_VIBES_META );
1221 + } else {
1222 + update_user_meta( $user->ID, OPENSTATION_AGENT_VIBES_META, $vibes );
1223 + }
1224 + }
1225 + }
1226 +
1227 + if ( isset( $fields['face'] ) ) {
1228 + $face = openstation_agent_sanitize_face_json( $fields['face'] );
1229 + $before = (string) get_user_meta( $user->ID, OPENSTATION_AGENT_FACE_META, true );
1230 + if ( $face !== $before ) {
1231 + $changed['face'] = array(
1232 + 'from' => $before,
1233 + 'to' => $face,
1234 + );
1235 + if ( '' === $face ) {
1236 + delete_user_meta( $user->ID, OPENSTATION_AGENT_FACE_META );
1237 + } else {
1238 + update_user_meta( $user->ID, OPENSTATION_AGENT_FACE_META, $face );
1239 + }
1240 + }
1241 + }
1242 +
1243 + if ( isset( $fields['faceSeed'] ) ) {
1244 + $seed = absint( $fields['faceSeed'] );
1245 + $before = openstation_agent_get_face_seed( $user->ID );
1246 + if ( $seed !== $before ) {
1247 + $changed['faceSeed'] = array(
1248 + 'from' => $before,
1249 + 'to' => $seed,
1250 + );
1251 + update_user_meta( $user->ID, OPENSTATION_AGENT_FACE_SEED_META, $seed );
1007 1252 }
1008 1253 }
1009 1254
1010 1255 if ( ! empty( $changed ) ) {