Recommendation: GSI CLEP (v1)
Structured RPC protocol for chat and link messages that adds region-, prim-, and script-scope targeting; channel-separated domains; and cross-border routing support
The Global Scripting Institute (GSI) is an informal organization of Second Life® users that design and test standards for efficient, flexible, and readable scripts in Second Life. "Second Life®" and "Second Life Grid™" are trademarks of Linden Research, Inc., d/b/a Linden Lab. The Global Scripting Institute and its catalog are not affiliated with or sponsored by Linden Research.
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
The Chat/Link_message Encoding Protocol (CLEP) is a protocol for flexible and targeted inter-script communication in SLua and Linden Scripting Language (LSL).
SLua-style syntax is used throughout this document, but all specifications also apply to the equivalent LSL functions and event callbacks.
CLEP defines a structured message format sent over chat, link message, HTTP, or email. Compatible scripts send and receive messages on string "channels" called domains.
CLEP messages consist of any number of headers and a single payload.
If any headers are included, they MUST be prepended to the payload, MUST be separated by newline (\n) characters, and the header name and value MUST separated by a colon (:) character. The header name MUST NOT contain any newlines or colons; the header value MUST NOT contain any newlines.
The payload MUST be a serialized JSON object that contains the following key-value pairs:
| Key | Required? | Type | Value |
|---|---|---|---|
domain |
REQUIRED | string | MUST be any string signifying the CLEP domain. Listeners SHOULD drop messages that do not match a domain they are listening to. |
id |
REQUIRED | uuid | MUST be ll.GenerateKey() or some other unique UUID. |
method |
REQUIRED | {string} | MUST be an array of any strings signifying a specific operation, such as {"File", "Write"}. Methods are canonically described using dot notation, such as File.Write. |
source -> region |
RECOMMENDED | string | If included, MUST be ll.GetRegionName(). If omitted, message SHALL NOT be forwarded via cross-region relays. |
source -> prim |
RECOMMENDED | uuid | If included, MUST be ll.GetKey(). If omitted, message SHALL NOT be forwarded via any relays. |
source -> script |
RECOMMENDED | string | If included, MUST be ll.GetScriptName(). If omitted, message SHALL NOT be responded to. |
source -> email |
OPTIONAL | string | If included, MUST be the "reply-to" email address used to reach the source via ll.Email(). Intended for use only when communicating via ll.Email(). |
source -> url |
OPTIONAL | string | If included, MUST be the "HTTP-in" URL used to reach the source via ll.HTTPRequest(). Intended for use only when communicating via ll.HTTPRequest() and ll.HTTPResponse() alone is insufficient. |
target -> region |
OPTIONAL | string | If included, MUST be the name of the region in which the targeted script(s) exist. If omitted, message SHALL NOT be target-routed via cross-region relays. |
target -> prim |
OPTIONAL | uuid | If included, MUST be the uuid of the prim in which the targeted script(s) exist. If omitted, message SHALL NOT be forwarded via any relays. |
target -> root |
OPTIONAL | uuid | If included, MUST be the uuid of the root prim of the linkset in which the targeted script(s) exist. SHOULD NOT be used in conjunction with target -> prim. |
target -> link |
OPTIONAL | number | If included, MUST be the link number or LINK_* constant targeting the prim(s) of the same linkset in which the targeted script(s) exist. MUST NOT be used in conjunction with target -> prim. |
target -> script |
OPTIONAL | string | If included, MUST be the name of the targeted script(s). |
params |
OPTIONAL | any? | MAY be any valid JSON, such as a string, object, et cetera. |
result |
OPTIONAL | any? | MAY be any valid JSON. If this message is a request (not a response), this pair MUST BE omitted. |
The JSON object MAY be encoded using any of these methods:
The resulting string MAY be sent using any of these methods, where json is the encoded JSON:
link_number, 0, json, "")target_prim_uuid, CHUF_channel, json)CHUF_channel, json)CHUF_channel, json)CHUF_channel, json)CHUF_channel, json)target_script_url, {method = "POST"}, json)httprequest_id, 200, json)target_script_address, json, "")link_number, 0, json, "")target_prim_uuid, CHUF_channel, json)CHUF_channel, json)CHUF_channel, json)CHUF_channel, json)CHUF_channel, json)target_script_url, [HTTP_METHOD, "POST"], json)httprequest_id, 200, json)target_script_address, json, "")When CLEP messages are sent via chat, they MUST be sent on the channel generated by the CHUF for the CLEP domain used. See the CHUF recommendation for the functions necessary to generate the CHUF channel.
When CLEP messages are sent via any other method, channel numbers are not needed.
Scripters concerned about LSL support - either in their own scripts, or for compatibility with others' LSL scripts - should note the following caveats:
ll.MessageLinked(
LINK_THIS,
0,
lljson.slencode({
method = {"File", "Open"},
params = "/root/foo.bar", -- this can be any lljson-safe type
domain = "My Product",
target = {}, -- since we're already only sending to LINK_THIS, we don't need target.link
source = { -- these values MUST be included, even for link messages
region = ll.GetRegionName(),
prim = ll.GetKey(),
script = ll.GetScriptName()
},
id = ll.GenerateKey() -- MUST be included
}),
""
)
This example shows how responses SHOULD copy method, params, domain, source (as target), and id from the request:
ll.MessageLinked(
LINK_THIS,
0,
lljson.slencode({
method = method,
params = params,
domain = domain,
target = source,
source = {
region = ll.GetRegionName(),
prim = ll.GetKey(),
script = ll.GetScriptName()
},
id = id
}),
""
)
Keep in mind that if you sign messages with SNEP, headers are not signed and their authenticity is not guaranteed. The below example is only for demonstration; these values would typically be included in params instead.
-- Channel Hash Utility Function https://gsi.sh/rec/chuf
local domain = "Secret Debug Domain"
local channel = bit32.bor(ll.Hash(domain), 0xC0000000)
if channel >= 2^31 then channel -= 2^32 end
ll.RegionSay(
channel,
`Clock:{os.clock()}\n` ..
`Creator:{ll.GetCreator()}\n` ..
lljson.slencode({
method = {"Debug"},
params = {
loglevel = ll.LinksetDataRead("loglevel"),
message = "Some kind of debug message"
},
domain = domain,
target = {},
source = { -- these values MUST be included, even for link messages
region = ll.GetRegionName(),
prim = ll.GetKey(),
script = ll.GetScriptName()
},
id = ll.GenerateKey() -- MUST be included
})
)
What's the difference between methods, paramses, and results? Why separate these?
method is restricted to the {string} type because it is intended to be used for message routing and script organization. Each method should refer to a specific, unique operation.
params, on the other hand, can accept any type, including nil (undefined). Data necessary to perform the operation referenced by method should be sent in params so that it can be ignored if a script receives a message with an unsupported method.
A result in a message means it is a response to a prior request. Like params, it can be any type, except that when it is nil, the message is not a response to a prior request.
Why are channels hashed? Isn't it better to have a pre-shared channel?
See the CHUF specification for more information on channel hashing.
What is the purpose of headers?
Headers are used for signing messages with SNEP and for routing between relays. While headers MAY be used for any purpose, everything else SHOULD be sent as part of the payload.