diff options
| author | Rider Linden <rider@lindenlab.com> | 2026-08-24 14:52:41 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-24 14:52:41 -0700 |
| commit | 92289533c1b95bc0fb2cdcc1217ae539485ea244 (patch) | |
| tree | f9cb1ccd42d310d01136d58d1ce06712b8708efc | |
| parent | 30bd7c3e22ba7751ea59cd09cbfda733b583a871 (diff) | |
| parent | b7fb9d897e94948f3145232f9866297708506ced (diff) | |
Merge pull request #6178 from secondlife/rider/doc_update
Fix doc drift between viewer and plugin.
| -rw-r--r-- | doc/external-editor-json-rpc.md | 778 |
1 files changed, 565 insertions, 213 deletions
diff --git a/doc/external-editor-json-rpc.md b/doc/external-editor-json-rpc.md index 5c7d70185e..6fd5d20eea 100644 --- a/doc/external-editor-json-rpc.md +++ b/doc/external-editor-json-rpc.md @@ -1,5 +1,9 @@ # Viewer to External Editor JSON-RPC<br>Message Interfaces Documentation +> **This file is a mirror.** The canonical source is `doc/Message_Interfaces.md` in the +> object_publish extension repository. Do not edit this copy — edit the canonical file and +> re-copy it here. + This document describes all the message interfaces defined for WebSocket communication between the Second Life viewer and an external editor such as a VSCode extension. ## Table of Contents @@ -7,6 +11,7 @@ This document describes all the message interfaces defined for WebSocket communi - [Usage Flow](#usage-flow) - [VS Code Launch URI](#vs-code-launch-uri) - [JSON-RPC Method Summary](#json-rpc-method-summary) +- [Error Handling](#error-handling) - [Session Management Interfaces](#session-management-interfaces) - [SessionHandshake](#sessionhandshake) - [SessionHandshakeResponse](#sessionhandshakeresponse) @@ -33,8 +38,9 @@ This document describes all the message interfaces defined for WebSocket communi - [Handler and Configuration Interfaces](#handler-and-configuration-interfaces) - [WebSocketHandlers](#websockethandlers) - [ClientInfo](#clientinfo) -- [Object Content Interfaces](#object-content-interfaces) +- [Object Explorer Interfaces](#object-explorer-interfaces) - [Core Data Types](#core-data-types) + - [Common preconditions](#common-preconditions) - [ObjectPublish](#objectpublish) - [ObjectUnpublish](#objectunpublish) - [ObjectUpdate](#objectupdate) @@ -43,7 +49,9 @@ This document describes all the message interfaces defined for WebSocket communi - [ObjectItemCreate](#objectitemcreate) - [ObjectItemDelete](#objectitemdelete) - [ObjectScriptSetRunning](#objectscriptsetrunning) + - [ObjectScriptReset](#objectscriptreset) - [ObjectRequest](#objectrequest) + - [ObjectList](#objectlist) - [ObjectModify](#objectmodify) - [ObjectItemModify](#objectitemmodify) - [Command Interfaces](#command-interfaces) @@ -71,10 +79,10 @@ This document describes all the message interfaces defined for WebSocket communi - When subscription needs to be terminated, viewer sends `script.unsubscribe` notification with `ScriptUnsubscribe` data - Extension handles unsubscription by cleaning up local script tracking -4. **Object Content Publishing:** +4. **Object Explorer:** - - Viewer sends `object.publish` notification when an in-world object's contents are made available for editing - - Viewer sends `object.unpublish` notification when an object is removed or the owner stops publishing + - Viewer sends `object.publish` notification when an in-world object's contents are made available for editing (user clicks "Explore in IDE") + - Viewer sends `object.unpublish` notification when an object is removed or the user stops exploring - Viewer sends `object.update` notification when object inventory changes (full replacement or delta) - Extension calls `object.content.get` to fetch an item's content on demand - Extension calls `object.content.save` to write modified content back to the viewer @@ -106,8 +114,8 @@ vscode://lindenlab.sl-vscode-plugin/connect[?port=<port>][&object=<uuid>][&scrip | Parameter | Required | Description | | --------- | -------- | ----------- | -| `port` | No | Port number the viewer's WebSocket server is listening on. Overrides the user's configured port for this session. Defaults to the configured `slVscodeEdit.network.websocketPort` (default `9020`) if absent. Must be in range 1024-65535. | -| `object` | No | UUID of a root prim. After the handshake completes the extension calls `object.request` to ask the viewer to publish this object. The viewer then sends an `object.publish` notification and the object appears as a workspace folder in the Explorer. | +| `port` | No | Port number the viewer's WebSocket server is listening on. Overrides the user's configured port for this session. Defaults to the configured `slVscodeEdit.network.websocketPort` (default `9020`) if absent. Must be in range 1024–65535. | +| `object` | No | UUID of a root prim. After the handshake completes the extension calls `object.request` to ask the viewer to start exploring this object. The viewer then sends an `object.publish` notification and the object appears as a workspace folder in the Explorer. | | `script` | No | UUID of a script. After the handshake completes the extension locates the corresponding temp file via `script.list` and opens it, triggering the normal `script.subscribe` + live-sync flow. | `object` and `script` are mutually exclusive in typical use but both may be supplied; the extension will process both. @@ -121,7 +129,7 @@ vscode://lindenlab.sl-vscode-plugin/connect # Connect on a custom port vscode://lindenlab.sl-vscode-plugin/connect?port=9021 -# Connect and immediately publish a specific object +# Connect and immediately explore a specific object vscode://lindenlab.sl-vscode-plugin/connect?port=9020&object=550e8400-e29b-41d4-a716-446655440000 # Connect and open a specific script for editing @@ -134,18 +142,18 @@ When the URI contains an `object` or `script` parameter the extension acts only ``` URI received by extension - | - v -WebSocket connects -> session.handshake -> session.ok - | - |- object=<uuid> -> object.request({ object_id }) call - | | - | v (async, when viewer is ready) - | object.publish notification - | - \- script=<uuid> -> script.list call -> open temp file - | - v + │ + ▼ +WebSocket connects → session.handshake → session.ok + │ + ├─ object=<uuid> → object.request({ object_id }) call + │ │ + │ ▼ (async, when viewer is ready) + │ object.publish notification + │ + └─ script=<uuid> → script.list call → open temp file + │ + ▼ script.subscribe + live-sync ``` @@ -153,56 +161,110 @@ WebSocket connects -> session.handshake -> session.ok | Method | Direction | Type | Interface/Parameters | | ------------------------------- | ------------------ | ------------ | -------------------------- | -| `session.handshake` | Viewer -> Extension | Call | `SessionHandshake` | -| `session.handshake` (response) | Extension -> Viewer | Response | `SessionHandshakeResponse` | -| `session.ok` | Viewer -> Extension | Notification | _(no interface)_ | +| `session.handshake` | Viewer → Extension | Call | `SessionHandshake` | +| `session.handshake` (response) | Extension → Viewer | Response | `SessionHandshakeResponse` | +| `session.ok` | Viewer → Extension | Notification | _(no interface)_ | | `session.disconnect` | Bidirectional | Notification | `SessionDisconnect` | | `session.ping` | Bidirectional | Call | `SessionPing` | | `session.ping` (response) | Bidirectional | Response | `SessionPingResponse` | -| `script.subscribe` | Extension -> Viewer | Call | `ScriptSubscribe` | -| `script.subscribe` (response) | Viewer -> Extension | Response | `ScriptSubscribeResponse` | -| `script.unsubscribe` | Viewer -> Extension | Notification | `ScriptUnsubscribe` | -| `script.list` | Extension -> Viewer | Call | _(no parameters)_ | -| `script.list` (response) | Viewer -> Extension | Response | `ScriptList` | -| `language.syntax.id` | Extension -> Viewer | Call | _(no parameters)_ | -| `language.syntax.id` (response) | Viewer -> Extension | Response | `{ id: string }` | -| `language.syntax` | Extension -> Viewer | Call | `{ kind: string }` | -| `language.syntax` (response) | Viewer -> Extension | Response | `LanguageInfo` | -| `language.syntax.cache` | Extension -> Viewer | Call | _(no parameters)_ | -| `language.syntax.cache` (response) | Viewer -> Extension | Response | `SyntaxCacheList` | -| `language.syntax.get` | Extension -> Viewer | Call | `{ filename: string, as_json?: boolean }` | -| `language.syntax.get` (response) | Viewer -> Extension | Response | `SyntaxCacheFile` | -| `language.syntax.change` | Viewer -> Extension | Notification | `SyntaxChange` | -| `script.compiled` | Viewer -> Extension | Notification | `CompilationResult` | -| `runtime.debug` | Viewer -> Extension | Notification | `RuntimeDebug` | -| `runtime.error` | Viewer -> Extension | Notification | `RuntimeError` | -| `object.publish` | Viewer -> Extension | Notification | `ObjectPublishMessage` | -| `object.unpublish` | Viewer -> Extension | Notification | `ObjectUnpublishMessage` || `object.unpublish` | Extension → Viewer | Call | `ObjectUnpublishParams` | -| `object.unpublish` (response) | Viewer → Extension | Response | `ObjectUnpublishResponse` || `object.update` | Viewer -> Extension | Notification | `ObjectUpdateMessage` | -| `object.content.get` | Extension -> Viewer | Call | `ObjectContentGetParams` | -| `object.content.get` (response) | Viewer -> Extension | Response | `ObjectContentGetResponse` | -| `object.content.save` | Extension -> Viewer | Call | `ObjectContentSaveParams` | -| `object.content.save` (response)| Viewer -> Extension | Response | `ObjectContentSaveResponse`| -| `object.item.create` | Extension -> Viewer | Call | `ObjectItemCreateParams` | -| `object.item.create` (response) | Viewer -> Extension | Response | `ObjectItemCreateResponse` | -| `object.item.delete` | Extension -> Viewer | Call | `ObjectItemDeleteParams` | -| `object.item.delete` (response) | Viewer -> Extension | Response | `ObjectItemDeleteResponse` | -| `object.script.set_running` | Extension -> Viewer | Call | `ObjectScriptSetRunningParams` | -| `object.script.set_running` (response) | Viewer -> Extension | Response | `ObjectScriptSetRunningResponse` | -| `object.script.reset` | Extension -> Viewer | Call | `ObjectScriptResetParams` | -| `object.script.reset` (response)| Viewer -> Extension | Response | `ObjectScriptResetResponse` | -| `object.request` | Extension -> Viewer | Call | `ObjectRequestParams` | -| `object.request` (response) | Viewer -> Extension | Response | `ObjectRequestResponse` | -| `object.list` | Extension -> Viewer | Call | `{}` (no params) | -| `object.list` (response) | Viewer -> Extension | Response | `ObjectListResponse` | -| `object.modify` | Extension -> Viewer | Call | `ObjectModifyParams` | -| `object.modify` (response) | Viewer -> Extension | Response | `ObjectModifyResponse` | -| `object.item.modify` | Extension -> Viewer | Call | `ObjectItemModifyParams` | -| `object.item.modify` (response) | Viewer -> Extension | Response | `ObjectItemModifyResponse` | -| `command.execute` | Bidirectional | Call | `CommandExecuteParams` | -| `command.execute` (response) | Bidirectional | Response | `CommandExecuteResponse` | -| `command.list` | Bidirectional | Call | _(no params)_ | -| `command.list` (response) | Bidirectional | Response | `CommandListResponse` | +| `script.subscribe` | Extension → Viewer | Call | `ScriptSubscribe` | +| `script.subscribe` (response) | Viewer → Extension | Response | `ScriptSubscribeResponse` | +| `script.unsubscribe` | Viewer → Extension | Notification | `ScriptUnsubscribe` | +| `script.unsubscribe` | Extension → Viewer | Call | `ScriptUnsubscribeParams` | +| `script.unsubscribe` (response) | Viewer → Extension | Response | `null` | +| `script.list` | Extension → Viewer | Call | _(no parameters)_ | +| `script.list` (response) | Viewer → Extension | Response | `ScriptList` | +| `language.syntax.id` | Extension → Viewer | Call | _(no parameters)_ | +| `language.syntax.id` (response) | Viewer → Extension | Response | `{ id: string }` | +| `language.syntax` | Extension → Viewer | Call | `{ kind: string }` | +| `language.syntax` (response) | Viewer → Extension | Response | `LanguageInfo` | +| `language.syntax.cache` | Extension → Viewer | Call | _(no parameters)_ | +| `language.syntax.cache` (response) | Viewer → Extension | Response | `SyntaxCacheList` | +| `language.syntax.get` | Extension → Viewer | Call | `{ filename: string, as_json?: boolean }` | +| `language.syntax.get` (response) | Viewer → Extension | Response | `SyntaxCacheFile` | +| `language.syntax.change` | Viewer → Extension | Notification | `SyntaxChange` | +| `script.compiled` | Viewer → Extension | Notification | `CompilationResult` | +| `runtime.debug` | Viewer → Extension | Notification | `RuntimeDebug` | +| `runtime.error` | Viewer → Extension | Notification | `RuntimeError` | +| `object.publish` | Viewer → Extension | Notification | `ObjectPublishMessage` | +| `object.unpublish` | Viewer → Extension | Notification | `ObjectUnpublishMessage` | +| `object.unpublish` | Extension → Viewer | Call | `ObjectUnpublishParams` | +| `object.unpublish` (response) | Viewer → Extension | Response | `ObjectUnpublishResponse` | +| `object.update` | Viewer → Extension | Notification | `ObjectUpdateMessage` | +| `object.content.get` | Extension → Viewer | Call | `ObjectContentGetParams` | +| `object.content.get` (response) | Viewer → Extension | Response | `ObjectContentGetResponse` | +| `object.content.save` | Extension → Viewer | Call | `ObjectContentSaveParams` | +| `object.content.save` (response)| Viewer → Extension | Response | `ObjectContentSaveResponse`| +| `object.item.create` | Extension → Viewer | Call | `ObjectItemCreateParams` | +| `object.item.create` (response) | Viewer → Extension | Response | `ObjectItemCreateResponse` | +| `object.item.delete` | Extension → Viewer | Call | `ObjectItemDeleteParams` | +| `object.item.delete` (response) | Viewer → Extension | Response | `ObjectItemDeleteResponse` | +| `object.script.set_running` | Extension → Viewer | Call | `ObjectScriptSetRunningParams` | +| `object.script.set_running` (response) | Viewer → Extension | Response | `ObjectScriptSetRunningResponse` | +| `object.script.reset` | Extension → Viewer | Call | `ObjectScriptResetParams` | +| `object.script.reset` (response)| Viewer → Extension | Response | `ObjectScriptResetResponse` | +| `object.request` | Extension → Viewer | Call | `ObjectRequestParams` | +| `object.request` (response) | Viewer → Extension | Response | `ObjectRequestResponse` | +| `object.list` | Extension → Viewer | Call | `{}` (no params) | +| `object.list` (response) | Viewer → Extension | Response | `ObjectListResponse` | +| `object.modify` | Extension → Viewer | Call | `ObjectModifyParams` | +| `object.modify` (response) | Viewer → Extension | Response | `ObjectModifyResponse` | +| `object.item.modify` | Extension → Viewer | Call | `ObjectItemModifyParams` | +| `object.item.modify` (response) | Viewer → Extension | Response | `ObjectItemModifyResponse` | +| `command.execute` | Bidirectional | Call | `CommandExecuteParams` | +| `command.execute` (response) | Bidirectional | Response | `CommandExecuteResponse` | +| `command.list` | Bidirectional | Call | _(no params)_ | +| `command.list` (response) | Bidirectional | Response | `CommandListResponse` | + +## Error Handling + +A call that fails returns a JSON-RPC 2.0 `error` object rather than a result. There is no partial +success: a response carries either `result` or `error`, never both. + +```json +{ + "jsonrpc": "2.0", + "id": 12, + "error": { + "code": -32602, + "message": "Invalid params: No syntax category specified" + } +} +``` + +**Standard JSON-RPC codes:** + +| Code | Meaning | +| ---- | ------- | +| `-32700` | Parse error — invalid JSON was received | +| `-32600` | Invalid Request — the JSON sent is not a valid Request object | +| `-32601` | Method not found | +| `-32602` | Invalid params | +| `-32603` | Internal error | + +**Server-specific codes.** The range `-32000` to `-32099` is reserved for server errors. The +transport defines the following; `-32001` and `-32003` are the ones handlers commonly raise. + +| Code | Meaning | +| ---- | ------- | +| `-32000` | Connection closed unexpectedly | +| `-32001` | Request timed out | +| `-32002` | Authentication required | +| `-32003` | Access denied | +| `-32004` | Too many requests | +| `-32005` | Service temporarily unavailable | +| `-32006` | Message exceeds maximum size | +| `-32007` | Session expired or invalid | + +**Message format.** Standard errors prefix the handler's detail text with a fixed label, so +`error.message` reads `"Invalid params: <detail>"`, `"Internal error: <detail>"`, +`"Method not found: <method>"`, and so on. Server-specific errors carry the detail text alone +(e.g. `"Access denied"`). Clients should branch on `error.code`, not on `error.message`. + +**`success` is not an error channel.** Several results carry a `success` field. Where a method +reports failure through a JSON-RPC error, that field is `true` on every response it ever +appears in, and a failed call produces no result to inspect. Do not treat a missing or false +`success` as the failure signal unless the method's own section documents it as one. ## Session Management Interfaces @@ -243,6 +305,7 @@ interface SessionHandshake { - `compilation`: Viewer will forward compilation results via `script.compiled` - `syntax_cache`: Viewer supports `language.syntax.cache` and `language.syntax.get` for retrieving syntax definition files - `commands`: Both sides support `command.execute` and `command.list` + - `unified_diagnostics`: Viewer supports the unified diagnostic reporting format ### SessionHandshakeResponse @@ -270,7 +333,14 @@ interface SessionHandshakeResponse { - `protocol_version`: Protocol version the client supports - `challenge_response` (optional): The UUID read from the temporary file identified by the `challenge` field in the handshake. Must be provided if `challenge` was present, otherwise the connection will be closed. - `languages`: Array of languages supported by the client -- `features`: Dictionary of features supported by the client +- `features`: Dictionary of features supported by the client. Known flags: + - `live_sync`: Client supports live script synchronisation + - `error_reporting`: Client accepts `runtime.error` notifications + - `unified_diagnostics`: Client supports the unified diagnostic reporting format + - `object_publish`: Client supports the object explorer methods and notifications + - `commands`: Both sides support `command.execute` and `command.list` + - `debugging`: Advertised as `false`. Reserved; no debugging support. + - `breakpoints`: Advertised as `false`. Reserved; no breakpoint support. - `script_name` (optional): Name of the script currently open in the editor - `script_language` (optional): Language of the script currently open in the editor (e.g. `"lsl"`, `"luau"`) @@ -309,6 +379,10 @@ interface SessionDisconnect { Heartbeat call used to verify the connection is alive and measure latency. Either side can initiate a ping; the recipient responds with the original timestamp plus its own server time. +In practice the extension initiates and the viewer only answers — the viewer never sends +`session.ping` itself. The extension pings every 30 seconds and tears the connection down after +two consecutive failures. + ```typescript interface SessionPing { timestamp: number; @@ -330,7 +404,7 @@ interface SessionPingResponse { **Response Fields:** -- `timestamp`: The original timestamp from the request (echoed back) +- `timestamp`: The original timestamp from the request. Echoed back only when the request supplied one. - `server_time`: Unix timestamp in milliseconds when the response was generated **Example Request:** @@ -411,23 +485,24 @@ Requests the in-memory keyword definitions for a specific language. These defini ```typescript interface LanguageInfo { id: string; - defs?: object; // Present only on success + defs: object; success: boolean; - error?: string; // Present only on failure } ``` **Response Fields:** - `id`: The current syntax version identifier -- `defs` (optional): The keyword definitions object. Only present when `success` is `true`. Structure varies by language. -- `success`: Whether the definitions were found and returned successfully -- `error` (optional): Human-readable error description. Only present when `success` is `false` +- `defs`: The keyword definitions object. Structure varies by language. +- `success`: Always `true`. Failures are returned as JSON-RPC errors — see below. -**Error cases:** +**Errors:** -- No `kind` parameter supplied: `success: false`, `error: "No syntax category specified"` -- Unknown `kind` value: `success: false`, `error: "Unknown syntax category requested"` +| Condition | Code | `error.message` | +| --------- | ---- | --------------- | +| No `kind` parameter supplied | `-32602` | `Invalid params: No syntax category specified` | +| Unknown `kind` value | `-32602` | `Invalid params: Unknown syntax category requested` | +| Definitions unavailable for a valid `kind` | `-32603` | `Internal error: Syntax definitions are unavailable` | ### Language Syntax Cache List @@ -464,7 +539,7 @@ interface SyntaxCacheList { | `lua_keywords_pretty.xml` | Luau keyword definitions in formatted LLSD XML format | | `secondlife_selene.yml` | Luau Selene linter configuration in YAML format | -Not all files may be present in every cache - the actual list returned by `language.syntax.cache` reflects only what is available on the viewer's local filesystem at the time of the request. +Not all files may be present in every cache — the actual list returned by `language.syntax.cache` reflects only what is available on the viewer's local filesystem at the time of the request. ### Language Syntax Cache Get @@ -490,23 +565,24 @@ Requests the content of a specific file from the syntax definition cache. The fi ```typescript interface SyntaxCacheFile { - content?: string | object; // Present only on success. String if as_json is false/omitted, object if as_json is true + content: string | object; // String if as_json is false/omitted, object if as_json is true success: boolean; - error?: string; // Present only on failure } ``` **Response Fields:** -- `content`: The file content. Only present when `success` is `true`. Is a raw text string when `as_json` is omitted or `false`; is a parsed object when `as_json` is `true`. -- `success`: Whether the file was found and read successfully -- `error` (optional): Human-readable error description. Only present when `success` is `false` +- `content`: The file content. Is a raw text string when `as_json` is omitted or `false`; is a parsed object when `as_json` is `true`. +- `success`: Always `true`. Failures are returned as JSON-RPC errors — see below. -**Error cases:** +**Errors:** -- No `filename` parameter supplied: `success: false`, `error: "No filename specified"` -- Name not found in cache: `success: false`, `error: "Requested syntax cache file not found"` -- File could not be loaded: `success: false`, `error: "Failed to load syntax cache file"` (or `"Failed to load and format syntax cache file."` when `as_json` is `true`) +| Condition | Code | `error.message` | +| --------- | ---- | --------------- | +| No `filename` parameter supplied | `-32602` | `Invalid params: No filename specified` | +| Name not found in cache | `-32602` | `Invalid params: Requested syntax cache file not found` | +| File could not be loaded (`as_json` omitted or `false`) | `-32603` | `Internal error: Failed to load syntax cache file` | +| File could not be loaded or parsed (`as_json` is `true`) | `-32603` | `Internal error: Failed to load and format syntax cache file.` | ## Script Subscription Interfaces @@ -541,9 +617,10 @@ interface ScriptSubscribeResponse { script_id: string; success: boolean; status: number; - object_id?: string; - item_id?: string; - message?: string; + message: string; + object_id?: string; // Success only + root_id?: string; // Success only + item_id?: string; // Success only } ``` @@ -553,19 +630,30 @@ interface ScriptSubscribeResponse { - `success`: Whether the subscription was successful - `status`: Numeric status code indicating the result: - `0`: Success - - `1`: Invalid editor - the script editor panel is no longer open - - `2`: Invalid subscription - no subscription found for the given `script_id` - - `3`: Already subscribed - another connection is already subscribed to this script + - `1`: Invalid editor — the script editor panel is no longer open + - `2`: Invalid subscription — no subscription found for the given `script_id` + - `3`: Already subscribed — another connection is already subscribed to this script - `4`: Internal server error -- `object_id` (optional): The in-world UUID of the object containing the script -- `item_id` (optional): The inventory item UUID of the script within the object -- `message` (optional): Additional information about the subscription result +- `message`: Always present. Fixed text matching `status`: + +| `status` | `message` | +| -------- | --------- | +| `0` | `OK` | +| `1` | `Invalid editor handle` | +| `2` | `No subscription found for script` | +| `3` | `Script already subscribed` | +| `4` | `Internal server error` | + +- `object_id`: UUID of the **prim** that owns the script. For a script in a child prim this is the child's UUID, not the linkset root's. Present only when `success` is `true`. +- `root_id`: UUID of the root prim of the linkset containing the script. Present only when `success` is `true`. If the prim cannot be resolved, `root_id` is set equal to `object_id`, so equality does not by itself mean the script lives in the root prim. +- `item_id`: The inventory item UUID of the script within the prim. Present only when `success` is `true`. ### ScriptUnsubscribe **JSON-RPC Method:** `script.unsubscribe` (notification from viewer) -Notification sent by the viewer when a script subscription should be terminated. +Notification sent by the viewer when a script subscription should be terminated. It is delivered +only to the connection holding the subscription, not to all connections. ```typescript interface ScriptUnsubscribe { @@ -577,6 +665,23 @@ interface ScriptUnsubscribe { - `script_id`: Unique identifier for the script to unsubscribe from +**JSON-RPC Method:** `script.unsubscribe` (call from extension to viewer) + +The extension may also call `script.unsubscribe` to end a subscription it holds. + +```typescript +interface ScriptUnsubscribeParams { + script_id: string; // Script whose subscription should be dropped +} +``` + +The result is `null`. + +The viewer drops the subscription only when the calling connection owns it. The call is +idempotent: an unknown `script_id`, or one held by a different connection, is ignored and still +returns a successful `null` result. A client therefore cannot use the response to detect that it +targeted the wrong script. + ### ScriptList **JSON-RPC Method:** `script.list` (call from extension to viewer) @@ -631,7 +736,6 @@ Result of a compilation operation in the viewer. ```typescript interface CompilationResult { - /** Deprecated: retained during migration to item-based routing. */ script_id: string; success: boolean; running: boolean; @@ -646,6 +750,25 @@ interface CompilationResult { - `running`: Whether the compiled script is currently running - `diagnostics` (optional): Array of `Diagnostic` records if any occurred +**Delivery:** routed only to the connection subscribed to that script. This differs from +`runtime.debug` and `runtime.error`, which are broadcast to every connection. + +**Two compile-feedback paths.** Compilation results reach a client by one of two routes, +depending on how the save was made: + +| Save route | Feedback | +| ---------- | -------- | +| `object.content.save` (object explorer) | `compiled` and `diagnostics` returned inline in the response | +| Live-sync editing of a subscribed script | `script.compiled` notification | + +A client using only the object explorer never receives `script.compiled`; a client waiting for +`script.compiled` after an `object.content.save` will wait indefinitely. Consolidating these two +paths is tracked separately. + +**Known limitation.** `script.compiled` is produced only while the viewer's script editor for that +script is open. If that editor has closed, compilation results are dropped without notice — no +result, no error, and not necessarily a preceding `script.unsubscribe`. Tracked separately. + ## Runtime Event Interfaces ### RuntimeDebug @@ -656,24 +779,27 @@ Debug message notification sent by the viewer during script execution. ```typescript interface RuntimeDebug { - /** Deprecated: use item.item_id when available. */ - script_id: string; + script_id: string; // Not currently sent — see note below object_id: string; - prim_id?: string; - item_id?: string; + prim_id: string; + item_id: string; object_name: string; message: string; - channel?: "debug" | "owner_say"; - item?: ItemRef; + channel: "debug" | "owner_say"; + item: ItemRef; } ``` **Fields:** -- `script_id`: Unique identifier for the script generating the debug message -- `object_id`: Unique identifier for the object containing the script +- `script_id`: Identifier for the script generating the debug message +- `object_id`: UUID of the root prim of the object containing the script +- `prim_id`: UUID of the prim that owns the script +- `item_id`: Inventory item UUID of the script - `object_name`: Human-readable name of the object - `message`: The debug message content +- `channel`: Source of the text. `"debug"` for script debug output, `"owner_say"` for owner-directed chat. +- `item`: Reference identifying the originating script. See `ItemRef` under `RuntimeError`. ### RuntimeError @@ -683,39 +809,54 @@ Runtime error notification sent by the viewer when a script encounters an error ```typescript interface RuntimeError { - /** Deprecated: use item.item_id when available. */ - script_id: string; + script_id: string; // Not currently sent — see note below object_id: string; - prim_id?: string; - item_id?: string; + prim_id: string; + item_id: string; object_name: string; message: string; error: string; line: number; - column?: number; - stack?: string[]; - channel?: "debug" | "owner_say"; - item?: ItemRef; + column: number; + stack: string[]; + channel: "debug" | "owner_say"; + item: ItemRef; } interface ItemRef { root_id: string; - prim_id?: string; - item_id?: string; - name?: string; - language?: "lsl" | "luau"; + prim_id: string; + item_id: string; + name: string; + language: "lsl" | "luau"; } ``` **Fields:** -- `script_id`: Unique identifier for the script that encountered the error -- `object_id`: Unique identifier for the object containing the script +- `script_id`: Identifier for the script that encountered the error +- `object_id`: UUID of the root prim of the object containing the script +- `prim_id`: UUID of the prim that owns the script +- `item_id`: Inventory item UUID of the script - `object_name`: Human-readable name of the object - `message`: The full raw chat text of the runtime error message as received from the simulator - `error`: Extracted runtime error description. This remains a top-level compatibility field while the protocol stays on version `1.0`. - `line`: Line number where the error occurred when the runtime format can be parsed; otherwise `0`. -- `stack` (optional): Stack trace lines if they could be extracted from the error message +- `column`: Column position where the error occurred when the runtime format can be parsed; otherwise `0`. +- `stack`: Stack trace lines extracted from the error message. Always present; an empty array when no trace could be extracted, so test its length rather than its presence. +- `channel`: Source of the text. `"debug"` for script debug output, `"owner_say"` for owner-directed chat. +- `item`: Reference identifying the originating script. + - `root_id`: UUID of the root prim of the linkset. + - `prim_id`: UUID of the prim that owns the script. + - `item_id`: Inventory item UUID of the script. + - `name`: Script name as it appears in the prim's inventory. + - `language`: The script's source language. Independent of the compile target; the VM is not carried in runtime messages. + +**Note on `script_id`:** this field is part of the contract but is **not currently sent** by the +viewer for either `runtime.debug` or `runtime.error`. Implementation is tracked separately. + +**Delivery:** `runtime.debug` and `runtime.error` are broadcast to all connections. An event is +emitted when the originating object is published or its script is subscribed. ## Handler and Configuration Interfaces @@ -771,9 +912,14 @@ interface ClientInfo { --- -## Object Content Interfaces +## Object Explorer Interfaces -These interfaces support publishing in-world object inventories (scripts and notecards) to the external editor as a browseable virtual filesystem. The extension exposes published objects under the `sl://objects/` URI scheme. +These interfaces support exploring in-world object inventories (scripts and notecards) from the external editor as a browseable virtual filesystem. The extension exposes explored objects under the `sl://objects/` URI scheme. + +Explored objects are shared across all connections rather than owned by the connection that +requested them. `object.publish`, `object.update` and `object.unpublish` are broadcast to every +connected client, so a client will receive notifications for objects it never requested, and must +drop an object from its own state when an `object.unpublish` for it arrives. ### Core Data Types @@ -808,7 +954,7 @@ interface ObjectInventoryItem { /** A linked (child) prim within a linkset */ interface LinkedObject { link_id: string; // UUID of the linked prim - link_number: number; // Link number (root=1, children>=2) + link_number: number; // Link number (root=1, children≥2) link_name: string; link_description?: string; inventory: ObjectInventoryItem[]; @@ -827,18 +973,59 @@ interface PublishedObject { region?: string; owner_id?: string; permissions?: ObjectPermissions; + can_save_back?: boolean; // Whether Save Back to Contents is currently available for this object inventory: ObjectInventoryItem[]; // Root prim's scripts and notecards linked_objects?: LinkedObject[]; // Child prims } ``` -**Script display extensions** (synthetic, derived from `subtype`): +**Display names.** The extension synthesises a file extension for scripts when presenting items in +the virtual filesystem: + +| Item | Extension | +| ---- | --------- | +| Script, `subtype` `0` (LSL) | `.lsl` | +| Script, `subtype` `1` (Luau) | `.luau` | +| Notecard | _(none)_ | + +Notecards receive no synthetic extension; the inventory name is used verbatim, so an extension the +user gave the notecard is preserved and none is added. + +These extensions exist only for display and are never part of the item's inventory name. Names sent +to and received from the viewer — including in `object.item.create` and `object.item.modify` — are +always the pure inventory name, without an extension. -| `subtype` | Extension | -| --------- | --------- | -| `0` (LSL) | `.lsl` | -| `1` (Luau)| `.luau` | -| notecard | `.txt` | +### Common preconditions + +Every method that addresses an item by `prim_id` + `item_id` — `object.content.get`, +`object.content.save`, `object.item.delete` and `object.item.modify` — runs the same validation +before doing any work, in this order: + +1. Both `prim_id` and `item_id` are present. +2. The prim exists. +3. The object containing the prim is currently published. +4. The item exists in that prim's inventory. +5. The item is a script or a notecard. +6. The caller holds the permissions that method requires. + +An object must be published before any of its items can be addressed. Learning an object's id from +`object.list` is not sufficient on its own — the object must be published, which `object.list` +reports and `object.request` initiates. + +**Errors:** + +| Condition | Code | `error.message` | +| --------- | ---- | --------------- | +| `prim_id` or `item_id` missing | `-32602` | `Invalid params: prim_id and item_id are required` | +| Prim not found | `-32602` | `Invalid params: Prim not found` | +| Object is not published | `-32003` | `Object is not published` | +| Item not in the prim's inventory | `-32602` | `Invalid params: Item not found in prim inventory` | +| Item is not a script or notecard | `-32602` | `Invalid params: Item is not a script or notecard` | +| Required item permission denied | `-32003` | `Insufficient permissions` | +| Modify denied on the containing prim | `-32003` | `No modify permission on object` | + +Writes require modify permission on both the item **and** the prim that contains it. A no-modify +object can be published and read, but its contents cannot be changed. --- @@ -856,7 +1043,8 @@ interface ObjectPublishMessage { **Fields:** -- `object`: The full published object tree, including root prim inventory and all linked prim inventories. +- `object`: The full object tree being explored, including root prim inventory and all linked prim inventories. + - `can_save_back` (optional): Capability hint for UI actions. When `true`, the object currently supports the `viewer.object.save_back_to_contents` command. --- @@ -864,7 +1052,7 @@ interface ObjectPublishMessage { **JSON-RPC Method:** `object.unpublish` (notification from viewer) -Sent when the viewer removes a previously published object - for example when the owner deselects it, moves away, or the object is deleted. +Sent when the viewer stops exploring a previously explored object — for example when the user stops exploring it from the viewer UI, the extension calls `object.unpublish`, or the object is deleted or linked into another object. ```typescript interface ObjectUnpublishMessage { @@ -875,12 +1063,23 @@ interface ObjectUnpublishMessage { **Fields:** -- `object_id`: UUID of the root prim that is being unpublished -- `reason` (optional): Human-readable explanation (e.g. `"object deleted"`, `"out of range"`) +- `object_id`: UUID of the root prim that is no longer being explored +- `reason` (optional): Machine-readable token identifying why exploring stopped. One of: + +| Value | Meaning | +| ----- | ------- | +| `manual` | The extension called `object.unpublish` for this object. | +| `republish` | The object is being re-published; a fresh `object.publish` follows. | +| `user` | The user stopped exploring the object from the viewer UI. | +| `deleted` | The object was deleted. | +| `linked` | The object became a child prim of a linkset and is no longer a root. | + +The field is omitted only when no reason was supplied; in practice every unpublish carries one +of the values above. **JSON-RPC Method:** `object.unpublish` (call from extension to viewer) -The extension may also call `object.unpublish` to manually stop tracking an object. The viewer will stop publishing it and send a corresponding `object.unpublish` notification back to the caller. +The extension may also call `object.unpublish` to manually stop exploring an object. The viewer will stop and broadcast a corresponding `object.unpublish` notification to all connections. ```typescript interface ObjectUnpublishParams { @@ -895,10 +1094,10 @@ interface ObjectUnpublishResponse { **Fields:** -- `object_id`: UUID of the root prim to unpublish. -- `success`: `true` if the object was published and has been removed. +- `object_id`: UUID of the root prim to stop exploring. +- `success`: `true` if the object was being explored and has been removed. -**Note:** The viewer also sends an `object.unpublish` notification to the caller immediately after responding. Extensions should handle that notification idempotently. +**Note:** The viewer also broadcasts an `object.unpublish` notification immediately after responding. The caller receives it too, so extensions should handle that notification idempotently. --- @@ -906,43 +1105,70 @@ interface ObjectUnpublishResponse { **JSON-RPC Method:** `object.update` (notification from viewer) -Sent when the inventory of a published object changes. Supports two modes: -- **Full replacement**: `inventory` and/or `linked_objects` fields replace the entire prior state. -- **Delta update**: `changes` field describes only what changed. Takes precedence over full replacement fields when present. +Sent when an explored object's inventory, properties, or linkset membership change. -```typescript -interface InventoryChanges { - added?: ObjectInventoryItem[]; - removed?: string[]; // item_ids removed - modified?: ObjectInventoryItem[]; // metadata-only changes - content_changed?: string[]; // item_ids whose content changed (invalidates cache) - running_changed?: { item_id: string; running: boolean }[]; // running state toggled -} +**Shapes currently emitted.** The viewer sends exactly the following five payloads: -interface LinkedObjectChanges { - added?: LinkedObject[]; - removed?: string[]; // link_ids removed - modified?: { - link_id: string; - link_name?: string; - inventory?: InventoryChanges; - }[]; -} +| Trigger | Payload | +| ------- | ------- | +| Root prim inventory changed | `object_id`, `inventory` (complete replacement array) | +| Child prim inventory changed | `object_id`, `changes.linked_objects.modified[]` with `link_id` and `inventory` (complete replacement array for that child) | +| Root prim name/description changed | `object_id`, `object_name` and/or `object_description` | +| Child prim name/description changed | `object_id`, `changes.linked_objects.modified[]` with `link_id` and `link_name` and/or `link_description` | +| Linkset membership changed | `object_id`, `linked_objects` (complete replacement array covering every child prim) | +`inventory` and `linked_objects` are always complete replacements of the prior state, never +increments. Linkset membership changes are coalesced behind a short flush delay, so several +rapid link or unlink operations may arrive as a single update. + +```typescript interface ObjectUpdateMessage { object_id: string; object_name?: string; - // Full replacement (used when changes is absent) + object_description?: string; + // Full replacement inventory?: ObjectInventoryItem[]; linked_objects?: LinkedObject[]; - // Delta (takes precedence when present) changes?: { - inventory?: InventoryChanges; + inventory?: InventoryChanges; // Not implemented — see below linked_objects?: LinkedObjectChanges; }; } + +interface LinkedObjectChanges { + added?: LinkedObject[]; // Not implemented — see below + removed?: string[]; // Not implemented — see below + modified?: { + link_id: string; + link_name?: string; + link_description?: string; + inventory?: ObjectInventoryItem[]; // Complete replacement array, not a delta + }[]; +} +``` + +**Note:** `changes.linked_objects.modified` is emitted, as shown in the table above. +`changes.linked_objects.added` and `changes.linked_objects.removed` are not. + +#### Not implemented + +The delta sub-protocol below is specified but is **not currently emitted by the viewer**. +Implementation is tracked separately. Clients must rely on the full-replacement shapes listed +above; code written to consume these types will never run against the current viewer. + +```typescript +interface InventoryChanges { + added?: ObjectInventoryItem[]; + removed?: string[]; // item_ids removed + modified?: ObjectInventoryItem[]; // metadata-only changes + content_changed?: string[]; // item_ids whose content changed (invalidates cache) + running_changed?: { item_id: string; running: boolean }[]; // running state toggled +} ``` +Also not implemented: `LinkedObjectChanges.added` and `LinkedObjectChanges.removed`. Linkset +membership changes are sent as a complete `linked_objects` replacement array instead. + --- ### ObjectContentGet @@ -953,7 +1179,7 @@ Requests the text content of a script or notecard. The extension calls this lazi ```typescript interface ObjectContentGetParams { - prim_id: string; // UUID of any prim (root or child) - no object_id + link_id needed + prim_id: string; // UUID of any prim (root or child) — no object_id + link_id needed item_id: string; } @@ -970,7 +1196,15 @@ interface ObjectContentGetResponse { - `prim_id`: UUID of the prim that owns the item. Child prims are addressable directly by UUID without knowing the root object_id. - `item_id`: Inventory item UUID. - `success`: `true` on success. -- `content`: The raw text content of the item. For notecards, the `Linden text version 2` envelope is stripped - only the body text is returned. +- `content`: The raw text content of the item. For notecards, the `Linden text version 2` envelope is stripped — only the body text is returned. + +**Permissions.** Scripts require both `PERM_COPY` and `PERM_MODIFY` on the item: the source of a +no-copy or no-modify script is never exposed. Notecards require no permission at all, so that +no-modify notecards remain readable in the external editor. See +[Common preconditions](#common-preconditions) for the shared checks and errors. + +**Timeout.** 30 seconds to fetch the asset, after which the call fails with `-32001` +(`Asset fetch timed out`). --- @@ -986,6 +1220,7 @@ interface ObjectContentSaveParams { item_id: string; content: string; vm?: "mono" | "lsl2" | "luau"; + running?: boolean; // Scripts only: run state applied after compilation. Defaults to false. } interface ObjectContentSaveResponse { @@ -994,7 +1229,6 @@ interface ObjectContentSaveResponse { item_id?: string; compiled?: boolean; diagnostics?: Diagnostic[]; - message?: string; } ``` @@ -1004,10 +1238,19 @@ interface ObjectContentSaveResponse { - `item_id`: UUID of the saved inventory item. - `content`: Raw script/notecard source text to store. - `vm` (optional): Scripts only compile target. Accepted values are `"mono"`, `"lsl2"`, `"luau"`. When `"luau"` is specified for an LSL script (as opposed to a native Luau script), the viewer automatically selects the correct LSL-on-Luau compile path. If omitted, inferred from item metadata or content analysis. +- `running` (optional): Scripts only. The run state the viewer applies to the script once the upload and compilation complete. Defaults to `false` when omitted. To preserve a script's current run state across a save, echo the `running` value from the corresponding `ObjectInventoryItem` in the most recent `object.publish` or `object.update`. - `success`: Whether the upload/save operation succeeded. - `compiled` (optional): Scripts only. `true` when compilation succeeded, `false` when source saved but compile failed. -- `diagnostics` (optional): Scripts only. Array of `Diagnostic` records when `compiled` is `false`. -- `message` (optional): Error description on failure. +- `diagnostics` (optional): Scripts only. Compiler diagnostics when `compiled` is `false`. + +> **Warning:** Omitting `running` does not leave the script's run state unchanged — it stops the script. A client that saves a running script without sending `running: true` will silently stop it. + +**Permissions.** Requires `PERM_MODIFY` on the item and modify permission on the containing prim. +See [Common preconditions](#common-preconditions) for the shared checks and errors. + +**Timeouts.** Scripts allow 60 seconds for upload and compilation; notecards allow 30 seconds. A +client's own timeout must exceed the longer of the two. Exceeding either fails the call with +`-32001`. --- @@ -1015,18 +1258,17 @@ interface ObjectContentSaveResponse { **JSON-RPC Method:** `object.item.create` (call from extension to viewer) -Creates a new script in a prim's inventory. The call is asynchronous - the viewer sends -`RezScript` to the simulator and waits for the inventory-changed callback before returning -the created item's details. The simulator may rename the item if a duplicate name exists. - -Notecard creation is not yet supported and will return an error. +Creates a new script or notecard in a prim's inventory. The call is asynchronous and returns the +created item's details once the simulator confirms the item exists. The simulator may rename the +item if a duplicate name exists. ```typescript interface ObjectItemCreateParams { prim_id: string; // UUID of the prim to create the item in - name: string; // Pure SL inventory name - no file extension - type: InventoryItemType; // "script" ("notecard" reserved for future) - vm: ScriptVM; // Required for scripts: "luau" | "mono" | "lsl2" + name: string; // Pure SL inventory name — no file extension + type: InventoryItemType; // "script" | "notecard" + vm?: ScriptVM; // Scripts only (required): "luau" | "mono" | "lsl2" + text?: string; // Notecards only (optional): initial body text } // On success, returns an ObjectInventoryItem with prim_id: @@ -1035,12 +1277,32 @@ interface ObjectItemCreateResponse extends ObjectInventoryItem { } ``` +**Fields:** + +- `prim_id`: UUID of the prim to create the item in. Child prims are addressable directly by UUID. +- `name`: Pure SL inventory name, without a file extension. +- `type`: `"script"` or `"notecard"`. +- `vm`: Scripts only. Required when `type` is `"script"`; accepted values are `"luau"`, `"mono"` and `"lsl2"`. Ignored for notecards. +- `text` (optional): Notecards only. Initial body text for the new notecard. Ignored for scripts. + **Notes:** - The response matches the `ObjectInventoryItem` structure (same fields as items in `object.publish` and `object.update` notifications). - The `name` in the response may differ from the request if the simulator renamed it. - An `object.update` notification will also fire for the prim (since inventory changed). -- Timeout: 30 seconds. Returns a JSON-RPC internal error if the simulator does not respond. + +**Errors:** + +| Condition | Code | `error.message` | +| --------- | ---- | --------------- | +| `type` is not `"script"` or `"notecard"` | `-32602` | `Invalid params: Unsupported item type: <type>` | +| `prim_id` missing | `-32602` | `Invalid params: prim_id is required` | +| Prim not found | `-32602` | `Invalid params: Prim not found` | +| `name` missing | `-32602` | `Invalid params: name is required` | +| `vm` missing or invalid for a script | `-32602` | `Invalid params: vm must be 'luau', 'mono', or 'lsl2'` | +| Object is not published | `-32003` | `Object is not published` | +| Another `object.item.create` is already in flight for this prim | `-32600` | `Invalid Request: An item.create is already in flight for this prim` | +| Simulator did not respond within 30 seconds | `-32001` | `Timed out waiting for item creation` | --- @@ -1063,6 +1325,9 @@ interface ObjectItemDeleteResponse { } ``` +**Permissions.** Requires `PERM_MODIFY` on the item and modify permission on the containing prim. +See [Common preconditions](#common-preconditions) for the shared checks and errors. + --- ### ObjectScriptSetRunning @@ -1080,10 +1345,29 @@ interface ObjectScriptSetRunningParams { interface ObjectScriptSetRunningResponse { success: boolean; - message?: string; } ``` +**`success` means dispatched, not applied.** The viewer sends a message to the simulator and +returns immediately. `success: true` confirms the message was sent — it does not confirm the +simulator started or stopped the script. Confirmation, if it arrives, comes later as an +`object.update` notification. Treat the response as an acknowledgement and wait for the update +before reporting the new run state to the user. + +**Permissions.** Requires `PERM_MODIFY` on the script. This method validates independently of the +shared item validator and accepts scripts only. + +**Errors:** + +| Condition | Code | `error.message` | +| --------- | ---- | --------------- | +| `prim_id` or `item_id` missing | `-32602` | `Invalid params: prim_id and item_id are required` | +| Prim not found | `-32602` | `Invalid params: Prim not found` | +| Object is not published | `-32003` | `Object is not published` | +| Script not in the prim's inventory | `-32602` | `Invalid params: Script not found in prim inventory` | +| Item is not a script | `-32602` | `Invalid params: Item is not a script` | +| No modify permission on the script | `-32003` | `No modify permission on script` | + --- ### ObjectScriptReset @@ -1100,10 +1384,18 @@ interface ObjectScriptResetParams { interface ObjectScriptResetResponse { success: boolean; - message?: string; } ``` +**`success` means dispatched, not applied.** As with `object.script.set_running`, the viewer sends +a message to the simulator and returns immediately. `success: true` does not confirm the script +was reset. + +**Permissions.** Requires `PERM_MODIFY` on the script. This method validates independently of the +shared item validator and accepts scripts only. + +**Errors:** identical to `object.script.set_running` above. + --- ### ObjectRequest @@ -1116,20 +1408,29 @@ This is typically called immediately after the handshake completes when the exte ```typescript interface ObjectRequestParams { - object_id: string; // UUID of the root prim to request publishing for + object_id: string; // UUID of the root prim to request exploring } interface ObjectRequestResponse { success: boolean; - message?: string; // reason on failure (e.g. "object not found", "permission denied") } ``` **Fields:** -- `object_id`: UUID of the root prim of the linkset to publish. -- `success`: Whether the viewer accepted the request. A `true` response does not mean `object.publish` has been sent yet - it means the viewer will send it. -- `message` (optional): Human-readable failure reason. Only present when `success` is `false`. +- `object_id`: UUID of the root prim of the linkset to explore. +- `success`: Whether the viewer accepted the request. A `true` response does not mean `object.publish` has been sent yet — it means the viewer will send it. Failures are returned as JSON-RPC errors — see below. + +**Permissions.** Requires modify permission on the object. + +**Errors:** + +| Condition | Code | `error.message` | +| --------- | ---- | --------------- | +| `object_id` missing | `-32602` | `Invalid params: No object_id specified` | +| Object not found | `-32602` | `Invalid params: Object not found` | +| No modify permission on the object | `-32003` | `Permission denied` | +| Publish could not be started | `-32603` | `Internal error: Failed to initiate publish` | **Sequence:** 1. Extension calls `object.request` @@ -1142,21 +1443,21 @@ interface ObjectRequestResponse { **JSON-RPC Method:** `object.list` (call from extension to viewer) -Requests the complete list of currently published objects. Called by the extension immediately after the handshake completes (`session.ok`) to restore state for any objects the viewer already has published. +Requests the complete list of currently explored objects. Called by the extension immediately after the handshake completes (`session.ok`) to restore state for any objects the viewer already has open for exploration. -The viewer responds synchronously with all published objects in the same format as `object.publish` notifications. No follow-up notifications are sent. +The viewer responds synchronously with all explored objects in the same format as `object.publish` notifications. No follow-up notifications are sent. ```typescript // No request parameters interface ObjectListResponse { - objects: PublishedObject[]; // All currently published objects; empty array if none + objects: PublishedObject[]; // All currently explored objects; empty array if none } ``` **Fields:** -- `objects`: Array of `PublishedObject` records (same shape as the `object` field in `object.publish`). Empty array when no objects are currently published. +- `objects`: Array of `PublishedObject` records (same shape as the `object` field in `object.publish`). Empty array when no objects are currently being explored. **Sequence:** 1. Viewer sends `session.ok` @@ -1184,7 +1485,6 @@ interface ObjectModifyParams { interface ObjectModifyResponse { success: boolean; prim_id: string; // Echoed back from request - message?: string; // Error description on failure } ``` @@ -1195,8 +1495,24 @@ interface ObjectModifyResponse { - `description` (optional): New description for the prim. If omitted, description remains unchanged. - `permissions` (optional): Permission changes. - `next_owner`: Permission mask applied when the object is transferred. Uses same bit flags as `ItemPermissions` (e.g., `PERM_MODIFY=0x4000`, `PERM_COPY=0x8000`, `PERM_TRANSFER=0x2000`). -- `success`: Whether the update operation succeeded. -- `message` (optional): Error description. Only present when `success` is `false`. +- `success`: Whether the property messages were dispatched. + +**`success` means dispatched, not applied.** Each supplied property is sent to the simulator as a +separate message and the viewer returns immediately. `success: true` confirms the messages were +sent, not that any of them took effect — and because they travel independently, one may be applied +while another is not. Confirmation arrives later as an `object.update` notification. + +**Permissions.** Requires modify permission on the prim. + +**Errors:** + +| Condition | Code | `error.message` | +| --------- | ---- | --------------- | +| `prim_id` missing | `-32602` | `Invalid params: prim_id is required` | +| No property supplied | `-32602` | `Invalid params: At least one property (name, description, or permissions) must be specified` | +| Prim not found | `-32602` | `Invalid params: Prim not found` | +| Object is not published | `-32003` | `Object is not published` | +| No modify permission on the prim | `-32003` | `No modify permission on object` | **Notes:** - At least one property field (`name`, `description`, or `permissions`) must be specified. @@ -1226,7 +1542,6 @@ interface ObjectItemModifyResponse { success: boolean; prim_id: string; // Echoed back from request item_id: string; // Echoed back from request - message?: string; // Error description on failure } ``` @@ -1238,8 +1553,16 @@ interface ObjectItemModifyResponse { - `description` (optional): New description for the item. If omitted, description remains unchanged. - `permissions` (optional): Permission changes. - `next_owner`: Permission mask applied when the item is transferred. Uses same bit flags as `ItemPermissions` (e.g., `PERM_MODIFY=0x4000`, `PERM_COPY=0x8000`, `PERM_TRANSFER=0x2000`). -- `success`: Whether the update operation succeeded. -- `message` (optional): Error description. Only present when `success` is `false`. +- `success`: Whether the property messages were dispatched. + +**`success` means dispatched, not applied.** The viewer sends the change to the simulator and +returns immediately; confirmation arrives later as an `object.update` notification. + +**Permissions.** Requires `PERM_MODIFY` on the item and modify permission on the containing prim. +See [Common preconditions](#common-preconditions) for the shared checks and errors. + +**Errors:** as listed under [Common preconditions](#common-preconditions), plus `-32602` when no +property field is supplied. **Notes:** - At least one property field (`name`, `description`, or `permissions`) must be specified. @@ -1268,29 +1591,35 @@ interface CommandExecuteParams { interface CommandExecuteResponse { success: boolean; result?: unknown; // optional command-specific return value - error_code?: number; - message?: string; } ``` **Fields:** - `command`: Namespaced command identifier. The prefix before the first `.` identifies the side that owns and executes the command: - - `viewer.*` - commands executed by the viewer (e.g. `viewer.teleport`, `viewer.script.recompile_all`) - - `editor.*` - commands executed by the extension (e.g. `editor.open_file`, `editor.show_message`) + - `viewer.*` — commands executed by the viewer (e.g. `viewer.teleport`, `viewer.camera.focus`) + - `editor.*` — commands executed by the extension (e.g. `editor.open_file`, `editor.show_message`) - `params` (optional): Command-specific argument map. Structure varies by command. -- `success`: Whether the command was found and executed without error. -- `result` (optional): Command-specific return value. Only present when `success` is `true` and the command produces output. -- `error_code` (optional): Numeric failure code. Only present when `success` is `false`: - - `1` - Unknown command - - `2` - Invalid or missing parameters - - `3` - Not permitted - - `4` - Execution error -- `message` (optional): Human-readable error description. Only present when `success` is `false`. +- `success`: `true` when the command executed. Failures are returned as JSON-RPC errors — see below. +- `result` (optional): Command-specific return value. Only present when the command produces output. + +**Errors:** -**Capability gate:** A side MUST NOT send `command.execute` unless the peer advertised `commands: true` in the handshake. A receiver that receives the call without having negotiated the feature MUST respond with `success: false, error_code: 1`. +| Condition | Code | `error.message` | +| --------- | ---- | --------------- | +| `command` missing | `-32602` | `Invalid params: command is required` | +| Command not registered | `-32602` | `Invalid params: Unknown command: <command>` | -**Example - extension asks viewer to teleport:** +The invoked command's own handler may raise further errors — `-32602` for bad arguments, +`-32003` when the action is not permitted, `-32603` on internal failure. Clients must handle any +error code, not only the two above. + +**Capability gate:** A side MUST NOT send `command.execute` unless the peer advertised +`commands: true` in the handshake. A receiver that receives the call without having negotiated the +feature should respond with a JSON-RPC error. **Not currently enforced on receive by the viewer** +— the gate is applied only when sending. Implementation is tracked separately. + +**Example — extension asks viewer to teleport:** ```json { @@ -1299,7 +1628,7 @@ interface CommandExecuteResponse { "id": 7, "params": { "command": "viewer.teleport", - "params": { "region": "Aditi", "position": [128, 128, 25] } + "params": { "object_id": "550e8400-e29b-41d4-a716-446655440000" } } } ``` @@ -1312,7 +1641,29 @@ interface CommandExecuteResponse { } ``` -**Example - viewer asks extension to show a message:** +**Example — extension asks viewer to save object back to contents:** + +```json +{ + "jsonrpc": "2.0", + "method": "command.execute", + "id": 9, + "params": { + "command": "viewer.object.save_back_to_contents", + "params": { "object_id": "550e8400-e29b-41d4-a716-446655440000" } + } +} +``` + +```json +{ + "jsonrpc": "2.0", + "id": 9, + "result": { "success": true, "result": { "object_id": "550e8400-e29b-41d4-a716-446655440000" } } +} +``` + +**Example — viewer asks extension to show a message:** ```json { @@ -1361,20 +1712,21 @@ interface CommandParamInfo { - `commands`: Array of commands the responder supports. Each entry describes one command. - `command`: The namespaced command identifier. - `description` (optional): Human-readable description of what the command does. - - `params` (optional): Map of parameter names to their type descriptors. + - `params` (optional): Map of parameter names to their type descriptors. **Not currently + populated** — the viewer returns only `command` and `description`, so parameter discovery + does not work. Implementation is tracked separately. **Known viewer commands:** | Command | Required params | Description | |---------|----------------|-------------| -| `viewer.teleport` | `region: string` | Teleport agent to a region. Optional `position: [x, y, z]`. | -| `viewer.script.recompile_all` | `object_id: string` | Recompile all scripts in an object. | -| `viewer.script.reset_all` | `object_id: string` | Reset all scripts in an object. | -| `viewer.camera.focus` | `object_id: string` | Move camera focus to an in-world object. | +| `viewer.teleport` | `object_id: string` | Teleport agent to an in-world object. | +| `viewer.camera.focus` | `object_id: string` | Zoom camera to an in-world object (same behavior as context menu Zoom In). | +| `viewer.object.save_back_to_contents` | `object_id: string` | Save an in-world object back to source object contents. | **Known extension commands:** | Command | Required params | Description | |---------|----------------|-------------| | `editor.open_file` | `path: string` | Open a file in the editor. Optional `line: number`. | -| `editor.show_message` | `message: string` | Show a notification. Optional `level: "info" | "warn" | "error"`. | +| `editor.show_message` | `message: string` | Show a notification. Optional `level: "info" \| "warn" \| "error"`. | |
