Files
AIGame/AIClient/Packages/com.unity.pipeline/Runtime/Models/CommandExecutionRequest.cs
T

199 lines
8.3 KiB
C#

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
}
}
}