199 lines
8.3 KiB
C#
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
|
|
}
|
|
}
|
|
} |