Add Unity Pipeline package support

This commit is contained in:
ud18010
2026-07-31 17:34:04 +08:00
parent d1a526094e
commit 786b7ffff9
590 changed files with 51995 additions and 2 deletions
@@ -0,0 +1,42 @@
using System;
using Newtonsoft.Json;
namespace Unity.Pipeline.Models
{
/// <summary>
/// Canonical identity of an object an authoring command created or acted on, returned as the
/// command result so an agent can reference it in a follow-up call (via <see cref="ObjectRef"/>).
/// Asset objects carry assetPath/guid(/fileId); loaded/scene objects carry instanceId/hierarchyPath.
/// </summary>
[Serializable]
public class AuthoringResult
{
/// <summary>Canonical GlobalObjectId string for the object.</summary>
[JsonProperty("globalId")]
public string GlobalId { get; set; }
/// <summary>Project-relative asset path, if the object is an asset.</summary>
[JsonProperty("assetPath")]
public string AssetPath { get; set; }
/// <summary>Asset GUID, if the object is an asset.</summary>
[JsonProperty("guid")]
public string Guid { get; set; }
/// <summary>Local file id within the asset, for sub-assets.</summary>
[JsonProperty("fileId")]
public long? FileId { get; set; }
/// <summary>Instance id, if the object is loaded/in a scene.</summary>
[JsonProperty("instanceId")]
public ObjectId? InstanceId { get; set; }
/// <summary>Scene hierarchy path, for scene objects.</summary>
[JsonProperty("hierarchyPath")]
public string HierarchyPath { get; set; }
/// <summary>Object type name (e.g. "GameObject", "Material", "DefaultAsset").</summary>
[JsonProperty("type")]
public string Type { get; set; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6b92c58d435dcee4e8312f67b90af25c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,49 @@
using System;
using Newtonsoft.Json;
namespace Unity.Pipeline.Models
{
/// <summary>
/// Base class for all responses. Serve also as the base class for all errors.
/// </summary>
[Serializable]
public class BaseResponse
{
/// <summary>
/// Response message if no error
/// </summary>
[JsonProperty("message")]
public string Message { get; set; }
/// <summary>
/// Error message if the command failed.
/// </summary>
[JsonProperty("error")]
public string Error { get; set; }
/// <summary>
/// Additional error details for debugging.
/// </summary>
[JsonProperty("errorDetails")]
public string ErrorDetails { get; set; }
/// <summary>
/// When was the response created.
/// </summary>
[JsonProperty("executedAt")]
public DateTime ExecutedAt { get; set; }
/// <summary>
/// Create a failed execution response.
/// </summary>
public static BaseResponse Failure(string error, string errorDetails)
{
return new BaseResponse
{
ExecutedAt = DateTime.UtcNow,
Error = error,
ErrorDetails = errorDetails
};
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 00ec05951f34dac4494abb6eb6c90026
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,93 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Unity.Pipeline.Models
{
/// <summary>
/// Request model for /api/exec endpoint.
/// Contains command name and parameters for remote command execution.
/// </summary>
[Serializable]
public class CommandExecutionRequest
{
/// <summary>
/// Name of the command to execute.
/// Must match a registered [CliCommand] name.
/// </summary>
[JsonProperty("command", Required = Required.Always)]
public string Command { get; set; }
/// <summary>
/// Parameters for the command execution.
/// Contains key-value pairs matching command parameter names.
/// </summary>
[JsonProperty("parameters")]
public JObject Parameters { get; set; }
/// <summary>
/// Optional timeout for command execution in milliseconds.
/// Default: 60000 (60 seconds)
/// </summary>
[JsonProperty("timeout")]
public int? Timeout { get; set; }
/// <summary>
/// Create a new command execution request.
/// </summary>
public CommandExecutionRequest()
{
Parameters = new JObject();
}
/// <summary>
/// Create a command execution request with specified command and parameters.
/// </summary>
public CommandExecutionRequest(string command, JObject parameters = null)
{
Command = command ?? throw new ArgumentNullException(nameof(command));
Parameters = parameters ?? new JObject();
}
/// <summary>
/// Get a parameter value as the specified type.
/// Returns the default value if the parameter is not found or cannot be converted.
/// </summary>
public T GetParameter<T>(string name, T defaultValue = default(T))
{
if (Parameters == null || !Parameters.ContainsKey(name))
return defaultValue;
try
{
return Parameters[name].ToObject<T>();
}
catch
{
return defaultValue;
}
}
/// <summary>
/// Check if a parameter exists in the request.
/// </summary>
public bool HasParameter(string name)
{
return Parameters != null && Parameters.ContainsKey(name);
}
/// <summary>
/// Validate the request structure.
/// </summary>
public string Validate()
{
if (string.IsNullOrWhiteSpace(Command))
return "Command name is required";
if (Timeout.HasValue && Timeout.Value <= 0)
return "Timeout must be a positive number";
return null; // No validation errors
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cb08d2e5232819b43bbec0549c02a689
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,68 @@
using System;
using Newtonsoft.Json;
namespace Unity.Pipeline.Models
{
/// <summary>
/// Response model for /api/exec endpoint.
/// Contains execution result and metadata for remote command execution.
/// </summary>
[Serializable]
public class CommandExecutionResponse : BaseResponse
{
/// <summary>
/// Whether the command executed successfully.
/// </summary>
[JsonProperty("success")]
public bool Success { get; set; }
/// <summary>
/// Name of the command that was executed.
/// </summary>
[JsonProperty("command")]
public string Command { get; set; }
/// <summary>
/// Result returned by the command (if any).
/// </summary>
[JsonProperty("result")]
public object Result { get; set; }
/// <summary>
/// Execution duration in milliseconds.
/// </summary>
[JsonProperty("executionTimeMs")]
public long? ExecutionTimeMs { get; set; }
/// <summary>
/// Create a successful execution response.
/// </summary>
public static CommandExecutionResponse CmdSuccess(string command, object result = null, long? executionTimeMs = null)
{
return new CommandExecutionResponse
{
Success = true,
Command = command,
ExecutedAt = DateTime.UtcNow,
Result = result,
ExecutionTimeMs = executionTimeMs
};
}
/// <summary>
/// Create a failed execution response.
/// </summary>
public static CommandExecutionResponse CmdFailure(string cmd, string error, string errorDetails)
{
return new CommandExecutionResponse
{
Command = cmd,
Success = false,
ExecutedAt = DateTime.UtcNow,
Error = error,
ErrorDetails = errorDetails
};
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1911e492692621e4998635d7ddce0489
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,46 @@
using System;
using Newtonsoft.Json;
namespace Unity.Pipeline.Models
{
/// <summary>
/// A single captured Unity console entry, as returned by the <c>console</c> command.
/// </summary>
[Serializable]
public class ConsoleLogEntry
{
/// <summary>
/// Monotonic sequence number assigned when the entry was captured. Acts as the cursor for
/// incremental ("--follow") retrieval: pass the highest seq seen back as the <c>since</c>
/// parameter to get only newer entries. Never reused, and increasing for the life of the
/// buffer (including across domain reloads, since the buffer is persisted).
/// </summary>
[JsonProperty("seq")]
public long Seq { get; set; }
/// <summary>
/// When the entry was captured (UTC).
/// </summary>
[JsonProperty("timestampUtc")]
public DateTime TimestampUtc { get; set; }
/// <summary>
/// Normalized severity: "log", "warn", or "error". Unity's Error/Exception/Assert log types
/// all map to "error"; Warning maps to "warn"; Log maps to "log".
/// </summary>
[JsonProperty("level")]
public string Level { get; set; }
/// <summary>
/// The log message text.
/// </summary>
[JsonProperty("message")]
public string Message { get; set; }
/// <summary>
/// The stack trace associated with the entry, if any. Empty for most plain logs.
/// </summary>
[JsonProperty("stackTrace")]
public string StackTrace { get; set; }
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: bd1bd665815e523ee0da6504d78923df
@@ -0,0 +1,42 @@
using System;
using Newtonsoft.Json;
namespace Unity.Pipeline.Models
{
/// <summary>
/// Response model for the <c>console</c> command: a page of captured console entries plus
/// the cursor needed to fetch the next page.
/// </summary>
[Serializable]
public class ConsoleLogResponse
{
/// <summary>
/// The matching entries, oldest first, after applying the level filter, <c>since</c> cursor,
/// and <c>tail</c> limit.
/// </summary>
[JsonProperty("entries")]
public ConsoleLogEntry[] Entries { get; set; }
/// <summary>
/// The highest <see cref="ConsoleLogEntry.Seq"/> currently held in the buffer (regardless of
/// the level filter). A "--follow" client passes this back as <c>since</c> on the next poll
/// so it resumes exactly where it left off, even if the only new entries were filtered out.
/// </summary>
[JsonProperty("cursor")]
public long Cursor { get; set; }
/// <summary>
/// Number of entries returned in <see cref="Entries"/>.
/// </summary>
[JsonProperty("returned")]
public int Returned { get; set; }
/// <summary>
/// True when the requested <c>since</c> cursor pointed at entries that have already been
/// evicted from the bounded buffer, meaning the consumer missed some entries. Lets a
/// "--follow" client warn about a gap instead of silently skipping output.
/// </summary>
[JsonProperty("dropped")]
public bool Dropped { get; set; }
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3382b91cda66d80db6ab090f9fbeac16
@@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Unity.Pipeline.Models
{
/// <summary>
/// Response from code evaluation commands containing results, output, and diagnostics.
/// </summary>
[Serializable]
public class EvalResponse :CommandExecutionResponse
{
/// <summary>
/// Console output captured during execution.
/// </summary>
[JsonProperty("output")]
public string Output { get; set; }
/// <summary>
/// Compilation diagnostics (errors, warnings) from Roslyn.
/// </summary>
[JsonProperty("diagnostics")]
public List<DiagnosticInfo> Diagnostics { get; set; } = new List<DiagnosticInfo>();
/// <summary>
/// Create a successful evaluation response.
/// </summary>
public static EvalResponse EvalSuccess(object result, string output = null, long executionTimeMs = 0, List<DiagnosticInfo> diagnostics = null)
{
return new EvalResponse
{
Success = true,
Result = result,
Output = output,
ExecutionTimeMs = executionTimeMs,
Diagnostics = diagnostics ?? new List<DiagnosticInfo>()
};
}
/// <summary>
/// Create a failed evaluation response.
/// </summary>
public static EvalResponse EvalFailure(string error, string errorDetails = null, long executionTimeMs = 0, List<DiagnosticInfo> diagnostics = null)
{
return new EvalResponse
{
Success = false,
Error = error,
ErrorDetails = errorDetails,
ExecutionTimeMs = executionTimeMs,
Diagnostics = diagnostics ?? new List<DiagnosticInfo>()
};
}
}
/// <summary>
/// Compilation diagnostic information from Roslyn.
/// </summary>
[Serializable]
public class DiagnosticInfo
{
/// <summary>
/// Severity level: "error", "warning", "info".
/// </summary>
[JsonProperty("severity")]
public string Severity { get; set; }
/// <summary>
/// Diagnostic message text.
/// </summary>
[JsonProperty("message")]
public string Message { get; set; }
/// <summary>
/// Line number (0-based).
/// </summary>
[JsonProperty("line")]
public int Line { get; set; }
/// <summary>
/// Column number (0-based).
/// </summary>
[JsonProperty("column")]
public int Column { get; set; }
/// <summary>
/// Diagnostic identifier (e.g., "CS1002").
/// </summary>
[JsonProperty("id")]
public string Id { get; set; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7f93e07b346878148b61c36fd3ece752
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,203 @@
using System;
using System.IO;
using System.Diagnostics;
using Newtonsoft.Json;
using UnityEngine;
using Unity.Pipeline.Security;
namespace Unity.Pipeline.Models
{
/// <summary>
/// Manages instance descriptor files for CLI discovery.
/// Written to Library/Pipeline/.unity-pipeline-port (under the project's git-ignored Library
/// folder) for CLI tools to find running Editor instances.
/// </summary>
[Serializable]
public class InstanceDescriptor
{
private const string DescriptorFileName = ".unity-pipeline-port";
/// <summary>
/// Process ID of the Unity Editor
/// </summary>
[JsonProperty("pid")]
public int Pid { get; set; }
/// <summary>
/// HTTP port the pipeline server is listening on
/// </summary>
[JsonProperty("port")]
public int Port { get; set; }
/// <summary>
/// Full path to the Unity project
/// </summary>
[JsonProperty("projectPath")]
public string ProjectPath { get; set; }
/// <summary>
/// Name of the Unity project
/// </summary>
[JsonProperty("projectName")]
public string ProjectName { get; set; }
/// <summary>
/// Unity Editor version string
/// </summary>
[JsonProperty("unityVersion")]
public string UnityVersion { get; set; }
/// <summary>
/// Editor mode: "editor", "batchmode"
/// </summary>
[JsonProperty("mode")]
public string Mode { get; set; }
/// <summary>
/// When the Editor instance was started
/// </summary>
[JsonProperty("startedAt")]
public DateTime StartedAt { get; set; }
/// <summary>
/// Last heartbeat timestamp
/// </summary>
[JsonProperty("lastHeartbeat")]
public DateTime LastHeartbeat { get; set; }
/// <summary>
/// Security token for code evaluation commands
/// </summary>
[JsonProperty("evalToken")]
public string EvalToken { get; set; }
/// <summary>
/// Create instance descriptor for current Editor session
/// </summary>
public static InstanceDescriptor CreateCurrent(int port)
{
try
{
var projectPath = Path.GetDirectoryName(Application.dataPath);
var projectName = Path.GetFileName(projectPath);
var pid = Process.GetCurrentProcess().Id;
var unityVersion = Application.unityVersion;
var isBatchMode = Application.isBatchMode;
// Generate eval token at server startup for CLI auto-discovery
var evalToken = SecurityTokenManager.GetOrCreateToken();
return new InstanceDescriptor
{
Pid = pid,
Port = port,
ProjectPath = projectPath,
ProjectName = projectName,
UnityVersion = unityVersion,
Mode = isBatchMode ? "batchmode" : "editor",
StartedAt = DateTime.UtcNow,
LastHeartbeat = DateTime.UtcNow,
EvalToken = evalToken
};
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"CreateCurrent failed: {ex.Message}");
UnityEngine.Debug.LogError($"Stack trace: {ex.StackTrace}");
throw;
}
}
/// <summary>
/// Write instance descriptor to project root
/// </summary>
public static void WriteToProjectRoot(InstanceDescriptor descriptor)
{
try
{
var filePath = GetDescriptorFilePath(descriptor.ProjectPath);
var isNewFile = !File.Exists(filePath);
// The descriptor lives under Library/Pipeline, which may not exist yet.
Directory.CreateDirectory(Path.GetDirectoryName(filePath));
var json = JsonConvert.SerializeObject(descriptor, Formatting.Indented);
File.WriteAllText(filePath, json);
// The descriptor carries the auth token; keep it readable only by the current user.
// Applied once on creation (heartbeat rewrites preserve the existing permissions).
if (isNewFile)
FilePermissions.RestrictToCurrentUser(filePath);
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"WriteToProjectRoot failed: {ex.Message}");
UnityEngine.Debug.LogError($"Descriptor: {descriptor?.ProjectPath}, PID: {descriptor?.Pid}");
throw;
}
}
/// <summary>
/// Read instance descriptor from project root
/// </summary>
public static InstanceDescriptor ReadFromProjectRoot(string projectPath)
{
var filePath = GetDescriptorFilePath(projectPath);
if (!File.Exists(filePath))
return null;
try
{
var json = File.ReadAllText(filePath);
return JsonConvert.DeserializeObject<InstanceDescriptor>(json);
}
catch
{
// Invalid or corrupted file
return null;
}
}
/// <summary>
/// Remove instance descriptor from project root
/// </summary>
public static void RemoveFromProjectRoot(string projectPath)
{
var filePath = GetDescriptorFilePath(projectPath);
if (File.Exists(filePath))
{
try
{
File.Delete(filePath);
}
catch
{
// Ignore cleanup errors
}
}
}
/// <summary>
/// Update heartbeat timestamp in existing descriptor file
/// </summary>
public static void UpdateHeartbeat(string projectPath)
{
var descriptor = ReadFromProjectRoot(projectPath);
if (descriptor != null)
{
descriptor.LastHeartbeat = DateTime.UtcNow;
WriteToProjectRoot(descriptor);
}
}
/// <summary>
/// Absolute path to the editor descriptor file for a project: the git-ignored
/// Library/Pipeline folder under the project root. Public so discovery code and tests
/// derive the location from one place.
/// </summary>
public static string GetDescriptorFilePath(string projectPath)
{
return Path.Combine(projectPath, "Library", "Pipeline", DescriptorFileName);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c19a2cde67d600341b17d8056b8393e2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,57 @@
using System;
using Newtonsoft.Json;
namespace Unity.Pipeline.Models
{
/// <summary>
/// Agent-supplied handle that references an existing Unity object — an asset or a
/// loaded/scene object. Supply ONE form; <see cref="Unity.Pipeline.Editor.Authoring.ObjectResolver"/>
/// tries them in order: globalId, path, guid (+ optional fileId), instanceId, hierarchyPath.
/// </summary>
[Serializable]
[JsonConverter(typeof(ObjectRefConverter))]
public class ObjectRef
{
/// <summary>Canonical GlobalObjectId string (addresses assets and scene objects uniformly).</summary>
[JsonProperty("globalId")]
public string GlobalId { get; set; }
/// <summary>Project-relative asset path, e.g. "Assets/Foo/Bar.prefab".</summary>
[JsonProperty("path")]
public string Path { get; set; }
/// <summary>Asset GUID.</summary>
[JsonProperty("guid")]
public string Guid { get; set; }
/// <summary>Local file id, used with <see cref="Guid"/> to address a sub-asset.</summary>
[JsonProperty("fileId")]
public long? FileId { get; set; }
/// <summary>Instance id of a loaded/scene object.</summary>
[JsonProperty("instanceId")]
public ObjectId? InstanceId { get; set; }
/// <summary>Scene hierarchy path, e.g. "/Root/Child/Leaf".</summary>
[JsonProperty("hierarchyPath")]
public string HierarchyPath { get; set; }
[JsonIgnore]
public bool IsEmpty =>
string.IsNullOrEmpty(GlobalId) &&
string.IsNullOrEmpty(Path) &&
string.IsNullOrEmpty(Guid) &&
InstanceId == null &&
string.IsNullOrEmpty(HierarchyPath);
public override string ToString()
{
if (!string.IsNullOrEmpty(GlobalId)) return GlobalId;
if (!string.IsNullOrEmpty(Path)) return Path;
if (!string.IsNullOrEmpty(Guid)) return FileId.HasValue ? $"guid:{Guid}:{FileId}" : $"guid:{Guid}";
if (InstanceId.HasValue) return $"instanceId:{InstanceId}";
if (!string.IsNullOrEmpty(HierarchyPath)) return HierarchyPath;
return "<empty>";
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e970b2bf3a890d743bea6785f8af3765
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,188 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Unity.Pipeline.Models
{
/// <summary>
/// JSON converter for <see cref="ObjectRef"/> that accepts BOTH a structured object
/// (all six fields via their JSON property names) AND a single canonical string handle.
///
/// The MCP tool schemas intentionally expose object-reference parameters as plain strings, so an
/// agent typically passes e.g. <c>"target": "/Player"</c> or <c>"target": "GlobalObjectId_V1-..."</c>.
/// Without this converter, Newtonsoft's <c>ToObject&lt;ObjectRef&gt;()</c> (used by
/// <c>BasePipelineServer.ExtractCommandParameters</c>) gets a JValue(string), returns null, and the
/// command fails Required-parameter validation. This converter coerces the string into the matching
/// field, mirroring the priority order of <c>ObjectResolver.TryResolve</c> and <c>ObjectRef.ToString()</c>.
///
/// Registered on the class itself via <c>[JsonConverter(typeof(ObjectRefConverter))]</c> so it applies
/// everywhere an <see cref="ObjectRef"/> is (de)serialized — no server or command changes required.
/// </summary>
public class ObjectRefConverter : JsonConverter<ObjectRef>
{
private static readonly Regex s_GuidWithFileId = new Regex(@"^guid:([0-9a-fA-F]+):([0-9]+)$", RegexOptions.Compiled);
private static readonly Regex s_GuidOnly = new Regex(@"^guid:([0-9a-fA-F]+)$", RegexOptions.Compiled);
private static readonly Regex s_InstanceId = new Regex(@"^instanceId:(-?[0-9]+)$", RegexOptions.Compiled);
private static readonly Regex s_PlainInt = new Regex(@"^-?[0-9]+$", RegexOptions.Compiled);
private static readonly Regex s_Hex32 = new Regex(@"^[0-9a-fA-F]{32}$", RegexOptions.Compiled);
public override ObjectRef ReadJson(JsonReader reader, Type objectType, ObjectRef existingValue,
bool hasExistingValue, JsonSerializer serializer)
{
var token = JToken.Load(reader);
switch (token.Type)
{
case JTokenType.Null:
return null;
case JTokenType.String:
return FromString((string)token);
case JTokenType.Integer:
// A bare JSON number is treated as an instance id (e.g. "target": 48184).
return new ObjectRef { InstanceId = token.ToObject<ObjectId>() };
case JTokenType.Object:
return FromObject((JObject)token);
default:
return null;
}
}
public override void WriteJson(JsonWriter writer, ObjectRef value, JsonSerializer serializer)
{
if (value == null)
{
writer.WriteNull();
return;
}
// Write a standard JSON object (matching the [JsonProperty] names on ObjectRef). Done
// manually rather than via serializer.Serialize to avoid re-entering this converter.
writer.WriteStartObject();
writer.WritePropertyName("globalId");
writer.WriteValue(value.GlobalId);
writer.WritePropertyName("path");
writer.WriteValue(value.Path);
writer.WritePropertyName("guid");
writer.WriteValue(value.Guid);
writer.WritePropertyName("fileId");
if (value.FileId.HasValue) writer.WriteValue(value.FileId.Value); else writer.WriteNull();
writer.WritePropertyName("instanceId");
// Route the id through ObjectIdConverter so it serializes as its raw numeric form
// (the 64-bit EntityId on 6000.4+, the int below). Not this converter — no recursion.
if (value.InstanceId.HasValue) serializer.Serialize(writer, value.InstanceId.Value); else writer.WriteNull();
writer.WritePropertyName("hierarchyPath");
writer.WriteValue(value.HierarchyPath);
writer.WriteEndObject();
}
/// <summary>
/// Map a structured JSON object to an ObjectRef by reading each field directly (rather than
/// <c>ToObject&lt;ObjectRef&gt;()</c>, which would recurse back into this converter).
///
/// Keys are matched case-INSENSITIVELY: agents commonly vary the casing (e.g. "instanceID" for
/// instanceId, "fileID" for fileId), and because this custom converter bypasses Newtonsoft's
/// default case-insensitive property binding, an exact-case lookup would otherwise drop the
/// field and yield an empty (silently unresolved) handle.
/// </summary>
private static ObjectRef FromObject(JObject jo)
{
return new ObjectRef
{
GlobalId = Str(jo, "globalId"),
Path = Str(jo, "path"),
Guid = Str(jo, "guid"),
FileId = Long(jo, "fileId"),
InstanceId = ObjId(jo, "instanceId"),
HierarchyPath = Str(jo, "hierarchyPath"),
};
}
/// <summary>Find a property value by name, ignoring case (Newtonsoft's JObject lookup is case-sensitive).</summary>
private static JToken Get(JObject jo, string key)
{
foreach (var p in jo.Properties())
{
if (string.Equals(p.Name, key, StringComparison.OrdinalIgnoreCase))
return p.Value;
}
return null;
}
private static string Str(JObject jo, string key)
{
var t = Get(jo, key);
return t == null || t.Type == JTokenType.Null ? null : t.Value<string>();
}
private static ObjectId? ObjId(JObject jo, string key)
{
var t = Get(jo, key);
return t == null || t.Type == JTokenType.Null ? (ObjectId?)null : t.ToObject<ObjectId>();
}
private static long? Long(JObject jo, string key)
{
var t = Get(jo, key);
return t == null || t.Type == JTokenType.Null ? (long?)null : t.Value<long?>();
}
/// <summary>
/// Parse a single canonical string handle into the matching field. Priority order matches
/// <c>ObjectResolver.TryResolve</c> / <c>ObjectRef.ToString()</c>; a bare name falls back to
/// the hierarchy path (the most common agent input).
/// </summary>
internal static ObjectRef FromString(string s)
{
if (string.IsNullOrWhiteSpace(s))
return null;
s = s.Trim();
if (s.StartsWith("GlobalObjectId_V1", StringComparison.Ordinal))
return new ObjectRef { GlobalId = s };
// "Assets/..." and "Packages/..." (and the bare roots) are asset paths that
// AssetDatabase.LoadMainAssetAtPath can resolve; everything else falls through to the
// hierarchy-path fallback at the end.
if (s.StartsWith("Assets/", StringComparison.Ordinal)
|| s.StartsWith("Packages/", StringComparison.Ordinal)
|| s == "Assets" || s == "Packages")
return new ObjectRef { Path = s };
var guidFile = s_GuidWithFileId.Match(s);
if (guidFile.Success && long.TryParse(guidFile.Groups[2].Value, out var fileId))
return new ObjectRef { Guid = guidFile.Groups[1].Value, FileId = fileId };
var guidOnly = s_GuidOnly.Match(s);
if (guidOnly.Success)
return new ObjectRef { Guid = guidOnly.Groups[1].Value };
var inst = s_InstanceId.Match(s);
if (inst.Success && ObjectId.TryParse(inst.Groups[1].Value, out var prefixedId))
return new ObjectRef { InstanceId = prefixedId };
// A plain integer is an instance id. On 6000.0-6000.3 negative ids are valid (unsaved scene
// objects); on 6000.4+ the id is the unsigned 64-bit EntityId, so a leading '-' won't parse
// and falls through to the hierarchy-path handling below.
if (s_PlainInt.IsMatch(s) && ObjectId.TryParse(s, out var plainId))
return new ObjectRef { InstanceId = plainId };
// Exactly 32 hex chars with no other prefix is a bare asset GUID.
if (s_Hex32.IsMatch(s))
return new ObjectRef { Guid = s };
// A relative string with a file extension (e.g. "Materials/Floor.mat", "Enemy.prefab") is
// most likely an authoring-root-relative asset path — route it to Path so the resolver can
// normalize it under the authoring root. A leading "/" always means a scene hierarchy path,
// and an extension-less string ("Player", "Root/Child") stays one. Dotted GameObject names
// ("Cube.001") land here too, but the resolver falls back to a hierarchy lookup for those.
if (!s.StartsWith("/", StringComparison.Ordinal) && Path.HasExtension(s))
return new ObjectRef { Path = s };
// Leading-"/" canonical paths and bare names resolve as a scene hierarchy path.
return new ObjectRef { HierarchyPath = s };
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e31382a242c2a084e97ba76716798aa3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,203 @@
using System;
using Newtonsoft.Json;
using UnityEngine;
using Unity.Pipeline.Config;
using System.IO;
using Unity.Pipeline.Security;
namespace Unity.Pipeline.Models
{
/// <summary>
/// Instance descriptor for runtime Unity Player builds with Pipeline server.
/// Different from Editor InstanceDescriptor to reflect runtime context and security.
/// </summary>
[Serializable]
public class RuntimeInstanceDescriptor
{
private const string RuntimeDescriptorFileName = ".unity-pipeline-runtime-port";
/// <summary>
/// Process ID of the Unity Player application
/// </summary>
[JsonProperty("pid")]
public int Pid { get; set; }
/// <summary>
/// HTTP port the pipeline server is listening on (7900-7999 range)
/// </summary>
[JsonProperty("port")]
public int Port { get; set; }
/// <summary>
/// Unity runtime platform (Windows, macOS, Linux, etc.)
/// </summary>
[JsonProperty("platform")]
public string Platform { get; set; }
/// <summary>
/// Unity Player version string
/// </summary>
[JsonProperty("unityVersion")]
public string UnityVersion { get; set; }
/// <summary>
/// Unique build identifier
/// </summary>
[JsonProperty("buildGuid")]
public string BuildGuid { get; set; }
/// <summary>
/// When the runtime instance was started
/// </summary>
[JsonProperty("startedAt")]
public DateTime StartedAt { get; set; }
/// <summary>
/// Last heartbeat timestamp
/// </summary>
[JsonProperty("lastHeartbeat")]
public DateTime LastHeartbeat { get; set; }
/// <summary>
/// Working directory where the application is running
/// </summary>
[JsonProperty("workingDirectory")]
public string WorkingDirectory { get; set; }
/// <summary>
/// Security token used to authorize requests to this runtime server.
/// </summary>
[JsonProperty("evalToken")]
public string EvalToken { get; set; }
/// <summary>
/// Create runtime instance descriptor for current Player application.
/// </summary>
public static RuntimeInstanceDescriptor CreateCurrent(int port, RuntimePipelineConfig config)
{
try
{
var pid = System.Diagnostics.Process.GetCurrentProcess().Id;
var unityVersion = Application.unityVersion;
var platform = Application.platform.ToString();
var buildGuid = Application.buildGUID;
var workingDir = Directory.GetCurrentDirectory();
var token = SecurityTokenManager.GetOrCreateToken();
return new RuntimeInstanceDescriptor
{
Pid = pid,
Port = port,
Platform = platform,
UnityVersion = unityVersion,
BuildGuid = buildGuid,
StartedAt = DateTime.UtcNow,
LastHeartbeat = DateTime.UtcNow,
WorkingDirectory = workingDir,
EvalToken = token
};
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"Pipeline: CreateCurrent failed: {ex.Message}");
throw;
}
}
/// <summary>
/// Write runtime instance descriptor to working directory.
/// </summary>
public static void WriteToWorkingDirectory(RuntimeInstanceDescriptor descriptor)
{
try
{
var filePath = GetDescriptorFilePath();
var isNewFile = !File.Exists(filePath);
var json = JsonConvert.SerializeObject(descriptor, Formatting.Indented);
File.WriteAllText(filePath, json);
// The descriptor carries the auth token; keep it readable only by the current user.
// Applied once on creation (heartbeat rewrites preserve the existing permissions).
if (isNewFile)
FilePermissions.RestrictToCurrentUser(filePath);
System.Console.WriteLine($"Pipeline: Runtime descriptor written to {filePath}");
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"Pipeline: Failed to write runtime descriptor: {ex.Message}");
throw;
}
}
/// <summary>
/// Read runtime instance descriptor from working directory.
/// </summary>
public static RuntimeInstanceDescriptor ReadFromWorkingDirectory()
{
var filePath = GetDescriptorFilePath();
if (!File.Exists(filePath))
return null;
try
{
var json = File.ReadAllText(filePath);
return JsonConvert.DeserializeObject<RuntimeInstanceDescriptor>(json);
}
catch (Exception ex)
{
UnityEngine.Debug.LogWarning($"Pipeline: Failed to read runtime descriptor: {ex.Message}");
return null;
}
}
/// <summary>
/// Remove runtime instance descriptor from working directory.
/// </summary>
public static void RemoveFromWorkingDirectory()
{
var filePath = GetDescriptorFilePath();
if (File.Exists(filePath))
{
try
{
File.Delete(filePath);
}
catch (Exception ex)
{
UnityEngine.Debug.LogWarning($"Pipeline: Failed to remove runtime descriptor: {ex.Message}");
}
}
}
/// <summary>
/// Update heartbeat timestamp in existing descriptor file.
/// </summary>
public static void UpdateHeartbeat()
{
try
{
var descriptor = ReadFromWorkingDirectory();
if (descriptor != null)
{
descriptor.LastHeartbeat = DateTime.UtcNow;
WriteToWorkingDirectory(descriptor);
}
}
catch (Exception ex)
{
UnityEngine.Debug.LogWarning($"Pipeline: Failed to update runtime heartbeat: {ex.Message}");
}
}
private static string GetDescriptorFilePath()
{
var workingDir = new FileInfo($"{Application.dataPath}/..").FullName;
return Path.Combine(workingDir, RuntimeDescriptorFileName);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a7787403e02096545b7798cd3d5d076d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,55 @@
using System;
using Newtonsoft.Json;
namespace Unity.Pipeline.Models
{
/// <summary>
/// Response model for /api/status endpoint.
/// Provides Editor state information for CLI tools and automation scripts.
/// </summary>
[Serializable]
public class StatusResponse
{
/// <summary>
/// Current Editor status: "ready", "compiling", "busy"
/// </summary>
[JsonProperty("status")]
public string Status { get; set; }
/// <summary>
/// Whether Unity is currently compiling scripts
/// </summary>
[JsonProperty("compiling")]
public bool Compiling { get; set; }
/// <summary>
/// Whether a domain reload is in progress
/// </summary>
[JsonProperty("domainReloadInProgress")]
public bool DomainReloadInProgress { get; set; }
/// <summary>
/// Current play mode state: "stopped", "playing", "paused"
/// </summary>
[JsonProperty("playMode")]
public string PlayMode { get; set; }
/// <summary>
/// Timestamp of last heartbeat update
/// </summary>
[JsonProperty("lastHeartbeat")]
public DateTime LastHeartbeat { get; set; }
/// <summary>
/// Full path to the Unity project
/// </summary>
[JsonProperty("projectPath")]
public string ProjectPath { get; set; }
/// <summary>
/// Unity Editor version string
/// </summary>
[JsonProperty("unityVersion")]
public string UnityVersion { get; set; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 86f7bba2bb7101544a486545cb8e2f70
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using Unity.Pipeline.Models;
namespace Unity.Pipeline
{
/// <summary>
/// Response for test execution commands, containing summary and detailed results
/// </summary>
[Serializable]
public class TestExecutionResponse : CommandExecutionResponse
{
public TestSummary Summary { get; set; }
public List<TestResult> Results { get; set; }
public double Duration { get; set; }
public string StatusPath { get; set; } // For async mode
public string Mode { get; set; } // EditMode, PlayMode, or All
public string FilterApplied { get; set; } // What filter was used, if any
public TestExecutionResponse()
{
Results = new List<TestResult>();
Summary = new TestSummary();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 27c96238399817b4c8ffb66925bcbc6f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,17 @@
using System;
namespace Unity.Pipeline
{
/// <summary>
/// Individual test result information
/// </summary>
[Serializable]
public class TestResult
{
public string FullName { get; set; }
public string Status { get; set; } // Passed, Failed, Skipped, Inconclusive
public double Duration { get; set; }
public string Message { get; set; }
public string StackTrace { get; set; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7ccc266e8f58db84dab523cd4dabc489
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,26 @@
using System;
namespace Unity.Pipeline
{
/// <summary>
/// Summary statistics for test execution
/// </summary>
[Serializable]
public class TestSummary
{
public int Total { get; set; }
public int Passed { get; set; }
public int Failed { get; set; }
public int Skipped { get; set; }
public int Inconclusive { get; set; }
public TestSummary()
{
Total = 0;
Passed = 0;
Failed = 0;
Skipped = 0;
Inconclusive = 0;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: df11040e7d969ff4fa8aaa417c44bd44
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: