mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 08:02:31 +00:00
* fix(cli): name PowerShell when it strips quotes from JSON flags Windows PowerShell 5.1 does not escape inner quotes when building a native command line, so `--options '["a","b"]'` reaches orca.exe as `--options [a,b]`. The value is correct when printed and damaged by the time argv is parsed, so the resulting "invalid JSON" error blamed the user's input rather than the shell. #16743 recovered this for `--deps`, which is safe only because generated task IDs have a fixed 12-hex grammar. The same mangling hits `--options`, `--payload` and `--result`, and those are NOT safely recoverable: `["1","2"]` and `[1,2]` arrive at argv identically, so a general repair would silently turn strings into numbers. Detect instead. `getOptionalJsonFlag` rejects the damaged shape up front with an error that names the shell and shows the workaround. It fires only when the value is bracketed, quote-free, fails JSON.parse, AND consists entirely of bare tokens that quoting would rescue, so valid JSON is untouched. Also share the generated-id contract: `task-deps-flag` hardcoded /^task_[0-9a-f]{12}$/i, which silently diverges if `generateId`'s byte count changes. It now calls `isGeneratedId`, with a test pinning the two together. Verified on a Windows host. Measured argv, which the new test pins as a fixture: PS_VALUE=["task_b2a580db74d8","task_c3b691ec85e9"] ARGV=["--deps","[task_b2a580db74d8,task_c3b691ec85e9]"] Before: Invalid --options: must be a JSON array of strings After: --options arrived as [a,b], which is not valid JSON. Windows PowerShell 5.1 strips the inner quotes ... * fix(cli): scope JSON-flag detection to genuinely JSON flags Review found the detector wired to two flags that are not JSON: - `orchestration ask --options` is documented `<csv>` and the runtime splits it on commas, so `--options [a,b]` was a legitimate value being rejected. - `task-update --result` is stored verbatim and reused as dispatch failure text; existing tests pass free text, so a bracketed `[ok]` was being rejected. Both revert to `getOptionalStringFlag`. Only `gate-create --options` (`<json_array>`) and `send --payload` (`<json>`) are JSON-parsed and keep it. Three further review fixes: - Objects now require a `key:value` pair per entry. `{a,b}` and `{a:b,c}` were reported as quote-stripped although quoting them cannot produce valid JSON. - The raw value is no longer echoed. A `--payload` can carry secrets and this message reaches `--json` output; the flag name and guidance are enough. - The message hedges the shell attribution. Detection inspects only the value's shape, so it also fires when a macOS/Linux user forgets to quote, where PowerShell is not involved. Verified against a Windows host, all six cases: both JSON flags fire on the mangled shape and pass valid JSON through to the runtime; both non-JSON flags now reach the runtime again; and the secret in `{token:hunter2}` appears zero times in the error output.
139 lines
4.0 KiB
TypeScript
139 lines
4.0 KiB
TypeScript
import { RuntimeClientError } from './runtime/types'
|
|
import { REPEATED_FLAG_SEPARATOR } from './args'
|
|
import { describeQuoteStrippedJsonFlag } from './quote-stripped-json-flag'
|
|
|
|
export function getRequiredStringFlag(flags: Map<string, string | boolean>, name: string): string {
|
|
const value = flags.get(name)
|
|
if (typeof value === 'string' && value.length > 0) {
|
|
return value
|
|
}
|
|
throw new RuntimeClientError('invalid_argument', `Missing required --${name}`)
|
|
}
|
|
|
|
export function getRequiredStringFlagAllowingEmpty(
|
|
flags: Map<string, string | boolean>,
|
|
name: string
|
|
): string {
|
|
const value = flags.get(name)
|
|
if (typeof value === 'string') {
|
|
return value
|
|
}
|
|
throw new RuntimeClientError('invalid_argument', `Missing required --${name}`)
|
|
}
|
|
|
|
export function getOptionalStringFlag(
|
|
flags: Map<string, string | boolean>,
|
|
name: string
|
|
): string | undefined {
|
|
const value = flags.get(name)
|
|
return typeof value === 'string' && value.length > 0 ? value : undefined
|
|
}
|
|
|
|
/**
|
|
* A JSON-valued flag, rejected up front when a native argv boundary stripped its quotes so the
|
|
* error names the shell instead of the user's value (#16706). The value itself is still parsed
|
|
* downstream; this only catches the damaged shape.
|
|
*/
|
|
export function getOptionalJsonFlag(
|
|
flags: Map<string, string | boolean>,
|
|
name: string
|
|
): string | undefined {
|
|
const value = getOptionalStringFlag(flags, name)
|
|
if (value === undefined) {
|
|
return undefined
|
|
}
|
|
const mangled = describeQuoteStrippedJsonFlag(name, value)
|
|
if (mangled) {
|
|
throw new RuntimeClientError('invalid_argument', mangled)
|
|
}
|
|
return value
|
|
}
|
|
|
|
export function getRepeatedStringFlag(
|
|
flags: Map<string, string | boolean>,
|
|
name: string
|
|
): string[] {
|
|
const value = getOptionalStringFlag(flags, name)
|
|
return value === undefined
|
|
? []
|
|
: value.split(REPEATED_FLAG_SEPARATOR).filter((entry) => entry.length > 0)
|
|
}
|
|
|
|
export function getOptionalNumberFlag(
|
|
flags: Map<string, string | boolean>,
|
|
name: string
|
|
): number | undefined {
|
|
const value = flags.get(name)
|
|
if (typeof value !== 'string' || value.length === 0) {
|
|
return undefined
|
|
}
|
|
const parsed = Number(value)
|
|
if (!Number.isFinite(parsed)) {
|
|
throw new RuntimeClientError('invalid_argument', `Invalid numeric value for --${name}`)
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
export function getOptionalPositiveIntegerFlag(
|
|
flags: Map<string, string | boolean>,
|
|
name: string
|
|
): number | undefined {
|
|
const value = getOptionalNumberFlag(flags, name)
|
|
if (value === undefined) {
|
|
return undefined
|
|
}
|
|
if (!Number.isInteger(value) || value <= 0) {
|
|
throw new RuntimeClientError('invalid_argument', `Invalid positive integer for --${name}`)
|
|
}
|
|
return value
|
|
}
|
|
|
|
export function getOptionalNonNegativeIntegerFlag(
|
|
flags: Map<string, string | boolean>,
|
|
name: string
|
|
): number | undefined {
|
|
const value = getOptionalNumberFlag(flags, name)
|
|
if (value === undefined) {
|
|
return undefined
|
|
}
|
|
if (!Number.isInteger(value) || value < 0) {
|
|
throw new RuntimeClientError('invalid_argument', `Invalid non-negative integer for --${name}`)
|
|
}
|
|
return value
|
|
}
|
|
|
|
export function getRequiredPositiveNumber(
|
|
flags: Map<string, string | boolean>,
|
|
name: string
|
|
): number {
|
|
const raw = getRequiredStringFlag(flags, name)
|
|
const value = Number(raw)
|
|
if (!Number.isFinite(value) || value <= 0) {
|
|
throw new RuntimeClientError('invalid_argument', `--${name} must be a positive number`)
|
|
}
|
|
return value
|
|
}
|
|
|
|
export function getRequiredFiniteNumber(
|
|
flags: Map<string, string | boolean>,
|
|
name: string
|
|
): number {
|
|
const raw = getRequiredStringFlag(flags, name)
|
|
const value = Number(raw)
|
|
if (!Number.isFinite(value)) {
|
|
throw new RuntimeClientError('invalid_argument', `--${name} must be a valid number`)
|
|
}
|
|
return value
|
|
}
|
|
|
|
export function getOptionalNullableNumberFlag(
|
|
flags: Map<string, string | boolean>,
|
|
name: string
|
|
): number | null | undefined {
|
|
const value = flags.get(name)
|
|
if (value === 'null') {
|
|
return null
|
|
}
|
|
return getOptionalNumberFlag(flags, name)
|
|
}
|