Initial commit: AIGame Unity client with URP, HybridCLR, YooAsset and tooling.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
udx
2026-09-21 14:21:07 +08:00
co-authored by Cursor
commit f3f66ab063
931 changed files with 112366 additions and 0 deletions
@@ -0,0 +1,101 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace Unity.Pipeline
{
/// <summary>
/// One machine-readable argument-binding defect, reported in <c>/api/exec</c>'s
/// <c>argProblems</c> array.
///
/// The vocabulary matches the <c>unity</c> CLI's own <c>ArgProblemKind</c> one-for-one, plus
/// <see cref="ArgProblemKind.TypeMismatch"/>, which needs the declared CLR type and so can
/// only come from the server. Field names mirror the CLI's record exactly, so neither side
/// needs a translation table.
///
/// Deliberately machine-readable rather than pre-rendered English: the CLI renders AND
/// LOCALIZES these itself across 10 locales. A server sending prose would make
/// <c>unity command</c> the one surface that answers in English while the rest of the CLI
/// localizes. The envelope's <c>errorDetails</c> carries the English fallback for curl, MCP,
/// and for kinds a client has never heard of.
/// </summary>
public class ArgProblem
{
/// <summary>The kind of issue with provided argument.</summary>
[JsonProperty("kind")]
[JsonConverter(typeof(StringEnumConverter), true)]
public ArgProblemKind Kind { get; set; }
/// <summary>The offending raw token, where the defect is about a token.</summary>
[JsonProperty("token", NullValueHandling = NullValueHandling.Ignore)]
public string Token { get; set; }
/// <summary>The parameter name involved, where the defect is about a name.</summary>
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
public string Name { get; set; }
/// <summary>A did-you-mean candidate for <see cref="ArgProblemKind.UnknownName"/>.</summary>
[JsonProperty("suggestion", NullValueHandling = NullValueHandling.Ignore)]
public string Suggestion { get; set; }
/// <summary>Declared positional slots, for <see cref="ArgProblemKind.ExcessPositional"/>.</summary>
[JsonProperty("capacity", NullValueHandling = NullValueHandling.Ignore)]
public int? Capacity { get; set; }
/// <summary>Positionals supplied, for <see cref="ArgProblemKind.ExcessPositional"/>.</summary>
[JsonProperty("given", NullValueHandling = NullValueHandling.Ignore)]
public int? Given { get; set; }
/// <summary>
/// The declared CLR type name, for <see cref="ArgProblemKind.TypeMismatch"/>. Sent as
/// <c>Int32</c>-style short names, matching what the catalog's schema already reports.
/// </summary>
[JsonProperty("expectedType", NullValueHandling = NullValueHandling.Ignore)]
public string ExpectedType { get; set; }
}
/// <summary>
/// Kinds of argument-binding defect. Serialized camelCase (<c>unknownName</c>) to match the
/// CLI's wire expectations; the C# spelling stays PascalCase per package convention.
/// </summary>
public enum ArgProblemKind
{
// Shape — schema-independent.
/// <summary>
/// A flag with no name: <c>--=</c> or <c>--=value</c>.
///
/// Not a bare <c>--</c>: the first one is the end-of-flags separator and is consumed
/// before any name parsing, and every later one is a positional.
/// </summary>
EmptyName,
/// <summary><c>--key=</c> with nothing after the <c>=</c>.</summary>
EmptyValue,
/// <summary>A <c>-x</c> token in positional position (never a flag's value).</summary>
SingleDash,
/// <summary>The same <c>--key</c> supplied more than once.</summary>
Duplicate,
// Names — requires the declared-parameter array.
/// <summary><c>--nope</c>, or <c>--group-by</c> when the command declares <c>group_by</c>.</summary>
UnknownName,
/// <summary>A bare <c>key=value</c> positional whose left side names a real parameter.</summary>
BareAssignment,
/// <summary>More positionals than the command has parameter slots.</summary>
ExcessPositional,
/// <summary>A positional whose slot was already filled by an explicit flag.</summary>
PositionalConflict,
/// <summary>
/// A token the declared parameter type cannot accept. Detectable only by the server,
/// which has the real <see cref="System.Type"/>.
/// </summary>
TypeMismatch,
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9735aca395c65e64ea689c065eee20c3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -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,68 @@
using System;
using System.Collections.Generic;
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>
/// Machine-actionable warnings for the caller: things the server dropped, coerced, or
/// wants the agent to correct (e.g. an unrecognized option value), without failing the
/// request. Null when there is nothing to say, and omitted from the JSON in EVERY mode
/// (an empty warnings array carries no information — attribute-level gating, so this
/// holds on all endpoints and in verbose serialization too). (AUTHAPI-21)
/// </summary>
[JsonProperty("warnings", NullValueHandling = NullValueHandling.Ignore)]
public List<string> Warnings { 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>
/// <remarks>
/// On the /api/exec wire this is emitted only in verbose mode — the lean/verbose gating lives
/// in <c>ExecResponseSerializer</c>, NOT here, so every other serialization of this type
/// (status endpoints, logs, tests) keeps the field. (AUTHAPI-21)
/// </remarks>
[JsonProperty("executedAt")]
public DateTime ExecutedAt { get; set; }
/// <summary>
/// Create a failed execution response.
/// </summary>
/// <param name="error">Error message.</param>
/// <param name="errorDetails">Additional error details for debugging.</param>
/// <returns>A response with <see cref="Error"/>/<see cref="ErrorDetails"/> set.</returns>
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,199 @@
using System;
using System.Collections.Generic;
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]
class CommandExecutionRequest
{
/// <summary>
/// Name of the command to execute.
/// Must match a registered [CliCommand] name.
///
/// Optional, because <see cref="Argv"/> and <see cref="CommandLine"/> carry the name in
/// their first token instead; the server assigns it here before anything downstream reads
/// it. Exactly one of the three forms may be present - <see cref="Validate"/> enforces
/// that.
/// </summary>
[JsonProperty("command")]
public string Command { get; set; }
/// <summary>
/// An unparsed command line: the server tokenizes it (see
/// <see cref="CommandLineTokenizer"/>) and then binds it. For clients that genuinely
/// hold raw text — MCP, a chat box, a pasted line. Mutually exclusive with
/// <see cref="Command"/> and <see cref="Argv"/>.
/// </summary>
[JsonProperty("commandLine")]
public string CommandLine { get; set; }
/// <summary>
/// Pre-split tokens, first of which is the command name. Preferred over
/// <see cref="CommandLine"/> by any caller that already has argv — on POSIX a CLI never
/// holds the original command STRING, so making it re-quote tokens for the server to
/// re-split would be a lossy round-trip across two independently-versioned quoting
/// dialects. Mutually exclusive with <see cref="Command"/> and <see cref="CommandLine"/>.
/// </summary>
[JsonProperty("argv")]
public List<string> Argv { get; set; }
/// <summary>
/// True when this request arrived in a raw form, so the reply should echo the bound
/// parameters back. <c>[JsonIgnore]</c> is MANDATORY — this type is serialized
/// (ExecResponseSerializer), and an internal flag must never reach the wire.
/// </summary>
[JsonIgnore]
public bool IsRawForm => CommandLine != null || Argv != null;
/// <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>
/// Opt into the full response envelope. When false (default) the server returns a lean,
/// token-efficient reply — compact JSON, the envelope's own null keys omitted, and the
/// always-on metadata (executedAt, command, executionTimeMs) dropped; the command's
/// result payload keeps explicit nulls (see <see cref="OmitNulls"/>). Set true to get
/// every envelope field back for debugging/correlation. (AUTHAPI-21)
/// </summary>
[JsonProperty("verbose")]
public bool Verbose { get; set; }
/// <summary>
/// When true, drop null keys from the whole reply INCLUDING the command's result payload.
/// Off by default: an absent payload key would otherwise be indistinguishable from a
/// nonexistent or misspelled one. Opt in when the result schema is already known and the
/// nulls are pure bytes — e.g. bulk list reads where a null column repeats per item.
/// Orthogonal to <see cref="Verbose"/> (envelope metadata). (AUTHAPI-21)
/// </summary>
[JsonProperty("omitNulls")]
public bool OmitNulls { get; set; }
/// <summary>
/// When true, run the command as a detached job (CLI-335): the response returns a job
/// id immediately and the client collects the result later via GET /api/job?id=…,
/// surviving its own HTTP timeout. Execution still queues through the server's
/// one-command-at-a-time exec gate.
/// </summary>
[JsonProperty("job")]
public bool Job { 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>
/// <param name="command">Name of the command to execute.</param>
/// <param name="parameters">Parameters for the command execution.</param>
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>
/// <typeparam name="T">The type to convert the parameter value to.</typeparam>
/// <param name="name">Parameter name.</param>
/// <param name="defaultValue">Value to return if the parameter is missing or unconvertible.</param>
/// <returns>The parameter's value, or <paramref name="defaultValue"/>.</returns>
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>
/// <param name="name">Parameter name.</param>
/// <returns>True if the parameter is present.</returns>
public bool HasParameter(string name)
{
return Parameters != null && Parameters.ContainsKey(name);
}
/// <summary>
/// Validate the request structure.
/// </summary>
/// <returns>An error message, or null if the request is valid.</returns>
public string Validate()
{
// Exactly one of the three request forms. Counting them first keeps every
// combination -- including all three at once -- on one message.
var forms = 0;
if (Command != null) forms++;
if (CommandLine != null) forms++;
if (Argv != null) forms++;
if (forms > 1)
return "Specify exactly one of 'command', 'commandLine' or 'argv'";
if (IsRawForm)
{
// Rejected, not ignored: a silently-dropped payload is indistinguishable from
// one that was never sent, which is the ambiguity the lean envelope already
// legislates against.
if (Parameters != null && Parameters.Count > 0)
return "'parameters' cannot be combined with 'commandLine' or 'argv'; the arguments belong in the command line itself";
if (Argv != null)
{
if (Argv.Count == 0)
return "'argv' must contain at least the command name";
for (var i = 0; i < Argv.Count; i++)
if (Argv[i] == null)
return "'argv' must not contain null elements";
if (string.IsNullOrWhiteSpace(Argv[0]))
return "'argv' must start with the command name";
}
else if (string.IsNullOrWhiteSpace(CommandLine))
{
return "'commandLine' must not be empty";
}
}
else 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,208 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
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>
/// <remarks>
/// Like <c>command</c> and <c>executedAt</c>, emitted on the /api/exec wire only in verbose
/// mode — the gating lives in <c>ExecResponseSerializer</c>, not on this model. (AUTHAPI-21)
/// </remarks>
[JsonProperty("executionTimeMs")]
public long? ExecutionTimeMs { get; set; }
/// <summary>
/// Coarse machine-readable server state marker. Set to "busy" when the command was not
/// executed because the server's host cannot service it yet (e.g. the Editor is still
/// settling after a cold start). Omitted from the JSON on ordinary success/failure
/// responses, so existing envelopes are unchanged.
/// </summary>
[JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)]
public string Status { get; set; }
/// <summary>
/// True when the failure is transient and the same request is expected to succeed after a
/// short wait (poll /api/status until it reports "ready"). Omitted from the JSON on
/// ordinary success/failure responses.
/// </summary>
[JsonProperty("retryable", NullValueHandling = NullValueHandling.Ignore)]
public bool? Retryable { get; set; }
/// <summary>
/// Stable, machine-readable failure discriminator (e.g. <c>INVALID_COMMAND_ARGS</c>).
///
/// HTTP status cannot serve as the discriminator here: every /api/exec failure is 400, so
/// status alone cannot separate "your arguments are wrong" from "the command threw".
/// Spelled to match the <c>unity</c> CLI's own InvalidArgsCode literal, so neither side
/// needs a translation table.
/// </summary>
[JsonProperty("errorCode", NullValueHandling = NullValueHandling.Ignore)]
public string ErrorCode { get; set; }
/// <summary>
/// Every argument-binding defect found, one entry each — the binder accumulates rather
/// than bailing, so a user fixing a command line sees all of its problems at once.
/// Machine-readable rather than prose, because the <c>unity</c> CLI renders and localizes
/// these itself; <see cref="BaseResponse.ErrorDetails"/> carries the English fallback for
/// clients that do not.
/// </summary>
[JsonProperty("argProblems", NullValueHandling = NullValueHandling.Ignore)]
public List<ArgProblem> ArgProblems { get; set; }
/// <summary>
/// The resolved command's catalog entry, shaped like a /api/commands <c>commands[]</c>
/// element. Sent with an argument error so a client can render usage without a separate
/// schema request. Free to produce - the command is already resolved at that point.
/// </summary>
[JsonProperty("commandSchema", NullValueHandling = NullValueHandling.Ignore)]
public JObject CommandSchema { get; set; }
/// <summary>
/// What the server bound from a raw command line. Set ONLY for argv/commandLine requests,
/// leaving the structured path's envelope unchanged. A client that does not bind locally
/// has no other way to report what the command actually received.
/// </summary>
[JsonProperty("parameters", NullValueHandling = NullValueHandling.Ignore)]
public JObject BoundParameters { get; set; }
/// <summary>
/// Create the argument-binding failure response (sent with HTTP 400): the command was
/// resolved but its arguments could not be bound, so nothing executed.
/// </summary>
internal static CommandExecutionResponse CmdInvalidArgs(string cmd, string details,
List<ArgProblem> problems, JObject commandSchema)
{
return new CommandExecutionResponse
{
Command = cmd,
Success = false,
ExecutedAt = DateTime.UtcNow,
Error = "Invalid Command Arguments",
ErrorDetails = details,
ErrorCode = "INVALID_COMMAND_ARGS",
ArgProblems = problems,
CommandSchema = commandSchema
};
}
/// <summary>
/// The specific cause of a "busy" response, matching /api/status's own "status" values for
/// the same conditions ("settling", "blocked_by_dialog") — a caller can branch on this
/// directly instead of inferring the cause from whether "dialogs" happens to be populated.
/// Omitted from the JSON on ordinary success/failure responses.
/// </summary>
[JsonProperty("busyReason", NullValueHandling = NullValueHandling.Ignore)]
public string BusyReason { get; set; }
/// <summary>
/// Dialogs (see EditorDialogEvents) that opened at or after this call started. Omitted
/// when none occurred, so existing envelopes are unchanged. Lets a caller learn "this
/// call took 40s because a dialog was shown" even without polling /api/dialog
/// concurrently.
/// </summary>
[JsonProperty("dialogsDuringExecution", NullValueHandling = NullValueHandling.Ignore)]
public List<object> DialogsDuringExecution { get; set; }
/// <summary>
/// Currently-open dialog(s) (see EditorDialogEvents), attached to a busy response when a
/// MainThreadRequired command was rejected because a modal dialog is blocking the main
/// thread. Omitted otherwise.
/// </summary>
[JsonProperty("dialogs", NullValueHandling = NullValueHandling.Ignore)]
public List<object> Dialogs { get; set; }
/// <summary>
/// Create a successful execution response.
/// </summary>
/// <param name="command">Name of the executed command.</param>
/// <param name="result">The command's result payload.</param>
/// <param name="executionTimeMs">How long the command took to execute, in milliseconds.</param>
/// <returns>A successful response wrapping <paramref name="result"/>.</returns>
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>
/// <param name="cmd">Name of the executed command.</param>
/// <param name="error">Error message.</param>
/// <param name="errorDetails">Additional error details for debugging.</param>
/// <returns>A failed response with <see cref="BaseResponse.Error"/>/<see cref="BaseResponse.ErrorDetails"/> set.</returns>
public static CommandExecutionResponse CmdFailure(string cmd, string error, string errorDetails)
{
return new CommandExecutionResponse
{
Command = cmd,
Success = false,
ExecutedAt = DateTime.UtcNow,
Error = error,
ErrorDetails = errorDetails
};
}
/// <summary>
/// Create the retryable "server busy" response (sent with HTTP 503): the command was not
/// executed because the host cannot service it yet — distinguishable from a genuine
/// command failure via status/retryable so callers know to retry shortly instead of
/// guessing (AUTHAPI-35). <paramref name="busyReason"/> is the specific cause ("settling",
/// "blocked_by_dialog", ...); omit it only for a generic/legacy busy state.
/// </summary>
/// <param name="cmd">Name of the command that could not be serviced yet.</param>
/// <param name="details">Human-readable reason the host is busy.</param>
/// <param name="busyReason">The specific cause of the busy state, or null for a generic/legacy busy state.</param>
/// <returns>A retryable "server busy" response.</returns>
public static CommandExecutionResponse CmdBusy(string cmd, string details, string busyReason = null)
{
return new CommandExecutionResponse
{
Command = cmd,
Success = false,
ExecutedAt = DateTime.UtcNow,
Error = "Server Busy",
ErrorDetails = details,
Status = "busy",
Retryable = true,
BusyReason = busyReason
};
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1911e492692621e4998635d7ddce0489
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,63 @@
using System;
using Newtonsoft.Json;
namespace Unity.Pipeline.Models
{
/// <summary>
/// Editor ground truth about the console, sampled on the main thread and served alongside
/// buffered entries so a client can tell whether the buffer agrees with the Editor console
/// without a second round trip. Null when served from a player build, which has no Editor
/// console.
/// </summary>
[Serializable]
public class ConsoleGroundTruth
{
/// <summary>When the sample was taken (UTC).</summary>
[JsonProperty("sampledUtc")]
public DateTime SampledUtc { get; set; }
/// <summary>
/// How long ago the sample was taken. Sampling runs on the main thread, so it stops while
/// the Editor is blocked compiling or importing; a large age means the rest of this object
/// describes an older state of the console.
/// </summary>
[JsonProperty("ageMs")]
public long AgeMs { get; set; }
/// <summary>
/// True while the project has editor compile errors. Read from
/// <c>EditorUtility.scriptCompilationFailed</c>, which is native state independent of the
/// log callback and of any domain reload, so it stays true for as long as the errors do.
/// </summary>
[JsonProperty("compilationFailed")]
public bool CompilationFailed { get; set; }
/// <summary>True while script compilation is in progress.</summary>
[JsonProperty("compiling")]
public bool Compiling { get; set; }
/// <summary>
/// Errors the Editor console holds. This is a console-lifetime total, so comparing it with
/// <see cref="ConsoleLevelCounts.Error"/> shows whether the buffer is missing entries the
/// console still displays.
/// </summary>
[JsonProperty("consoleErrors")]
public int ConsoleErrors { get; set; }
/// <summary>Warnings the Editor console holds.</summary>
[JsonProperty("consoleWarnings")]
public int ConsoleWarnings { get; set; }
/// <summary>Informational entries the Editor console holds.</summary>
[JsonProperty("consoleLogs")]
public int ConsoleLogs { get; set; }
/// <summary>
/// True once the buffer has been backfilled from the Editor console's own store. False
/// means that backfill is unavailable on this Editor version, which is a benign explanation
/// for the buffer holding fewer entries than <see cref="ConsoleErrors"/> and friends report.
/// </summary>
[JsonProperty("seeded")]
public bool Seeded { get; set; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 10d7f65f989380a4aa80b77361180394
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,63 @@
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>
/// Unity's own <see cref="UnityEngine.LogType"/> name for the entry ("Log", "Warning",
/// "Error", "Assert", "Exception"), keeping the distinction that <see cref="Level"/>
/// collapses.
/// </summary>
[JsonProperty("logType")]
public string LogType { 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; }
/// <summary>
/// True when the entry was read from the Editor console's own store rather than captured
/// live. Its <see cref="TimestampUtc"/> is when it was read, not when it was logged, and
/// its position relative to live-captured entries reflects the read, not the original
/// chronology.
/// </summary>
[JsonProperty("seeded")]
public bool Seeded { get; set; }
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: bd1bd665815e523ee0da6504d78923df
@@ -0,0 +1,102 @@
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.
/// Only meaningful paired with <see cref="Session"/>: sequence numbers restart from zero
/// with the capture session, so a cursor from an earlier session names different entries.
/// </summary>
[JsonProperty("cursor")]
public long Cursor { get; set; }
/// <summary>
/// Identifies the capture session <see cref="Cursor"/> belongs to. A "--follow" client
/// passes this back as <c>since_session</c> alongside <c>since</c>; a cursor from a
/// different session is refused rather than silently misapplied.
/// </summary>
[JsonProperty("session")]
public string Session { 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. Also true
/// whenever <see cref="Reset"/> is.
/// </summary>
[JsonProperty("dropped")]
public bool Dropped { get; set; }
/// <summary>
/// True when the requested cursor could not be honored — it came from another session, or
/// it was past the current counter — so the cursor was ignored and the tail returned
/// instead. A "--follow" client should adopt <see cref="Cursor"/> and <see cref="Session"/>
/// and treat the returned entries as a fresh start.
/// </summary>
[JsonProperty("reset")]
public bool Reset { get; set; }
/// <summary>
/// How many entries of each severity the buffer currently holds, before the level filter
/// and <c>tail</c> limit. Compare with <see cref="ConsoleGroundTruth.ConsoleErrors"/> and
/// friends to see whether the buffer is missing entries the Editor console still shows.
/// </summary>
[JsonProperty("counts")]
public ConsoleLevelCounts Counts { get; set; }
/// <summary>
/// Editor ground truth about the console at the time it was last sampled. Null in a player
/// build, and before the first sample after an Editor load.
/// </summary>
[JsonProperty("groundTruth")]
public ConsoleGroundTruth GroundTruth { get; set; }
}
/// <summary>
/// Entry counts per severity retained in the buffer, before the level filter and <c>tail</c>
/// limit are applied. Compare with the Editor console's own totals in
/// <see cref="ConsoleGroundTruth"/> to see whether the buffer is missing entries the console
/// still shows.
/// </summary>
[Serializable]
public class ConsoleLevelCounts
{
/// <summary>
/// Retained entries at error severity. Unity's Error, Exception and Assert log types all
/// count here.
/// </summary>
[JsonProperty("error")]
public int Error { get; set; }
/// <summary>Retained entries at warning severity.</summary>
[JsonProperty("warn")]
public int Warn { get; set; }
/// <summary>Retained entries at informational severity.</summary>
[JsonProperty("log")]
public int Log { get; set; }
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3382b91cda66d80db6ab090f9fbeac16
@@ -0,0 +1,126 @@
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]
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>
/// <param name="result">The evaluated expression's result.</param>
/// <param name="output">Console output captured during execution.</param>
/// <param name="executionTimeMs">How long execution took, in milliseconds.</param>
/// <param name="diagnostics">Compilation diagnostics (errors, warnings) from Roslyn.</param>
/// <returns>A successful response wrapping <paramref name="result"/>.</returns>
public static EvalResponse EvalSuccess(object result, string output = null, long executionTimeMs = 0, List<DiagnosticInfo> diagnostics = null)
{
return new EvalResponse
{
Success = true,
Command = "eval",
Result = result,
Output = output,
ExecutionTimeMs = executionTimeMs,
Diagnostics = diagnostics ?? new List<DiagnosticInfo>(),
ExecutedAt = DateTime.UtcNow
};
}
/// <summary>
/// Create a failed evaluation response.
/// </summary>
/// <param name="error">Error message.</param>
/// <param name="errorDetails">Additional error details for debugging.</param>
/// <param name="executionTimeMs">How long execution took, in milliseconds.</param>
/// <param name="diagnostics">Compilation diagnostics (errors, warnings) from Roslyn.</param>
/// <returns>A failed response.</returns>
public static EvalResponse EvalFailure(string error, string errorDetails = null, long executionTimeMs = 0, List<DiagnosticInfo> diagnostics = null)
{
return new EvalResponse
{
Success = false,
Command = "eval",
Error = error,
ErrorDetails = errorDetails,
ExecutionTimeMs = executionTimeMs,
Diagnostics = diagnostics ?? new List<DiagnosticInfo>(),
ExecutedAt = DateTime.UtcNow
};
}
}
/// <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; }
/// <summary>
/// User source file the diagnostic maps to through a <c>#line</c> directive, or null when
/// the compiled source carries no mapping (eval snippets, generated scaffolding).
/// </summary>
[JsonProperty("file", NullValueHandling = NullValueHandling.Ignore)]
public string File { get; set; }
/// <summary>
/// 1-based line in <see cref="File"/>; null when <see cref="File"/> is null.
/// </summary>
[JsonProperty("fileLine", NullValueHandling = NullValueHandling.Ignore)]
public int? FileLine { get; set; }
/// <summary>
/// Trimmed text of the compiled source line the diagnostic points at. For code reload that
/// is the generated override source, which is what actually failed to compile.
/// </summary>
[JsonProperty("source", NullValueHandling = NullValueHandling.Ignore)]
public string Source { 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,273 @@
using System;
using System.IO;
using System.Diagnostics;
using Newtonsoft.Json;
using UnityEngine;
using Unity.Pipeline.Security;
#if UNITY_6000_5_OR_NEWER
using Unity.Scripting.LifecycleManagement;
#endif
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]
#if UNITY_6000_5_OR_NEWER
[NoAutoStaticsCleanup]
#endif
class InstanceDescriptor
{
private const string DescriptorFileName = ".unity-pipeline-port";
/// <summary>
/// Corrective guidance surfaced via <see cref="Info"/> when the Editor is interactive and
/// wasn't launched with -automated (UUM-149977). Read by every client before it can even
/// connect (see <see cref="Info"/>), unlike a console warning which was logged once per
/// server instance and nobody watches anyway.
/// </summary>
private const string AutomationInfoMessage =
"This editor has not been opened in automated mode and therefore could get stuck on modal dialogs. Human intervention may be required.";
/// <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>
/// Corrective guidance for the client, e.g. warning that this instance could get stuck on
/// a modal dialog. Null when there is nothing to say (launched with -automated, or
/// batchmode — which can't show modals regardless, see <see cref="Mode"/>) — omitted from
/// the JSON entirely, same convention as <see cref="BaseResponse.Warnings"/>. Read this
/// before issuing commands: it's the one piece of state every client sees, since reading
/// the descriptor is a mandatory first step just to connect (UUM-149977). There is no
/// separate "automated" flag: it would only ever disagree with a null/non-null `Info` for
/// batchmode instances, and that distinction carries no information — batchmode can't show
/// modal dialogs either way.
/// </summary>
[JsonProperty("info", NullValueHandling = NullValueHandling.Ignore)]
public string Info { get; set; }
/// <summary>
/// Optional wire features this server understands (e.g. <c>exec.argv</c>), mirrored from
/// <see cref="BasePipelineServer.Capabilities"/> so the two carriers cannot drift.
///
/// Carried in the descriptor because every client already reads this file locally to
/// obtain the eval token, so negotiation costs no extra request. A descriptor with NO
/// capabilities key at all comes from a server too old to have the field, and a client
/// must then assume it supports none of these features.
/// </summary>
[JsonProperty("capabilities", NullValueHandling = NullValueHandling.Ignore)]
public string[] Capabilities { get; set; }
/// <summary>
/// Create instance descriptor for current Editor session
/// </summary>
/// <param name="port">Port the server is listening on.</param>
/// <param name="automated">Whether the Editor was launched with -automated.</param>
/// <returns>A populated descriptor for this Editor session.</returns>
public static InstanceDescriptor CreateCurrent(int port, bool automated)
{
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
{
Capabilities = BasePipelineServer.Capabilities,
Pid = pid,
Port = port,
ProjectPath = projectPath,
ProjectName = projectName,
UnityVersion = unityVersion,
Mode = isBatchMode ? "batchmode" : "editor",
StartedAt = DateTime.UtcNow,
LastHeartbeat = DateTime.UtcNow,
EvalToken = evalToken,
// Only in an interactive Editor: batchmode (CI, upm-pvp, UTR) can't show modal
// popups, so the guidance would be pure noise there.
Info = (!automated && !isBatchMode) ? AutomationInfoMessage : null
};
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"CreateCurrent failed: {ex.Message}");
UnityEngine.Debug.LogError($"Stack trace: {ex.StackTrace}");
throw;
}
}
/// <summary>
/// Serializes descriptor writes. Heartbeat rewrites can now run concurrently (requests
/// are processed in parallel since the /api/progress work); a torn concurrent write
/// would leave clients reading half a descriptor.
/// </summary>
private static readonly object m_WriteGate = new object();
/// <summary>
/// Write instance descriptor to project root
/// </summary>
/// <param name="descriptor">The descriptor to write.</param>
public static void WriteToProjectRoot(InstanceDescriptor descriptor)
{
lock (m_WriteGate)
{
WriteToProjectRootLocked(descriptor);
}
}
private static void WriteToProjectRootLocked(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>
/// <param name="projectPath">Absolute path to the Unity project.</param>
/// <returns>The descriptor, or null if missing/corrupted.</returns>
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>
/// <param name="projectPath">Absolute path to the Unity project.</param>
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>
/// <param name="projectPath">Absolute path to the Unity project.</param>
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. Kept in one place so discovery code
/// and tests can't drift from where this class itself reads/writes.
/// </summary>
/// <param name="projectPath">Absolute path to the Unity project.</param>
/// <returns>The descriptor file's absolute path.</returns>
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,60 @@
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; }
/// <summary>True when none of the reference forms above are set.</summary>
[JsonIgnore]
public bool IsEmpty =>
string.IsNullOrEmpty(GlobalId) &&
string.IsNullOrEmpty(Path) &&
string.IsNullOrEmpty(Guid) &&
InstanceId == null &&
string.IsNullOrEmpty(HierarchyPath);
/// <summary>The reference in its most specific set form, for diagnostics/logging.</summary>
/// <returns>The formatted string.</returns>
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,199 @@
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>
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);
/// <summary>Read an <see cref="ObjectRef"/> from a structured object, a canonical string, or a bare instance id.</summary>
/// <param name="reader">The JSON reader.</param>
/// <param name="objectType">The declared property type.</param>
/// <param name="existingValue">The existing value, if any.</param>
/// <param name="hasExistingValue">Whether <paramref name="existingValue"/> is valid.</param>
/// <param name="serializer">The active serializer.</param>
/// <returns>The parsed reference, or null for a JSON null.</returns>
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;
}
}
/// <summary>Write an <see cref="ObjectRef"/> as a structured JSON object.</summary>
/// <param name="writer">The JSON writer.</param>
/// <param name="value">The reference to write, or null.</param>
/// <param name="serializer">The active serializer.</param>
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,251 @@
using System;
using Newtonsoft.Json;
using UnityEngine;
using Unity.Pipeline.Config;
using System.IO;
using Unity.Pipeline.Security;
#if UNITY_6000_5_OR_NEWER
using Unity.Scripting.LifecycleManagement;
#endif
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]
#if UNITY_6000_5_OR_NEWER
[NoAutoStaticsCleanup]
#endif
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>
/// Optional wire features this server understands (e.g. <c>exec.argv</c>), mirrored from
/// <see cref="BasePipelineServer.Capabilities"/> so the two carriers cannot drift.
///
/// Carried in the descriptor because every client already reads this file locally to
/// obtain the eval token, so negotiation costs no extra request. A descriptor with NO
/// capabilities key at all comes from a server too old to have the field, and a client
/// must then assume it supports none of these features.
/// </summary>
[JsonProperty("capabilities", NullValueHandling = NullValueHandling.Ignore)]
public string[] Capabilities { get; set; }
/// <summary>
/// Create runtime instance descriptor for current Player application.
/// </summary>
/// <param name="port">Port the runtime server is listening on.</param>
/// <param name="config">The runtime pipeline configuration (currently unused, reserved for future fields).</param>
/// <returns>A populated descriptor for this Player instance.</returns>
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
{
Capabilities = BasePipelineServer.Capabilities,
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>
/// Serializes descriptor writes so concurrent heartbeat rewrites (e.g. overlapping
/// /api/status probes) can't leave a torn file on disk. Mirrors InstanceDescriptor's
/// m_WriteGate.
/// </summary>
private static readonly object m_WriteGate = new object();
/// <summary>
/// Write runtime instance descriptor to working directory.
/// </summary>
/// <param name="descriptor">The descriptor to write.</param>
public static void WriteToWorkingDirectory(RuntimeInstanceDescriptor descriptor)
{
lock (m_WriteGate)
{
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>
/// <returns>The descriptor, or null if missing/corrupted.</returns>
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}");
}
}
/// <summary>
/// Test-only override of the detected platform, so the Android branch below is reachable
/// without a real device. Null (the default) uses the real <see cref="Application.platform"/>.
/// </summary>
internal static RuntimePlatform? OverridePlatform;
internal static string GetDescriptorFilePath()
{
var platform = OverridePlatform ?? Application.platform;
// On Android, Application.dataPath points inside the read-only, system-owned APK
// install directory (e.g. ".../base.apk") — the app cannot write there. persistentDataPath
// is the one location Unity guarantees is writable on Android (UUM-151968).
var workingDir = platform == RuntimePlatform.Android
? Application.persistentDataPath
: 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,62 @@
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; }
/// <summary>
/// The dialog currently blocking the main thread, present only when
/// <see cref="Status"/> is "blocked_by_dialog".
/// </summary>
[JsonProperty("dialog", NullValueHandling = NullValueHandling.Ignore)]
public object Dialog { 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,33 @@
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]
class TestExecutionResponse : CommandExecutionResponse
{
/// <summary>Aggregate pass/fail/skip counts for the run.</summary>
public TestSummary Summary { get; set; }
/// <summary>Per-test results.</summary>
public List<TestResult> Results { get; set; }
/// <summary>Total run duration, in seconds.</summary>
public double Duration { get; set; }
/// <summary>Path to poll for status when the run was started in async mode.</summary>
public string StatusPath { get; set; } // For async mode
/// <summary>Which suite was run: EditMode, PlayMode, or All.</summary>
public string Mode { get; set; } // EditMode, PlayMode, or All
/// <summary>The test filter that was applied, if any.</summary>
public string FilterApplied { get; set; } // What filter was used, if any
/// <summary>Create an empty response with initialized collections.</summary>
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,22 @@
using System;
namespace Unity.Pipeline
{
/// <summary>
/// Individual test result information
/// </summary>
[Serializable]
class TestResult
{
/// <summary>Fully-qualified test name.</summary>
public string FullName { get; set; }
/// <summary>Outcome: Passed, Failed, Skipped, or Inconclusive.</summary>
public string Status { get; set; } // Passed, Failed, Skipped, Inconclusive
/// <summary>How long the test took, in seconds.</summary>
public double Duration { get; set; }
/// <summary>Failure message, if any.</summary>
public string Message { get; set; }
/// <summary>Failure stack trace, if any.</summary>
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,32 @@
using System;
namespace Unity.Pipeline
{
/// <summary>
/// Summary statistics for test execution
/// </summary>
[Serializable]
class TestSummary
{
/// <summary>Total number of tests run.</summary>
public int Total { get; set; }
/// <summary>Number of tests that passed.</summary>
public int Passed { get; set; }
/// <summary>Number of tests that failed.</summary>
public int Failed { get; set; }
/// <summary>Number of tests skipped.</summary>
public int Skipped { get; set; }
/// <summary>Number of tests that ran but produced no pass/fail verdict.</summary>
public int Inconclusive { get; set; }
/// <summary>Create a zeroed summary.</summary>
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: