@chrrxs/robloxstudio-mcp-inspector 2.22.4 → 2.22.5
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/dist/index.js +991 -102
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +3 -3
- package/studio-plugin/MCPPlugin.rbxmx +3 -3
package/dist/index.js
CHANGED
|
@@ -974,7 +974,7 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
|
|
|
974
974
|
serverVersion: serverConfig?.version,
|
|
975
975
|
capabilities: studioLifecycleCallable ? {
|
|
976
976
|
studioLifecycle: {
|
|
977
|
-
protocolVersion:
|
|
977
|
+
protocolVersion: 3,
|
|
978
978
|
endpoint: "/mcp/manage_instance"
|
|
979
979
|
}
|
|
980
980
|
} : {},
|
|
@@ -3024,34 +3024,124 @@ var init_roblox_cookie_client = __esm({
|
|
|
3024
3024
|
}
|
|
3025
3025
|
return response;
|
|
3026
3026
|
}
|
|
3027
|
-
async
|
|
3027
|
+
async uploadImage(options) {
|
|
3028
3028
|
if (!this.cookie) {
|
|
3029
3029
|
throw new Error("ROBLOSECURITY cookie is not set.");
|
|
3030
3030
|
}
|
|
3031
|
-
const
|
|
3032
|
-
const
|
|
3033
|
-
|
|
3034
|
-
|
|
3031
|
+
const creator = await this.resolveCreator(options.userId, options.groupId);
|
|
3032
|
+
const request = {
|
|
3033
|
+
assetType: "Image",
|
|
3034
|
+
displayName: options.displayName,
|
|
3035
|
+
description: options.description,
|
|
3036
|
+
creationContext: { creator }
|
|
3037
|
+
};
|
|
3038
|
+
const formData = new FormData();
|
|
3039
|
+
formData.append("request", JSON.stringify(request));
|
|
3040
|
+
formData.append("fileContent", new Blob([new Uint8Array(options.fileContent)], { type: this.getImageMimeType(options.fileName) }), options.fileName);
|
|
3041
|
+
const response = await this.fetchWithCsrf("https://apis.roblox.com/assets/user-auth/v1/assets", {
|
|
3035
3042
|
method: "POST",
|
|
3036
|
-
|
|
3037
|
-
"Content-Type": "application/octet-stream",
|
|
3038
|
-
"User-Agent": "RobloxStudio/WinInet",
|
|
3039
|
-
Requester: "Client"
|
|
3040
|
-
},
|
|
3041
|
-
body: new Uint8Array(fileContent)
|
|
3043
|
+
body: formData
|
|
3042
3044
|
});
|
|
3045
|
+
const operation = await this.readOperation(response, "Image upload");
|
|
3046
|
+
return { assetId: await this.completeOperation(operation) };
|
|
3047
|
+
}
|
|
3048
|
+
async resolveCreator(userId, groupId) {
|
|
3049
|
+
if (groupId)
|
|
3050
|
+
return { groupId };
|
|
3051
|
+
if (userId)
|
|
3052
|
+
return { userId };
|
|
3053
|
+
const response = await this.fetchWithCsrf("https://users.roblox.com/v1/users/authenticated");
|
|
3043
3054
|
if (!response.ok) {
|
|
3044
3055
|
const body = await response.text();
|
|
3045
|
-
throw new Error(`
|
|
3056
|
+
throw new Error(`Failed to resolve authenticated Roblox user (${response.status}): ${body}`);
|
|
3046
3057
|
}
|
|
3047
|
-
const
|
|
3048
|
-
|
|
3049
|
-
|
|
3058
|
+
const authenticatedUser = await response.json();
|
|
3059
|
+
const resolvedUserId = String(authenticatedUser.id ?? "");
|
|
3060
|
+
if (!/^\d+$/.test(resolvedUserId) || resolvedUserId === "0") {
|
|
3061
|
+
throw new Error("Authenticated Roblox user response did not include a valid user ID.");
|
|
3050
3062
|
}
|
|
3051
|
-
return {
|
|
3052
|
-
|
|
3053
|
-
|
|
3063
|
+
return { userId: resolvedUserId };
|
|
3064
|
+
}
|
|
3065
|
+
getImageMimeType(fileName) {
|
|
3066
|
+
const extension = fileName.split(".").pop()?.toLowerCase();
|
|
3067
|
+
const mimeTypes = {
|
|
3068
|
+
png: "image/png",
|
|
3069
|
+
jpg: "image/jpeg",
|
|
3070
|
+
jpeg: "image/jpeg",
|
|
3071
|
+
bmp: "image/bmp",
|
|
3072
|
+
tga: "image/tga"
|
|
3054
3073
|
};
|
|
3074
|
+
const mimeType = extension ? mimeTypes[extension] : void 0;
|
|
3075
|
+
if (mimeType)
|
|
3076
|
+
return mimeType;
|
|
3077
|
+
throw new Error(`Unsupported image format: .${extension ?? "(none)"}. Supported: png/jpg/jpeg/bmp/tga`);
|
|
3078
|
+
}
|
|
3079
|
+
async readOperation(response, action) {
|
|
3080
|
+
const body = await response.text();
|
|
3081
|
+
if (!response.ok) {
|
|
3082
|
+
throw new Error(`${action} failed (${response.status}): ${body}`);
|
|
3083
|
+
}
|
|
3084
|
+
try {
|
|
3085
|
+
return JSON.parse(body);
|
|
3086
|
+
} catch {
|
|
3087
|
+
throw new Error(`${action} returned malformed JSON: ${body}`);
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
operationAssetId(operation) {
|
|
3091
|
+
const rawAssetId = operation.response?.assetId;
|
|
3092
|
+
if (rawAssetId === void 0)
|
|
3093
|
+
return null;
|
|
3094
|
+
const assetId = Number(rawAssetId);
|
|
3095
|
+
if (!Number.isSafeInteger(assetId) || assetId <= 0) {
|
|
3096
|
+
throw new Error(`Image upload returned an invalid asset ID: ${String(rawAssetId)}`);
|
|
3097
|
+
}
|
|
3098
|
+
return assetId;
|
|
3099
|
+
}
|
|
3100
|
+
operationError(operation) {
|
|
3101
|
+
if (operation.error?.message)
|
|
3102
|
+
return operation.error.message;
|
|
3103
|
+
if (operation.response?.message && operation.response.assetId === void 0) {
|
|
3104
|
+
return operation.response.message;
|
|
3105
|
+
}
|
|
3106
|
+
if (operation.message)
|
|
3107
|
+
return operation.message;
|
|
3108
|
+
return null;
|
|
3109
|
+
}
|
|
3110
|
+
async completeOperation(operation) {
|
|
3111
|
+
const initialError = this.operationError(operation);
|
|
3112
|
+
if (initialError)
|
|
3113
|
+
throw new Error(`Image upload failed: ${initialError}`);
|
|
3114
|
+
const initialAssetId = this.operationAssetId(operation);
|
|
3115
|
+
if (initialAssetId !== null)
|
|
3116
|
+
return initialAssetId;
|
|
3117
|
+
if (operation.done) {
|
|
3118
|
+
throw new Error("Image upload completed without an asset ID.");
|
|
3119
|
+
}
|
|
3120
|
+
const operationId = operation.operationId ?? operation.path?.split("/").pop();
|
|
3121
|
+
if (!operationId) {
|
|
3122
|
+
throw new Error("Image upload response did not include an operation ID.");
|
|
3123
|
+
}
|
|
3124
|
+
return this.pollOperation(operationId);
|
|
3125
|
+
}
|
|
3126
|
+
async pollOperation(operationId, maxAttempts = 30, intervalMs = 2e3) {
|
|
3127
|
+
const url = `https://apis.roblox.com/assets/user-auth/v1/operations/${encodeURIComponent(operationId)}`;
|
|
3128
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
3129
|
+
const response = await this.fetchWithCsrf(url);
|
|
3130
|
+
const operation = await this.readOperation(response, "Image upload status");
|
|
3131
|
+
const operationError = this.operationError(operation);
|
|
3132
|
+
if (operationError)
|
|
3133
|
+
throw new Error(`Image upload failed: ${operationError}`);
|
|
3134
|
+
const assetId = this.operationAssetId(operation);
|
|
3135
|
+
if (assetId !== null)
|
|
3136
|
+
return assetId;
|
|
3137
|
+
if (operation.done) {
|
|
3138
|
+
throw new Error("Image upload completed without an asset ID.");
|
|
3139
|
+
}
|
|
3140
|
+
if (attempt < maxAttempts - 1) {
|
|
3141
|
+
await new Promise((resolve5) => setTimeout(resolve5, intervalMs));
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
throw new Error(`Image upload timed out after ${maxAttempts * intervalMs / 1e3}s. Operation ID: ${operationId}`);
|
|
3055
3145
|
}
|
|
3056
3146
|
async getAssetDetails(assetIds) {
|
|
3057
3147
|
if (!this.cookie) {
|
|
@@ -3121,7 +3211,7 @@ function isProcessAlive(pid) {
|
|
|
3121
3211
|
return error.code === "EPERM";
|
|
3122
3212
|
}
|
|
3123
3213
|
}
|
|
3124
|
-
var REGISTRY_VERSION, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, EVENT_RETENTION_DAYS, TERMINAL_RECORD_RETENTION_MS, DEFAULT_CONFIRMED_EXIT_MISSES, DEFAULT_CONFIRMED_EXIT_GRACE_MS, activeLockTokens, ManagedInstanceRegistry;
|
|
3214
|
+
var REGISTRY_VERSION, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, EVENT_RETENTION_DAYS, TERMINAL_RECORD_RETENTION_MS, DEFAULT_CONFIRMED_EXIT_MISSES, DEFAULT_CONFIRMED_EXIT_GRACE_MS, activeLockTokens, activeLockPaths, ManagedInstanceRegistry;
|
|
3125
3215
|
var init_managed_instance_registry = __esm({
|
|
3126
3216
|
"../core/dist/managed-instance-registry.js"() {
|
|
3127
3217
|
"use strict";
|
|
@@ -3134,6 +3224,7 @@ var init_managed_instance_registry = __esm({
|
|
|
3134
3224
|
DEFAULT_CONFIRMED_EXIT_MISSES = 2;
|
|
3135
3225
|
DEFAULT_CONFIRMED_EXIT_GRACE_MS = 5e3;
|
|
3136
3226
|
activeLockTokens = /* @__PURE__ */ new Set();
|
|
3227
|
+
activeLockPaths = /* @__PURE__ */ new Set();
|
|
3137
3228
|
ManagedInstanceRegistry = class {
|
|
3138
3229
|
dir;
|
|
3139
3230
|
constructor(dir = defaultManagedInstanceRegistryDir()) {
|
|
@@ -3208,6 +3299,7 @@ var init_managed_instance_registry = __esm({
|
|
|
3208
3299
|
try {
|
|
3209
3300
|
await fs.mkdir(lockDir);
|
|
3210
3301
|
activeLockTokens.add(owner.token);
|
|
3302
|
+
activeLockPaths.add(lockDir);
|
|
3211
3303
|
try {
|
|
3212
3304
|
await fs.writeFile(path.join(lockDir, "owner.json"), `${JSON.stringify(owner)}
|
|
3213
3305
|
`, {
|
|
@@ -3216,6 +3308,7 @@ var init_managed_instance_registry = __esm({
|
|
|
3216
3308
|
});
|
|
3217
3309
|
} catch (error) {
|
|
3218
3310
|
activeLockTokens.delete(owner.token);
|
|
3311
|
+
activeLockPaths.delete(lockDir);
|
|
3219
3312
|
await fs.rm(lockDir, { recursive: true, force: true });
|
|
3220
3313
|
throw error;
|
|
3221
3314
|
}
|
|
@@ -3228,7 +3321,7 @@ var init_managed_instance_registry = __esm({
|
|
|
3228
3321
|
const stat2 = await fs.stat(lockDir);
|
|
3229
3322
|
const currentOwner = await this.readLockOwner(lockDir, stat2.isDirectory());
|
|
3230
3323
|
const ownerIsActive = currentOwner?.pid === process.pid ? activeLockTokens.has(currentOwner.token) : currentOwner ? isProcessAlive(currentOwner.pid) : void 0;
|
|
3231
|
-
if (ownerIsActive === false || currentOwner === void 0 && Date.now() - stat2.mtimeMs > LOCK_STALE_MS) {
|
|
3324
|
+
if (ownerIsActive === false || currentOwner === void 0 && !activeLockPaths.has(lockDir) && Date.now() - stat2.mtimeMs > LOCK_STALE_MS) {
|
|
3232
3325
|
await fs.rm(lockDir, { recursive: true, force: true });
|
|
3233
3326
|
continue;
|
|
3234
3327
|
}
|
|
@@ -3253,6 +3346,7 @@ var init_managed_instance_registry = __esm({
|
|
|
3253
3346
|
} catch {
|
|
3254
3347
|
} finally {
|
|
3255
3348
|
activeLockTokens.delete(owner.token);
|
|
3349
|
+
activeLockPaths.delete(lockDir);
|
|
3256
3350
|
}
|
|
3257
3351
|
}
|
|
3258
3352
|
}
|
|
@@ -3638,39 +3732,554 @@ function quoteWindowsCommandLineArg(value) {
|
|
|
3638
3732
|
}
|
|
3639
3733
|
function buildWindowsStudioStartScriptFromConvertedExe(windowsExe, args, processEnvironment) {
|
|
3640
3734
|
const environmentPatch = parseStudioProcessEnvironmentPatch(processEnvironment);
|
|
3641
|
-
const commandLine = args.map(quoteWindowsCommandLineArg).join(" ");
|
|
3735
|
+
const commandLine = [windowsExe, ...args].map(quoteWindowsCommandLineArg).join(" ");
|
|
3642
3736
|
return [
|
|
3643
3737
|
...environmentPatch ? powershellEnvironmentPatchStatements(environmentPatch) : [],
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3738
|
+
`Add-Type -TypeDefinition @'
|
|
3739
|
+
using System;
|
|
3740
|
+
using System.ComponentModel;
|
|
3741
|
+
using System.Runtime.InteropServices;
|
|
3742
|
+
using System.Text;
|
|
3743
|
+
|
|
3744
|
+
public sealed class McpSuspendedStudio : IDisposable
|
|
3745
|
+
{
|
|
3746
|
+
private const uint CREATE_SUSPENDED = 0x00000004;
|
|
3747
|
+
private const uint CREATE_BREAKAWAY_FROM_JOB = 0x01000000;
|
|
3748
|
+
private const uint RESUME_FAILED = 0xFFFFFFFF;
|
|
3749
|
+
private const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
|
|
3750
|
+
private const int JOB_OBJECT_EXTENDED_LIMIT_INFORMATION = 9;
|
|
3751
|
+
private const uint WAIT_OBJECT_0 = 0x00000000;
|
|
3752
|
+
private const uint WAIT_TIMEOUT = 0x00000102;
|
|
3753
|
+
private const uint WAIT_FAILED = 0xFFFFFFFF;
|
|
3754
|
+
|
|
3755
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
3756
|
+
private struct StartupInfo
|
|
3757
|
+
{
|
|
3758
|
+
public int cb;
|
|
3759
|
+
public string lpReserved;
|
|
3760
|
+
public string lpDesktop;
|
|
3761
|
+
public string lpTitle;
|
|
3762
|
+
public uint dwX;
|
|
3763
|
+
public uint dwY;
|
|
3764
|
+
public uint dwXSize;
|
|
3765
|
+
public uint dwYSize;
|
|
3766
|
+
public uint dwXCountChars;
|
|
3767
|
+
public uint dwYCountChars;
|
|
3768
|
+
public uint dwFillAttribute;
|
|
3769
|
+
public uint dwFlags;
|
|
3770
|
+
public short wShowWindow;
|
|
3771
|
+
public short cbReserved2;
|
|
3772
|
+
public IntPtr lpReserved2;
|
|
3773
|
+
public IntPtr hStdInput;
|
|
3774
|
+
public IntPtr hStdOutput;
|
|
3775
|
+
public IntPtr hStdError;
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3778
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
3779
|
+
private struct ProcessInformation
|
|
3780
|
+
{
|
|
3781
|
+
public IntPtr hProcess;
|
|
3782
|
+
public IntPtr hThread;
|
|
3783
|
+
public uint dwProcessId;
|
|
3784
|
+
public uint dwThreadId;
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3787
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
3788
|
+
private struct FileTime
|
|
3789
|
+
{
|
|
3790
|
+
public uint Low;
|
|
3791
|
+
public uint High;
|
|
3792
|
+
}
|
|
3793
|
+
|
|
3794
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
3795
|
+
private struct BasicLimitInformation
|
|
3796
|
+
{
|
|
3797
|
+
public long PerProcessUserTimeLimit;
|
|
3798
|
+
public long PerJobUserTimeLimit;
|
|
3799
|
+
public uint LimitFlags;
|
|
3800
|
+
public UIntPtr MinimumWorkingSetSize;
|
|
3801
|
+
public UIntPtr MaximumWorkingSetSize;
|
|
3802
|
+
public uint ActiveProcessLimit;
|
|
3803
|
+
public UIntPtr Affinity;
|
|
3804
|
+
public uint PriorityClass;
|
|
3805
|
+
public uint SchedulingClass;
|
|
3806
|
+
}
|
|
3807
|
+
|
|
3808
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
3809
|
+
private struct IoCounters
|
|
3810
|
+
{
|
|
3811
|
+
public ulong ReadOperationCount;
|
|
3812
|
+
public ulong WriteOperationCount;
|
|
3813
|
+
public ulong OtherOperationCount;
|
|
3814
|
+
public ulong ReadTransferCount;
|
|
3815
|
+
public ulong WriteTransferCount;
|
|
3816
|
+
public ulong OtherTransferCount;
|
|
3817
|
+
}
|
|
3818
|
+
|
|
3819
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
3820
|
+
private struct ExtendedLimitInformation
|
|
3821
|
+
{
|
|
3822
|
+
public BasicLimitInformation BasicLimitInformation;
|
|
3823
|
+
public IoCounters IoInfo;
|
|
3824
|
+
public UIntPtr ProcessMemoryLimit;
|
|
3825
|
+
public UIntPtr JobMemoryLimit;
|
|
3826
|
+
public UIntPtr PeakProcessMemoryUsed;
|
|
3827
|
+
public UIntPtr PeakJobMemoryUsed;
|
|
3828
|
+
}
|
|
3829
|
+
|
|
3830
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
3831
|
+
private static extern bool CreateProcessW(
|
|
3832
|
+
string applicationName,
|
|
3833
|
+
StringBuilder commandLine,
|
|
3834
|
+
IntPtr processAttributes,
|
|
3835
|
+
IntPtr threadAttributes,
|
|
3836
|
+
bool inheritHandles,
|
|
3837
|
+
uint creationFlags,
|
|
3838
|
+
IntPtr environment,
|
|
3839
|
+
string currentDirectory,
|
|
3840
|
+
ref StartupInfo startupInfo,
|
|
3841
|
+
out ProcessInformation processInformation);
|
|
3842
|
+
|
|
3843
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
3844
|
+
private static extern IntPtr CreateJobObjectW(IntPtr jobAttributes, string name);
|
|
3845
|
+
|
|
3846
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
3847
|
+
private static extern bool SetInformationJobObject(
|
|
3848
|
+
IntPtr job,
|
|
3849
|
+
int informationClass,
|
|
3850
|
+
IntPtr information,
|
|
3851
|
+
uint informationLength);
|
|
3852
|
+
|
|
3853
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
3854
|
+
private static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
|
|
3855
|
+
|
|
3856
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
3857
|
+
private static extern bool GetProcessTimes(
|
|
3858
|
+
IntPtr process,
|
|
3859
|
+
out FileTime creation,
|
|
3860
|
+
out FileTime exit,
|
|
3861
|
+
out FileTime kernel,
|
|
3862
|
+
out FileTime user);
|
|
3863
|
+
|
|
3864
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
3865
|
+
private static extern uint ResumeThread(IntPtr thread);
|
|
3866
|
+
|
|
3867
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
3868
|
+
private static extern bool TerminateProcess(IntPtr process, uint exitCode);
|
|
3869
|
+
|
|
3870
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
3871
|
+
private static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);
|
|
3872
|
+
|
|
3873
|
+
[DllImport("kernel32.dll")]
|
|
3874
|
+
private static extern bool CloseHandle(IntPtr handle);
|
|
3875
|
+
|
|
3876
|
+
private IntPtr process;
|
|
3877
|
+
private IntPtr thread;
|
|
3878
|
+
|
|
3879
|
+
private IntPtr job;
|
|
3880
|
+
public uint ProcessId { get; private set; }
|
|
3881
|
+
public ulong StartedAtFileTime { get; private set; }
|
|
3882
|
+
|
|
3883
|
+
private McpSuspendedStudio(ProcessInformation processInformation, IntPtr jobHandle)
|
|
3884
|
+
{
|
|
3885
|
+
process = processInformation.hProcess;
|
|
3886
|
+
thread = processInformation.hThread;
|
|
3887
|
+
job = jobHandle;
|
|
3888
|
+
ProcessId = processInformation.dwProcessId;
|
|
3889
|
+
FileTime creation;
|
|
3890
|
+
FileTime exit;
|
|
3891
|
+
FileTime kernel;
|
|
3892
|
+
FileTime user;
|
|
3893
|
+
if (!GetProcessTimes(process, out creation, out exit, out kernel, out user))
|
|
3894
|
+
throw new Win32Exception(Marshal.GetLastWin32Error(), "GetProcessTimes failed");
|
|
3895
|
+
StartedAtFileTime = ((ulong)creation.High << 32) | creation.Low;
|
|
3896
|
+
}
|
|
3897
|
+
|
|
3898
|
+
private static void ConfigureKillOnClose(IntPtr jobHandle, bool enabled)
|
|
3899
|
+
{
|
|
3900
|
+
var information = new ExtendedLimitInformation();
|
|
3901
|
+
information.BasicLimitInformation.LimitFlags = enabled ? JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE : 0;
|
|
3902
|
+
int length = Marshal.SizeOf(typeof(ExtendedLimitInformation));
|
|
3903
|
+
IntPtr buffer = Marshal.AllocHGlobal(length);
|
|
3904
|
+
try
|
|
3905
|
+
{
|
|
3906
|
+
Marshal.StructureToPtr(information, buffer, false);
|
|
3907
|
+
if (!SetInformationJobObject(jobHandle, JOB_OBJECT_EXTENDED_LIMIT_INFORMATION, buffer, (uint)length))
|
|
3908
|
+
throw new Win32Exception(Marshal.GetLastWin32Error(), "SetInformationJobObject failed");
|
|
3909
|
+
}
|
|
3910
|
+
finally
|
|
3911
|
+
{
|
|
3912
|
+
Marshal.FreeHGlobal(buffer);
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3915
|
+
|
|
3916
|
+
private static void TerminateAndWait(IntPtr processHandle)
|
|
3917
|
+
{
|
|
3918
|
+
if (!TerminateProcess(processHandle, 1))
|
|
3919
|
+
{
|
|
3920
|
+
int error = Marshal.GetLastWin32Error();
|
|
3921
|
+
if (error != 5)
|
|
3922
|
+
throw new Win32Exception(error, "TerminateProcess failed");
|
|
3923
|
+
}
|
|
3924
|
+
uint wait = WaitForSingleObject(processHandle, 15000);
|
|
3925
|
+
if (wait == WAIT_TIMEOUT)
|
|
3926
|
+
throw new TimeoutException("Timed out waiting for the terminated Studio process");
|
|
3927
|
+
if (wait == WAIT_FAILED)
|
|
3928
|
+
throw new Win32Exception(Marshal.GetLastWin32Error(), "WaitForSingleObject failed");
|
|
3929
|
+
if (wait != WAIT_OBJECT_0)
|
|
3930
|
+
throw new InvalidOperationException("Unexpected Studio process wait result: " + wait);
|
|
3931
|
+
}
|
|
3932
|
+
|
|
3933
|
+
public static McpSuspendedStudio Start(string application, string commandLine)
|
|
3934
|
+
{
|
|
3935
|
+
IntPtr jobHandle = CreateJobObjectW(IntPtr.Zero, null);
|
|
3936
|
+
if (jobHandle == IntPtr.Zero)
|
|
3937
|
+
throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateJobObjectW failed");
|
|
3938
|
+
try
|
|
3939
|
+
{
|
|
3940
|
+
ConfigureKillOnClose(jobHandle, true);
|
|
3941
|
+
var startup = new StartupInfo();
|
|
3942
|
+
startup.cb = Marshal.SizeOf(startup);
|
|
3943
|
+
ProcessInformation created;
|
|
3944
|
+
uint flags = CREATE_SUSPENDED | CREATE_BREAKAWAY_FROM_JOB;
|
|
3945
|
+
bool started = CreateProcessW(application, new StringBuilder(commandLine), IntPtr.Zero, IntPtr.Zero, false, flags, IntPtr.Zero, null, ref startup, out created);
|
|
3946
|
+
if (!started && Marshal.GetLastWin32Error() == 5)
|
|
3947
|
+
started = CreateProcessW(application, new StringBuilder(commandLine), IntPtr.Zero, IntPtr.Zero, false, CREATE_SUSPENDED, IntPtr.Zero, null, ref startup, out created);
|
|
3948
|
+
if (!started)
|
|
3949
|
+
throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateProcessW failed");
|
|
3950
|
+
try
|
|
3951
|
+
{
|
|
3952
|
+
if (!AssignProcessToJobObject(jobHandle, created.hProcess))
|
|
3953
|
+
throw new Win32Exception(Marshal.GetLastWin32Error(), "AssignProcessToJobObject failed");
|
|
3954
|
+
return new McpSuspendedStudio(created, jobHandle);
|
|
3955
|
+
}
|
|
3956
|
+
catch
|
|
3957
|
+
{
|
|
3958
|
+
try
|
|
3959
|
+
{
|
|
3960
|
+
TerminateAndWait(created.hProcess);
|
|
3961
|
+
}
|
|
3962
|
+
finally
|
|
3963
|
+
{
|
|
3964
|
+
CloseHandle(created.hThread);
|
|
3965
|
+
CloseHandle(created.hProcess);
|
|
3966
|
+
}
|
|
3967
|
+
throw;
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
catch
|
|
3971
|
+
{
|
|
3972
|
+
CloseHandle(jobHandle);
|
|
3973
|
+
throw;
|
|
3974
|
+
}
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
public void Resume()
|
|
3978
|
+
{
|
|
3979
|
+
if (thread == IntPtr.Zero)
|
|
3980
|
+
throw new InvalidOperationException("Studio launch was already resumed or disposed");
|
|
3981
|
+
if (ResumeThread(thread) == RESUME_FAILED)
|
|
3982
|
+
throw new Win32Exception(Marshal.GetLastWin32Error(), "ResumeThread failed");
|
|
3983
|
+
CloseHandle(thread);
|
|
3984
|
+
thread = IntPtr.Zero;
|
|
3985
|
+
}
|
|
3986
|
+
|
|
3987
|
+
public void Release()
|
|
3988
|
+
{
|
|
3989
|
+
if (thread != IntPtr.Zero)
|
|
3990
|
+
throw new InvalidOperationException("Studio launch cannot be released before it is resumed");
|
|
3991
|
+
if (job == IntPtr.Zero)
|
|
3992
|
+
throw new InvalidOperationException("Studio launch was already released or disposed");
|
|
3993
|
+
ConfigureKillOnClose(job, false);
|
|
3994
|
+
CloseHandle(job);
|
|
3995
|
+
job = IntPtr.Zero;
|
|
3996
|
+
}
|
|
3997
|
+
|
|
3998
|
+
public void Abort()
|
|
3999
|
+
{
|
|
4000
|
+
if (process != IntPtr.Zero)
|
|
4001
|
+
TerminateAndWait(process);
|
|
4002
|
+
}
|
|
4003
|
+
|
|
4004
|
+
public void Dispose()
|
|
4005
|
+
{
|
|
4006
|
+
if (thread != IntPtr.Zero)
|
|
4007
|
+
{
|
|
4008
|
+
CloseHandle(thread);
|
|
4009
|
+
thread = IntPtr.Zero;
|
|
4010
|
+
}
|
|
4011
|
+
if (job != IntPtr.Zero)
|
|
4012
|
+
{
|
|
4013
|
+
CloseHandle(job);
|
|
4014
|
+
job = IntPtr.Zero;
|
|
4015
|
+
}
|
|
4016
|
+
if (process != IntPtr.Zero)
|
|
4017
|
+
{
|
|
4018
|
+
CloseHandle(process);
|
|
4019
|
+
process = IntPtr.Zero;
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
4023
|
+
'@`,
|
|
4024
|
+
`$launch = [McpSuspendedStudio]::Start(${powershellStringLiteral(windowsExe)}, ${powershellStringLiteral(commandLine)})`,
|
|
4025
|
+
"try {",
|
|
4026
|
+
"[Console]::Out.WriteLine((ConvertTo-Json @{ pid = $launch.ProcessId; started = [string]$launch.StartedAtFileTime } -Compress)); [Console]::Out.Flush()",
|
|
4027
|
+
"$accepted = $false",
|
|
4028
|
+
"$command = [Console]::In.ReadLine()",
|
|
4029
|
+
'if ($command -eq "MCP_STUDIO_LAUNCH_ACCEPT") {',
|
|
4030
|
+
"$launch.Resume()",
|
|
4031
|
+
'[Console]::Out.WriteLine("MCP_STUDIO_LAUNCH_RESUMED"); [Console]::Out.Flush()',
|
|
4032
|
+
"$command = [Console]::In.ReadLine()",
|
|
4033
|
+
'if ($command -eq "MCP_STUDIO_LAUNCH_COMPLETE") { $launch.Release(); $accepted = $true }',
|
|
4034
|
+
'elseif ($command -eq "MCP_STUDIO_LAUNCH_ABORT") { $launch.Abort(); $accepted = $true }',
|
|
4035
|
+
'else { throw "Resumed Studio launch was not completed or aborted." }',
|
|
4036
|
+
'} elseif ($command -eq "MCP_STUDIO_LAUNCH_ABORT") { $launch.Abort(); $accepted = $true }',
|
|
4037
|
+
'else { throw "Studio launch identity was not accepted." }',
|
|
4038
|
+
"} finally {",
|
|
4039
|
+
"try { if (-not $accepted) { $launch.Abort() } } finally { $launch.Dispose() }",
|
|
4040
|
+
"}"
|
|
4041
|
+
].join("\n");
|
|
4042
|
+
}
|
|
4043
|
+
function buildWindowsStudioStopScript(processId, startedAt) {
|
|
4044
|
+
if (!Number.isSafeInteger(processId) || processId <= 0)
|
|
4045
|
+
throw new Error("processId must be a positive integer.");
|
|
4046
|
+
if (!/^[1-9]\d*$/u.test(startedAt))
|
|
4047
|
+
throw new Error("startedAt must be a positive FILETIME string.");
|
|
4048
|
+
return [
|
|
4049
|
+
`$processId = [int]${processId}`,
|
|
4050
|
+
`$expectedStartedAt = [long]${startedAt}`,
|
|
4051
|
+
"$studio = $null",
|
|
4052
|
+
"try { $studio = [System.Diagnostics.Process]::GetProcessById($processId) } catch [System.ArgumentException] { return }",
|
|
4053
|
+
"try {",
|
|
4054
|
+
"$actualStartedAt = $studio.StartTime.ToUniversalTime().ToFileTimeUtc()",
|
|
4055
|
+
"if ($actualStartedAt -ne $expectedStartedAt) { return }",
|
|
4056
|
+
"if (-not $studio.HasExited) {",
|
|
4057
|
+
"try { $studio.Kill() } catch { if (-not $studio.HasExited) { throw } }",
|
|
4058
|
+
"$studio.WaitForExit()",
|
|
4059
|
+
"}",
|
|
4060
|
+
"} finally { if ($null -ne $studio) { $studio.Dispose() } }"
|
|
3655
4061
|
].join("; ");
|
|
3656
4062
|
}
|
|
3657
|
-
async function
|
|
4063
|
+
async function stopWindowsStudio(processId, startedAt) {
|
|
4064
|
+
await powershellAsync(buildWindowsStudioStopScript(processId, startedAt));
|
|
4065
|
+
}
|
|
4066
|
+
async function spawnWindowsStudio(exe, args, processEnvironment) {
|
|
3658
4067
|
const windowsExe = await toStudioLaunchArgAsync(exe);
|
|
3659
4068
|
const script = buildWindowsStudioStartScriptFromConvertedExe(windowsExe, args, processEnvironment);
|
|
3660
|
-
const
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
4069
|
+
const launcher = spawn("powershell.exe", ["-NoProfile", "-Command", script], {
|
|
4070
|
+
cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
|
|
4071
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
4072
|
+
windowsHide: true
|
|
4073
|
+
});
|
|
4074
|
+
return new Promise((resolve5, reject) => {
|
|
4075
|
+
let stdout = "";
|
|
4076
|
+
let controlOutput = "";
|
|
4077
|
+
let stderr = "";
|
|
4078
|
+
let identity;
|
|
4079
|
+
let initialFailure;
|
|
4080
|
+
let identitySettled = false;
|
|
4081
|
+
let controlState = "pending";
|
|
4082
|
+
let resumeSettled = false;
|
|
4083
|
+
let completeResume;
|
|
4084
|
+
const resumeAcknowledgment = new Promise((complete) => {
|
|
4085
|
+
completeResume = complete;
|
|
4086
|
+
});
|
|
4087
|
+
const signalResume = (error) => {
|
|
4088
|
+
if (resumeSettled)
|
|
4089
|
+
return;
|
|
4090
|
+
resumeSettled = true;
|
|
4091
|
+
completeResume(error);
|
|
4092
|
+
};
|
|
4093
|
+
let completeLauncher;
|
|
4094
|
+
const launcherCompletion = new Promise((complete) => {
|
|
4095
|
+
completeLauncher = complete;
|
|
4096
|
+
});
|
|
4097
|
+
const waitForLauncherCompletion = (timeoutMessage) => new Promise((complete) => {
|
|
4098
|
+
let settled = false;
|
|
4099
|
+
const forceTimeout = setTimeout(() => {
|
|
4100
|
+
if (settled)
|
|
4101
|
+
return;
|
|
4102
|
+
settled = true;
|
|
4103
|
+
const error = new Error(timeoutMessage);
|
|
4104
|
+
launcher.kill("SIGKILL");
|
|
4105
|
+
signalResume(error);
|
|
4106
|
+
completeLauncher(error);
|
|
4107
|
+
complete(error);
|
|
4108
|
+
}, 1e4);
|
|
4109
|
+
void launcherCompletion.then((error) => {
|
|
4110
|
+
if (settled)
|
|
4111
|
+
return;
|
|
4112
|
+
settled = true;
|
|
4113
|
+
clearTimeout(forceTimeout);
|
|
4114
|
+
complete(error);
|
|
4115
|
+
});
|
|
4116
|
+
});
|
|
4117
|
+
const waitForResumeAcknowledgment = () => new Promise((complete) => {
|
|
4118
|
+
const forceTimeout = setTimeout(() => {
|
|
4119
|
+
const error = new Error("Timed out resuming the suspended Studio process.");
|
|
4120
|
+
launcher.kill("SIGKILL");
|
|
4121
|
+
signalResume(error);
|
|
4122
|
+
completeLauncher(error);
|
|
4123
|
+
}, 1e4);
|
|
4124
|
+
void resumeAcknowledgment.then((error) => {
|
|
4125
|
+
clearTimeout(forceTimeout);
|
|
4126
|
+
complete(error);
|
|
4127
|
+
});
|
|
4128
|
+
});
|
|
4129
|
+
const throwWithExactCleanup = async (error) => {
|
|
4130
|
+
if (!identity)
|
|
4131
|
+
throw error;
|
|
4132
|
+
try {
|
|
4133
|
+
await stopWindowsStudio(identity.pid, identity.startedAt);
|
|
4134
|
+
} catch (cleanupError) {
|
|
4135
|
+
throw new Error(`${error.message} Exact Studio cleanup also failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
|
|
4136
|
+
}
|
|
4137
|
+
throw error;
|
|
4138
|
+
};
|
|
4139
|
+
const authorize = async () => {
|
|
4140
|
+
if (controlState === "resumed")
|
|
4141
|
+
return;
|
|
4142
|
+
if (controlState !== "pending")
|
|
4143
|
+
throw new Error(`Studio launch cannot be authorized from ${controlState}.`);
|
|
4144
|
+
controlState = "accepting";
|
|
4145
|
+
launcher.stdin.write("MCP_STUDIO_LAUNCH_ACCEPT\n");
|
|
4146
|
+
const error = await waitForResumeAcknowledgment();
|
|
4147
|
+
if (error)
|
|
4148
|
+
await throwWithExactCleanup(error);
|
|
4149
|
+
controlState = "resumed";
|
|
4150
|
+
};
|
|
4151
|
+
const release = async () => {
|
|
4152
|
+
if (controlState === "released")
|
|
4153
|
+
return;
|
|
4154
|
+
if (controlState !== "resumed")
|
|
4155
|
+
throw new Error(`Studio launch cannot be released from ${controlState}.`);
|
|
4156
|
+
controlState = "releasing";
|
|
4157
|
+
launcher.stdin.end("MCP_STUDIO_LAUNCH_COMPLETE\n");
|
|
4158
|
+
const error = await waitForLauncherCompletion("Timed out releasing the authorized Studio process.");
|
|
4159
|
+
if (error)
|
|
4160
|
+
await throwWithExactCleanup(error);
|
|
4161
|
+
controlState = "released";
|
|
4162
|
+
};
|
|
4163
|
+
const abort = async () => {
|
|
4164
|
+
if (controlState === "released")
|
|
4165
|
+
return;
|
|
4166
|
+
if (controlState !== "aborting") {
|
|
4167
|
+
controlState = "aborting";
|
|
4168
|
+
launcher.stdin.end("MCP_STUDIO_LAUNCH_ABORT\n");
|
|
4169
|
+
}
|
|
4170
|
+
const error = await waitForLauncherCompletion("Timed out aborting the owned Studio process.");
|
|
4171
|
+
if (error && identity) {
|
|
4172
|
+
await stopWindowsStudio(identity.pid, identity.startedAt);
|
|
4173
|
+
} else if (error) {
|
|
4174
|
+
throw error;
|
|
4175
|
+
}
|
|
4176
|
+
};
|
|
4177
|
+
const timeout = setTimeout(() => {
|
|
4178
|
+
if (identitySettled)
|
|
4179
|
+
return;
|
|
4180
|
+
identitySettled = true;
|
|
4181
|
+
const failure = initialFailure = new Error("Timed out while capturing the exact Studio process identity.");
|
|
4182
|
+
controlState = "aborting";
|
|
4183
|
+
launcher.stdin.end("MCP_STUDIO_LAUNCH_ABORT\n");
|
|
4184
|
+
void waitForLauncherCompletion("Timed out aborting the Studio identity launcher.").then((completionError) => {
|
|
4185
|
+
if (!identity)
|
|
4186
|
+
reject(completionError ?? failure);
|
|
4187
|
+
});
|
|
4188
|
+
}, 15e3);
|
|
4189
|
+
launcher.stdout.on("data", (chunk) => {
|
|
4190
|
+
if (identitySettled) {
|
|
4191
|
+
controlOutput = (controlOutput + chunk.toString("utf8")).slice(-1024);
|
|
4192
|
+
if (controlOutput.includes("MCP_STUDIO_LAUNCH_RESUMED"))
|
|
4193
|
+
signalResume();
|
|
4194
|
+
return;
|
|
4195
|
+
}
|
|
4196
|
+
stdout += chunk.toString("utf8");
|
|
4197
|
+
if (stdout.length > 1024 * 1024) {
|
|
4198
|
+
const failure = initialFailure = new Error("Studio launcher identity output exceeded 1 MiB.");
|
|
4199
|
+
identitySettled = true;
|
|
4200
|
+
controlState = "aborting";
|
|
4201
|
+
launcher.stdin.end("MCP_STUDIO_LAUNCH_ABORT\n");
|
|
4202
|
+
void waitForLauncherCompletion("Timed out aborting the oversized Studio identity response.").then((completionError) => {
|
|
4203
|
+
if (!identity)
|
|
4204
|
+
reject(completionError ?? failure);
|
|
4205
|
+
});
|
|
4206
|
+
return;
|
|
4207
|
+
}
|
|
4208
|
+
const newline = stdout.indexOf("\n");
|
|
4209
|
+
if (newline < 0)
|
|
4210
|
+
return;
|
|
4211
|
+
identitySettled = true;
|
|
4212
|
+
clearTimeout(timeout);
|
|
4213
|
+
try {
|
|
4214
|
+
const parsed = JSON.parse(stdout.slice(0, newline).trim());
|
|
4215
|
+
const nativePid = Number(parsed.pid);
|
|
4216
|
+
const nativeStartedAt = typeof parsed.started === "string" && /^[1-9]\d*$/u.test(parsed.started) ? parsed.started : void 0;
|
|
4217
|
+
if (!Number.isSafeInteger(nativePid) || nativePid <= 0 || nativeStartedAt === void 0) {
|
|
4218
|
+
throw new Error("PowerShell returned an invalid native Studio process identity.");
|
|
4219
|
+
}
|
|
4220
|
+
identity = { pid: nativePid, startedAt: nativeStartedAt };
|
|
4221
|
+
resolve5({
|
|
4222
|
+
pid: nativePid,
|
|
4223
|
+
nativePid,
|
|
4224
|
+
nativeStartedAt,
|
|
4225
|
+
unref: () => {
|
|
4226
|
+
},
|
|
4227
|
+
authorize,
|
|
4228
|
+
release,
|
|
4229
|
+
abort
|
|
4230
|
+
});
|
|
4231
|
+
} catch (error) {
|
|
4232
|
+
const failure = initialFailure = error instanceof Error ? error : new Error(String(error));
|
|
4233
|
+
controlState = "aborting";
|
|
4234
|
+
launcher.stdin.end("MCP_STUDIO_LAUNCH_ABORT\n");
|
|
4235
|
+
void waitForLauncherCompletion("Timed out aborting the invalid Studio identity response.").then((completionError) => {
|
|
4236
|
+
if (!identity)
|
|
4237
|
+
reject(completionError ?? failure);
|
|
4238
|
+
});
|
|
4239
|
+
}
|
|
4240
|
+
});
|
|
4241
|
+
launcher.stdin.on("error", (error) => {
|
|
4242
|
+
if (!initialFailure)
|
|
4243
|
+
initialFailure = error;
|
|
4244
|
+
});
|
|
4245
|
+
launcher.stderr.on("data", (chunk) => {
|
|
4246
|
+
if (stderr.length <= 1024 * 1024)
|
|
4247
|
+
stderr += chunk.toString("utf8");
|
|
4248
|
+
});
|
|
4249
|
+
launcher.once("error", (error) => {
|
|
4250
|
+
clearTimeout(timeout);
|
|
4251
|
+
signalResume(error);
|
|
4252
|
+
completeLauncher(error);
|
|
4253
|
+
if (!identity)
|
|
4254
|
+
reject(error);
|
|
4255
|
+
});
|
|
4256
|
+
launcher.once("exit", (code) => {
|
|
4257
|
+
clearTimeout(timeout);
|
|
4258
|
+
void (async () => {
|
|
4259
|
+
if (code === 0 && identity && !initialFailure) {
|
|
4260
|
+
if (!resumeSettled)
|
|
4261
|
+
signalResume(new Error("Studio launcher exited before resume acknowledgment."));
|
|
4262
|
+
completeLauncher();
|
|
4263
|
+
return;
|
|
4264
|
+
}
|
|
4265
|
+
let cleanupError;
|
|
4266
|
+
if (identity) {
|
|
4267
|
+
try {
|
|
4268
|
+
await stopWindowsStudio(identity.pid, identity.startedAt);
|
|
4269
|
+
} catch (error2) {
|
|
4270
|
+
cleanupError = error2;
|
|
4271
|
+
}
|
|
4272
|
+
}
|
|
4273
|
+
const detail = initialFailure?.message ?? (stderr.trim() || `PowerShell launcher exited with code ${code}.`);
|
|
4274
|
+
const cleanupDetail = cleanupError ? ` Exact Studio cleanup also failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}` : "";
|
|
4275
|
+
const error = new Error(`${detail}${cleanupDetail}`);
|
|
4276
|
+
signalResume(error);
|
|
4277
|
+
completeLauncher(error);
|
|
4278
|
+
if (!identity)
|
|
4279
|
+
reject(error);
|
|
4280
|
+
})();
|
|
4281
|
+
});
|
|
4282
|
+
});
|
|
3674
4283
|
}
|
|
3675
4284
|
function resolveEntrypointDir() {
|
|
3676
4285
|
const entrypoint = process.argv[1];
|
|
@@ -3895,7 +4504,7 @@ function delay2(ms) {
|
|
|
3895
4504
|
function basenameAny(filePath) {
|
|
3896
4505
|
return path2.basename(filePath.replace(/\\/g, "/"));
|
|
3897
4506
|
}
|
|
3898
|
-
var BASEPLATE_TEMP_DIR, BASEPLATE_TEMP_NAME, BASEPLATE_TEMPLATE_NAME, execFileAsync, ENVIRONMENT_VARIABLE_NAME, STALE_BASEPLATE_MAX_AGE_MS, BASEPLATE_TEMP_SWEEP_NAME, StudioInstanceManager;
|
|
4507
|
+
var BASEPLATE_TEMP_DIR, BASEPLATE_TEMP_NAME, BASEPLATE_TEMPLATE_NAME, retainedLaunchControls, execFileAsync, ENVIRONMENT_VARIABLE_NAME, STALE_BASEPLATE_MAX_AGE_MS, BASEPLATE_TEMP_SWEEP_NAME, LAUNCH_COMPLETION_TIMEOUT_MS, StudioInstanceManager;
|
|
3899
4508
|
var init_studio_instance_manager = __esm({
|
|
3900
4509
|
"../core/dist/studio-instance-manager.js"() {
|
|
3901
4510
|
"use strict";
|
|
@@ -3903,19 +4512,24 @@ var init_studio_instance_manager = __esm({
|
|
|
3903
4512
|
BASEPLATE_TEMP_DIR = path2.join(os2.tmpdir(), "robloxstudio-mcp-baseplates");
|
|
3904
4513
|
BASEPLATE_TEMP_NAME = /^Baseplate-\d+-\d+\.rbxl$/;
|
|
3905
4514
|
BASEPLATE_TEMPLATE_NAME = "Baseplate.rbxl";
|
|
4515
|
+
retainedLaunchControls = /* @__PURE__ */ new Map();
|
|
3906
4516
|
execFileAsync = promisify(execFile);
|
|
3907
4517
|
ENVIRONMENT_VARIABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
3908
4518
|
STALE_BASEPLATE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
3909
4519
|
BASEPLATE_TEMP_SWEEP_NAME = /^Baseplate-(\d+)-\d+\.rbxl(\.lock)?$/;
|
|
4520
|
+
LAUNCH_COMPLETION_TIMEOUT_MS = 3 * 60 * 1e3;
|
|
3910
4521
|
StudioInstanceManager = class {
|
|
3911
4522
|
managedByInstanceId = /* @__PURE__ */ new Map();
|
|
3912
4523
|
pending = /* @__PURE__ */ new Set();
|
|
3913
4524
|
connectionTimers = /* @__PURE__ */ new Map();
|
|
4525
|
+
launchCompletionTimers = /* @__PURE__ */ new Map();
|
|
4526
|
+
launchControls = retainedLaunchControls;
|
|
3914
4527
|
registry;
|
|
3915
4528
|
processAdapter;
|
|
3916
4529
|
confirmedExitMisses;
|
|
3917
4530
|
confirmedExitGraceMs;
|
|
3918
4531
|
snapshotCacheMs;
|
|
4532
|
+
launchCompletionTimeoutMs;
|
|
3919
4533
|
coordinatorTimer;
|
|
3920
4534
|
coordinatorRefresh;
|
|
3921
4535
|
cachedSnapshot;
|
|
@@ -3927,6 +4541,7 @@ var init_studio_instance_manager = __esm({
|
|
|
3927
4541
|
this.confirmedExitMisses = options.confirmedExitMisses ?? 2;
|
|
3928
4542
|
this.confirmedExitGraceMs = options.confirmedExitGraceMs ?? 5e3;
|
|
3929
4543
|
this.snapshotCacheMs = options.snapshotCacheMs ?? 0;
|
|
4544
|
+
this.launchCompletionTimeoutMs = options.launchCompletionTimeoutMs ?? LAUNCH_COMPLETION_TIMEOUT_MS;
|
|
3930
4545
|
}
|
|
3931
4546
|
async list() {
|
|
3932
4547
|
const snapshot = await this.getProcessSnapshot();
|
|
@@ -3962,6 +4577,62 @@ var init_studio_instance_manager = __esm({
|
|
|
3962
4577
|
const registryRecord = await this.registry.findAnyByRecordId(launchId);
|
|
3963
4578
|
return registryRecord ? this.refresh(this.fromRegistryRecord(registryRecord), snapshot) : void 0;
|
|
3964
4579
|
}
|
|
4580
|
+
peekByLaunchId(launchId) {
|
|
4581
|
+
return [...this.managedByInstanceId.values(), ...this.pending].find((record) => record.recordId === launchId);
|
|
4582
|
+
}
|
|
4583
|
+
async authorizeByLaunchId(launchId) {
|
|
4584
|
+
const record = this.peekByLaunchId(launchId);
|
|
4585
|
+
if (!record)
|
|
4586
|
+
throw new Error(`Launch ${launchId} is not retained by this broker process.`);
|
|
4587
|
+
if (record.closedAt !== void 0 || record.state === "failed" || record.state === "exited") {
|
|
4588
|
+
throw new Error(`Launch ${launchId} is no longer eligible for process authorization.`);
|
|
4589
|
+
}
|
|
4590
|
+
if (record.processAuthorizationState === "authorized")
|
|
4591
|
+
return record;
|
|
4592
|
+
const control = this.launchControls.get(launchId);
|
|
4593
|
+
if (record.processAuthorizationState !== "pending" || !control) {
|
|
4594
|
+
throw new Error(`Launch ${launchId} has no retained process authorization handle.`);
|
|
4595
|
+
}
|
|
4596
|
+
try {
|
|
4597
|
+
this.armLaunchCompletionTimer(record);
|
|
4598
|
+
await control.authorize();
|
|
4599
|
+
record.processAuthorizationState = "authorized";
|
|
4600
|
+
await this.persist(record);
|
|
4601
|
+
this.armLaunchCompletionTimer(record);
|
|
4602
|
+
return record;
|
|
4603
|
+
} catch (error) {
|
|
4604
|
+
record.processAuthorizationState = "pending";
|
|
4605
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4606
|
+
await this.markFailed(record, `Studio process authorization failed: ${detail}`);
|
|
4607
|
+
throw error;
|
|
4608
|
+
}
|
|
4609
|
+
}
|
|
4610
|
+
async completeByLaunchId(launchId) {
|
|
4611
|
+
const record = this.peekByLaunchId(launchId);
|
|
4612
|
+
if (!record)
|
|
4613
|
+
throw new Error(`Launch ${launchId} is not retained by this broker process.`);
|
|
4614
|
+
if (record.closedAt !== void 0 || record.state === "failed" || record.state === "exited") {
|
|
4615
|
+
throw new Error(`Launch ${launchId} is no longer eligible for ownership release.`);
|
|
4616
|
+
}
|
|
4617
|
+
if (record.processAuthorizationState === "released")
|
|
4618
|
+
return record;
|
|
4619
|
+
const control = this.launchControls.get(launchId);
|
|
4620
|
+
if (record.processAuthorizationState !== "authorized" || !control) {
|
|
4621
|
+
throw new Error(`Launch ${launchId} has no authorized ownership handle to release.`);
|
|
4622
|
+
}
|
|
4623
|
+
try {
|
|
4624
|
+
this.clearLaunchCompletionTimer(record);
|
|
4625
|
+
await control.release();
|
|
4626
|
+
record.processAuthorizationState = "released";
|
|
4627
|
+
this.launchControls.delete(launchId);
|
|
4628
|
+
await this.persist(record);
|
|
4629
|
+
return record;
|
|
4630
|
+
} catch (error) {
|
|
4631
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4632
|
+
await this.markFailed(record, `Studio process ownership release failed: ${detail}`);
|
|
4633
|
+
throw error;
|
|
4634
|
+
}
|
|
4635
|
+
}
|
|
3965
4636
|
async pendingLaunches() {
|
|
3966
4637
|
const now = Date.now();
|
|
3967
4638
|
const bootId = await this.getCurrentBootId();
|
|
@@ -3991,12 +4662,29 @@ var init_studio_instance_manager = __esm({
|
|
|
3991
4662
|
await this.persist(record);
|
|
3992
4663
|
}
|
|
3993
4664
|
async markFailed(record, reason) {
|
|
3994
|
-
if (record.closedAt !== void 0 || record.state
|
|
4665
|
+
if (record.closedAt !== void 0 || record.state === "failed" || record.state === "exited")
|
|
3995
4666
|
return record;
|
|
3996
4667
|
record.state = "failed";
|
|
3997
4668
|
record.failedAt = Date.now();
|
|
3998
4669
|
record.failureReason = reason;
|
|
3999
4670
|
this.clearConnectionTimer(record);
|
|
4671
|
+
this.clearLaunchCompletionTimer(record);
|
|
4672
|
+
const control = record.recordId ? this.launchControls.get(record.recordId) : void 0;
|
|
4673
|
+
if (record.processAuthorizationState !== "released" && control) {
|
|
4674
|
+
try {
|
|
4675
|
+
await control.abort();
|
|
4676
|
+
const exitedAt = Date.now();
|
|
4677
|
+
record.exitedAt = exitedAt;
|
|
4678
|
+
record.closedAt = exitedAt;
|
|
4679
|
+
record.processObservationStatus = "not_running";
|
|
4680
|
+
record.lastProcessObservationAt = exitedAt;
|
|
4681
|
+
record.lastSuccessfulProcessObservationAt = exitedAt;
|
|
4682
|
+
this.cleanupManagedRecord(record);
|
|
4683
|
+
this.markClosedInMemory(record);
|
|
4684
|
+
} catch (error) {
|
|
4685
|
+
record.failureReason += ` Exact suspended-process cleanup also failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
4686
|
+
}
|
|
4687
|
+
}
|
|
4000
4688
|
await this.persist(record);
|
|
4001
4689
|
return record;
|
|
4002
4690
|
}
|
|
@@ -4008,11 +4696,7 @@ var init_studio_instance_manager = __esm({
|
|
|
4008
4696
|
if (record.closedAt !== void 0)
|
|
4009
4697
|
return record;
|
|
4010
4698
|
if (record.state === "launching" && record.connectionDeadlineAt !== void 0 && Date.now() >= record.connectionDeadlineAt) {
|
|
4011
|
-
record
|
|
4012
|
-
record.failedAt = Date.now();
|
|
4013
|
-
record.failureReason = "Studio launched, but the MCP plugin did not connect before timeout.";
|
|
4014
|
-
this.clearConnectionTimer(record);
|
|
4015
|
-
await this.persist(record);
|
|
4699
|
+
return this.markFailed(record, "Studio launched, but the MCP plugin did not connect before timeout.");
|
|
4016
4700
|
}
|
|
4017
4701
|
return record;
|
|
4018
4702
|
}
|
|
@@ -4036,6 +4720,9 @@ var init_studio_instance_manager = __esm({
|
|
|
4036
4720
|
if (options.studioExecutable !== void 0 && (typeof options.studioExecutable !== "string" || options.studioExecutable.length === 0 || options.studioExecutable.includes("\0"))) {
|
|
4037
4721
|
throw new Error("studio_executable must be a non-empty string without null characters.");
|
|
4038
4722
|
}
|
|
4723
|
+
if (options.requireProcessIdentity && !this.processAdapter.spawnStudio && process.platform !== "win32" && !isWsl()) {
|
|
4724
|
+
throw new Error("require_process_identity is supported only by the identity-retaining Windows launcher or a custom process adapter.");
|
|
4725
|
+
}
|
|
4039
4726
|
const preparedOptions = prepareStudioLaunchOptions(options);
|
|
4040
4727
|
const bootId = await this.getCurrentBootId();
|
|
4041
4728
|
const before = new Set(initialSnapshot.status === "ok" ? initialSnapshot.processes.map((proc2) => proc2.Id) : []);
|
|
@@ -4051,8 +4738,8 @@ var init_studio_instance_manager = __esm({
|
|
|
4051
4738
|
try {
|
|
4052
4739
|
if (this.processAdapter.spawnStudio) {
|
|
4053
4740
|
proc = await this.processAdapter.spawnStudio(exe, args, spawnOptions);
|
|
4054
|
-
} else if (isWsl()) {
|
|
4055
|
-
proc = await
|
|
4741
|
+
} else if (process.platform === "win32" || isWsl()) {
|
|
4742
|
+
proc = await spawnWindowsStudio(exe, args, processEnvironment);
|
|
4056
4743
|
} else {
|
|
4057
4744
|
const child = spawn(exe, args, spawnOptions);
|
|
4058
4745
|
proc = {
|
|
@@ -4071,6 +4758,37 @@ var init_studio_instance_manager = __esm({
|
|
|
4071
4758
|
cleanupManagedBaseplateFiles({ source: preparedOptions.source, localPlaceFile: preparedOptions.localPlaceFile });
|
|
4072
4759
|
throw error;
|
|
4073
4760
|
}
|
|
4761
|
+
if (options.requireProcessIdentity && (!Number.isSafeInteger(proc.nativePid) || (proc.nativePid ?? 0) <= 0 || proc.nativeStartedAt === void 0 || !/^[1-9]\d*$/u.test(proc.nativeStartedAt) || !proc.authorize || !proc.release || !proc.abort)) {
|
|
4762
|
+
const reason = "Studio launcher did not return an exact creation identity and suspended-process control handles.";
|
|
4763
|
+
let abortError;
|
|
4764
|
+
try {
|
|
4765
|
+
if (proc.abort) {
|
|
4766
|
+
await proc.abort();
|
|
4767
|
+
} else if (proc.nativePid && proc.nativeStartedAt) {
|
|
4768
|
+
await this.closeProcess(proc.nativePid, proc.nativeStartedAt);
|
|
4769
|
+
} else {
|
|
4770
|
+
throw new Error("the process adapter did not retain an abort handle or exact process identity");
|
|
4771
|
+
}
|
|
4772
|
+
} catch (error) {
|
|
4773
|
+
abortError = error;
|
|
4774
|
+
}
|
|
4775
|
+
cleanupManagedBaseplateFiles({ source: preparedOptions.source, localPlaceFile: preparedOptions.localPlaceFile });
|
|
4776
|
+
const abortDetail = abortError ? ` Exact child cleanup also failed: ${abortError instanceof Error ? abortError.message : String(abortError)}` : " The owned process was stopped.";
|
|
4777
|
+
throw new Error(`${reason}${abortDetail}`);
|
|
4778
|
+
}
|
|
4779
|
+
if (!options.requireProcessIdentity && proc.authorize) {
|
|
4780
|
+
try {
|
|
4781
|
+
await proc.authorize();
|
|
4782
|
+
await proc.release?.();
|
|
4783
|
+
} catch (error) {
|
|
4784
|
+
try {
|
|
4785
|
+
await proc.abort?.();
|
|
4786
|
+
} finally {
|
|
4787
|
+
cleanupManagedBaseplateFiles({ source: preparedOptions.source, localPlaceFile: preparedOptions.localPlaceFile });
|
|
4788
|
+
}
|
|
4789
|
+
throw error;
|
|
4790
|
+
}
|
|
4791
|
+
}
|
|
4074
4792
|
const launchedAt = Date.now();
|
|
4075
4793
|
const record = {
|
|
4076
4794
|
recordId: randomUUID2(),
|
|
@@ -4085,11 +4803,12 @@ var init_studio_instance_manager = __esm({
|
|
|
4085
4803
|
placeVersion: preparedOptions.placeVersion,
|
|
4086
4804
|
localPlaceFile: preparedOptions.localPlaceFile,
|
|
4087
4805
|
launchedAt,
|
|
4088
|
-
connectionDeadlineAt: launchedAt + (options.connectionTimeoutMs ?? 12e4),
|
|
4806
|
+
connectionDeadlineAt: options.requireProcessIdentity ? void 0 : launchedAt + (options.connectionTimeoutMs ?? 12e4),
|
|
4089
4807
|
state: "launching",
|
|
4090
4808
|
ownerPid: process.pid,
|
|
4091
4809
|
bootId,
|
|
4092
4810
|
deleteLocalPlaceFileOnClose: options.source === "baseplate",
|
|
4811
|
+
processAuthorizationState: options.requireProcessIdentity && proc.authorize && proc.release ? "pending" : "released",
|
|
4093
4812
|
processObservationStatus: "running",
|
|
4094
4813
|
lastProcessObservationAt: launchedAt,
|
|
4095
4814
|
lastSuccessfulProcessObservationAt: launchedAt,
|
|
@@ -4098,16 +4817,26 @@ var init_studio_instance_manager = __esm({
|
|
|
4098
4817
|
this.pending.add(record);
|
|
4099
4818
|
try {
|
|
4100
4819
|
await this.persist(record);
|
|
4820
|
+
if (record.recordId && record.processAuthorizationState === "pending" && proc.authorize && proc.release && proc.abort) {
|
|
4821
|
+
this.launchControls.set(record.recordId, {
|
|
4822
|
+
authorize: proc.authorize,
|
|
4823
|
+
release: proc.release,
|
|
4824
|
+
abort: proc.abort
|
|
4825
|
+
});
|
|
4826
|
+
this.armLaunchCompletionTimer(record);
|
|
4827
|
+
}
|
|
4101
4828
|
} catch (error) {
|
|
4102
4829
|
this.pending.delete(record);
|
|
4103
4830
|
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
4104
4831
|
let stopError;
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
await
|
|
4108
|
-
}
|
|
4109
|
-
|
|
4832
|
+
try {
|
|
4833
|
+
if (proc.abort) {
|
|
4834
|
+
await proc.abort();
|
|
4835
|
+
} else if (processId) {
|
|
4836
|
+
await this.closeProcess(processId, record.nativeProcessStartedAt);
|
|
4110
4837
|
}
|
|
4838
|
+
} catch (caught) {
|
|
4839
|
+
stopError = caught;
|
|
4111
4840
|
}
|
|
4112
4841
|
cleanupManagedBaseplateFiles(record);
|
|
4113
4842
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -4183,12 +4912,37 @@ var init_studio_instance_manager = __esm({
|
|
|
4183
4912
|
}
|
|
4184
4913
|
async close(record) {
|
|
4185
4914
|
if (record.closedAt !== void 0) {
|
|
4186
|
-
return {
|
|
4915
|
+
return {
|
|
4916
|
+
status: "already_closed",
|
|
4917
|
+
launchId: record.recordId,
|
|
4918
|
+
instanceId: record.instanceId
|
|
4919
|
+
};
|
|
4187
4920
|
}
|
|
4188
4921
|
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
4189
4922
|
if (!processId) {
|
|
4190
4923
|
throw new Error(`Cannot close ${record.instanceId ?? "Studio launch"} because its process id was not detected.`);
|
|
4191
4924
|
}
|
|
4925
|
+
const control = record.recordId ? this.launchControls.get(record.recordId) : void 0;
|
|
4926
|
+
if (record.processAuthorizationState !== "released" && control) {
|
|
4927
|
+
await control.abort();
|
|
4928
|
+
const closedAt2 = Date.now();
|
|
4929
|
+
record.closedAt = closedAt2;
|
|
4930
|
+
record.exitedAt = closedAt2;
|
|
4931
|
+
if (record.state !== "failed")
|
|
4932
|
+
record.state = "exited";
|
|
4933
|
+
record.processObservationStatus = "not_running";
|
|
4934
|
+
record.lastProcessObservationAt = closedAt2;
|
|
4935
|
+
record.lastSuccessfulProcessObservationAt = closedAt2;
|
|
4936
|
+
record.lastProcessObservationError = void 0;
|
|
4937
|
+
this.cleanupManagedRecord(record);
|
|
4938
|
+
this.markClosedInMemory(record);
|
|
4939
|
+
await this.persist(record);
|
|
4940
|
+
return {
|
|
4941
|
+
status: "closed",
|
|
4942
|
+
launchId: record.recordId,
|
|
4943
|
+
instanceId: record.instanceId
|
|
4944
|
+
};
|
|
4945
|
+
}
|
|
4192
4946
|
const snapshot = await this.getProcessSnapshot(true);
|
|
4193
4947
|
const observation = this.observeRecord(record, snapshot);
|
|
4194
4948
|
if (observation.status === "unknown") {
|
|
@@ -4196,7 +4950,6 @@ var init_studio_instance_manager = __esm({
|
|
|
4196
4950
|
throw new Error(`Cannot verify the managed Studio process because process observation failed: ${observation.error}`);
|
|
4197
4951
|
}
|
|
4198
4952
|
if (observation.status === "not_running") {
|
|
4199
|
-
this.cleanupManagedRecord(record);
|
|
4200
4953
|
await this.markProcessExited(record, void 0, observation.reason === "identity_mismatch" ? "Studio process identity changed; the retained PID was not reused." : record.failureReason);
|
|
4201
4954
|
await this.registry.logEvent({
|
|
4202
4955
|
event: "registry_close_already_stopped",
|
|
@@ -4206,10 +4959,14 @@ var init_studio_instance_manager = __esm({
|
|
|
4206
4959
|
reason: observation.reason === "identity_mismatch" ? "identity_mismatch" : "pid_not_running",
|
|
4207
4960
|
action: "marked_closed_and_cleaned_baseplate"
|
|
4208
4961
|
});
|
|
4209
|
-
return {
|
|
4962
|
+
return {
|
|
4963
|
+
status: "already_closed",
|
|
4964
|
+
launchId: record.recordId,
|
|
4965
|
+
instanceId: record.instanceId
|
|
4966
|
+
};
|
|
4210
4967
|
}
|
|
4211
4968
|
try {
|
|
4212
|
-
await this.closeProcess(processId);
|
|
4969
|
+
await this.closeProcess(processId, record.nativeProcessStartedAt);
|
|
4213
4970
|
} catch (error) {
|
|
4214
4971
|
const retry = await this.getProcessSnapshot(true);
|
|
4215
4972
|
const retryObservation = this.observeRecord(record, retry);
|
|
@@ -4223,9 +4980,12 @@ var init_studio_instance_manager = __esm({
|
|
|
4223
4980
|
reason: "stop_raced_with_exit",
|
|
4224
4981
|
action: "marked_closed_and_cleaned_baseplate"
|
|
4225
4982
|
});
|
|
4226
|
-
this.cleanupManagedRecord(record);
|
|
4227
4983
|
await this.markProcessExited(record, void 0, record.failureReason);
|
|
4228
|
-
return {
|
|
4984
|
+
return {
|
|
4985
|
+
status: "already_closed",
|
|
4986
|
+
launchId: record.recordId,
|
|
4987
|
+
instanceId: record.instanceId
|
|
4988
|
+
};
|
|
4229
4989
|
}
|
|
4230
4990
|
const closedAt = Date.now();
|
|
4231
4991
|
record.closedAt = closedAt;
|
|
@@ -4239,7 +4999,11 @@ var init_studio_instance_manager = __esm({
|
|
|
4239
4999
|
this.cleanupManagedRecord(record);
|
|
4240
5000
|
this.markClosedInMemory(record);
|
|
4241
5001
|
await this.persist(record);
|
|
4242
|
-
return {
|
|
5002
|
+
return {
|
|
5003
|
+
status: "closed",
|
|
5004
|
+
launchId: record.recordId,
|
|
5005
|
+
instanceId: record.instanceId
|
|
5006
|
+
};
|
|
4243
5007
|
}
|
|
4244
5008
|
async closeConnectedInstance(instance) {
|
|
4245
5009
|
const snapshot = await this.getProcessSnapshot(true);
|
|
@@ -4250,15 +5014,18 @@ var init_studio_instance_manager = __esm({
|
|
|
4250
5014
|
if (!process2) {
|
|
4251
5015
|
throw new Error(`Could not find a Studio process for connected instance "${instance.instanceId}".`);
|
|
4252
5016
|
}
|
|
4253
|
-
await this.closeProcess(process2.Id);
|
|
5017
|
+
await this.closeProcess(process2.Id, process2.StartTimeUtcFileTime);
|
|
4254
5018
|
}
|
|
4255
|
-
async closeProcess(processId) {
|
|
5019
|
+
async closeProcess(processId, startedAt) {
|
|
4256
5020
|
if (this.processAdapter.stopProcess) {
|
|
4257
|
-
await this.processAdapter.stopProcess(processId);
|
|
5021
|
+
await this.processAdapter.stopProcess(processId, startedAt);
|
|
4258
5022
|
return;
|
|
4259
5023
|
}
|
|
4260
5024
|
if (process.platform === "win32" || isWsl()) {
|
|
4261
|
-
|
|
5025
|
+
if (startedAt === void 0) {
|
|
5026
|
+
throw new Error("Cannot stop a Windows Studio process without its creation-time identity.");
|
|
5027
|
+
}
|
|
5028
|
+
await stopWindowsStudio(processId, startedAt);
|
|
4262
5029
|
} else {
|
|
4263
5030
|
try {
|
|
4264
5031
|
process.kill(processId, "SIGTERM");
|
|
@@ -4329,13 +5096,55 @@ var init_studio_instance_manager = __esm({
|
|
|
4329
5096
|
return {
|
|
4330
5097
|
currentBootId: await this.getCurrentBootId(),
|
|
4331
5098
|
observeProcess: (record) => this.observeRecord(this.fromRegistryRecord(record), snapshot),
|
|
4332
|
-
cleanupRecord: (record) =>
|
|
5099
|
+
cleanupRecord: (record) => {
|
|
5100
|
+
if (record.processAuthorizationState !== "released" && this.launchControls.has(record.recordId))
|
|
5101
|
+
return;
|
|
5102
|
+
this.cleanupManagedRecord(record);
|
|
5103
|
+
},
|
|
4333
5104
|
confirmedExitMisses: this.confirmedExitMisses,
|
|
4334
5105
|
confirmedExitGraceMs: this.confirmedExitGraceMs
|
|
4335
5106
|
};
|
|
4336
5107
|
}
|
|
4337
5108
|
async sweepRegistry(snapshot) {
|
|
4338
|
-
|
|
5109
|
+
const sweepOptions = await this.registrySweepOptions(snapshot);
|
|
5110
|
+
await this.registry.sweep(sweepOptions);
|
|
5111
|
+
const persisted = await this.registry.listOpenUnchecked();
|
|
5112
|
+
for (const registryRecord of persisted) {
|
|
5113
|
+
const ownerPid = registryRecord.ownerPid;
|
|
5114
|
+
if (registryRecord.processAuthorizationState === "released" || this.launchControls.has(registryRecord.recordId) || registryRecord.bootId !== sweepOptions.currentBootId || ownerPid === void 0 || ownerPid !== process.pid && isProcessAlive2(ownerPid)) {
|
|
5115
|
+
continue;
|
|
5116
|
+
}
|
|
5117
|
+
const record = this.fromRegistryRecord(registryRecord);
|
|
5118
|
+
if (this.observeRecord(record, snapshot).status !== "running")
|
|
5119
|
+
continue;
|
|
5120
|
+
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
5121
|
+
if (!processId || !record.nativeProcessStartedAt) {
|
|
5122
|
+
record.state = "failed";
|
|
5123
|
+
record.failedAt = Date.now();
|
|
5124
|
+
record.failureReason = "Orphaned unreleased Studio launch has no exact process identity for cleanup.";
|
|
5125
|
+
await this.persist(record);
|
|
5126
|
+
continue;
|
|
5127
|
+
}
|
|
5128
|
+
try {
|
|
5129
|
+
await this.closeProcess(processId, record.nativeProcessStartedAt);
|
|
5130
|
+
const closedAt = Date.now();
|
|
5131
|
+
record.state = "failed";
|
|
5132
|
+
record.failedAt = closedAt;
|
|
5133
|
+
record.failureReason = "Orphaned unreleased Studio launch was stopped after its broker owner exited.";
|
|
5134
|
+
record.exitedAt = closedAt;
|
|
5135
|
+
record.closedAt = closedAt;
|
|
5136
|
+
record.processObservationStatus = "not_running";
|
|
5137
|
+
record.lastProcessObservationAt = closedAt;
|
|
5138
|
+
record.lastSuccessfulProcessObservationAt = closedAt;
|
|
5139
|
+
this.cleanupManagedRecord(record);
|
|
5140
|
+
await this.persist(record);
|
|
5141
|
+
} catch (error) {
|
|
5142
|
+
record.state = "failed";
|
|
5143
|
+
record.failedAt = Date.now();
|
|
5144
|
+
record.failureReason = `Failed to stop orphaned unreleased Studio launch: ${error instanceof Error ? error.message : String(error)}`;
|
|
5145
|
+
await this.persist(record);
|
|
5146
|
+
}
|
|
5147
|
+
}
|
|
4339
5148
|
}
|
|
4340
5149
|
observeRecord(record, snapshot) {
|
|
4341
5150
|
if (snapshot.status === "error") {
|
|
@@ -4372,9 +5181,16 @@ var init_studio_instance_manager = __esm({
|
|
|
4372
5181
|
return false;
|
|
4373
5182
|
}
|
|
4374
5183
|
cleanupManagedRecord(record) {
|
|
5184
|
+
if (record.recordId)
|
|
5185
|
+
this.launchControls.delete(record.recordId);
|
|
5186
|
+
if (record.recordId)
|
|
5187
|
+
this.clearLaunchCompletionTimer(record);
|
|
4375
5188
|
if (record.source !== "baseplate")
|
|
4376
5189
|
return;
|
|
4377
|
-
cleanupManagedBaseplateFiles({
|
|
5190
|
+
cleanupManagedBaseplateFiles({
|
|
5191
|
+
source: "baseplate",
|
|
5192
|
+
localPlaceFile: record.localPlaceFile
|
|
5193
|
+
});
|
|
4378
5194
|
}
|
|
4379
5195
|
markClosedInMemory(record) {
|
|
4380
5196
|
record.closedAt = record.closedAt ?? Date.now();
|
|
@@ -4382,10 +5198,38 @@ var init_studio_instance_manager = __esm({
|
|
|
4382
5198
|
this.managedByInstanceId.delete(record.instanceId);
|
|
4383
5199
|
this.pending.delete(record);
|
|
4384
5200
|
this.clearConnectionTimer(record);
|
|
5201
|
+
this.clearLaunchCompletionTimer(record);
|
|
5202
|
+
}
|
|
5203
|
+
armLaunchCompletionTimer(record) {
|
|
5204
|
+
if (!record.recordId)
|
|
5205
|
+
return;
|
|
5206
|
+
this.clearLaunchCompletionTimer(record);
|
|
5207
|
+
const timeout = setTimeout(() => {
|
|
5208
|
+
this.runInBackground("aborting an uncompleted Studio launch", this.markFailed(record, "Carbon did not complete Studio launch ownership transfer before timeout."));
|
|
5209
|
+
}, this.launchCompletionTimeoutMs);
|
|
5210
|
+
if (typeof timeout === "object" && "unref" in timeout)
|
|
5211
|
+
timeout.unref();
|
|
5212
|
+
this.launchCompletionTimers.set(record.recordId, timeout);
|
|
5213
|
+
}
|
|
5214
|
+
clearLaunchCompletionTimer(record) {
|
|
5215
|
+
if (!record.recordId)
|
|
5216
|
+
return;
|
|
5217
|
+
const timer = this.launchCompletionTimers.get(record.recordId);
|
|
5218
|
+
clearTimeout(timer);
|
|
5219
|
+
this.launchCompletionTimers.delete(record.recordId);
|
|
4385
5220
|
}
|
|
4386
5221
|
async markProcessExited(record, exitCode, reason) {
|
|
4387
5222
|
if (record.closedAt !== void 0)
|
|
4388
5223
|
return record;
|
|
5224
|
+
const control = record.recordId ? this.launchControls.get(record.recordId) : void 0;
|
|
5225
|
+
let controlFailure;
|
|
5226
|
+
if (record.processAuthorizationState !== "released" && control) {
|
|
5227
|
+
try {
|
|
5228
|
+
await control.abort();
|
|
5229
|
+
} catch (error) {
|
|
5230
|
+
controlFailure = error instanceof Error ? error.message : String(error);
|
|
5231
|
+
}
|
|
5232
|
+
}
|
|
4389
5233
|
const exitedAt = Date.now();
|
|
4390
5234
|
record.exitedAt = exitedAt;
|
|
4391
5235
|
record.closedAt = exitedAt;
|
|
@@ -4399,6 +5243,12 @@ var init_studio_instance_manager = __esm({
|
|
|
4399
5243
|
record.exitCode = exitCode;
|
|
4400
5244
|
if (reason)
|
|
4401
5245
|
record.failureReason = reason;
|
|
5246
|
+
if (controlFailure) {
|
|
5247
|
+
record.failureReason = [
|
|
5248
|
+
record.failureReason,
|
|
5249
|
+
`Launch ownership cleanup failed: ${controlFailure}`
|
|
5250
|
+
].filter(Boolean).join(" ");
|
|
5251
|
+
}
|
|
4402
5252
|
this.cleanupManagedRecord(record);
|
|
4403
5253
|
this.markClosedInMemory(record);
|
|
4404
5254
|
await this.persist(record);
|
|
@@ -4545,6 +5395,7 @@ var init_studio_instance_manager = __esm({
|
|
|
4545
5395
|
ownerPid: record.ownerPid,
|
|
4546
5396
|
bootId: record.bootId,
|
|
4547
5397
|
processObservationStatus: record.processObservationStatus,
|
|
5398
|
+
processAuthorizationState: record.processAuthorizationState,
|
|
4548
5399
|
lastProcessObservationAt: record.lastProcessObservationAt,
|
|
4549
5400
|
lastSuccessfulProcessObservationAt: record.lastSuccessfulProcessObservationAt,
|
|
4550
5401
|
lastProcessObservationError: record.lastProcessObservationError,
|
|
@@ -4568,7 +5419,7 @@ var init_studio_instance_manager = __esm({
|
|
|
4568
5419
|
placeVersion: record.placeVersion,
|
|
4569
5420
|
localPlaceFile: record.localPlaceFile,
|
|
4570
5421
|
launchedAt: record.launchedAt,
|
|
4571
|
-
connectionDeadlineAt: record.connectionDeadlineAt ?? (state === "launching" ? record.launchedAt + 12e4 : void 0),
|
|
5422
|
+
connectionDeadlineAt: record.connectionDeadlineAt ?? (state === "launching" && record.processAuthorizationState === void 0 ? record.launchedAt + 12e4 : void 0),
|
|
4572
5423
|
state,
|
|
4573
5424
|
connectedAt: record.attachedAt,
|
|
4574
5425
|
failedAt: record.failedAt,
|
|
@@ -4580,6 +5431,7 @@ var init_studio_instance_manager = __esm({
|
|
|
4580
5431
|
bootId: record.bootId,
|
|
4581
5432
|
deleteLocalPlaceFileOnClose: record.deleteLocalPlaceFileOnClose,
|
|
4582
5433
|
processObservationStatus: record.processObservationStatus,
|
|
5434
|
+
processAuthorizationState: record.processAuthorizationState ?? "released",
|
|
4583
5435
|
lastProcessObservationAt: record.lastProcessObservationAt,
|
|
4584
5436
|
lastSuccessfulProcessObservationAt: record.lastSuccessfulProcessObservationAt,
|
|
4585
5437
|
lastProcessObservationError: record.lastProcessObservationError,
|
|
@@ -7701,7 +8553,8 @@ var init_tools = __esm({
|
|
|
7701
8553
|
if (typeof state !== "object" || state === null || Array.isArray(state)) {
|
|
7702
8554
|
return state;
|
|
7703
8555
|
}
|
|
7704
|
-
const
|
|
8556
|
+
const rest = { ...state };
|
|
8557
|
+
delete rest.devices;
|
|
7705
8558
|
return rest;
|
|
7706
8559
|
}
|
|
7707
8560
|
_assertCanRestoreDeviceSimulatorState(state) {
|
|
@@ -8630,7 +9483,8 @@ ${code}`
|
|
|
8630
9483
|
};
|
|
8631
9484
|
entrySummaries.push(entrySummary);
|
|
8632
9485
|
try {
|
|
8633
|
-
const
|
|
9486
|
+
const settings = { ...entry };
|
|
9487
|
+
delete settings.label;
|
|
8634
9488
|
const applied = await this._executeDeviceSimulatorOperation(resolved.instanceId, resolved.role, "set", { settings });
|
|
8635
9489
|
entrySummary.applied = applied;
|
|
8636
9490
|
if (settleMs > 0)
|
|
@@ -9020,6 +9874,9 @@ ${code}`
|
|
|
9020
9874
|
managed: true,
|
|
9021
9875
|
state: record.state,
|
|
9022
9876
|
pid: record.nativeProcessId ?? record.spawnPid,
|
|
9877
|
+
process_started_at_file_time: record.nativeProcessStartedAt,
|
|
9878
|
+
process_authorized: record.processAuthorizationState !== "pending",
|
|
9879
|
+
process_ownership_released: record.processAuthorizationState === "released",
|
|
9023
9880
|
process_running: record.closedAt !== void 0 || record.exitedAt !== void 0 ? false : record.processObservationStatus === "running" ? true : record.processObservationStatus === "not_running" ? false : null,
|
|
9024
9881
|
process_observation_status: record.processObservationStatus ?? "unknown",
|
|
9025
9882
|
last_process_observation_at: record.lastProcessObservationAt ? new Date(record.lastProcessObservationAt).toISOString() : void 0,
|
|
@@ -9051,8 +9908,8 @@ ${code}`
|
|
|
9051
9908
|
if (instance_id && launch_id) {
|
|
9052
9909
|
throw new Error("manage_instance accepts only one of instance_id or launch_id.");
|
|
9053
9910
|
}
|
|
9054
|
-
if (action !== "launch" && action !== "close" && action !== "status" && action !== "list_place_versions") {
|
|
9055
|
-
throw new Error("manage_instance requires action=launch|close|status|list_place_versions");
|
|
9911
|
+
if (action !== "launch" && action !== "authorize" && action !== "complete" && action !== "close" && action !== "status" && action !== "list_place_versions") {
|
|
9912
|
+
throw new Error("manage_instance requires action=launch|authorize|complete|close|status|list_place_versions");
|
|
9056
9913
|
}
|
|
9057
9914
|
if (action === "list_place_versions") {
|
|
9058
9915
|
if (!this.openCloudClient.hasApiKey()) {
|
|
@@ -9077,9 +9934,21 @@ ${code}`
|
|
|
9077
9934
|
body.next_page_token = response.nextPageToken;
|
|
9078
9935
|
return this._textResult(body);
|
|
9079
9936
|
}
|
|
9080
|
-
if (action === "
|
|
9937
|
+
if (action === "close" || action === "status") {
|
|
9081
9938
|
await this.managedConnectionAssociations;
|
|
9082
9939
|
}
|
|
9940
|
+
if (action === "authorize") {
|
|
9941
|
+
if (!launch_id)
|
|
9942
|
+
throw new Error("manage_instance action=authorize requires launch_id.");
|
|
9943
|
+
const record2 = await this.instanceManager.authorizeByLaunchId(launch_id);
|
|
9944
|
+
return this._textResult(this._managedStatus(record2));
|
|
9945
|
+
}
|
|
9946
|
+
if (action === "complete") {
|
|
9947
|
+
if (!launch_id)
|
|
9948
|
+
throw new Error("manage_instance action=complete requires launch_id.");
|
|
9949
|
+
const record2 = await this.instanceManager.completeByLaunchId(launch_id);
|
|
9950
|
+
return this._textResult(this._managedStatus(record2));
|
|
9951
|
+
}
|
|
9083
9952
|
if (action === "status") {
|
|
9084
9953
|
if (launch_id) {
|
|
9085
9954
|
const record2 = await this.instanceManager.getByLaunchId(launch_id);
|
|
@@ -9219,7 +10088,11 @@ ${code}`
|
|
|
9219
10088
|
});
|
|
9220
10089
|
}
|
|
9221
10090
|
const universeId = launchSource === "published_place" || launchSource === "place_revision" ? await this._deriveUniverseId(placeId) : void 0;
|
|
9222
|
-
|
|
10091
|
+
if (request.require_process_identity !== void 0 && typeof request.require_process_identity !== "boolean") {
|
|
10092
|
+
throw new Error("require_process_identity must be a boolean when provided.");
|
|
10093
|
+
}
|
|
10094
|
+
const requireProcessIdentity = request.require_process_identity === true;
|
|
10095
|
+
const waitForConnection = !requireProcessIdentity && request.wait_for_connection !== false;
|
|
9223
10096
|
const timeoutMs = this._optionalPositiveInteger(request.timeout_ms, "timeout_ms") ?? 12e4;
|
|
9224
10097
|
const beforeKeys = new Set(this.bridge.getPublicInstances().map((instance) => this._publicInstanceKey(instance)));
|
|
9225
10098
|
const record = await this.instanceManager.launch({
|
|
@@ -9230,7 +10103,8 @@ ${code}`
|
|
|
9230
10103
|
placeVersion,
|
|
9231
10104
|
connectionTimeoutMs: timeoutMs,
|
|
9232
10105
|
studioExecutable,
|
|
9233
|
-
processEnvironment
|
|
10106
|
+
processEnvironment,
|
|
10107
|
+
...requireProcessIdentity ? { requireProcessIdentity: true } : {}
|
|
9234
10108
|
});
|
|
9235
10109
|
if (!waitForConnection) {
|
|
9236
10110
|
return this._textResult({
|
|
@@ -10527,11 +11401,15 @@ ${code}`
|
|
|
10527
11401
|
}
|
|
10528
11402
|
async uploadGenerateModelReferenceImage(imageContent, instance_id) {
|
|
10529
11403
|
if (this.cookieClient.hasCookie()) {
|
|
10530
|
-
const result2 = await this.cookieClient.
|
|
10531
|
-
|
|
10532
|
-
|
|
10533
|
-
|
|
10534
|
-
|
|
11404
|
+
const result2 = await this.cookieClient.uploadImage({
|
|
11405
|
+
fileContent: imageContent,
|
|
11406
|
+
fileName: "generate-model-reference.png",
|
|
11407
|
+
displayName: STUDIO_ASSISTANT_SOURCE_IMAGE_LABEL,
|
|
11408
|
+
description: STUDIO_ASSISTANT_SOURCE_IMAGE_LABEL,
|
|
11409
|
+
userId: process.env.ROBLOX_CREATOR_USER_ID,
|
|
11410
|
+
groupId: process.env.ROBLOX_CREATOR_GROUP_ID
|
|
11411
|
+
});
|
|
11412
|
+
return result2.assetId;
|
|
10535
11413
|
}
|
|
10536
11414
|
if (!this.openCloudClient.hasApiKey()) {
|
|
10537
11415
|
throw new Error("image_path and image_base64 require Roblox asset upload credentials because GenerateModelAsync only accepts rbxassetid:// or rbxasset:// image inputs. Set ROBLOX_OPEN_CLOUD_API_KEY plus ROBLOX_CREATOR_USER_ID or ROBLOX_CREATOR_GROUP_ID, or pass image_asset_id.");
|
|
@@ -10565,8 +11443,17 @@ ${code}`
|
|
|
10565
11443
|
}
|
|
10566
11444
|
const fileContent = fs3.readFileSync(filePath);
|
|
10567
11445
|
const fileName = path5.basename(filePath);
|
|
11446
|
+
const resolvedGroupId = groupId || process.env.ROBLOX_CREATOR_GROUP_ID;
|
|
11447
|
+
const resolvedUserId = userId || process.env.ROBLOX_CREATOR_USER_ID;
|
|
10568
11448
|
if (assetType === "Decal" && this.cookieClient.hasCookie()) {
|
|
10569
|
-
const result2 = await this.cookieClient.
|
|
11449
|
+
const result2 = await this.cookieClient.uploadImage({
|
|
11450
|
+
fileContent,
|
|
11451
|
+
fileName,
|
|
11452
|
+
displayName,
|
|
11453
|
+
description: description || "",
|
|
11454
|
+
userId: resolvedUserId,
|
|
11455
|
+
groupId: resolvedGroupId
|
|
11456
|
+
});
|
|
10570
11457
|
return {
|
|
10571
11458
|
content: [{
|
|
10572
11459
|
type: "text",
|
|
@@ -10575,9 +11462,9 @@ ${code}`
|
|
|
10575
11462
|
response: {
|
|
10576
11463
|
assetId: String(result2.assetId),
|
|
10577
11464
|
displayName,
|
|
10578
|
-
assetType,
|
|
10579
|
-
decalId:
|
|
10580
|
-
imageId: String(result2.
|
|
11465
|
+
assetType: "Image",
|
|
11466
|
+
decalId: null,
|
|
11467
|
+
imageId: String(result2.assetId)
|
|
10581
11468
|
}
|
|
10582
11469
|
})
|
|
10583
11470
|
}]
|
|
@@ -10587,8 +11474,6 @@ ${code}`
|
|
|
10587
11474
|
const cookieHint = assetType === "Decal" ? " Alternatively, set ROBLOSECURITY to use cookie auth." : "";
|
|
10588
11475
|
throw new Error(`No auth configured for ${assetType} upload. Set ROBLOX_OPEN_CLOUD_API_KEY (needs asset:write scope).${cookieHint}`);
|
|
10589
11476
|
}
|
|
10590
|
-
const resolvedGroupId = groupId || process.env.ROBLOX_CREATOR_GROUP_ID;
|
|
10591
|
-
const resolvedUserId = userId || process.env.ROBLOX_CREATOR_USER_ID;
|
|
10592
11477
|
if (!resolvedUserId && !resolvedGroupId) {
|
|
10593
11478
|
throw new Error("Creator identity required for Open Cloud upload. Set ROBLOX_CREATOR_USER_ID or ROBLOX_CREATOR_GROUP_ID, or pass userId/groupId as parameters.");
|
|
10594
11479
|
}
|
|
@@ -12253,14 +13138,14 @@ var init_definitions = __esm({
|
|
|
12253
13138
|
{
|
|
12254
13139
|
name: "manage_instance",
|
|
12255
13140
|
category: "write",
|
|
12256
|
-
description: 'Launch, close, inspect, and find revisions for Studio instances. Every launch returns launch_id, native pid, source, and lifecycle state; status and close accept launch_id before the plugin connects and instance_id after association. Use action="launch" with source="baseplate" for a blank place, or source="local_file" with local_place_file for a local place; neither uses place_id. Use action="list_place_versions" with place_id to retrieve version numbers through Open Cloud asset versions, then action="launch" with source="place_revision", place_id, and place_version to open an older revision. action="launch" source="published_place" opens the latest published place and is blocked if that place_id is already connected; source="place_revision" is allowed because Studio opens explicit past revisions as anonymous local copies. Requires ROBLOX_OPEN_CLOUD_API_KEY with asset:read for list_place_versions.',
|
|
13141
|
+
description: 'Launch, authorize, complete, close, inspect, and find revisions for Studio instances. Every launch returns launch_id, native pid, source, and lifecycle state; status and close accept launch_id before the plugin connects and instance_id after association. Use action="launch" with source="baseplate" for a blank place, or source="local_file" with local_place_file for a local place; neither uses place_id. A process-identity launch requires action="authorize" after injection is prepared, followed by action="complete" only after the injected runtime is independently attested. Use action="list_place_versions" with place_id to retrieve version numbers through Open Cloud asset versions, then action="launch" with source="place_revision", place_id, and place_version to open an older revision. action="launch" source="published_place" opens the latest published place and is blocked if that place_id is already connected; source="place_revision" is allowed because Studio opens explicit past revisions as anonymous local copies. Requires ROBLOX_OPEN_CLOUD_API_KEY with asset:read for list_place_versions.',
|
|
12257
13142
|
inputSchema: {
|
|
12258
13143
|
type: "object",
|
|
12259
13144
|
properties: {
|
|
12260
13145
|
action: {
|
|
12261
13146
|
type: "string",
|
|
12262
|
-
enum: ["launch", "close", "status", "list_place_versions"],
|
|
12263
|
-
description: "Instance management action."
|
|
13147
|
+
enum: ["launch", "authorize", "complete", "close", "status", "list_place_versions"],
|
|
13148
|
+
description: "Instance management action. authorize resumes a protocol-v3 launch after the caller has prepared process-scoped injection. complete releases broker process ownership after the caller independently attests that injection finished."
|
|
12264
13149
|
},
|
|
12265
13150
|
source: {
|
|
12266
13151
|
type: "string",
|
|
@@ -12279,13 +13164,17 @@ var init_definitions = __esm({
|
|
|
12279
13164
|
type: "number",
|
|
12280
13165
|
description: 'Required for source="place_revision". Use action="list_place_versions" to discover available version numbers.'
|
|
12281
13166
|
},
|
|
13167
|
+
require_process_identity: {
|
|
13168
|
+
type: "boolean",
|
|
13169
|
+
description: 'For action="launch": require an exact native PID and process creation time, return launch_id immediately, and retain broker ownership of the native process until action="complete" succeeds. The process remains suspended until action="authorize" begins injection. If identity capture, authorization, or ownership completion fails, the broker stops the launched process.'
|
|
13170
|
+
},
|
|
12282
13171
|
wait_for_connection: {
|
|
12283
13172
|
type: "boolean",
|
|
12284
|
-
description: 'For action="launch": wait until the MCP plugin connects and return instance_id (default true). false returns launch_id immediately and continues association/failure tracking asynchronously.'
|
|
13173
|
+
description: 'For action="launch": wait until the MCP plugin connects and return instance_id (default true). false returns launch_id immediately and continues association/failure tracking asynchronously. Ignored when require_process_identity=true, which always returns the suspended launch immediately.'
|
|
12285
13174
|
},
|
|
12286
13175
|
timeout_ms: {
|
|
12287
13176
|
type: "number",
|
|
12288
|
-
description: 'For action="launch": max milliseconds for plugin connection (default 120000). The deadline also applies asynchronously when wait_for_connection=false.'
|
|
13177
|
+
description: 'For action="launch": max milliseconds for plugin connection (default 120000). The deadline also applies asynchronously when wait_for_connection=false. It does not apply when require_process_identity=true; that protocol uses the broker ownership-completion lease through action="complete".'
|
|
12289
13178
|
},
|
|
12290
13179
|
studio_executable: {
|
|
12291
13180
|
type: "string",
|
|
@@ -13506,7 +14395,7 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
13506
14395
|
{
|
|
13507
14396
|
name: "upload_asset",
|
|
13508
14397
|
category: "write",
|
|
13509
|
-
description: "Upload any supported asset type to Roblox: Audio (mp3/ogg/wav/flac), Decal (png/jpg/bmp/tga), Model (fbx/gltf/glb/rbxm/rbxmx), Animation (rbxm/rbxmx), or Video (mp4/mov). Decal supports ROBLOSECURITY cookie auth or ROBLOX_OPEN_CLOUD_API_KEY. All other types require Open Cloud API key with asset:write scope + creator ID. Audio: max 7 min, 100 uploads/month (ID-verified). Video: max 5 min, requires 13+ ID-verified.",
|
|
14398
|
+
description: "Upload any supported asset type to Roblox: Audio (mp3/ogg/wav/flac), Decal (png/jpg/bmp/tga), Model (fbx/gltf/glb/rbxm/rbxmx), Animation (rbxm/rbxmx), or Video (mp4/mov). Decal supports ROBLOSECURITY cookie auth through the Asset Manager user-auth API and returns the direct Image asset ID, or ROBLOX_OPEN_CLOUD_API_KEY. All other types require Open Cloud API key with asset:write scope + creator ID. Audio: max 7 min, 100 uploads/month (ID-verified). Video: max 5 min, requires 13+ ID-verified.",
|
|
13510
14399
|
inputSchema: {
|
|
13511
14400
|
type: "object",
|
|
13512
14401
|
properties: {
|