Previously, `parseToolCall()` was using `computeAutoApproval()`, which was a somewhat parallel implementation of `canAutoApprove()` in order to get `SafeCommandReason` metadata for presenting information to the user. The only function that was using `SafeCommandReason` was `useMessageGrouping()`, but it turns out that function was unused, so this PR removes `computeAutoApproval()` and all code related to it. More importantly, I believe this fixes https://github.com/openai/codex/issues/87 because `computeAutoApproval()` was calling `parse()` from `shell-quote` without wrapping it in a try-catch. This PR updates `canAutoApprove()` to use a tighter try-catch block that is specific to `parse()` and returns an appropriate `SafetyAssessment` in the event of an error, based on the `ApprovalPolicy`. Signed-off-by: Michael Bolin <mbolin@openai.com>
106 lines
2.8 KiB
TypeScript
106 lines
2.8 KiB
TypeScript
import type {
|
|
ExecInput,
|
|
ExecOutputMetadata,
|
|
} from "./agent/sandbox/interface.js";
|
|
import type { ResponseFunctionToolCall } from "openai/resources/responses/responses.mjs";
|
|
|
|
import { log } from "node:console";
|
|
import { formatCommandForDisplay } from "src/format-command.js";
|
|
|
|
// The console utility import is intentionally explicit to avoid bundlers from
|
|
// including the entire `console` module when only the `log` function is
|
|
// required.
|
|
|
|
export function parseToolCallOutput(toolCallOutput: string): {
|
|
output: string;
|
|
metadata: ExecOutputMetadata;
|
|
} {
|
|
try {
|
|
const { output, metadata } = JSON.parse(toolCallOutput);
|
|
return {
|
|
output,
|
|
metadata,
|
|
};
|
|
} catch (err) {
|
|
return {
|
|
output: `Failed to parse JSON result`,
|
|
metadata: {
|
|
exit_code: 1,
|
|
duration_seconds: 0,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
export type CommandReviewDetails = {
|
|
cmd: Array<string>;
|
|
cmdReadableText: string;
|
|
};
|
|
|
|
/**
|
|
* Tries to parse a tool call and, if successful, returns an object that has
|
|
* both:
|
|
* - an array of strings to use with `ExecInput` and `canAutoApprove()`
|
|
* - a human-readable string to display to the user
|
|
*/
|
|
export function parseToolCall(
|
|
toolCall: ResponseFunctionToolCall,
|
|
): CommandReviewDetails | undefined {
|
|
const toolCallArgs = parseToolCallArguments(toolCall.arguments);
|
|
if (toolCallArgs == null) {
|
|
return undefined;
|
|
}
|
|
|
|
const { cmd } = toolCallArgs;
|
|
const cmdReadableText = formatCommandForDisplay(cmd);
|
|
|
|
return {
|
|
cmd,
|
|
cmdReadableText,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* If toolCallArguments is a string of JSON that can be parsed into an object
|
|
* with a "cmd" or "command" property that is an `Array<string>`, then returns
|
|
* that array. Otherwise, returns undefined.
|
|
*/
|
|
export function parseToolCallArguments(
|
|
toolCallArguments: string,
|
|
): ExecInput | undefined {
|
|
let json: unknown;
|
|
try {
|
|
json = JSON.parse(toolCallArguments);
|
|
} catch (err) {
|
|
log(`Failed to parse toolCall.arguments: ${toolCallArguments}`);
|
|
return undefined;
|
|
}
|
|
|
|
if (typeof json !== "object" || json == null) {
|
|
return undefined;
|
|
}
|
|
|
|
const { cmd, command } = json as Record<string, unknown>;
|
|
const commandArray = toStringArray(cmd) ?? toStringArray(command);
|
|
if (commandArray == null) {
|
|
return undefined;
|
|
}
|
|
|
|
// @ts-expect-error timeout and workdir may not exist on json.
|
|
const { timeout, workdir } = json;
|
|
return {
|
|
cmd: commandArray,
|
|
workdir: typeof workdir === "string" ? workdir : undefined,
|
|
timeoutInMillis: typeof timeout === "number" ? timeout : undefined,
|
|
};
|
|
}
|
|
|
|
function toStringArray(obj: unknown): Array<string> | undefined {
|
|
if (Array.isArray(obj) && obj.every((item) => typeof item === "string")) {
|
|
const arrayOfStrings: Array<string> = obj;
|
|
return arrayOfStrings;
|
|
} else {
|
|
return undefined;
|
|
}
|
|
}
|