@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 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: 1,
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 uploadDecal(fileContent, name, description) {
3027
+ async uploadImage(options) {
3028
3028
  if (!this.cookie) {
3029
3029
  throw new Error("ROBLOSECURITY cookie is not set.");
3030
3030
  }
3031
- const encodedName = encodeURIComponent(name);
3032
- const encodedDesc = encodeURIComponent(description);
3033
- const url = `https://data.roblox.com/data/upload/json?assetTypeId=13&name=${encodedName}&description=${encodedDesc}`;
3034
- const response = await this.fetchWithCsrf(url, {
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
- headers: {
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(`Decal upload failed (${response.status}): ${body}`);
3056
+ throw new Error(`Failed to resolve authenticated Roblox user (${response.status}): ${body}`);
3046
3057
  }
3047
- const result = await response.json();
3048
- if (!result.Success || !result.AssetId) {
3049
- throw new Error(`Decal upload failed: ${result.Message || "Unknown error"}`);
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
- assetId: result.AssetId,
3053
- backingAssetId: result.BackingAssetId || 0
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
- "$psi = New-Object System.Diagnostics.ProcessStartInfo",
3645
- `$psi.FileName = ${powershellStringLiteral(windowsExe)}`,
3646
- `$psi.Arguments = ${powershellStringLiteral(commandLine)}`,
3647
- // With UseShellExecute=false, Studio inherits the synchronous
3648
- // powershell.exe invocation's stdout/stderr pipe handles under WSL. Those
3649
- // handles keep the PowerShell invocation waiting until Studio exits even though
3650
- // PowerShell already printed the PID. Shell execution prevents that
3651
- // inheritance while Process.Start still returns the native Studio PID.
3652
- "$psi.UseShellExecute = $true",
3653
- "$studio = [System.Diagnostics.Process]::Start($psi)",
3654
- 'if ($null -eq $studio) { throw "Roblox Studio process did not start." }'
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 spawnWindowsStudioFromWsl(exe, args, processEnvironment) {
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 output = await powershellAsync(`${script}; [PSCustomObject]@{ pid = $studio.Id; started = $studio.StartTime.ToUniversalTime().ToFileTimeUtc().ToString() } | ConvertTo-Json -Compress`);
3661
- const parsed = JSON.parse(output);
3662
- const nativePid = Number(parsed.pid);
3663
- const nativeStartedAt = typeof parsed.started === "string" && /^\d+$/u.test(parsed.started) ? parsed.started : void 0;
3664
- if (!Number.isSafeInteger(nativePid) || nativePid <= 0) {
3665
- throw new Error(`Could not determine the Windows Studio process id from: ${nativePid}`);
3666
- }
3667
- return {
3668
- pid: nativePid,
3669
- nativePid,
3670
- nativeStartedAt,
3671
- unref: () => {
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 !== "launching")
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.state = "failed";
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 spawnWindowsStudioFromWsl(exe, args, processEnvironment);
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
- if (processId) {
4106
- try {
4107
- await this.closeProcess(processId);
4108
- } catch (caught) {
4109
- stopError = caught;
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 { status: "already_closed", launchId: record.recordId, instanceId: record.instanceId };
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 { status: "already_closed", launchId: record.recordId, instanceId: record.instanceId };
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 { status: "already_closed", launchId: record.recordId, instanceId: record.instanceId };
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 { status: "closed", launchId: record.recordId, instanceId: record.instanceId };
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
- await powershellAsync(`Stop-Process -Id ${Math.trunc(processId)} -Force -ErrorAction Stop`);
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) => this.cleanupManagedRecord(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
- await this.registry.sweep(await this.registrySweepOptions(snapshot));
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({ source: "baseplate", localPlaceFile: record.localPlaceFile });
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 { devices: _devices, ...rest } = state;
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 { label: _label, ...settings } = entry;
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 === "status" || action === "close") {
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
- const waitForConnection = request.wait_for_connection !== false;
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.uploadDecal(imageContent, STUDIO_ASSISTANT_SOURCE_IMAGE_LABEL, STUDIO_ASSISTANT_SOURCE_IMAGE_LABEL);
10531
- if (result2.backingAssetId && result2.backingAssetId > 0) {
10532
- return result2.backingAssetId;
10533
- }
10534
- return this.resolveUploadedReferenceImageId(String(result2.assetId), instance_id);
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.uploadDecal(fileContent, displayName, description || "");
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: String(result2.assetId),
10580
- imageId: String(result2.backingAssetId)
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: {