fix(core): tell a missing command apart from a host that refuses to run commands
The protocol reserved 127 for "this host does not run commands", and the Swift runner used the same code for "the binary is not there" (via a `/usr/bin/env` fallback) — so alibaba's feed read a missing `bl` as a refusal and told the user to capture a console session, when the actual problem was that the command did not exist. The tray surfaced exactly that wrong sentence within minutes of being wired up. - a command that cannot be found or spawned answers 126 ("found it, could not run it"), which the feed treats as an ordinary CLI failure, and the header now names both codes. - a non-zero exit with empty stdout carries stderr as the body, so the feed has something to show. - the CLI failure reason includes the host's first line of text: "bailian CLI failed (exit 126): bl is not on PATH" rather than "exit 126". Mirrored in the TypeScript reference so parity holds.
35 unmodified lines363738394041424340444546474817 unmodified lines35 unmodified lines
usage_state *usage_state_new(void);void usage_state_free(usage_state *state);
/* A command request is answered with the exit status and stdout. Two statuses are worth naming: * 127 means this host will not run commands at all (a feed may then fall back to another path), * 126 means the command could not be run (missing binary, failed spawn) and is an ordinary failure. */* Advance feed state. * Advance feed state. * Returns either: * {"requests":[{"kind":"http","method","url","headers":[["k","v"]],"body"?}]} * or: * {"requests":[{"kind":"command","command","args":[],"stdin"?,"timeoutMs"?}]}17 unmodified lines466 unmodified linesExpand all4674684694704714724734744754764774784794804814824834844854714864874884894903 unmodified lines494495496497483498499500501502116 unmodified linesExpand all466 unmodified linesExpand all report: ?usage.ReportOutcome = null, reason: ?[]const u8 = null,};
/// A CLI that wrote to stderr before failing has already explained itself, and that explanation is/// more useful than the exit code: "bl is not on PATH" beats "exit 126". The body is provider-visible/// text, so it is trimmed to one bounded line before it goes into a report.fn cliFailureReason(alloc: std.mem.Allocator, status: i64, resp0: std.json.ObjectMap) ![]const u8 { const body = usage.stringOf(resp0.get("body")); if (body) |text| { const first_line = std.mem.trim(u8, text[0..(std.mem.indexOfScalar(u8, text, '\n') orelse text.len)], " \t\r"); if (first_line.len > 0) { const capped = first_line[0..@min(first_line.len, 120)]; return std.fmt.allocPrint(alloc, "bailian CLI failed (exit {d}): {s}", .{ status, capped }); } } return std.fmt.allocPrint(alloc, "bailian CLI failed (exit {d})", .{status});}
pub fn reportFromCli(fn reportFromCli( alloc: std.mem.Allocator, cred_root: ?std.json.ObjectMap, resp0: std.json.ObjectMap, region: AlibabaRegion,3 unmodified lines
const status = usage.toInt(i64, usage.asNumber(resp0.get("status"))) orelse -1;
if (status != 0 and status != 200) { const reason = try std.fmt.allocPrint(alloc, "bailian CLI failed (exit {d})", .{status}); const reason = try cliFailureReason(alloc, status, resp0); return .{ .reason = reason }; }
const body_str = usage.asString(resp0.get("body"));116 unmodified linesExpand all27 unmodified lines2829303132323334353637383957 unmodified lines97989910010010110110210310410510610710834 unmodified lines14314414514614714814914415015115215315428 unmodified lines18318418518618118718818919019119219319419519619719819920038 unmodified lines27 unmodified lines if let runCommand { do { responses.append(try await runCommand(request)) } catch { responses.append(UsageResponse(status: 1, body: error.localizedDescription)) responses.append(UsageResponse(status: 126, body: error.localizedDescription)) } } else { // The one answer a feed is allowed to read as "this host will not run things". responses.append(UsageResponse(status: 127, body: "this host does not run commands (\(request.command ?? ""))")) } } else { responses.append(try await send(request, session: session))57 unmodified lines if let foundURL { process.executableURL = foundURL process.arguments = args } else { process.executableURL = URL(fileURLWithPath: "/usr/bin/env") process.arguments = [cmd] + args // Not falling back to `/usr/bin/env`: it would exit 127 with the same code the protocol // reserves for "this host does not run commands", and the feed would read a missing // binary as a refusal. 126 says what actually happened. return UsageResponse(status: 126, body: "\(cmd) is not on PATH") } }
process.standardOutput = stdoutPipe34 unmodified lines try process.run() } catch { stdoutPipe.fileHandleForReading.readabilityHandler = nil stderrPipe.fileHandleForReading.readabilityHandler = nil // 126 is "found it, could not run it", which is not the same answer as 127 ("this host does // not run commands"): a feed that treats 127 as "fall back to another path" must not be told // that when the real problem is a missing binary. return UsageResponse(status: 127, body: error.localizedDescription) return UsageResponse(status: 126, body: error.localizedDescription) }
if let stdin = request.stdin, let data = stdin.data(using: .utf8) { try? stdinPipe.fileHandleForWriting.write(contentsOf: data)28 unmodified lines } let trailingErr = stderrPipe.fileHandleForReading.availableData _ = trailingErr
let body = String(data: stdoutData, encoding: .utf8) ?? "" var body = String(data: stdoutData, encoding: .utf8) ?? "" let exitCode = Int(process.terminationStatus)
// A command that wrote nothing to stdout but failed still has something to say, and saying it // on stderr is how a CLI reports a missing argument or a bad credential. Without this the feed // only sees an empty body and cannot tell the user why nothing came back. if exitCode != 0, body.isEmpty, let errorText = String(data: stderrData, encoding: .utf8), !errorText.isEmpty { body = errorText }
return UsageResponse(status: exitCode, body: body) }
/// The core asks for one credential kind; a real app resolves it from its own store (or from38 unmodified lines