41 KiB
Prism automation architecture
This document is a planning artifact. It records confirmed repository facts and proposes a small automation subsystem that can grow from the current prism exec <file.lua> primitive without creating unrelated queues, endpoint families, poll loops, and result formats for every future command.
External Roblox API notes were checked against Creator Hub pages on 2026-07-20:
Selectiondocuments plugin access toGet,Set,Add,Remove, andSelectionChanged: https://create.roblox.com/docs/reference/engine/classes/SelectionStudioCaptureServicedocuments plugin-security screenshot methods: https://create.roblox.com/docs/reference/engine/classes/StudioCaptureServiceStudioScreenshotCapturedocuments plugin-securityGetBuffer,GetErrors, andScaleAsync: https://create.roblox.com/docs/reference/engine/classes/StudioScreenshotCaptureRunServicedocumentsRunandStopas plugin-security methods, plusIsEdit,IsRunning,IsRunMode,IsStudio,IsServer, andRunState: https://create.roblox.com/docs/reference/engine/classes/RunServiceStudioTestServicedocuments plugin-securityExecutePlayModeAsync,ExecuteRunModeAsync, and multiplayer test methods: https://create.roblox.com/docs/reference/engine/classes/StudioTestServiceChangeHistoryServicedocumentsTryBeginRecordingandFinishRecording, and saysSetWaypointis being replaced by recordings: https://create.roblox.com/docs/reference/engine/classes/ChangeHistoryServiceInstance.UniqueIdis documented as Roblox security, so it is not a plugin-accessible persistent identifier for this design: https://create.roblox.com/docs/reference/engine/classes/Instance
1. Current Prism architecture
Confirmed facts:
- CLI subcommands are declared in
src/cli/mod.rs.Options::rundispatchesSubcommand::ExectoExecCommand::run. src/cli/exec.rsimplementsrojo exec <FILE>with--addressand--port, reads UTF-8 source, strips one leading BOM, enforcesMAX_SOURCE_SIZE_BYTES, checks/api/rojo, submits MessagePack to/api/exec/jobs, polls/api/exec/jobs/{id}, and renders terminal results and logs.src/exec.rsowns the current server-side exec queue.ExecJobStoreusesMutex,HashMap, andVecDeque, supports one claimed job at once, and tracksPending,Claimed,Succeeded,Failed, andTimedOut.src/serve_session.rsstores oneExecJobStoreperServeSessionasexec_job_store: ExecJobStoreand exposesexec_job_store(&self) -> &ExecJobStore.src/web/mod.rsbuilds the raw Hyper server withmake_service_fnandservice_fn, sends/api...requests tosrc/web/api.rs, and handles non-API requests through the UI module.src/web/api.rsmanually parses/api/exec/jobs,/api/exec/jobs/next,/api/exec/jobs/{id}, and/api/exec/jobs/{id}/complete. It rejects non-loopback peers using the same locality check as/api/open, cleans expired jobs on exec requests, and returns MessagePack API errors.src/web/interface.rsdefines the wire structs and enums for exec, the sync protocol, WebSocket packets, binary model serialization, and the commonErrorResponse.src/web/util.rsdefines MessagePack helpers usingrmp_serdewith human-readable struct maps andapplication/msgpack.plugin/src/ApiContext.luaowns plugin HTTP methods. Exec currently addsclaimNextExecJob()andcompleteExecJob(jobId, payload)using the existing Promise and MessagePack wrappers.plugin/src/ServeSession.luaowns the plugin connection lifecycle. It createsExec.new, starts it only after initial sync and WebSocket creation, and stops it from__stopInternalbefore disconnectingApiContext.plugin/src/Exec.luaowns the production exec poller and executor. It claims at roughly 250 ms intervals only in edit mode, executes a fresh temporaryModuleScript, records one undo action withChangeHistoryService:TryBeginRecordingandFinishRecording, captures bounded logs throughrojoExec.print/warn, encodes structured scalar/array/table results, retries completion, and treats timeouts as soft.plugin/src/Types.luavalidates API payloads witht, including exec claim/status/result/log structs.plugin/http/init.lua,plugin/http/Error.lua, andplugin/http/Response.luaprovide Promise-based Roblox HTTP wrappers. Non-2xx responses reject asHttp.Error; this is whyApiContext.completeExecJobhas a compatibility check for HTTP 409 text.
Proposal:
Prism should treat the current exec flow as the first implementation of a general automation pattern: CLI submits a typed job to the server, the connected plugin claims and runs it on the current Studio DataModel, the plugin posts a typed completion, and the CLI renders the result. The generalization should be narrow and typed, not a generic remote RPC system.
2. Design goals
- One session-scoped automation store, one claim loop in the plugin, one result envelope, and one CLI polling pattern for short request/response commands.
- Typed automation handlers for inspect, search, selection, camera, focus, screenshot, play/stop, doctor plugin checks, snapshot, diff, preview, and future playtesting.
- Preserve arbitrary
execas a trusted escape hatch and a development tool, but do not build ordinary product commands as string-generated Luau wrappers around exec. - Support small MessagePack job responses, large/binary artifacts, long-running streams, and previewable DataModel modifications as separate transport modes under one automation namespace.
- Keep WebSocket live sync and
MessageQueuebehavior unchanged unless a future phase deliberately extends them. - Keep all automation endpoints local-only unless the user later designs authentication.
3. Non-goals
- No public network RPC framework.
- No authentication in the immediate MVP beyond loopback/local peer checks.
- No multi-user Studio collaboration semantics in the first version.
- No promise that Preview Mode is a security sandbox.
- No attempt to use undocumented persistent Roblox identifiers.
- No routing of play-client versus play-server automation until playtesting is designed.
- No reuse of the live-sync WebSocket packet schema for unrelated automation events.
4. Command classification
Confirmed command list comes from the user request. Classifications below are proposals.
| Command | Classification | Reasoning |
|---|---|---|
exec |
Arbitrary exec wrapper and specialized automation job | Already implemented as trusted Luau source. It should remain special because it accepts arbitrary code and has source-size/undo semantics unlike typed commands. |
inspect |
Typed one-shot automation job | Needs Studio DataModel access and a structured request/result. Should not generate source. |
search |
Typed one-shot automation job | Pure DataModel traversal with filters and bounded results. |
selection |
Typed one-shot automation job | Documented Selection:Get() is plugin-accessible and returns current selection. |
select |
Typed one-shot automation job | Documented Selection:Set() is plugin-accessible; needs reference resolution and edit-mode behavior verified. |
camera |
Typed one-shot automation job | Reads Workspace.CurrentCamera properties. No source execution needed. |
focus |
Studio lifecycle/typed one-shot automation job | Mutates camera state to frame an object. Needs bounding box handling and edit-mode verification. |
screenshot |
Binary artifact job | Screenshot capture returns a buffer according to current docs. The CLI should download an artifact rather than receive a huge MessagePack completion. Requires Studio spike. |
play |
Studio lifecycle command | RunService:Run and StudioTestService play methods exist with plugin security, but actual installed-plugin behavior must be spiked. |
stop |
Studio lifecycle command | RunService:Stop and StudioTestService:LeaveTest exist with plugin/security indications, but behavior depends on mode. |
doctor |
CLI-only plus typed one-shot automation job | CLI can check server reachability/protocol. Plugin status and Studio mode require a plugin-side automation status handler. |
snapshot |
Typed one-shot automation job, possibly artifact job for large output | Needs bounded DataModel traversal and deterministic encoding. Large snapshots should use artifacts. |
diff |
CLI-only when comparing saved snapshots; typed one-shot when diffing live Studio | Diffing two local snapshot files is CLI-only. Diffing live Studio against a file or current server state needs automation. |
watch |
Persistent WebSocket stream | Live events must not use 250 ms HTTP polling. |
profile |
Typed one-shot automation job, unsupported parts require verification | Hierarchy/class/script counts are easy. Memory, script performance, and MicroProfiler data may be unavailable/private. |
preview |
Typed one-shot automation job with preview/apply protocol | Requires execution plus snapshot/diff/patch capture. Not a secure sandbox. |
| Automated playtesting | Unsupported without further Studio verification, later WebSocket/artifact/lifecycle system | Requires play/stop, input injection, observations, logs, screenshots, assertions, reports, and client/server routing. |
Features that can be wrappers over exec: quick user-authored scripts, early prototypes of inspect/search, and manual experiments. Features that deserve dedicated handlers: inspect, search, selection/select, camera/focus, screenshot, play/stop, doctor status, snapshot/diff, watch, profile, preview/apply. Dedicated handlers give typed validation, bounded traversal, stable output, and clearer safety behavior.
5. Shared automation job model
Proposal:
ExecJobStore should evolve into AutomationJobStore, but not by erasing all domain types into arbitrary maps. The generic store should own queue mechanics, deadlines, terminal retention, one active claim, capacity limits, local cleanup, and common state transitions. Job payloads should remain typed enums in Rust and Luau.
Suggested Rust shape:
pub struct AutomationJob {
pub id: Uuid,
pub kind: AutomationJobKind,
pub request: AutomationRequest,
pub state: AutomationJobState,
pub created_at: SystemTime,
pub claim_deadline: Instant,
pub execution_deadline: Option<Instant>,
pub result: Option<AutomationResult>,
pub logs: Option<Vec<AutomationLog>>,
pub error: Option<AutomationError>,
}
pub enum AutomationRequest {
Exec(ExecRequest),
Inspect(InspectRequest),
Search(SearchRequest),
Selection(SelectionRequest),
Select(SelectRequest),
Camera(CameraRequest),
Focus(FocusRequest),
Screenshot(ScreenshotRequest),
Play(PlayRequest),
Stop(StopRequest),
Snapshot(SnapshotRequest),
Preview(PreviewRequest),
}
AutomationJobState should keep the existing lifecycle names: pending, claimed, succeeded, failed, timedOut, plus future canceled only when a real cancellation endpoint exists. kind should be an explicit enum such as exec, inspect, search, screenshot, not a free-form string.
Arbitrary exec should remain a specialized job type:
- It carries trusted source,
scriptName, source limits, undo label, and privileged result/log semantics. - It should continue to be visually/audibly branded as Prism exec.
- Typed handlers should not depend on
ModuleScript.Sourceorrequireunless they are intentionally executing arbitrary source.
6. HTTP and WebSocket layout
Confirmed current layout:
/api/rojoreturns server/session info./api/read/{ids},/api/write,/api/serialize, and/api/ref-patchsupport live sync and sync fallback./api/socket/{cursor}is a WebSocket for server-to-plugin live-sync messages./api/exec/jobs...is a local-only HTTP MessagePack job family.
Proposal:
Use one future endpoint family for typed one-shot jobs:
POST /api/automation/jobsGET /api/automation/jobs/nextGET /api/automation/jobs/{id}POST /api/automation/jobs/{id}/completePOST /api/automation/jobs/{id}/cancel
Keep /api/exec/jobs... during transition as a compatibility alias or thin wrapper around AutomationJobKind::Exec. New features should not add /api/inspect/jobs, /api/search/jobs, etc.
Use separate transport for artifacts:
POST /api/automation/jobs/{id}/artifactsfor plugin uploads.GET /api/automation/artifacts/{artifactId}for CLI downloads.DELETE /api/automation/artifacts/{artifactId}is optional; server cleanup by age and count is enough for MVP.
Use separate WebSocket stream endpoints for long-running subscriptions:
POST /api/automation/streamsto create a watch/profile/playtest stream request.GET /api/automation/streams/{streamId}/socketorGET /api/automation/socket/{streamId}for CLI event consumption.- The plugin can either claim stream jobs over the automation job queue and then post events, or connect to a plugin-side automation socket later. MVP should prefer plugin HTTP event posts to the server and CLI WebSocket downloads from the server. That avoids requiring Studio to maintain a second outgoing WebSocket before the shape is proven.
Do not reuse MessageQueue<AppliedPatchSet> or SocketPacketType::Messages for automation. The current WebSocket is specifically live-sync server-to-plugin patch delivery.
7. Plugin session identity
Confirmed current state:
src/serve_session.rshas a server-sideSessionId, returned by/api/rojo.- Existing read/write/serialize/ref-patch requests include
sessionIdto detect server changes. - Exec routes do not include a plugin-session identifier.
- The server does not currently track connected Studio plugin sessions as first-class records.
plugin/src/App/init.luauses a Studio-side__Rojo_SessionLockinServerStoragefor Team Create sync ownership, but this is not an HTTP server session registry.- Multiple WebSocket clients can technically connect to
/api/socket/{cursor}becausesrc/web/api.rsspawns a handler per upgrade. Exec prevents concurrent work by allowing only one claimed job, not by identifying the claimant.
Proposal for MVP:
- Add a
pluginSessionIdgenerated by the plugin after/api/rojosucceeds and passed in automation claim/complete calls. - The server should remember the active automation plugin session for the current
ServeSessionafter first claim/status heartbeat. - If no plugin is connected, one-shot jobs can remain pending until claim timeout.
doctorshould report this as "server reachable, plugin not claiming automation jobs".
Future multi-session handling:
- Store
AutomationStudioSession { pluginSessionId, serverSessionId, placeId, gameId, studioUserId?, connectedAt, lastSeenAt, mode, label }. - A job may have
targetSession: "default" | { pluginSessionId }. - MVP should reject multiple active automation sessions or require an explicit chooser later. Do not auto-broadcast arbitrary exec to every Studio window.
8. Instance references
Problem:
Roblox Instances can be renamed, reparented, destroyed, duplicated, or recreated between commands. Documented Instance.UniqueId is Roblox security, so Prism should not rely on it. Full paths are human-friendly but not stable across rename/reparent. Debug IDs are not a good protocol foundation unless a live spike proves plugin access and stability, and even then they should be treated as diagnostic, not persistent.
MVP proposal:
- Accept target syntax as escaped DataModel paths, plus aliases:
game/orDataModelworkspaceorWorkspaceselection@<session-local-id>for references returned by previous commands in the same plugin session
- Return references as:
{
"kind": "instance",
"id": "pinst-00000042",
"path": "Workspace.Map.Model",
"className": "Model",
"name": "Model",
"ancestry": [
{ "name": "Workspace", "className": "Workspace" },
{ "name": "Map", "className": "Folder" },
{ "name": "Model", "className": "Model" }
]
}
- The plugin owns a session-local weak registry from generated IDs to Instances. Returned references include path/class/name snapshots for human output and stale-reference diagnostics.
- On use,
@idresolves through the registry and validatesParent ~= nil,IsDescendantOf(game), class name if supplied, and optional ancestry hash. If stale, return a typed stale-reference error. - Escaped paths should be deterministic and unambiguous: split on
., allow quoted segments for names containing.,\, control characters, or leading/trailing whitespace, for exampleWorkspace."Map.With.Dot"."Part \"A\"". The exact grammar should be specified before implementation and tested.
Later evolution:
- Add reference leases with
expiresAtand a CLI-visiblesessionId. - Add
refreshReferenceandresolvePathhandlers. - If Roblox exposes a documented, plugin-accessible persistent identifier later, layer it in as an optional validation field, not as the only reference.
9. Roblox value encoding
Confirmed exec value schema:
- Rust
src/web/interface.rsexposes taggedExecValuevariants:nil,string,number,boolean,array, andtable. - Plugin
plugin/src/Exec.luarejects NaN/infinity, cycles, sparse arrays, mixed array/dictionary tables, non-string dictionary keys, and unsupported values. It sorts table keys deterministically.
Proposal:
Create AutomationValue as a superset of ExecValue:
- Scalars:
nil,string, finitenumber,boolean. - Structured: dense
array, sorted string-keytable. - Roblox primitives:
vector2,vector3,cframe,color3,udim,udim2,rect,enumItem,brickColor,numberRange,numberSequence,colorSequence,physicalProperties. - Instance references:
{ kind = "instanceRef", value = InstanceReference }. - Binary artifacts are never embedded as
AutomationValue; use artifact IDs.
Keep encoding small and explicit. Values that cannot be represented safely should return a typed unsupportedValue error with a path such as properties.CFrame or result.items[3].
10. Inspect and search
prism inspect <target> proposal:
Request:
{
"kind": "inspect",
"target": "Workspace.Map",
"depth": 1,
"includeProperties": "default",
"includeChildren": true,
"maxChildren": 200
}
Response:
{
"reference": { "kind": "instance", "id": "pinst-42", "path": "Workspace.Map", "className": "Model" },
"name": "Map",
"className": "Model",
"path": "Workspace.Map",
"properties": {},
"attributes": {},
"tags": [],
"children": []
}
Design choices:
- Target syntax should use the shared escaped path/reference grammar.
- Defaults: depth
1, max children200, max total instances2_000, max propertiesdefault. --jsonshould print deterministic JSON. Human output can be compact but should never be the only format.--depth,--children, and--propertiesmap directly to request fields.- Use a property allowlist per class for MVP. Avoid expensive or sensitive properties by default.
- Script source privacy: do not return
SourcefromScript,LocalScript, orModuleScriptunless an explicit future flag is designed. - Non-serializable Roblox datatypes return a placeholder error per property rather than failing the entire inspect.
- Sort attributes, tags, property keys, and children by current child order with deterministic tie behavior.
prism search <query> proposal:
- Avoid a full query language at first.
- Support flags:
--name,--class,--tag,--attribute key[=value],--ancestor <target>,--case-sensitive,--limit. - Bare
<query>should search name substring case-insensitively. - Return an array of instance references with name, className, path, selected matched fields, and a truncated preview.
11. Selection
Confirmed external fact:
- Roblox documents
Selection:Get()andSelection:Set()as usable by plugins and the command line.
Proposal:
prism selectionsubmits{ kind = "selectionGet" }and returns the selected instances as references in selection order.prism select <target...>resolves each target and submits{ kind = "selectionSet", references = [...] }.prism select --clearsubmits an empty set.- In edit mode, selection changes should not be wrapped in
ChangeHistoryServiceby default unless a live spike shows they are undoable and useful. Treat selection as Studio UI state, not DataModel mutation.
Studio verification required:
- Confirm installed plugin access to
Selection:Set()in edit mode. - Confirm behavior in Play Solo and whether changing selection during play should be rejected or allowed as a UI-only operation.
- Confirm how selection behaves for destroyed/stale references.
12. Camera and focus
Confirmed external facts:
- Roblox documents
Camera.CFrame,Focus,FieldOfView,ViewportSize,GetRenderCFrame, andZoomToExtents. Workspace.CurrentCamerais the current camera object in normal camera documentation.
Proposal:
prism camerareturns current camera CFrame, Focus, FieldOfView, FieldOfViewMode, CameraType, CameraSubject reference if serializable, and ViewportSize.prism focus <target>resolves an Instance and frames it:BasePart: useCFrameandSize.Model: useModel:GetBoundingBox(), reject empty models with no geometric descendants.Attachment: frame aroundWorldPositionwith a small default radius.selection: frame union of selected supported objects.
- Mutate
Workspace.CurrentCamerain edit mode by setting camera type/scriptable behavior only if needed and reversible enough for user expectations.
Studio verification required:
- Confirm installed plugin can reliably read and set
Workspace.CurrentCamerain edit mode. - Confirm whether
Camera:ZoomToExtentsworks from plugin context. - Confirm whether camera changes should be excluded from undo and snapshot diffs.
13. Screenshot feasibility and artifacts
Confirmed external facts:
- Current Creator Hub documents
StudioCaptureService:CanCaptureScreenshot(),RequestScreenshotPermissionAsync(), andCaptureScreenshot(options)with plugin security. StudioScreenshotCaptureexposesGetBuffer(): buffer, read-onlyBufferFormat,BufferStatus,OriginalSize,Position,Resolution,UICaptureMode,GetErrors(), andScaleAsync(), all with plugin-security access where listed.Capture.FilePathStringis Roblox script/security restricted on the separate in-experienceCaptureclass, so local file-path handoff should not be assumed.
Proposal:
- Treat
screenshotas a binary artifact job, not a normal completion payload. - Plugin captures a screenshot, validates
BufferStatus, readsGetBuffer(), and uploads the bytes to the server as an artifact associated with the job. - Completion returns:
{
"kind": "screenshot",
"artifactId": "...",
"contentType": "image/png",
"width": 1920,
"height": 1080,
"format": "png",
"byteLength": 1234567
}
Artifact transfer options:
- Best MVP: plugin uploads artifact bytes to
POST /api/automation/jobs/{id}/artifacts, CLI downloadsGET /api/automation/artifacts/{artifactId}as raw bytes. - Acceptable fallback: MessagePack
serde_bytes/Luau buffer chunks if RobloxRequestAsynccannot send raw binary safely. - Avoid base64 unless Roblox HTTP string-body constraints make binary unsafe; base64 adds size overhead and extra copies.
- Avoid local path handoff because plugin-accessible file paths are not established.
Recommended limits:
- Default max screenshot artifact: 16 MiB.
- Default max artifact retention: 5 minutes or 16 artifacts.
- Optional downscale in plugin before upload if requested size exceeds max.
Studio verification required:
- Confirm installed plugin can call
RequestScreenshotPermissionAsyncand how often permission prompts. - Confirm
CaptureScreenshotcaptures the 3D viewport, plugin widgets, game UI, or some combination. - Confirm edit mode versus Play Solo differences.
- Confirm buffer format and whether
GetBuffer()is PNG/JPEG bytes or raw pixels for eachBufferFormat. - Confirm whether user permission is required and how denial is reported.
14. Play and stop feasibility
Confirmed external facts:
- Roblox documents
RunService:Run()andRunService:Stop()with plugin security. - Roblox documents
StudioTestService:ExecutePlayModeAsync,ExecuteRunModeAsync,ExecuteMultiplayerTestAsync,CanLeaveTest, andLeaveTest; several have plugin security. - Existing Prism plugin code uses
RunService:IsEdit,IsRunning,IsStudio, andIsServer, and already has Play Solo auto-connect logic inplugin/src/App/init.lua.
Proposal:
prism playshould first be a Studio lifecycle command with{ kind = "play", mode = "playSolo" | "run" }.prism stopshould stop the active Studio test/run session if Prism started it or if a safe, explicit--forceis later designed.- The automation subsystem should record a
studioModein plugin status so CLI can explain when play/stop is unavailable.
Studio verification required:
- Confirm installed plugin can start Play Solo through
StudioTestServiceorRunServicewithout internal APIs. - Confirm whether
RunService:Run()starts Run mode, not Play Solo with a player. - Confirm how to stop a play session from the edit-mode plugin versus a play-mode auto-connected plugin.
- Confirm whether
StudioTestServicemethods require special beta flags or user permissions.
15. Doctor
Proposal:
CLI-only checks:
- Server reachable at address/port.
/api/rojoreturns MessagePack with expected protocol version.- Host/origin/local-only behavior is sane for loopback.
- Server version, project name, address, port, expected place IDs, blocked place IDs.
- Exec/automation submission route exists.
- Queue capacity and stale jobs if the server exposes status.
Plugin-status checks:
- Plugin connected and claiming automation.
- Plugin session ID and active Studio mode.
- HTTP requests enabled in Studio. Current
plugin/http/Error.luamaps disabled HTTP to a clear Prism error. - Duplicate plugin/session detection.
- Exec availability and automation handler versions.
- Mismatched plugin/server version or protocol.
- Current DataModel place/game IDs.
Implementation proposal:
- Add
{ kind = "doctor" }automation job returning a typed health object. - Later add
GET /api/automation/statusfor server-only state that does not need plugin execution.
16. Snapshot and diff
Snapshot proposal:
- A bounded DataModel snapshot should include session ID, root reference, traversal limits, deterministic node IDs, name, className, escaped path, attributes, tags, allowlisted properties, child order, and diagnostics for skipped values.
- Exclude script
Sourceby default. Include script metadata only unless explicitly requested. - Exclude or summarize terrain and binary content for MVP.
- Use deterministic hashes per node and for the whole snapshot.
- Large snapshots should be artifacts, not normal job result payloads.
Diff proposal:
+ instance: created instance with class/name/parent/order and properties.- instance: removed instance reference plus last known snapshot.~ property: changed allowlisted property.~ attribute: changed attribute.~ tag: added/removed tag.> reparent: parent changed.> reorder: sibling order changed.
Storage recommendation:
- CLI memory for immediate
snapshot | diffpipelines. - Disk for user-requested
.prism-snapshot.msgpackor JSON snapshot files. - Server state only for temporary preview IDs and artifacts.
- Plugin state only for session-local instance references and active watch subscriptions.
17. Watch streams
Proposal:
prism watch <target> should use a stream, not HTTP polling.
Potential events:
descendantAddeddescendantRemovedpropertyChangedattributeChangedtagChangedancestryChangedselectionChanged
Transport:
- CLI opens a WebSocket to the server for a stream ID.
- Plugin subscribes to Roblox events for the target and posts batches to the server, or in a later phase opens a plugin automation WebSocket.
- Server forwards ordered batches to CLI and applies backpressure limits.
Requirements:
- Subscription IDs and explicit unsubscribe.
- Disconnect cleanup on CLI disconnect, plugin disconnect, and server shutdown.
- Event coalescing over short windows, for example 50-100 ms.
- Event count/byte limits with a
droppedEventsmarker. - Initial snapshot option before live events.
- Ordering by plugin-side sequence number.
- No interference with
/api/socket/{cursor}live-sync packets orMessageQueue.
18. Profile feasibility
Proposal:
Call the first version profile only if the output is honest about scope.
Likely supported through plugin DataModel traversal:
- Instance counts.
- Class counts.
- Script counts by class.
- Approximate hierarchy depth and child fanout.
- Counts of tagged/attributed instances.
- Counts of oversized strings or selected expensive properties if included.
Requires Studio verification or may be unsupported/private:
- Memory categories.
- Physics metrics.
- Script performance.
- MicroProfiler data.
- Render stats and GPU timings.
Do not call hierarchy counts a full profiler. Use output labels such as hierarchySummary and unavailableMetrics.
19. Preview-mode threat model
Preview Mode is not a secure sandbox. It is a workflow for previewing and reverting many ordinary DataModel edits from trusted automation.
Potentially reversible DataModel effects:
- Creating/destroying Instances.
- Property changes.
- Attributes.
- Tags.
- Parenting and reparenting.
- Sibling order.
- Many script source changes, subject to source access and snapshot limits.
- Some terrain changes only if explicit terrain snapshot/patch support is built.
Potentially non-reversible or external effects:
- HTTP requests.
- DataStore/Open Cloud actions.
- Marketplace, publishing, or asset generation actions.
- Plugin settings.
- Selection changes.
- Camera changes.
- External logging/analytics.
- Detached tasks that keep running after preview.
- User prompts.
- Play-mode effects.
- Filesystem-like plugin capabilities, if any are exposed.
- Side effects outside
ChangeHistoryService.
Approach comparison:
- Begin recording, execute, capture diff, cancel recording: attractive but risky.
ChangeHistoryServiceis for undo history, not security, and cancellation semantics need live proof for all mutation classes. - Clone a bounded subtree and execute against the clone: safer for subtree-local edits but many scripts expect services such as
workspace,Lighting, orSelection; transparent redirection is hard. - Clone the whole DataModel into a temporary DataModel: not feasible with normal plugin APIs.
- Record mutations through an injected API: safest for deterministic apply, but not compatible with arbitrary Luau that mutates normal Roblox objects directly.
- Execute normally and derive a patch from before/after snapshots: feasible for trusted scripts and typed handlers; misses external effects and unsupported properties.
- Use Roblox undo as rollback: useful as a cleanup mechanism, not a security boundary.
- Re-run the script during apply: unsafe and non-deterministic because the script may depend on time, selection, random values, HTTP, or mutated state.
- Apply a captured deterministic patch instead of re-running: preferred when patch coverage is sufficient.
Recommended MVP:
- Name it Preview Mode, not sandbox.
- Run trusted source once in edit mode.
- Take a bounded before snapshot.
- Execute with a
ChangeHistoryServicerecording. - Take a bounded after snapshot.
- Compute a structured patch.
- Roll back live changes using Roblox undo or an explicit inverse patch, with live Studio verification deciding which is more reliable.
- Store the captured patch and before-hash in server state under a preview ID.
preview apply <preview-id>applies the captured patch, not the original source.- Reject apply if the referenced DataModel subtree changed since preview.
- Expire previews quickly.
20. Preview/apply protocol
Proposal:
Preview job request:
{
"kind": "preview",
"scriptName": "destructive-edit.lua",
"source": "...",
"target": "game",
"snapshotOptions": {
"maxInstances": 10000,
"propertySet": "default"
}
}
Preview result:
{
"kind": "preview",
"previewId": "...",
"baseHash": "...",
"patchArtifactId": "...",
"summary": {
"added": 3,
"removed": 0,
"propertiesChanged": 5,
"attributesChanged": 1,
"tagsChanged": 0,
"reparented": 0,
"reordered": 0
},
"rollback": {
"ok": true,
"method": "undo"
}
}
Apply request:
{
"kind": "previewApply",
"previewId": "...",
"expectedBaseHash": "..."
}
Apply result:
{
"kind": "previewApply",
"applied": true,
"undoLabel": "Prism Preview Apply: destructive-edit.lua"
}
21. Undo and rollback limitations
Confirmed facts:
- Current live sync uses
ChangeHistoryService:TryBeginRecording("Prism: Patch ...")andFinishRecording(...Commit)inplugin/src/ServeSession.lua. - Current exec uses
TryBeginRecording("Prism Exec: <scriptName>")and commits the recording inplugin/src/Exec.lua. - Current code does not use
SetWaypointfor exec. Creator Hub marksSetWaypointas headed toward deprecation in favor of recordings.
Proposal:
- Typed mutating automation jobs should use
TryBeginRecording/FinishRecording, notSetWaypoint. - Read-only jobs should not open a recording.
- Runtime failures after partial mutation should commit one undoable action when the user asked for real execution.
- Preview should isolate user experience by rolling back, but must disclose that external effects are not rollback-protected.
- The controller, not user code, owns
FinishRecording.
22. Automated playtesting prerequisites
Do not deeply design playtesting yet. Minimum prerequisites:
- Verified
playandstop. - Route execution to edit plugin, play server, and play client contexts.
- Stable plugin/session identity across edit/play transition.
- Input injection feasibility.
- Camera/screenshot support.
- UI inspection support.
- Log collection from relevant contexts.
- Assertion result schema.
- Artifact/report storage.
- Timeouts and cleanup for hung tests.
- Clear distinction between local Studio automation and publish/live-service effects.
23. Security and local-only boundaries
Confirmed current security boundaries:
src/web/mod.rsrejects cross-origin requests before API routing.- Exec routes in
src/web/api.rsreject non-loopback peers usingreject_non_local, matching/api/open. - There is no authentication on exec.
- Exec is trusted-source only.
Proposal:
- Keep all
/api/automation/...endpoints local-only for MVP. - Keep source execution and mutating jobs opt-in from the CLI.
- Do not expose arbitrary automation to non-loopback peers without a separate authentication design.
- Include protocol and handler versions in automation status so the CLI can fail closed on mismatches.
24. Exact likely file plan
Rust CLI:
- Modify
src/cli/mod.rsfor new subcommands. - Create or modify
src/cli/automation.rsfor shared submit/poll/render helpers. - Create
src/cli/inspect.rs,src/cli/search.rs,src/cli/selection.rs,src/cli/select.rs,src/cli/camera.rs,src/cli/focus.rs,src/cli/screenshot.rs,src/cli/play.rs,src/cli/stop.rs,src/cli/doctor.rs,src/cli/snapshot.rs,src/cli/diff.rs,src/cli/watch.rs,src/cli/profile.rs, andsrc/cli/preview.rsas phases justify. - Keep
src/cli/exec.rs, but route its HTTP job operations through shared automation helpers after the protocol migration.
Rust server/protocol:
- Create
src/automation.rsforAutomationJobStore, job states, errors, cleanup, artifact registry, and common limits. - Modify
src/serve_session.rsto replace or wrapexec_job_storewithautomation_job_store. - Modify
src/web/interface.rsforAutomationRequest,AutomationResult, values, references, artifacts, status, and stream messages. - Modify
src/web/api.rsfor/api/automation/jobs..., artifact routes, status routes, and stream setup. - Keep
src/web/util.rsMessagePack helpers. - Keep
src/exec.rsduring migration or reduce it to exec-specific request/value helpers once automation owns queue mechanics.
Studio plugin:
- Create
plugin/src/Automation.luafor the shared poller/dispatcher. - Create
plugin/src/AutomationHandlers/*.luafor inspect, search, selection, camera, screenshot, etc. - Create
plugin/src/AutomationValue.luaandplugin/src/InstanceReferences.lua. - Modify
plugin/src/ApiContext.luato add generic automation claim/complete/artifact methods. - Modify
plugin/src/ServeSession.luato start one automation poller after initial sync/WebSocket setup and stop it during disconnect. - Modify
plugin/src/Types.luafor automation validators. - Keep
plugin/src/Exec.lua, but make it an automation handler instead of its own poll loop.
Tests:
- Extend
tests/tests/serve.rsandtests/rojo_test/serve_util.rsfor generic automation HTTP routes and artifacts. - Add Rust unit tests in
src/automation.rs. - Add plugin TestEZ specs for
Automation.lua,AutomationValue.lua,InstanceReferences.lua, and each handler.
Documentation:
- Keep this file:
design/prism/AUTOMATION_ARCHITECTURE.md. - Later add command-specific design docs only for large uncertain areas such as screenshot, playtesting, and Preview Mode.
25. Testing strategy
Rust:
- Generic store: submit, claim, one active claim, complete success/failure/timeout/cancel, duplicate claim, duplicate completion, unknown job, capacity, size, cleanup, artifact retention.
- API routing: exact route matching,
/nextbefore{id}, malformed IDs, local-only rejection, MessagePack decode errors, source/payload/artifact limits. - CLI: clap parsing, file/target validation, server check, status rendering, JSON output, HTTP error decoding, artifact download, timeouts.
- WebSocket streams: stream creation, event ordering, backpressure, disconnect cleanup, no interference with live-sync socket tests.
Plugin:
- One automation poller starts once from
ServeSession. - No polling before connection or after disconnect.
- No duplicate claim while executing/completing.
- Handler dispatch validates kind.
- Instance reference creation/resolution/stale detection.
- Value encoding rejects unsupported and cyclic values.
- Each typed handler returns deterministic payloads.
- Watch subscriptions coalesce events and clean up.
- Screenshot/play/preview specs use injected dependencies; live Studio spikes verify actual APIs.
Manual Studio:
- Inspect Workspace and selected Instances.
- Search by name/class/tag/attribute.
- Selection get/set/clear.
- Camera read and focus on BasePart, Model, Attachment, and selection.
- Screenshot permission, viewport capture, output image validity, Play Solo behavior.
- Play/stop lifecycle behavior.
- Preview mutation, rollback, apply, stale apply rejection.
- Disconnect/reconnect recovery.
26. Open Studio verification questions
- Can installed plugins use
StudioCaptureServicescreenshot APIs without beta flags or special permissions? - What exactly is contained in
StudioScreenshotCapture:GetBuffer()for each buffer format? - Does screenshot capture include only the 3D viewport, plugin widgets, CoreGui/game UI, or different modes controlled by
UICaptureMode? - Can installed plugins start Play Solo reliably through
StudioTestService:ExecutePlayModeAsync, or only Run mode throughRunService:Run()? - Can an edit-mode plugin stop a play session it started after execution context changes?
- How does the current Prism auto-connect playtest path interact with an automation plugin session identity?
- Are
Selection:Setchanges acceptable in Play Solo, or should selection be edit-mode only? - Can
Workspace.CurrentCamerabe set and focused reliably in edit mode from an installed plugin? - Which property reads are expensive or restricted for inspect/snapshot across common services?
- Can undo cancellation reliably roll back all mutation classes needed for Preview Mode?
- How should Terrain be snapshotted, diffed, and rolled back if included?
- What memory/performance metrics are accessible to an installed plugin without private APIs?
27. Recommended implementation phases
- Automation status and identity: add plugin session IDs, server automation status, and
doctorchecks without moving exec. - Generic
AutomationJobStore: extract queue mechanics fromExecJobStore, keep/api/exec/jobs...compatible. - Generic automation HTTP routes: add
/api/automation/jobs...and make exec anAutomationRequest::Execadapter. - Plugin automation poller: replace the exec-specific poller with one dispatcher while preserving exec behavior.
- Shared values and instance references: implement
AutomationValueand session-local Instance refs. - Read-only typed commands: inspect, search, selection, camera.
- Mutating typed commands: select and focus with clear undo/Studio-mode rules.
- Screenshot spike and artifact transport.
- Snapshot and diff.
- Watch streams over a separate automation stream transport.
- Play/stop feasibility spike, then lifecycle commands if verified.
- Preview Mode MVP with before/after snapshots, rollback, stored patch, and apply without rerunning source.
- Profile hierarchy summary, with honest unavailable metrics.
- Automated playtesting design once play/stop, screenshots, streams, logs, and assertions exist.