using System; using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace Unity.Pipeline.Models { /// /// Request model for /api/exec endpoint. /// Contains command name and parameters for remote command execution. /// [Serializable] class CommandExecutionRequest { /// /// Name of the command to execute. /// Must match a registered [CliCommand] name. /// /// Optional, because and 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 - enforces /// that. /// [JsonProperty("command")] public string Command { get; set; } /// /// An unparsed command line: the server tokenizes it (see /// ) and then binds it. For clients that genuinely /// hold raw text — MCP, a chat box, a pasted line. Mutually exclusive with /// and . /// [JsonProperty("commandLine")] public string CommandLine { get; set; } /// /// Pre-split tokens, first of which is the command name. Preferred over /// 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 and . /// [JsonProperty("argv")] public List Argv { get; set; } /// /// True when this request arrived in a raw form, so the reply should echo the bound /// parameters back. [JsonIgnore] is MANDATORY — this type is serialized /// (ExecResponseSerializer), and an internal flag must never reach the wire. /// [JsonIgnore] public bool IsRawForm => CommandLine != null || Argv != null; /// /// Parameters for the command execution. /// Contains key-value pairs matching command parameter names. /// [JsonProperty("parameters")] public JObject Parameters { get; set; } /// /// Optional timeout for command execution in milliseconds. /// Default: 60000 (60 seconds) /// [JsonProperty("timeout")] public int? Timeout { get; set; } /// /// 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 ). Set true to get /// every envelope field back for debugging/correlation. (AUTHAPI-21) /// [JsonProperty("verbose")] public bool Verbose { get; set; } /// /// 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 (envelope metadata). (AUTHAPI-21) /// [JsonProperty("omitNulls")] public bool OmitNulls { get; set; } /// /// 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. /// [JsonProperty("job")] public bool Job { get; set; } /// /// Create a new command execution request. /// public CommandExecutionRequest() { Parameters = new JObject(); } /// /// Create a command execution request with specified command and parameters. /// /// Name of the command to execute. /// Parameters for the command execution. public CommandExecutionRequest(string command, JObject parameters = null) { Command = command ?? throw new ArgumentNullException(nameof(command)); Parameters = parameters ?? new JObject(); } /// /// Get a parameter value as the specified type. /// Returns the default value if the parameter is not found or cannot be converted. /// /// The type to convert the parameter value to. /// Parameter name. /// Value to return if the parameter is missing or unconvertible. /// The parameter's value, or . public T GetParameter(string name, T defaultValue = default(T)) { if (Parameters == null || !Parameters.ContainsKey(name)) return defaultValue; try { return Parameters[name].ToObject(); } catch { return defaultValue; } } /// /// Check if a parameter exists in the request. /// /// Parameter name. /// True if the parameter is present. public bool HasParameter(string name) { return Parameters != null && Parameters.ContainsKey(name); } /// /// Validate the request structure. /// /// An error message, or null if the request is valid. 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 } } }