@typetorch/framework 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -7
- package/out/analytics/SCHEMA.md +2 -2
- package/out/analytics/client.luau +26 -15
- package/out/analytics/protocol.d.ts +21 -0
- package/out/analytics/protocol.luau +28 -0
- package/out/analytics/server.luau +8 -3
- package/out/devtools/access.d.ts +71 -0
- package/out/devtools/access.luau +140 -0
- package/out/devtools/admin-server.d.ts +2 -1
- package/out/devtools/admin-server.luau +12 -14
- package/out/devtools/claude-ui.luau +52 -5
- package/out/devtools/claude.d.ts +6 -1
- package/out/devtools/claude.luau +106 -51
- package/out/devtools/client.luau +165 -79
- package/out/devtools/explorer-server.d.ts +4 -2
- package/out/devtools/explorer-server.luau +9 -4
- package/out/devtools/health.d.ts +18 -2
- package/out/devtools/health.luau +54 -13
- package/out/devtools/network-inspector.d.ts +7 -0
- package/out/devtools/network-inspector.luau +12 -14
- package/out/devtools/protocol.d.ts +5 -0
- package/out/devtools/remote-debug.d.ts +38 -0
- package/out/devtools/remote-debug.luau +250 -0
- package/out/devtools/server.d.ts +2 -2
- package/out/devtools/server.luau +29 -10
- package/out/devtools/toolbox-server.d.ts +8 -6
- package/out/devtools/toolbox-server.luau +16 -14
- package/out/kernel.d.ts +56 -0
- package/out/kernel.luau +26 -0
- package/out/version.d.ts +1 -1
- package/out/version.luau +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -152,8 +152,18 @@ export class CoinService extends Module implements OnStart {
|
|
|
152
152
|
- **Dev menu:** devs (Studio, project members, dev badge) get a DEV button, `Ctrl+Shift+D` and `/tt dev`: artifact
|
|
153
153
|
(with how long the server and client generations have run, the server's uptime and the build's age), server status,
|
|
154
154
|
logs (server, own client, other players' clients), client and server dex, network stats, module state, a branch and
|
|
155
|
-
build picker (Server > Branch) and a Claude prompt.
|
|
156
|
-
its header and resized from its corner (double-tap the
|
|
155
|
+
build picker (Server > Branch) and a Claude prompt. The sidebar, top to bottom: Artifact, Server, Network, Modules,
|
|
156
|
+
Manage, Logs, Dex, Claude. The window can be dragged by its header and resized from its corner (double-tap the
|
|
157
|
+
header to reset).
|
|
158
|
+
- **Server TPS (framework 0.4.2, kernel 0.4.2):** Server > Status has a *TPS* line next to Memory and Lua heap: the
|
|
159
|
+
last minute's average, its slowest second and the physics FPS (`59.9 avg, 52.0 min, physics 60`; the warning colour
|
|
160
|
+
under 50 on average). Older kernels show `needs kernel 0.4.2`. The same numbers ride on fleet heartbeats (the
|
|
161
|
+
explorer's fleet table).
|
|
162
|
+
- **Dev-only tools (framework 0.4.1, `devtools/access.ts`):** Claude, Logs > Upload, Dex edits on the server, Network
|
|
163
|
+
packet blocking and the hot-swap sound work under dev rules (private, reserved and Studio servers on a dev branch)
|
|
164
|
+
for every dev, and on a public server an owner switched to a dev branch (Server > Branch) for owners only. Everywhere
|
|
165
|
+
else the menu is read-only, with a short reason: "Dev branch only", "Owners only on public servers", "Owner-switched
|
|
166
|
+
servers only". The rules themselves don't change (`TypeTorch.channel`, signatures, owner-only rollbacks).
|
|
157
167
|
- **Panes and windows (dev menu):** every page shows in a pane with a small header: tap the title to pick another
|
|
158
168
|
page, or use Split right, Split down, Open in new window and Close. Drag the line between two panes to resize them.
|
|
159
169
|
The sidebar opens pages in the focused pane (the highlighted one; tap a pane to focus it). Floating windows move by
|
|
@@ -171,8 +181,8 @@ export class CoinService extends Module implements OnStart {
|
|
|
171
181
|
strings (cut), numbers, booleans, nil, datatypes through `tostring`, an Instance as its full path, a Player as its
|
|
172
182
|
name, `function`, `thread`, buffer sizes. Nothing is ever called (raw reads only: no metamethods, no getters); a
|
|
173
183
|
value that is its own ancestor shows as a cycle. A key filter (open nodes stay listed), Refresh and Auto (every 2 s).
|
|
174
|
-
Server state comes from the op `state.inspect` (rate-limited, size-capped):
|
|
175
|
-
|
|
184
|
+
Server state comes from the op `state.inspect` (rate-limited, size-capped): owners everywhere, other devs where the
|
|
185
|
+
dev-only tools are open to them, since server state can hold player data.
|
|
176
186
|
- **Logs > Upload:** sends the logs shown (Server: this server's log ring; Client: your own client's; Others: the
|
|
177
187
|
picked player's client) to the dev PC paired in the Claude tab, which saves them under `<repo>/.typetorch/logs/`.
|
|
178
188
|
No Claude run, no prompt quota. Not paired: "Pair in the Claude tab first".
|
|
@@ -182,11 +192,12 @@ export class CoinService extends Module implements OnStart {
|
|
|
182
192
|
verified head). A public switch lasts for that server's lifetime and is never stored, so new servers still boot the
|
|
183
193
|
signed prod head. Others see "Join", which moves only them to a reserved server. Manage > Servers "Load a build..."
|
|
184
194
|
does the same for this server, the ticked servers or a share of one branch (A/B pins), with one status line under
|
|
185
|
-
the list.
|
|
195
|
+
the list. A public server an owner switched to a dev branch opens the dev-only tools (Claude too) to owners.
|
|
186
196
|
- **Owners and devs:** two roles (no admins: an older kernel's or typetorch.json's "admin" counts as a dev). The Manage
|
|
187
197
|
group (Players, Servers, Bans) is for owners only.
|
|
188
|
-
- **Remote Claude** (dev-
|
|
189
|
-
|
|
198
|
+
- **Remote Claude** (a dev-only tool: dev rules, or owners on a public server an owner switched to a dev branch):
|
|
199
|
+
while `typetorch remote-claude` (`@typetorch/dev-server`) runs on a dev's machine, allowlisted devs prompt Claude
|
|
200
|
+
Code from the Claude tab. Each dev pairs once by pasting the pairing code printed by
|
|
190
201
|
typetorch-dev-server; the game server keeps the session URL and the tokens in memory and never sends them to clients.
|
|
191
202
|
No Roblox secret is needed.
|
|
192
203
|
|
package/out/analytics/SCHEMA.md
CHANGED
|
@@ -26,7 +26,7 @@ Integers are written with every digit (never exponent notation).
|
|
|
26
26
|
| `seq` | int64 | That artifact's deploy seq (`0` when unknown) |
|
|
27
27
|
| `branch` | string | Server branch |
|
|
28
28
|
| `channel` | string | Effective channel: `prod` or `dev` |
|
|
29
|
-
| `dev` | string | `desktop`, `phone`, `tablet`, `console`, `vr`, `unknown
|
|
29
|
+
| `dev` | string | The session's device class: `desktop`, `phone`, `tablet`, `console`, `vr`, `unknown`. Set once per session from the client's first hello (`unknown` for server-only events and until the client reports; frameworks after 0.4.2 keep it when a later hello says otherwise). How it is decided: the `session` / `device` row below |
|
|
30
30
|
| `newp` | bool | The event belongs to the player's first-ever session |
|
|
31
31
|
| `state` | string | The player's state **after** this event: `zone:<z>\|screen:<s>\|activity:<a>`, empty parts left out (`""` when nothing is known). Server-only events: `activity:<server activity>` or `""` |
|
|
32
32
|
| `exp` | string | JSON object of the player's active experiments, `{"onboarding":"short"}`; `{}` when none |
|
|
@@ -58,7 +58,7 @@ First-ever-session detail, one row per packed chunk.
|
|
|
58
58
|
| kind | name | props |
|
|
59
59
|
|---|---|---|
|
|
60
60
|
| session | `join` | `from`: `direct` (Home, search, sorts: Roblox doesn't say), `teleport` (same game), `teleport_game` (another game), `referral` (invite/referral link), `share` (launch data), `follow` (followed someone); `ctx?` (GameJoinContext.JoinSource name); `tp?` (had teleport data); `party?` (players teleported along); `age` (account age: `<1d`, `1-7d`, `7-30d`, `30-90d`, `90-365d`, `1-3y`, `3y+`); `prem` (Premium); `country?` (ISO code); `friends` (how many of the player's friends are in the server: one friend-list lookup per join, at most 10 pages, `0` when it fails); `ret` (days since the last visit, `-1` first visit or unknown); `late?` (the engine started over 60 s after the join). `t` is the join time |
|
|
61
|
-
| session | `device` (src client) | `input` (`kbm`, `touch`, `gamepad`, `vr`, `unknown`), `w`, `h` (viewport points), `touch`, `kb`, `mouse`, `pad`, `vr`. Once per session |
|
|
61
|
+
| session | `device` (src client) | `dev?` (the device class, as the `dev` column; frameworks after 0.4.2, older ones leave it out), `input` (`kbm`, `touch`, `gamepad`, `vr`, `unknown`), `w`, `h` (viewport points), `touch`, `kb`, `mouse`, `pad`, `vr`. Once per session. The class (Roblox gives game scripts no OS): `vr` when VRService.VREnabled; `console` when GuiService:IsTenFootInterface(); touch without a keyboard: `tablet` when the viewport's short side is 600 points or more, else `phone` (the first hello waits up to 2 s for a real viewport); a keyboard or mouse: `desktop` (touch laptops, tablets with a keyboard); else `unknown` |
|
|
62
62
|
| session | `leave` | `secs` (session length), `why`: `left`, `shutdown`, `gone` (left while no generation ran the engine), `swap60?` (left within 60 s of a hot swap) |
|
|
63
63
|
| tech | `server` | Every `techEvery` s: `fps` (physics), `hb` (Heartbeat rate), `mem` (MB, 10s), `players`, `ping50?`, `ping90?` (ms), `q`, `qr` (queued events, recordings), `dropped`, `sent`, `fails`, `health?`, `errors?` (kernel 0.3.2 health) |
|
|
64
64
|
| tech | `client` (src client) | Every `techEvery` s: `fps`, `mem` (MB, 10s), `ping?` (ms) |
|
|
@@ -26,6 +26,7 @@ local CHUNK_SECONDS = _protocol.CHUNK_SECONDS
|
|
|
26
26
|
local CHUNK_TARGET_BYTES = _protocol.CHUNK_TARGET_BYTES
|
|
27
27
|
local CLIENT_BATCH_BYTES = _protocol.CLIENT_BATCH_BYTES
|
|
28
28
|
local CLIENT_BATCH_MAX = _protocol.CLIENT_BATCH_MAX
|
|
29
|
+
local deviceClass = _protocol.deviceClass
|
|
29
30
|
local isEventName = _protocol.isEventName
|
|
30
31
|
local _props = TS.import(script, script.Parent, "props")
|
|
31
32
|
local encodeProps = _props.encodeProps
|
|
@@ -60,23 +61,28 @@ local IGNORED_GUIS = {
|
|
|
60
61
|
}
|
|
61
62
|
--* A screen that closes and opens again within this many seconds (ResetOnSpawn) never left.
|
|
62
63
|
local SCREEN_DEBOUNCE = 1
|
|
63
|
-
|
|
64
|
+
--* The camera's viewport in points (0 x 0 without a camera).
|
|
65
|
+
local function viewportSize()
|
|
64
66
|
local camera = Workspace.CurrentCamera
|
|
65
|
-
|
|
67
|
+
return if camera then camera.ViewportSize else Vector2.new(0, 0)
|
|
68
|
+
end
|
|
69
|
+
--* Seconds the first hello waits for a real viewport (a camera reports 1 x 1 for a moment at startup).
|
|
70
|
+
local VIEWPORT_WAIT = 2
|
|
71
|
+
local function deviceInfo()
|
|
72
|
+
local viewport = viewportSize()
|
|
66
73
|
local vr = VRService.VREnabled
|
|
67
74
|
local consoleOk, isConsole = pcall(function()
|
|
68
75
|
return GuiService:IsTenFootInterface()
|
|
69
76
|
end)
|
|
70
|
-
local dev =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
end
|
|
77
|
+
local dev = deviceClass({
|
|
78
|
+
vr = vr,
|
|
79
|
+
tenFoot = consoleOk and isConsole == true,
|
|
80
|
+
touch = UserInputService.TouchEnabled,
|
|
81
|
+
keyboard = UserInputService.KeyboardEnabled,
|
|
82
|
+
mouse = UserInputService.MouseEnabled,
|
|
83
|
+
w = viewport.X,
|
|
84
|
+
h = viewport.Y,
|
|
85
|
+
})
|
|
80
86
|
local last = UserInputService:GetLastInputType()
|
|
81
87
|
local input = "unknown"
|
|
82
88
|
if vr then
|
|
@@ -698,7 +704,7 @@ do
|
|
|
698
704
|
end
|
|
699
705
|
end)
|
|
700
706
|
if not ok then
|
|
701
|
-
warn("[src/analytics/client.ts:
|
|
707
|
+
warn("[src/analytics/client.ts:518]", `[analytics] server message failed: {string.sub(tostring(err), 1, 200)}`)
|
|
702
708
|
end
|
|
703
709
|
end)
|
|
704
710
|
trove:add(function()
|
|
@@ -805,6 +811,11 @@ do
|
|
|
805
811
|
end
|
|
806
812
|
function ClientAnalytics:startSending()
|
|
807
813
|
self.trove:add(task.spawn(function()
|
|
814
|
+
-- The device class reads the viewport (phone vs tablet): give the camera a moment to report a real size.
|
|
815
|
+
local waitUntil = os.clock() + VIEWPORT_WAIT
|
|
816
|
+
while math.min(viewportSize().X, viewportSize().Y) <= 1 and os.clock() < waitUntil and not self.stopped do
|
|
817
|
+
task.wait(0.1)
|
|
818
|
+
end
|
|
808
819
|
-- Hello until the server answers (its engine may start later than this one): every 5 s for a minute, then
|
|
809
820
|
-- every 30 s.
|
|
810
821
|
local tries = 0
|
|
@@ -839,7 +850,7 @@ do
|
|
|
839
850
|
end
|
|
840
851
|
function ClientAnalytics:track(kind, name, props)
|
|
841
852
|
if not isEventName(name) then
|
|
842
|
-
warn("[src/analytics/client.ts:
|
|
853
|
+
warn("[src/analytics/client.ts:637]", `[analytics] {kind} event name must be 1-64 characters: {string.sub(tostring(name), 1, 80)}`)
|
|
843
854
|
return nil
|
|
844
855
|
end
|
|
845
856
|
self:push(kind, name, encodeProps(props))
|
|
@@ -897,7 +908,7 @@ do
|
|
|
897
908
|
end
|
|
898
909
|
local control = _condition
|
|
899
910
|
if not isExperimentName(name) or not isVariantList(variants) then
|
|
900
|
-
warn("[src/analytics/client.ts:
|
|
911
|
+
warn("[src/analytics/client.ts:671]", `[analytics] experiment("{string.sub(tostring(name), 1, 64)}"): bad name or variants (1-16 distinct names)`)
|
|
901
912
|
return control
|
|
902
913
|
end
|
|
903
914
|
local deadline = os.clock() + 10
|
|
@@ -35,6 +35,27 @@ export interface HelloInfo {
|
|
|
35
35
|
/** Seconds from the client starting to the engine starting (first client generation only). */
|
|
36
36
|
load?: number;
|
|
37
37
|
}
|
|
38
|
+
/** What the client knows about its device when it says hello (UserInputService, VRService, GuiService, the camera). */
|
|
39
|
+
export interface DeviceFacts {
|
|
40
|
+
vr: boolean;
|
|
41
|
+
/** GuiService:IsTenFootInterface(): a console (Xbox, PlayStation). */
|
|
42
|
+
tenFoot: boolean;
|
|
43
|
+
touch: boolean;
|
|
44
|
+
keyboard: boolean;
|
|
45
|
+
mouse: boolean;
|
|
46
|
+
/** Viewport size in points (0 when the camera has none yet). */
|
|
47
|
+
w: number;
|
|
48
|
+
h: number;
|
|
49
|
+
}
|
|
50
|
+
/** A touch-only device whose viewport's short side is at least this many points is a tablet, else a phone. */
|
|
51
|
+
export declare const TABLET_MIN_SHORT_SIDE = 600;
|
|
52
|
+
/**
|
|
53
|
+
* The session's device class (the `dev` column), in this order: VR headset; console (ten-foot interface); touch without
|
|
54
|
+
* a keyboard: tablet when the viewport's short side is 600 points or more, else phone (also when the viewport isn't
|
|
55
|
+
* known); a keyboard or mouse: desktop (a touch laptop or a tablet with a keyboard counts here); else unknown. Roblox
|
|
56
|
+
* gives game scripts no OS, so this is the closest a game gets.
|
|
57
|
+
*/
|
|
58
|
+
export declare function deviceClass(f: DeviceFacts): DeviceKind;
|
|
38
59
|
/** [ageMs, kind, name, propsJson] */
|
|
39
60
|
export type ClientEvent = [
|
|
40
61
|
age: number,
|
|
@@ -22,6 +22,32 @@ local CLIENT_BATCH_BYTES = 12000
|
|
|
22
22
|
local CHUNK_TARGET_BYTES = 6000
|
|
23
23
|
local CHUNK_MAX_BYTES = 16384
|
|
24
24
|
local CHUNK_SECONDS = 10
|
|
25
|
+
--* What the client knows about its device when it says hello (UserInputService, VRService, GuiService, the camera).
|
|
26
|
+
--* A touch-only device whose viewport's short side is at least this many points is a tablet, else a phone.
|
|
27
|
+
local TABLET_MIN_SHORT_SIDE = 600
|
|
28
|
+
--[[
|
|
29
|
+
*
|
|
30
|
+
* The session's device class (the `dev` column), in this order: VR headset; console (ten-foot interface); touch without
|
|
31
|
+
* a keyboard: tablet when the viewport's short side is 600 points or more, else phone (also when the viewport isn't
|
|
32
|
+
* known); a keyboard or mouse: desktop (a touch laptop or a tablet with a keyboard counts here); else unknown. Roblox
|
|
33
|
+
* gives game scripts no OS, so this is the closest a game gets.
|
|
34
|
+
|
|
35
|
+
]]
|
|
36
|
+
local function deviceClass(f)
|
|
37
|
+
if f.vr then
|
|
38
|
+
return "vr"
|
|
39
|
+
end
|
|
40
|
+
if f.tenFoot then
|
|
41
|
+
return "console"
|
|
42
|
+
end
|
|
43
|
+
if f.touch and not f.keyboard then
|
|
44
|
+
return if math.min(f.w, f.h) >= TABLET_MIN_SHORT_SIDE then "tablet" else "phone"
|
|
45
|
+
end
|
|
46
|
+
if f.keyboard or f.mouse then
|
|
47
|
+
return "desktop"
|
|
48
|
+
end
|
|
49
|
+
return "unknown"
|
|
50
|
+
end
|
|
25
51
|
--* [ageMs, kind, name, propsJson]
|
|
26
52
|
local DEVICES = {
|
|
27
53
|
["desktop"] = true,
|
|
@@ -127,6 +153,7 @@ local function cleanClientEvent(value)
|
|
|
127
153
|
return { math.floor(age), kind, name, props }
|
|
128
154
|
end
|
|
129
155
|
return {
|
|
156
|
+
deviceClass = deviceClass,
|
|
130
157
|
cleanHello = cleanHello,
|
|
131
158
|
isEventName = isEventName,
|
|
132
159
|
cleanClientEvent = cleanClientEvent,
|
|
@@ -136,5 +163,6 @@ return {
|
|
|
136
163
|
CHUNK_TARGET_BYTES = CHUNK_TARGET_BYTES,
|
|
137
164
|
CHUNK_MAX_BYTES = CHUNK_MAX_BYTES,
|
|
138
165
|
CHUNK_SECONDS = CHUNK_SECONDS,
|
|
166
|
+
TABLET_MIN_SHORT_SIDE = TABLET_MIN_SHORT_SIDE,
|
|
139
167
|
CLIENT_KINDS = CLIENT_KINDS,
|
|
140
168
|
}
|
|
@@ -1284,9 +1284,14 @@ do
|
|
|
1284
1284
|
end
|
|
1285
1285
|
local first = session.device == nil
|
|
1286
1286
|
session.device = info
|
|
1287
|
-
session
|
|
1287
|
+
-- The device class is the session's: set by the first hello and kept (a later hello after a hot swap, or a keyboard
|
|
1288
|
+
-- plugged into a tablet, doesn't move the session's rows to another class), unless the first one didn't know.
|
|
1289
|
+
if first or session.dev == "unknown" then
|
|
1290
|
+
session.dev = info.dev
|
|
1291
|
+
end
|
|
1288
1292
|
if first and self.options.sessions ~= false then
|
|
1289
1293
|
self:emit(session, "session", "device", encodeProps({
|
|
1294
|
+
dev = info.dev,
|
|
1290
1295
|
input = info.input,
|
|
1291
1296
|
w = info.w,
|
|
1292
1297
|
h = info.h,
|
|
@@ -1494,7 +1499,7 @@ do
|
|
|
1494
1499
|
end
|
|
1495
1500
|
end)
|
|
1496
1501
|
if not ok then
|
|
1497
|
-
warn("[src/analytics/server.ts:
|
|
1502
|
+
warn("[src/analytics/server.ts:1077]", `[analytics] client message failed: {string.sub(tostring(err), 1, 200)}`)
|
|
1498
1503
|
end
|
|
1499
1504
|
end)
|
|
1500
1505
|
self.trove:add(function()
|
|
@@ -1777,7 +1782,7 @@ do
|
|
|
1777
1782
|
self.lastWarnAt = now
|
|
1778
1783
|
local more = if self.suppressed > 0 then ` (+{self.suppressed} more in the last minute)` else ""
|
|
1779
1784
|
self.suppressed = 0
|
|
1780
|
-
warn("[src/analytics/server.ts:
|
|
1785
|
+
warn("[src/analytics/server.ts:1272]", `[analytics] {message}{more}`)
|
|
1781
1786
|
return true
|
|
1782
1787
|
end
|
|
1783
1788
|
function ServerAnalytics:noteSuccess()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Channel, DevInfo, KernelStatus, ServerType } from "../kernel";
|
|
2
|
+
/**
|
|
3
|
+
* Framework 0.4.1: who may use the dev-only devtools on this server. Every such gate asks here: Claude (prompts, live
|
|
4
|
+
* tools, toolbox inserts, keeping the dev machine's session, the long-polls; claude.ts, toolbox-server.ts), Logs >
|
|
5
|
+
* Upload, Dex edits (explorer-server.ts), Modules > State reads for non-owners (server.ts), Manage's own respawn
|
|
6
|
+
* (admin-server.ts), and on the client (cosmetic; the server re-checks) Dex edits, Network packet blocking, the
|
|
7
|
+
* Claude tab, the hot-swap sound and the run_luau hint (op "access", `facts.access` of op "status").
|
|
8
|
+
*
|
|
9
|
+
* The owner's model: two channels, prod (the single prod branch) and dev (every other branch). The tools work:
|
|
10
|
+
* 1. under dev RULES (private, reserved and Studio servers on a dev branch with a dev build): every dev, as before;
|
|
11
|
+
* 2. on a PUBLIC server whose branch channel is dev and whose branch an OWNER switched to (kernel 0.3.4+ owner switch:
|
|
12
|
+
* `status().switched` names this branch, and the kernel follows it by dev rules, `status().signedOnly` false,
|
|
13
|
+
* which only its in-memory `ownerBranch` gives a public server): owners only;
|
|
14
|
+
* 3. nowhere else.
|
|
15
|
+
* The rules themselves don't change (signatures, owner-only rollbacks in the kernel, `TypeTorch.channel`, which games
|
|
16
|
+
* split data stores by). Older kernels have no `branchChannel` (before 0.3.9) or no `switched` (before 0.3.4): their
|
|
17
|
+
* public servers stay refused, as before.
|
|
18
|
+
*/
|
|
19
|
+
/** Why the dev-only tools are refused (an error code of the dev ops; client.ts and claude-ui.ts have short texts). */
|
|
20
|
+
export type DevRefusal = "dev_branch_only" | "owner_switch_only" | "owners_only";
|
|
21
|
+
/** The server's half of the rule: "dev" (rule 1), "owners" (rule 2) or why not. */
|
|
22
|
+
export type ServerAccess = "dev" | "owners" | "dev_branch_only" | "owner_switch_only";
|
|
23
|
+
/** What op "access" answers (and `facts.access` of op "status" holds: "ok" or the refusal). */
|
|
24
|
+
export type AccessReply = {
|
|
25
|
+
ok: true;
|
|
26
|
+
} | {
|
|
27
|
+
ok: false;
|
|
28
|
+
error: DevRefusal;
|
|
29
|
+
};
|
|
30
|
+
/** What the rule reads from the kernel (ServerKernel has all of it; tests pass stubs). */
|
|
31
|
+
export interface AccessKernel {
|
|
32
|
+
readonly branch: string;
|
|
33
|
+
readonly channel: Channel;
|
|
34
|
+
readonly rules?: Channel;
|
|
35
|
+
readonly branchChannel?: Channel;
|
|
36
|
+
readonly serverType: ServerType;
|
|
37
|
+
status(): Pick<KernelStatus, "branch" | "signedOnly" | "switched">;
|
|
38
|
+
devInfo(player: Player): DevInfo;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A public server's branch was set by an owner switch: the kernel's switch record names this branch (only owners switch
|
|
42
|
+
* or load builds on public servers), the kernel runs that branch now, and it follows it by dev rules (`signedOnly`
|
|
43
|
+
* false: the owner's dev branch). Absent fields (older kernels) mean no.
|
|
44
|
+
*/
|
|
45
|
+
export declare function ownerSwitchedHere(kernel: Pick<AccessKernel, "branch">, status: Pick<KernelStatus, "branch" | "signedOnly" | "switched">): boolean;
|
|
46
|
+
/** The server's half: rules dev, else a public dev branch an owner switched to, else refused. */
|
|
47
|
+
export declare function serverAccess(kernel: AccessKernel): ServerAccess;
|
|
48
|
+
/** Whether `player` may use the dev-only tools here (undefined) or why not. `server`: serverAccess, when known. */
|
|
49
|
+
export declare function devAccess(kernel: AccessKernel, player: Player, server?: ServerAccess): DevRefusal | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* The rule for one generation. The server half reads `kernel.status()` on public servers, so it is kept for `ttl`
|
|
52
|
+
* seconds (Claude's long-poll manager asks every second); the player half (the owner check) is asked every time.
|
|
53
|
+
*/
|
|
54
|
+
export declare class DevAccess {
|
|
55
|
+
private readonly kernel;
|
|
56
|
+
private readonly ttl;
|
|
57
|
+
private cached?;
|
|
58
|
+
private cachedAt;
|
|
59
|
+
constructor(kernel: AccessKernel, ttl?: number);
|
|
60
|
+
/** The server's half (see serverAccess). */
|
|
61
|
+
server(): ServerAccess;
|
|
62
|
+
/** Some player may use the tools here (rule 1 or 2): e.g. the server keeps the dev machine's session. */
|
|
63
|
+
serverOpen(): boolean;
|
|
64
|
+
/** Rule 2 (a public server an owner switched): owners only; a kept Claude session only while one is paired. */
|
|
65
|
+
ownersOnly(): boolean;
|
|
66
|
+
/** Undefined when `player` may use the tools here, else why not. */
|
|
67
|
+
refusal(player: Player): DevRefusal | undefined;
|
|
68
|
+
allows(player: Player): boolean;
|
|
69
|
+
/** Op "access". */
|
|
70
|
+
reply(player: Player): AccessReply;
|
|
71
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local _kernel = TS.import(script, script.Parent.Parent, "kernel")
|
|
4
|
+
local branchChannelOf = _kernel.branchChannelOf
|
|
5
|
+
local devRoleOf = _kernel.devRoleOf
|
|
6
|
+
local rulesOf = _kernel.rulesOf
|
|
7
|
+
--[[
|
|
8
|
+
*
|
|
9
|
+
* Framework 0.4.1: who may use the dev-only devtools on this server. Every such gate asks here: Claude (prompts, live
|
|
10
|
+
* tools, toolbox inserts, keeping the dev machine's session, the long-polls; claude.ts, toolbox-server.ts), Logs >
|
|
11
|
+
* Upload, Dex edits (explorer-server.ts), Modules > State reads for non-owners (server.ts), Manage's own respawn
|
|
12
|
+
* (admin-server.ts), and on the client (cosmetic; the server re-checks) Dex edits, Network packet blocking, the
|
|
13
|
+
* Claude tab, the hot-swap sound and the run_luau hint (op "access", `facts.access` of op "status").
|
|
14
|
+
*
|
|
15
|
+
* The owner's model: two channels, prod (the single prod branch) and dev (every other branch). The tools work:
|
|
16
|
+
* 1. under dev RULES (private, reserved and Studio servers on a dev branch with a dev build): every dev, as before;
|
|
17
|
+
* 2. on a PUBLIC server whose branch channel is dev and whose branch an OWNER switched to (kernel 0.3.4+ owner switch:
|
|
18
|
+
* `status().switched` names this branch, and the kernel follows it by dev rules, `status().signedOnly` false,
|
|
19
|
+
* which only its in-memory `ownerBranch` gives a public server): owners only;
|
|
20
|
+
* 3. nowhere else.
|
|
21
|
+
* The rules themselves don't change (signatures, owner-only rollbacks in the kernel, `TypeTorch.channel`, which games
|
|
22
|
+
* split data stores by). Older kernels have no `branchChannel` (before 0.3.9) or no `switched` (before 0.3.4): their
|
|
23
|
+
* public servers stay refused, as before.
|
|
24
|
+
|
|
25
|
+
]]
|
|
26
|
+
--* Why the dev-only tools are refused (an error code of the dev ops; client.ts and claude-ui.ts have short texts).
|
|
27
|
+
--* The server's half of the rule: "dev" (rule 1), "owners" (rule 2) or why not.
|
|
28
|
+
--* What op "access" answers (and `facts.access` of op "status" holds: "ok" or the refusal).
|
|
29
|
+
--* What the rule reads from the kernel (ServerKernel has all of it; tests pass stubs).
|
|
30
|
+
--[[
|
|
31
|
+
*
|
|
32
|
+
* A public server's branch was set by an owner switch: the kernel's switch record names this branch (only owners switch
|
|
33
|
+
* or load builds on public servers), the kernel runs that branch now, and it follows it by dev rules (`signedOnly`
|
|
34
|
+
* false: the owner's dev branch). Absent fields (older kernels) mean no.
|
|
35
|
+
|
|
36
|
+
]]
|
|
37
|
+
local function ownerSwitchedHere(kernel, status)
|
|
38
|
+
local switched = status.switched
|
|
39
|
+
if not (type(switched) == "table") then
|
|
40
|
+
return false
|
|
41
|
+
end
|
|
42
|
+
local _condition = status.signedOnly == false and status.branch == kernel.branch and switched.branch == kernel.branch
|
|
43
|
+
if _condition then
|
|
44
|
+
local _by = switched.by
|
|
45
|
+
_condition = type(_by) == "number"
|
|
46
|
+
end
|
|
47
|
+
return _condition
|
|
48
|
+
end
|
|
49
|
+
--* The server's half: rules dev, else a public dev branch an owner switched to, else refused.
|
|
50
|
+
local function serverAccess(kernel)
|
|
51
|
+
if rulesOf(kernel) == "dev" then
|
|
52
|
+
return "dev"
|
|
53
|
+
end
|
|
54
|
+
if branchChannelOf(kernel) ~= "dev" or kernel.serverType ~= "public" then
|
|
55
|
+
return "dev_branch_only"
|
|
56
|
+
end
|
|
57
|
+
local ok, status = pcall(function()
|
|
58
|
+
return kernel:status()
|
|
59
|
+
end)
|
|
60
|
+
if not ok or not (type(status) == "table") then
|
|
61
|
+
return "owner_switch_only"
|
|
62
|
+
end
|
|
63
|
+
return if ownerSwitchedHere(kernel, status) then "owners" else "owner_switch_only"
|
|
64
|
+
end
|
|
65
|
+
--* Whether `player` may use the dev-only tools here (undefined) or why not. `server`: serverAccess, when known.
|
|
66
|
+
local function devAccess(kernel, player, server)
|
|
67
|
+
if server == nil then
|
|
68
|
+
server = serverAccess(kernel)
|
|
69
|
+
end
|
|
70
|
+
if server == "dev" then
|
|
71
|
+
return nil
|
|
72
|
+
end
|
|
73
|
+
if server ~= "owners" then
|
|
74
|
+
return server
|
|
75
|
+
end
|
|
76
|
+
return if devRoleOf(kernel, player) == "owner" then nil else "owners_only"
|
|
77
|
+
end
|
|
78
|
+
--[[
|
|
79
|
+
*
|
|
80
|
+
* The rule for one generation. The server half reads `kernel.status()` on public servers, so it is kept for `ttl`
|
|
81
|
+
* seconds (Claude's long-poll manager asks every second); the player half (the owner check) is asked every time.
|
|
82
|
+
|
|
83
|
+
]]
|
|
84
|
+
local DevAccess
|
|
85
|
+
do
|
|
86
|
+
DevAccess = setmetatable({}, {
|
|
87
|
+
__tostring = function()
|
|
88
|
+
return "DevAccess"
|
|
89
|
+
end,
|
|
90
|
+
})
|
|
91
|
+
DevAccess.__index = DevAccess
|
|
92
|
+
function DevAccess.new(...)
|
|
93
|
+
local self = setmetatable({}, DevAccess)
|
|
94
|
+
return self:constructor(...) or self
|
|
95
|
+
end
|
|
96
|
+
function DevAccess:constructor(kernel, ttl)
|
|
97
|
+
if ttl == nil then
|
|
98
|
+
ttl = 1
|
|
99
|
+
end
|
|
100
|
+
self.kernel = kernel
|
|
101
|
+
self.ttl = ttl
|
|
102
|
+
self.cachedAt = -math.huge
|
|
103
|
+
end
|
|
104
|
+
function DevAccess:server()
|
|
105
|
+
local now = os.clock()
|
|
106
|
+
if self.cached == nil or now - self.cachedAt >= self.ttl then
|
|
107
|
+
self.cached = serverAccess(self.kernel)
|
|
108
|
+
self.cachedAt = now
|
|
109
|
+
end
|
|
110
|
+
return self.cached
|
|
111
|
+
end
|
|
112
|
+
function DevAccess:serverOpen()
|
|
113
|
+
local server = self:server()
|
|
114
|
+
return server == "dev" or server == "owners"
|
|
115
|
+
end
|
|
116
|
+
function DevAccess:ownersOnly()
|
|
117
|
+
return self:server() == "owners"
|
|
118
|
+
end
|
|
119
|
+
function DevAccess:refusal(player)
|
|
120
|
+
return devAccess(self.kernel, player, self:server())
|
|
121
|
+
end
|
|
122
|
+
function DevAccess:allows(player)
|
|
123
|
+
return self:refusal(player) == nil
|
|
124
|
+
end
|
|
125
|
+
function DevAccess:reply(player)
|
|
126
|
+
local refusal = self:refusal(player)
|
|
127
|
+
return if refusal == nil then {
|
|
128
|
+
ok = true,
|
|
129
|
+
} else {
|
|
130
|
+
ok = false,
|
|
131
|
+
error = refusal,
|
|
132
|
+
}
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
return {
|
|
136
|
+
ownerSwitchedHere = ownerSwitchedHere,
|
|
137
|
+
serverAccess = serverAccess,
|
|
138
|
+
devAccess = devAccess,
|
|
139
|
+
DevAccess = DevAccess,
|
|
140
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Trove } from "@rbxts/trove";
|
|
2
2
|
import { type Channel, type Role, type ServerKernel, type ServerType } from "../kernel";
|
|
3
|
+
import { DevAccess } from "./access";
|
|
3
4
|
/**
|
|
4
5
|
* Server half of the dev menu's Manage group (plans/10; "Admin" before framework 0.3.2): players (teleport to, bring,
|
|
5
6
|
* respawn, kick, ban), bans (unban, history) and the live servers of this universe (list, join, new server, shut down,
|
|
@@ -116,4 +117,4 @@ export declare function kernelHasFleetStatus(kernel: ServerKernel): boolean;
|
|
|
116
117
|
* Registers the "admin.*" dev ops and starts this server's heartbeat in the server list. Everything it connects or
|
|
117
118
|
* spawns lives in `trove` (the devtools trove, cleaned on swap).
|
|
118
119
|
*/
|
|
119
|
-
export declare function registerAdminOps(register: (op: string, handler: (player: Player, payload: unknown) => unknown) => void, kernel: ServerKernel, trove: Trove): void;
|
|
120
|
+
export declare function registerAdminOps(register: (op: string, handler: (player: Player, payload: unknown) => unknown) => void, kernel: ServerKernel, trove: Trove, access?: DevAccess): void;
|
|
@@ -9,7 +9,8 @@ local TeleportService = _services.TeleportService
|
|
|
9
9
|
local TextService = _services.TextService
|
|
10
10
|
local _kernel = TS.import(script, script.Parent.Parent, "kernel")
|
|
11
11
|
local branchChannelOf = _kernel.branchChannelOf
|
|
12
|
-
local
|
|
12
|
+
local devRoleOf = _kernel.devRoleOf
|
|
13
|
+
local DevAccess = TS.import(script, script.Parent, "access").DevAccess
|
|
13
14
|
local countBudget = TS.import(script, script.Parent.Parent, "budget").countBudget
|
|
14
15
|
local RateLimiter = TS.import(script, script.Parent.Parent, "net", "limits").RateLimiter
|
|
15
16
|
local _ab = TS.import(script, script.Parent, "ab")
|
|
@@ -183,7 +184,10 @@ end
|
|
|
183
184
|
* spawns lives in `trove` (the devtools trove, cleaned on swap).
|
|
184
185
|
|
|
185
186
|
]]
|
|
186
|
-
local function registerAdminOps(register, kernel, trove)
|
|
187
|
+
local function registerAdminOps(register, kernel, trove, access)
|
|
188
|
+
if access == nil then
|
|
189
|
+
access = DevAccess.new(kernel)
|
|
190
|
+
end
|
|
187
191
|
local saved = kernel:persist(PERSIST_KEY, function()
|
|
188
192
|
return {
|
|
189
193
|
modSeq = 0,
|
|
@@ -196,13 +200,7 @@ local function registerAdminOps(register, kernel, trove)
|
|
|
196
200
|
local jobLabel = if game.JobId ~= "" then game.JobId else "studio"
|
|
197
201
|
local jobKey = if game.JobId ~= "" then string.sub((string.gsub(game.JobId, "-", "")), 1, 24) else "studio"
|
|
198
202
|
local roleOf = function(player)
|
|
199
|
-
|
|
200
|
-
return kernel:devInfo(player)
|
|
201
|
-
end)
|
|
202
|
-
if not ok or not (type(info) == "table") or info.dev ~= true then
|
|
203
|
-
return nil
|
|
204
|
-
end
|
|
205
|
-
return normalRole(info.role)
|
|
203
|
+
return devRoleOf(kernel, player)
|
|
206
204
|
end
|
|
207
205
|
local actorOf = function(player)
|
|
208
206
|
-- The dispatcher checks this too; re-checked so these ops stay dev-only wherever they are registered.
|
|
@@ -247,7 +245,7 @@ local function registerAdminOps(register, kernel, trove)
|
|
|
247
245
|
return {
|
|
248
246
|
tp = not isSelf,
|
|
249
247
|
bring = not isSelf and actor.owner and actor.rank >= targetRank,
|
|
250
|
-
respawn = if isSelf then
|
|
248
|
+
respawn = if isSelf then actor.owner or access:allows(actor.player) else actor.owner and actor.rank >= targetRank,
|
|
251
249
|
kick = not isSelf and actor.owner and actor.rank > targetRank,
|
|
252
250
|
ban = not isSelf and actor.owner and actor.rank > targetRank,
|
|
253
251
|
}
|
|
@@ -326,7 +324,7 @@ local function registerAdminOps(register, kernel, trove)
|
|
|
326
324
|
return DataStoreService:GetDataStore(MOD_STORE):SetAsync(key, entry)
|
|
327
325
|
end)
|
|
328
326
|
if not ok then
|
|
329
|
-
warn("[src/devtools/admin-server.ts:
|
|
327
|
+
warn("[src/devtools/admin-server.ts:393]", `[manage] durable record {key} failed: {err}`)
|
|
330
328
|
end
|
|
331
329
|
end)
|
|
332
330
|
end
|
|
@@ -952,7 +950,7 @@ local function registerAdminOps(register, kernel, trove)
|
|
|
952
950
|
end)
|
|
953
951
|
if not ok then
|
|
954
952
|
failed += 1
|
|
955
|
-
warn("[src/devtools/admin-server.ts:
|
|
953
|
+
warn("[src/devtools/admin-server.ts:755]", `[manage] A/B publish failed: {err}`)
|
|
956
954
|
end
|
|
957
955
|
end
|
|
958
956
|
for branch, group in groups do
|
|
@@ -1183,7 +1181,7 @@ local function registerAdminOps(register, kernel, trove)
|
|
|
1183
1181
|
return nil
|
|
1184
1182
|
end
|
|
1185
1183
|
backoff(entry)
|
|
1186
|
-
warn("[src/devtools/admin-server.ts:
|
|
1184
|
+
warn("[src/devtools/admin-server.ts:884]", `[manage] migrate: {target.Name} failed ({result.Name}): {message}`)
|
|
1187
1185
|
end)
|
|
1188
1186
|
trove:connect(Players.PlayerRemoving, function(leaving)
|
|
1189
1187
|
local _leaving = leaving
|
|
@@ -1269,7 +1267,7 @@ local function registerAdminOps(register, kernel, trove)
|
|
|
1269
1267
|
return TeleportService:TeleportAsync(game.PlaceId, batch, options)
|
|
1270
1268
|
end)
|
|
1271
1269
|
if not ok then
|
|
1272
|
-
warn("[src/devtools/admin-server.ts:
|
|
1270
|
+
warn("[src/devtools/admin-server.ts:924]", `[manage] migrate: teleport of {#batch} failed: {err}`)
|
|
1273
1271
|
for _, target in batch do
|
|
1274
1272
|
local entry = attempts[target]
|
|
1275
1273
|
if entry then
|