feat: add support for inline output_schema option (#7)
This commit is contained in:
@@ -31,22 +31,23 @@ Provide either `prompt` or `prompt_file`; the other may be left empty. The actio
|
||||
|
||||
## Inputs
|
||||
|
||||
| Name | Required | Description | Default |
|
||||
| -------------------- | ------------- | -------------------------------------------------------------------------------------------------------------- | ---------------- |
|
||||
| `openai_api_key` | Yes | Secret used to authenticate the helper proxy with OpenAI. Store it in `secrets` and never hardcode it. | — |
|
||||
| `prompt` | Conditionally | Inline prompt text. Provide this or `prompt_file`. | `""` |
|
||||
| `prompt_file` | Conditionally | Path (relative to the repository root) of a file that contains the prompt. Provide this or `prompt`. | `""` |
|
||||
| `working_directory` | No | Directory passed to `codex exec --cd`. Defaults to the repository root. | `""` |
|
||||
| `codex_version` | No | Version of `@openai/codex` to install. | `0.42.0-alpha.3` |
|
||||
| `codex_args` | No | Extra arguments forwarded to `codex exec`. Accepts JSON arrays (`["--flag", "value"]`) or shell-style strings. | `""` |
|
||||
| `output_file` | No | File where the final Codex message is written. Leave empty to skip writing a file. | `""` |
|
||||
| `output_schema_file` | No | Schema file forwarded to `codex exec --output-schema`. Leave empty to skip passing the option. | `""` |
|
||||
| `model` | No | Model the agent should use. Leave empty to let Codex pick its default. | `""` |
|
||||
| `codex_home` | No | Directory to use as the Codex CLI home (config/cache). Uses the CLI default when empty. | `""` |
|
||||
| `safety_strategy` | No | Controls how the action restricts Codex privileges. See [Safety strategy](#safety-strategy). | `drop_sudo` |
|
||||
| `codex_user` | No | Username to run Codex as when `safety_strategy` is `unprivileged_user`. | `""` |
|
||||
| `require_repo_write` | No | Whether to require the triggering actor to have write access to the repository before running. | "true" |
|
||||
| `allow_bots` | No | Allow runs triggered by GitHub Apps/bot accounts to bypass the write-access check. | "false" |
|
||||
| Name | Required | Description | Default |
|
||||
| -------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
|
||||
| `openai_api_key` | Yes | Secret used to authenticate the helper proxy with OpenAI. Store it in `secrets` and never hardcode it. | — |
|
||||
| `prompt` | Conditionally | Inline prompt text. Provide this or `prompt_file`. | `""` |
|
||||
| `prompt_file` | Conditionally | Path (relative to the repository root) of a file that contains the prompt. Provide this or `prompt`. | `""` |
|
||||
| `output_file` | No | File where the final Codex message is written. Leave empty to skip writing a file. | `""` |
|
||||
| `working_directory` | No | Directory passed to `codex exec --cd`. Defaults to the repository root. | `""` |
|
||||
| `codex_version` | No | Version of `@openai/codex` to install. | `0.42.0-alpha.3` |
|
||||
| `codex_args` | No | Extra arguments forwarded to `codex exec`. Accepts JSON arrays (`["--flag", "value"]`) or shell-style strings. | `""` |
|
||||
| `output_schema` | No | Inline schema contents written to a temp file and passed to `codex exec --output-schema`. Mutually exclusive with `output_schema_file`. | `""` |
|
||||
| `output_schema_file` | No | Schema file forwarded to `codex exec --output-schema`. Leave empty to skip passing the option. | `""` |
|
||||
| `model` | No | Model the agent should use. Leave empty to let Codex pick its default. | `""` |
|
||||
| `codex_home` | No | Directory to use as the Codex CLI home (config/cache). Uses the CLI default when empty. | `""` |
|
||||
| `safety_strategy` | No | Controls how the action restricts Codex privileges. See [Safety strategy](#safety-strategy). | `drop_sudo` |
|
||||
| `codex_user` | No | Username to run Codex as when `safety_strategy` is `unprivileged_user`. | `""` |
|
||||
| `require_repo_write` | No | Whether to require the triggering actor to have write access to the repository before running. | "true" |
|
||||
| `allow_bots` | No | Allow runs triggered by GitHub Apps/bot accounts to bypass the write-access check. | "false" |
|
||||
|
||||
## Safety Strategy
|
||||
|
||||
|
||||
+10
-4
@@ -10,6 +10,10 @@ inputs:
|
||||
description: "Path to file that contains the prompt to pass to `codex exec`. `prompt` or `prompt_file` must be provided."
|
||||
required: false
|
||||
default: ""
|
||||
output_file:
|
||||
description: "Path to a JSON Schema file describing the model's final response shape."
|
||||
required: false
|
||||
default: ""
|
||||
openai_api_key:
|
||||
description: "OpenAI API key used by the Codex CLI."
|
||||
required: true
|
||||
@@ -25,8 +29,8 @@ inputs:
|
||||
description: "Additional args to pass through to `codex exec`. If this value starts with `[`, it will be parsed as a JSON array; otherwise, it will be parsed as a shell-like string."
|
||||
required: false
|
||||
default: ""
|
||||
output_file:
|
||||
description: "Path to a JSON Schema file describing the model's final response shape."
|
||||
output_schema:
|
||||
description: "Inline schema contents to use with `codex exec --output-schema`."
|
||||
required: false
|
||||
default: ""
|
||||
output_schema_file:
|
||||
@@ -178,10 +182,11 @@ runs:
|
||||
env:
|
||||
CODEX_PROMPT: ${{ inputs.prompt }}
|
||||
CODEX_PROMPT_FILE: ${{ inputs.prompt_file }}
|
||||
CODEX_OUTPUT_FILE: ${{ inputs.output_file }}
|
||||
CODEX_HOME: ${{ inputs.codex_home }}
|
||||
CODEX_WORKING_DIRECTORY: ${{ inputs.working_directory || github.workspace }}
|
||||
CODEX_ARGS: ${{ inputs.codex_args }}
|
||||
CODEX_OUTPUT_FILE: ${{ inputs.output_file }}
|
||||
CODEX_OUTPUT_SCHEMA: ${{ inputs.output_schema }}
|
||||
CODEX_OUTPUT_SCHEMA_FILE: ${{ inputs.output_schema_file }}
|
||||
CODEX_MODEL: ${{ inputs.model }}
|
||||
CODEX_SAFETY_STRATEGY: ${{ inputs.safety_strategy }}
|
||||
@@ -191,11 +196,12 @@ runs:
|
||||
node "${{ github.action_path }}/dist/main.js" run-codex-exec \
|
||||
--prompt "${CODEX_PROMPT}" \
|
||||
--prompt-file "${CODEX_PROMPT_FILE}" \
|
||||
--output-file "$CODEX_OUTPUT_FILE" \
|
||||
--codex-home "$CODEX_HOME" \
|
||||
--cd "$CODEX_WORKING_DIRECTORY" \
|
||||
--proxy-port "${{ steps.read_server_info.outputs.port }}" \
|
||||
--extra-args "$CODEX_ARGS" \
|
||||
--output-file "$CODEX_OUTPUT_FILE" \
|
||||
--output-schema "$CODEX_OUTPUT_SCHEMA" \
|
||||
--output-schema-file "$CODEX_OUTPUT_SCHEMA_FILE" \
|
||||
--model "$CODEX_MODEL" \
|
||||
--safety-strategy "$CODEX_SAFETY_STRATEGY" \
|
||||
|
||||
Vendored
+85
-30
File diff suppressed because one or more lines are too long
+40
-4
@@ -2,7 +2,12 @@ import { Command, Option } from "commander";
|
||||
import pkg from "../package.json" assert { type: "json" };
|
||||
|
||||
import { readServerInfo } from "./readServerInfo";
|
||||
import { PromptSource, runCodexExec, SafetyStrategy } from "./runCodexExec";
|
||||
import {
|
||||
OutputSchemaSource,
|
||||
PromptSource,
|
||||
runCodexExec,
|
||||
SafetyStrategy,
|
||||
} from "./runCodexExec";
|
||||
import { dropSudo } from "./dropSudo";
|
||||
import { ensureActorHasWriteAccess } from "./checkActorPermissions";
|
||||
import parseArgsStringToArgv from "string-argv";
|
||||
@@ -74,6 +79,10 @@ export async function main() {
|
||||
"--output-schema-file <FILE>",
|
||||
"Path to a schema file to pass to `codex exec --output-schema`."
|
||||
)
|
||||
.requiredOption(
|
||||
"--output-schema <SCHEMA>",
|
||||
"Inline schema contents to pass to `codex exec --output-schema`."
|
||||
)
|
||||
.requiredOption("--model <model>", "Model the agent should use")
|
||||
.requiredOption(
|
||||
"--safety-strategy <strategy>",
|
||||
@@ -93,6 +102,7 @@ export async function main() {
|
||||
extraArgs: Array<string>;
|
||||
outputFile: string;
|
||||
outputSchemaFile: string;
|
||||
outputSchema: string;
|
||||
model: string;
|
||||
safetyStrategy: string;
|
||||
codexUser: string;
|
||||
@@ -100,11 +110,12 @@ export async function main() {
|
||||
const {
|
||||
prompt,
|
||||
promptFile,
|
||||
outputFile,
|
||||
codexHome,
|
||||
cd,
|
||||
proxyPort,
|
||||
extraArgs,
|
||||
outputFile,
|
||||
outputSchema,
|
||||
outputSchemaFile,
|
||||
model,
|
||||
safetyStrategy,
|
||||
@@ -115,7 +126,7 @@ export async function main() {
|
||||
const normalizedPromptFile = emptyAsNull(promptFile);
|
||||
let promptSource: PromptSource;
|
||||
if (normalizedPrompt != null) {
|
||||
promptSource = { type: "text", content: normalizedPrompt };
|
||||
promptSource = { type: "inline", content: normalizedPrompt };
|
||||
} else if (normalizedPromptFile != null) {
|
||||
promptSource = { type: "file", path: normalizedPromptFile };
|
||||
} else {
|
||||
@@ -126,6 +137,31 @@ export async function main() {
|
||||
|
||||
// Custom option processing to coerces to null does not work with
|
||||
// Commander.js's requiredOption, so we have to post-process here.
|
||||
const normalizedOutputSchemaFile = emptyAsNull(outputSchemaFile);
|
||||
const normalizedOutputSchema = emptyAsNull(outputSchema);
|
||||
|
||||
if (
|
||||
normalizedOutputSchemaFile != null &&
|
||||
normalizedOutputSchema != null
|
||||
) {
|
||||
throw new Error(
|
||||
"Only one of `output_schema` or `output_schema_file` may be specified."
|
||||
);
|
||||
}
|
||||
|
||||
let outputSchemaSource: OutputSchemaSource | null = null;
|
||||
if (normalizedOutputSchema != null) {
|
||||
outputSchemaSource = {
|
||||
type: "inline",
|
||||
content: normalizedOutputSchema,
|
||||
};
|
||||
} else if (normalizedOutputSchemaFile != null) {
|
||||
outputSchemaSource = {
|
||||
type: "file",
|
||||
path: normalizedOutputSchemaFile,
|
||||
};
|
||||
}
|
||||
|
||||
await runCodexExec({
|
||||
prompt: promptSource,
|
||||
codexHome: emptyAsNull(codexHome),
|
||||
@@ -133,7 +169,7 @@ export async function main() {
|
||||
proxyPort,
|
||||
extraArgs,
|
||||
explicitOutputFile: emptyAsNull(outputFile),
|
||||
outputSchemaFile: emptyAsNull(outputSchemaFile),
|
||||
outputSchema: outputSchemaSource,
|
||||
model: emptyAsNull(model),
|
||||
safetyStrategy: toSafetyStrategy(safetyStrategy),
|
||||
codexUser: emptyAsNull(codexUser),
|
||||
|
||||
+92
-30
@@ -1,11 +1,11 @@
|
||||
import { spawn } from "child_process";
|
||||
import { mkdtemp, readFile, rm } from "fs/promises";
|
||||
import { mkdtemp, readFile, rm, writeFile } from "fs/promises";
|
||||
import path from "path";
|
||||
import { setOutput } from "@actions/core";
|
||||
|
||||
export type PromptSource =
|
||||
| {
|
||||
type: "text";
|
||||
type: "inline";
|
||||
content: string;
|
||||
}
|
||||
| {
|
||||
@@ -19,6 +19,16 @@ export type SafetyStrategy =
|
||||
| "unprivileged_user"
|
||||
| "unsafe";
|
||||
|
||||
export type OutputSchemaSource =
|
||||
| {
|
||||
type: "file";
|
||||
path: string;
|
||||
}
|
||||
| {
|
||||
type: "inline";
|
||||
content: string;
|
||||
};
|
||||
|
||||
export async function runCodexExec({
|
||||
prompt,
|
||||
codexHome,
|
||||
@@ -26,7 +36,7 @@ export async function runCodexExec({
|
||||
proxyPort,
|
||||
extraArgs,
|
||||
explicitOutputFile,
|
||||
outputSchemaFile,
|
||||
outputSchema,
|
||||
model,
|
||||
safetyStrategy,
|
||||
codexUser,
|
||||
@@ -37,14 +47,14 @@ export async function runCodexExec({
|
||||
proxyPort: number;
|
||||
extraArgs: Array<string>;
|
||||
explicitOutputFile: string | null;
|
||||
outputSchemaFile: string | null;
|
||||
outputSchema: OutputSchemaSource | null;
|
||||
model: string | null;
|
||||
safetyStrategy: SafetyStrategy;
|
||||
codexUser: string | null;
|
||||
}): Promise<void> {
|
||||
let input: string;
|
||||
switch (prompt.type) {
|
||||
case "text":
|
||||
case "inline":
|
||||
input = prompt.content;
|
||||
break;
|
||||
case "file":
|
||||
@@ -59,6 +69,8 @@ export async function runCodexExec({
|
||||
outputFile = await createTempOutputFile();
|
||||
}
|
||||
|
||||
const resolvedOutputSchema = await resolveOutputSchema(outputSchema);
|
||||
|
||||
const command: Array<string> = [];
|
||||
|
||||
if (safetyStrategy === "unprivileged_user") {
|
||||
@@ -89,8 +101,8 @@ export async function runCodexExec({
|
||||
outputFile.file
|
||||
);
|
||||
|
||||
if (outputSchemaFile != null) {
|
||||
command.push("--output-schema", outputSchemaFile);
|
||||
if (resolvedOutputSchema != null) {
|
||||
command.push("--output-schema", resolvedOutputSchema.file);
|
||||
}
|
||||
|
||||
if (model != null) {
|
||||
@@ -119,30 +131,34 @@ export async function runCodexExec({
|
||||
.map((a) => JSON.stringify(a))
|
||||
.join(" ")}`
|
||||
);
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(program, command, {
|
||||
env,
|
||||
stdio: ["pipe", "inherit", "inherit"],
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
const child = spawn(program, command, {
|
||||
env,
|
||||
stdio: ["pipe", "inherit", "inherit"],
|
||||
});
|
||||
child.stdin.write(input);
|
||||
child.stdin.end();
|
||||
|
||||
child.on("error", reject);
|
||||
|
||||
child.on("close", async (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`${program} exited with code ${code}`));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await finalizeExecution(outputFile);
|
||||
resolve(undefined);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
child.stdin.write(input);
|
||||
child.stdin.end();
|
||||
|
||||
child.on("error", reject);
|
||||
|
||||
child.on("close", async (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`${program} exited with code ${code}`));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await finalizeExecution(outputFile);
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
await cleanupOutputSchema(resolvedOutputSchema);
|
||||
}
|
||||
}
|
||||
|
||||
async function finalizeExecution(outputFile: OutputFile): Promise<void> {
|
||||
@@ -164,6 +180,17 @@ type OutputFile =
|
||||
file: string;
|
||||
};
|
||||
|
||||
type ResolvedOutputSchema =
|
||||
| {
|
||||
type: "explicit";
|
||||
file: string;
|
||||
}
|
||||
| {
|
||||
type: "temp";
|
||||
file: string;
|
||||
dir: string;
|
||||
};
|
||||
|
||||
async function createTempOutputFile(): Promise<OutputFile> {
|
||||
const dir = await mkdtemp("codex-exec-");
|
||||
return { type: "temp", file: path.join(dir, "output.md") };
|
||||
@@ -182,3 +209,38 @@ async function cleanupTempOutput(outputFile: OutputFile): Promise<void> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveOutputSchema(
|
||||
schema: OutputSchemaSource | null
|
||||
): Promise<ResolvedOutputSchema | null> {
|
||||
if (schema == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (schema.type) {
|
||||
case "file":
|
||||
return { type: "explicit", file: schema.path };
|
||||
case "inline": {
|
||||
const dir = await mkdtemp("codex-output-schema-");
|
||||
const file = path.join(dir, "schema.json");
|
||||
await writeFile(file, schema.content);
|
||||
return { type: "temp", file, dir };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupOutputSchema(
|
||||
schema: ResolvedOutputSchema | null
|
||||
): Promise<void> {
|
||||
if (schema == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (schema.type) {
|
||||
case "explicit":
|
||||
return;
|
||||
case "temp":
|
||||
await rm(schema.dir, { recursive: true, force: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user