Skip to content

Context & Permissions

TL;DR: Every generated Lua wrapper method runs CheckIfCanAccess{Property,Method,Constructor} before it touches the wrapped object. Those checks bitwise-AND the call’s required mask against the live CVRLuaContext on four axes: Environment (is the VM on the client or the server?), Object (is the script living on an avatar, prop, world, or event whitelist?), Owner (is the local user the wearer / spawner?), and Scope (is the wrapped object part of the script’s own content, external content, or something internal?). If any axis masks to zero, the wrapper throws ScriptRuntimeException with the failing axis in the message.

All four are [Flags] enums so a check can allow multiple values at once.

File: CVR-GameFiles/ABI.Scripting.CVRSTL.Common/CVRLuaEnvironmentContext.cs

[Flags]
public enum CVRLuaEnvironmentContext {
NONE = 0,
CLIENT = 1,
SERVER = 2,
ANY = 3,
}

Plus a parallel environment-flags enum that gates what the script is allowed to subscribe to (vs. what it’s actually doing right now):

File: CVR-GameFiles/ABI.Scripting.CVRSTL.Common/CVRLuaEnvironmentFlags.cs

[Flags]
public enum CVRLuaEnvironmentFlags {
NONE = 0,
ON_CLIENT = 1,
ON_SERVER = 2,
}

In the current codebase every CVRLuaContextPolicy is allowOnClient: true, allowOnServer: false — scripts only run client-side. The server axis exists so that future server-only Lua logic can be layered in without re-shipping the wrappers.

Object — what kind of content hosts the script?

Section titled “Object — what kind of content hosts the script?”

File: CVR-GameFiles/ABI.Scripting.CVRSTL.Common/CVRLuaObjectContext.cs

[Flags]
public enum CVRLuaObjectContext {
NONE = 0,
AVATAR = 1,
PROP = 2,
WORLD = 4,
EVENT_WHITELIST = 8,
ALL_BUT_EVENTS = 7,
ANY = 0xF,
}

Set once at CVRLuaContext construction when LuaScriptFactory.ForLuaBehaviour inspects the root content:

  • CVRAvatarAVATAR
  • CVRSpawnablePROP
  • world scene → WORLD
  • EVENT_WHITELIST is only asserted by the engine while calling into a reserved event path; regular wrapper methods mask with ALL_BUT_EVENTS to exclude it.

Owner — does the local user control the content?

Section titled “Owner — does the local user control the content?”

File: CVR-GameFiles/ABI.Scripting.CVRSTL.Common/CVRLuaOwnerContext.cs

[Flags]
public enum CVRLuaOwnerContext {
NONE = 0,
LOCAL = 1,
OTHER = 2,
ANY = 3,
}
  • AvatarLOCAL iff the avatar is worn by the local player. Mirrors the global IsWornByMe.
  • PropLOCAL iff spawned by the local player. Mirrors IsSpawnedByMe.
  • World — always LOCAL in practice (the local player is the only decider for world scripts).

Scope — what object is the call touching?

Section titled “Scope — what object is the call touching?”

File: CVR-GameFiles/ABI.Scripting.CVRSTL.Common/CVRLuaScopeContext.cs

[Flags]
public enum CVRLuaScopeContext {
NONE = 0,
SELF = 1,
EXTERNAL_CONTENT = 2,
INTERNAL = 4,
NOT_AVAILABLE = 8,
ANY_EXCEPT_INTERNAL = 0xB,
ANY = 0xF,
}

Scope is attached to the wrapper at the moment the host marshals an object into Lua. The Lua wrappers implement IScriptedWrapper, which carries a ScopeContext:

  • SELF — the wrapped GameObject / Component is a descendant of the script’s root transform. This is the common case: gameObject, transform, anything you reach via BoundObjects, and anything the wrapper creates on behalf of the script start SELF.
  • EXTERNAL_CONTENT — the wrapped reference came from outside the script root (e.g. a remote player’s Avatar, another prop you found via SpawnableAPI.AllSpawnables, a world helper API returning a foreign transform).
  • INTERNAL — reserved for engine-internal wrappers the script should not touch at all.
  • NOT_AVAILABLE — sentinel used on singleton-style API wrappers (PlayerAPI, InstancesAPI, etc.) that have no meaningful scope of their own. When a wrapper carries NOT_AVAILABLE, the scope check is skipped (see the interface pattern below).

File: CVR-GameFiles/ABI.Scripting.CVRSTL.Common/BaseScriptedWrapper.cs

Every generated wrapper calls one of three helpers from its base class:

protected void CheckIfCanAccessMethod(
string methodName,
bool isStatic,
CVRLuaEnvironmentContext envContextMask,
CVRLuaObjectContext objectContextMask,
CVRLuaOwnerContext ownerContextMask,
CVRLuaScopeContext scopeContextMask = CVRLuaScopeContext.ANY)
{
if ((envContextMask & Context.envContext) == 0) throw new ScriptRuntimeException(...);
if ((objectContextMask & Context.objContext) == 0) throw new ScriptRuntimeException(...);
if ((ownerContextMask & Context.ownerContext) == 0) throw new ScriptRuntimeException(...);
if (this is IScriptedWrapper { ScopeContext: not CVRLuaScopeContext.NOT_AVAILABLE } w
&& (scopeContextMask & w.ScopeContext) == 0)
{
throw new ScriptRuntimeException(...);
}
}

The two sibling methods — CheckIfCanAccessProperty (adds an isSet flag so the error mentions get or set) and CheckIfCanAccessConstructor — are identical otherwise.

The scope branch short-circuits when ScopeContext is NOT_AVAILABLE: singleton API wrappers like PlayerAPI or InstancesAPI are always callable regardless of scope.

The per-VM state lives in CVRLuaContext:

File: CVR-GameFiles/ABI.Scripting.CVRSTL.Common/CVRLuaContext.cs

public class CVRLuaContext {
public CVRBaseLuaBehaviour behaviour;
public CVRLuaEnvironmentContext envContext; // CLIENT | SERVER
public CVRLuaObjectContext objContext; // AVATAR | PROP | WORLD | EVENT_WHITELIST
public CVRLuaOwnerContext ownerContext; // LOCAL | OTHER
public CVRLuaEnvironmentFlags environmentFlags;
public CVRLuaContextPolicy Policy => objContext.toPolicy();
public bool AllowedOnServer, AllowedOnClient;
public bool RunningOnServer, RunningOnClient;
public bool RunningInAvatar, RunningInProp, RunningInWorld;
public bool IsWornByMe; public string WearerUserId;
public bool IsSpawnedByMe; public string SpawnerUserId;
}

The Policy is looked up from CVRLuaContextPolicy.AVATAR, ...PROP, or ...WORLD:

File: CVR-GameFiles/ABI.Scripting.CVRSTL.Common/CVRLuaContextPolicy.cs

public class CVRLuaContextPolicy {
public static readonly CVRLuaContextPolicy AVATAR =
new(allowOnClient: true, allowOnServer: false, CVRLuaEnvironmentFlags.ON_CLIENT);
public static readonly CVRLuaContextPolicy PROP =
new(allowOnClient: true, allowOnServer: false, CVRLuaEnvironmentFlags.ON_CLIENT);
public static readonly CVRLuaContextPolicy WORLD =
new(allowOnClient: true, allowOnServer: false, CVRLuaEnvironmentFlags.ON_CLIENT);
public readonly bool allowOnClient, allowOnServer;
public readonly CVRLuaEnvironmentFlags defaultEnvFlags;
}

All three policies are identical today — client-only. The enum-to-policy mapping lives in CVRLuaContextTypeExtensions:

File: CVR-GameFiles/ABI.Scripting.CVRSTL.Common/CVRLuaContextTypeExtensions.cs

public static CVRLuaContextPolicy toPolicy(this CVRLuaObjectContext ctxType) => ctxType switch {
CVRLuaObjectContext.AVATAR => CVRLuaContextPolicy.AVATAR,
CVRLuaObjectContext.PROP => CVRLuaContextPolicy.PROP,
CVRLuaObjectContext.WORLD => CVRLuaContextPolicy.WORLD,
_ => throw new ArgumentException("Invalid CVRLuaObjectContext in toPolicy()."),
};

Any EVENT_WHITELIST context would fail the switch — the policy is only meaningful for the three content types.

All examples come from CVR-GameFiles/ABI.Scripting.CVRSTL.Common.UnityEngine/_LUAINSTANCE_ScriptedCamera.cs.

public bool allowHDR {
get {
CheckIfCanAccessProperty("allowHDR",
isStatic: false, isSet: false,
CVRLuaEnvironmentContext.ANY,
CVRLuaObjectContext.ALL_BUT_EVENTS,
CVRLuaOwnerContext.ANY);
return _wrapped.allowHDR;
}
}

Any VM, any content type (except during an event-whitelist callback), any owner. Scope defaults to ANY — so reading allowHDR is allowed even on a camera that lives in external content you just found via the scene graph.

Write a property — Camera.allowHDR (set)

Section titled “Write a property — Camera.allowHDR (set)”
set {
CheckIfCanAccessProperty("allowHDR",
isStatic: false, isSet: true,
CVRLuaEnvironmentContext.ANY,
CVRLuaObjectContext.ALL_BUT_EVENTS,
CVRLuaOwnerContext.ANY,
CVRLuaScopeContext.SELF); // tightened
_wrapped.allowHDR = value;
}

Same three upstream axes, but scope must be SELF. You can read any camera’s HDR flag, but you can only change a camera inside your own content root.

This “read-from-anywhere, write-only-to-SELF” pattern repeats across the entire UnityEngine module.

Read a RenderTexture handle — Camera.activeTexture

Section titled “Read a RenderTexture handle — Camera.activeTexture”
public _LUAINSTANCE_ScriptedRenderTexture activeTexture {
get {
CheckIfCanAccessProperty("activeTexture",
isStatic: false, isSet: false,
CVRLuaEnvironmentContext.ANY,
CVRLuaObjectContext.ALL_BUT_EVENTS,
CVRLuaOwnerContext.ANY,
CVRLuaScopeContext.SELF);
return new _LUAINSTANCE_ScriptedRenderTexture(base.Context, _wrapped.activeTexture);
}
}

Some getters are scope-gated too — reading activeTexture hands back a live handle, so the wrapper ties access to SELF. The new _LUAINSTANCE_ScriptedRenderTexture inherits the scope from Context.

The script is attached to…objContextTypical ownerContextNotes
Your worn avatarAVATARLOCALIsWornByMe == true. PlayerAPI.LocalPlayer refers to you. Viseme / AAS wrappers gate writes to SELF.
A remote player’s avatarAVATAROTHERIsWornByMe == false. Writes to your own player-level API (SetFlight, etc.) fail the owner check.
A prop you spawnedPROPLOCALIsSpawnedByMe == true. You can mutate the prop’s own transforms/materials (scope SELF); you cannot shove another player’s prop around.
A prop spawned by someone elsePROPOTHERReads are fine; writes to the prop’s scripted state fail owner checks.
A world rootWORLDLOCALAlways LOCAL for the local player. Widest surface.

The host feeds all of these into CVRLuaContext at VM construction and never mutates them afterwards — switching avatars restarts the VM entirely.

ScriptRuntimeException is a plain MoonSharp exception, raised from the C# wrapper and delivered to Lua as a runtime error. You can pcall around a call if you expect it to fail, or just let it propagate — MoonSharp will log the stack.

The messages name the failing axis, e.g.:

  • Access to property allowHDR (set) denied in a AVATAR object context.
  • Access to method SetPosition denied in a OTHER owner context.
  • Access to property activeTexture (get) denied in a EXTERNAL_CONTENT scope context.
  • Access to constructor (static) UnityEngine.Texture2D denied in a SERVER environment context.

Debug.Log(Context) runs just before the throw so the full context tuple lands in the player log too.

These generalize ~95% of the binder surface; individual wrappers are free to be stricter.

Call shapeTypical environment × object × owner × scope
Property getter on a Unity-side value (e.g. transform.position)(ANY, ALL_BUT_EVENTS, ANY, ANY) — read anything in the scene.
Property setter on a Unity-side value (e.g. transform.position =)(ANY, ALL_BUT_EVENTS, ANY, SELF) — only mutate your own content.
Instance method that writes state (AudioSource:Play, Rigidbody:AddForce)(ANY, ALL_BUT_EVENTS, ANY, SELF).
Static helpers (UnityEngine.Mathf.Sin, UnityEngine.Physics.Raycast)(ANY, ALL_BUT_EVENTS, ANY, ANY) — no this to scope-check.
World-only CVR APIs (WorldAPI, certain gameplay helpers)(ANY, WORLD, ANY, -) — callable only from a world script.
Singleton APIs (PlayerAPI, InstancesAPI, AvatarAPI)Scope is NOT_AVAILABLE — the scope check is skipped entirely.

The overall principle is the same as the WASM sandbox: observe freely, modify conservatively. You can read anything in the scene — raycast the world from an avatar, query a remote player’s position, enumerate spawnables — but you may not write to external content.

When a wrapper call fails with ScriptRuntimeException, read the axis in the message and map it back:

  • environment context — the call isn’t allowed in CLIENT / SERVER right now. Only meaningful if/when a server runtime ships.
  • object context — the API is world-only (or avatar/prop-only). Move the logic to a script hosted by the right content type.
  • owner context — you don’t own this content. Gate the call with an ownership check: if IsWornByMe then ... or if IsSpawnedByMe then ....
  • scope context — you touched something outside your own content root. Bring the reference inside your root (via BoundObjects, a child transform, or the CCK editor) or use a read-only API instead.
  • CVR-GameFiles/ABI.Scripting.CVRSTL.Common/CVRLuaContext.cs — runtime context, with per-axis Running* shortcuts and IsWornByMe / IsSpawnedByMe.
  • CVR-GameFiles/ABI.Scripting.CVRSTL.Common/CVRLuaContextPolicy.cs — the AVATAR, PROP, WORLD policies (all client-only today).
  • CVR-GameFiles/ABI.Scripting.CVRSTL.Common/CVRLuaContextTypeExtensions.csobjContext -> policy switch.
  • CVR-GameFiles/ABI.Scripting.CVRSTL.Common/CVRLuaEnvironmentContext.csNONE, CLIENT, SERVER, ANY.
  • CVR-GameFiles/ABI.Scripting.CVRSTL.Common/CVRLuaEnvironmentFlags.csNONE, ON_CLIENT, ON_SERVER.
  • CVR-GameFiles/ABI.Scripting.CVRSTL.Common/CVRLuaObjectContext.csNONE, AVATAR, PROP, WORLD, EVENT_WHITELIST, ALL_BUT_EVENTS, ANY.
  • CVR-GameFiles/ABI.Scripting.CVRSTL.Common/CVRLuaOwnerContext.csNONE, LOCAL, OTHER, ANY.
  • CVR-GameFiles/ABI.Scripting.CVRSTL.Common/CVRLuaScopeContext.csNONE, SELF, EXTERNAL_CONTENT, INTERNAL, NOT_AVAILABLE, ANY_EXCEPT_INTERNAL, ANY.
  • CVR-GameFiles/ABI.Scripting.CVRSTL.Common/BaseScriptedWrapper.csCheckIfCanAccessConstructor, CheckIfCanAccessMethod, CheckIfCanAccessProperty.
  • Bindings Catalog — every wrapped type, organized by module.
  • Security — higher-level guidance for authors.
  • WASM Permissions — the three-axis model used by the sibling WASM runtime. Same shape, one fewer axis (no environment split).