Add Codex permission profile support
This commit is contained in:
@@ -15,7 +15,9 @@ In the following example, we define a workflow that is triggered whenever a user
|
||||
- Runs Codex with a `prompt` that includes the details specific to the PR.
|
||||
- Takes the output from Codex and posts it as a comment on the PR.
|
||||
|
||||
See [`security.md`](./docs/security.md) for tips on using `openai/codex-action` securely.
|
||||
See [`security.md`](./docs/security.md) for tips on using `openai/codex-action` securely and the
|
||||
[Codex permissions documentation](https://developers.openai.com/codex/permissions) for configuring
|
||||
filesystem and network access.
|
||||
|
||||
```yaml
|
||||
name: Perform a code review when a pull request is created.
|
||||
@@ -48,14 +50,15 @@ jobs:
|
||||
"+refs/pull/$PR_NUMBER/head"
|
||||
|
||||
# If you want Codex to build and run code, install any dependencies that
|
||||
# need to be downloaded before the "Run Codex" step because Codex's
|
||||
# default sandbox disables network access.
|
||||
# need to be downloaded before the "Run Codex" step. The recommended
|
||||
# :workspace permission profile does not grant network access.
|
||||
|
||||
- name: Run Codex
|
||||
id: run_codex
|
||||
uses: openai/codex-action@v1
|
||||
with:
|
||||
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
|
||||
permission-profile: ":workspace"
|
||||
prompt: |
|
||||
This is PR #${{ github.event.pull_request.number }} for ${{ github.repository }}.
|
||||
|
||||
@@ -103,7 +106,8 @@ jobs:
|
||||
| `prompt-file` | Path (relative to the repository root) of a file that contains the prompt. Provide this or `prompt`. | `""` |
|
||||
| `output-file` | File where the final Codex message is written. Leave empty to skip writing a file. | `""` |
|
||||
| `working-directory` | Directory passed to `codex exec --cd`. Defaults to the repository root. | `""` |
|
||||
| `sandbox` | Sandbox mode for Codex. One of `workspace-write` (default), `read-only` or `danger-full-access`. | `""` |
|
||||
| `sandbox` | Legacy sandbox mode. Prefer `permission-profile: ":workspace"` for new workflows. Mutually exclusive with `permission-profile`. | `""` |
|
||||
| `permission-profile` | Built-in or configured [Codex permission profile](https://developers.openai.com/codex/permissions) selected through `default_permissions`. | `""` |
|
||||
| `codex-version` | Version of `@openai/codex` to install. | `""` |
|
||||
| `codex-args` | Extra arguments forwarded to `codex exec`. Accepts JSON arrays (`["--flag", "value"]`) or shell-style strings. | `""` |
|
||||
| `output-schema` | Inline schema contents written to a temp file and passed to `codex exec --output-schema`. Mutually exclusive with `output-schema-file`. | `""` |
|
||||
@@ -117,6 +121,39 @@ jobs:
|
||||
| `allow-bots` | Allow runs triggered by trusted GitHub bot accounts (`github-actions[bot]`) to bypass the write-access check. | `false` |
|
||||
| `allow-bot-users` | List of GitHub bot usernames that can bypass the write-access check. `*` is not supported; list trusted bots explicitly. | `""` |
|
||||
|
||||
## Permission profiles
|
||||
|
||||
Codex permission profiles independently describe filesystem and network access. For workflows that
|
||||
need to edit the checked-out repository, prefer `permission-profile: ":workspace"` over relying on
|
||||
the action's legacy `workspace-write` fallback. Use `:read-only` for read-only workflows, or select a
|
||||
named profile defined in the `config.toml` under `codex-home` when the workflow needs a more specific
|
||||
policy. See the
|
||||
[Codex permissions documentation](https://developers.openai.com/codex/permissions) for the profile
|
||||
schema and enforcement details. Permission profiles are beta and require Codex CLI `0.138.0` or
|
||||
later; do not select one while pinning an older `codex-version`.
|
||||
|
||||
The action does not pass `--sandbox` when `permission-profile` is set because the profile and legacy
|
||||
sandbox systems do not compose. Supplying both inputs fails before Codex starts. The
|
||||
`safety-strategy: read-only` option also forces the legacy read-only sandbox and therefore cannot be
|
||||
combined with a permission profile. Keep `safety-strategy: drop-sudo` or use a deliberately
|
||||
configured unprivileged user when selecting a profile.
|
||||
|
||||
For backward compatibility, omitting both `permission-profile` and `sandbox` still runs Codex with
|
||||
the legacy `workspace-write` sandbox. Existing callers that set `sandbox` continue to use the legacy
|
||||
model. Do not set `sandbox_mode` in `codex-args` or a loaded `config.toml` when selecting a permission
|
||||
profile; Codex treats any legacy sandbox setting as opting out of permission profiles.
|
||||
|
||||
For example, use the built-in `:workspace` profile for a workflow that needs to modify the checkout:
|
||||
|
||||
```yaml
|
||||
- name: Run Codex with a permission profile
|
||||
uses: openai/codex-action@v1
|
||||
with:
|
||||
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
|
||||
permission-profile: ":workspace"
|
||||
prompt: Review the public change.
|
||||
```
|
||||
|
||||
## Safety Strategy
|
||||
|
||||
The `safety-strategy` input determines how much access Codex receives on the runner. Choosing the right option is critical, especially when sensitive secrets (like your OpenAI API key) are present.
|
||||
@@ -155,7 +192,7 @@ jobs:
|
||||
- Run this action after `actions/checkout@v5` so Codex has access to your repository contents.
|
||||
- To use a non-default Responses endpoint (for example Azure OpenAI), set `responses-api-endpoint` to the provider's URL while keeping `openai-api-key` populated; the proxy will still send `Authorization: Bearer <key>` upstream.
|
||||
- If you want Codex to have access to a narrow set of privileged functionality, consider running a local MCP server that can perform these actions and configure Codex to use it.
|
||||
- If you need more control over the CLI invocation, pass flags through `codex-args` or create a `config.toml` in `codex-home`.
|
||||
- If you need more control over the CLI invocation, pass flags through `codex-args` or create a `config.toml` in `codex-home`. Prefer a [permission profile](https://developers.openai.com/codex/permissions), starting with `:workspace` for workspace editing, over legacy sandbox flags for new integrations.
|
||||
- Once `openai/codex-action` is run once with `openai-api-key`, you can also call `codex` from subsequent scripts in your job. (You can omit `prompt` and `prompt-file` from the action in this case.)
|
||||
|
||||
## Azure
|
||||
|
||||
+13
-2
@@ -28,9 +28,18 @@ inputs:
|
||||
default: ""
|
||||
sandbox:
|
||||
description: |
|
||||
Sandbox mode for Codex. One of `workspace-write` (default), `read-only` or `danger-full-access`.
|
||||
Legacy sandbox mode for Codex. One of `workspace-write`, `read-only` or `danger-full-access`.
|
||||
Prefer `permission-profile: ":workspace"` for new workflows. Leave empty to use the legacy
|
||||
`workspace-write` fallback unless `permission-profile` is set.
|
||||
required: false
|
||||
default: "workspace-write"
|
||||
default: ""
|
||||
permission-profile:
|
||||
description: |
|
||||
Codex permission profile to select through `default_permissions`, such as `:read-only`,
|
||||
`:workspace`, or a named profile defined in `codex-home/config.toml`. Mutually exclusive
|
||||
with `sandbox`. See https://developers.openai.com/codex/permissions.
|
||||
required: false
|
||||
default: ""
|
||||
codex-version:
|
||||
description: "Version of `@openai/codex` to install."
|
||||
required: false
|
||||
@@ -330,6 +339,7 @@ runs:
|
||||
CODEX_HOME: ${{ steps.resolve_home.outputs.codex-home }}
|
||||
CODEX_WORKING_DIRECTORY: ${{ inputs['working-directory'] || github.workspace }}
|
||||
CODEX_SANDBOX: ${{ inputs.sandbox }}
|
||||
CODEX_PERMISSION_PROFILE: ${{ inputs['permission-profile'] }}
|
||||
CODEX_ARGS: ${{ inputs['codex-args'] }}
|
||||
CODEX_OUTPUT_SCHEMA: ${{ inputs['output-schema'] }}
|
||||
CODEX_OUTPUT_SCHEMA_FILE: ${{ inputs['output-schema-file'] }}
|
||||
@@ -351,6 +361,7 @@ runs:
|
||||
--output-schema "$CODEX_OUTPUT_SCHEMA" \
|
||||
--output-schema-file "$CODEX_OUTPUT_SCHEMA_FILE" \
|
||||
--sandbox "$CODEX_SANDBOX" \
|
||||
--permission-profile "$CODEX_PERMISSION_PROFILE" \
|
||||
--model "$CODEX_MODEL" \
|
||||
--effort "$CODEX_EFFORT" \
|
||||
--safety-strategy "$CODEX_SAFETY_STRATEGY" \
|
||||
|
||||
Vendored
+78
-17
File diff suppressed because one or more lines are too long
@@ -17,6 +17,25 @@ There is a lot of valuable context that can be used to fuel your invocation of C
|
||||
- **Repository instruction files**: when Codex operates on pull request-controlled content, files such as `AGENTS.md`, `AGENTS.override.md`, or configured fallback project docs from that content should be considered part of the untrusted input surface.
|
||||
- **Screenshots**: screenshots and other media have been known to be used as vehicles for prompt injection.
|
||||
|
||||
## Limit command permissions
|
||||
|
||||
Use `permission-profile` to select the narrowest filesystem and network policy that still lets Codex
|
||||
complete the task. For workflows that edit the checkout, prefer the built-in `:workspace` profile
|
||||
over the legacy `sandbox: workspace-write` setting. Use a custom profile when the workflow needs a
|
||||
more specific policy. See the
|
||||
[Codex permissions documentation](https://developers.openai.com/codex/permissions) for the available
|
||||
built-in profiles and configuration schema.
|
||||
|
||||
Permission profiles constrain commands that Codex runs; they do not replace the action's
|
||||
`safety-strategy`, which controls the privileges of the Codex process itself. Continue to use
|
||||
`drop-sudo` or a deliberately configured `unprivileged-user` when a profile grants filesystem writes
|
||||
or network access.
|
||||
|
||||
Permission profiles and the legacy `sandbox` input do not compose. The action rejects both inputs
|
||||
together, but a `sandbox_mode` setting in `codex-args` or a loaded `config.toml` also opts Codex into
|
||||
the legacy sandbox model. Review every loaded configuration layer when a workflow is expected to use
|
||||
a permission profile.
|
||||
|
||||
## Avoid shell injection in workflow steps
|
||||
|
||||
GitHub Actions expands `${{ ... }}` expressions before the shell runs your `run:` script. If you splice untrusted values such as branch names, issue titles, comment bodies, or action inputs directly into the script, those values can break shell quoting and execute arbitrary commands.
|
||||
|
||||
+16
-6
@@ -148,7 +148,11 @@ export async function main() {
|
||||
)
|
||||
.requiredOption(
|
||||
"--sandbox <SANDBOX>",
|
||||
"Sandbox mode override to pass to `codex exec`."
|
||||
"Legacy sandbox mode override to pass to `codex exec` (may be empty)."
|
||||
)
|
||||
.requiredOption(
|
||||
"--permission-profile <PROFILE>",
|
||||
"Permission profile to select through `default_permissions` (may be empty)."
|
||||
)
|
||||
.requiredOption("--model <model>", "Model the agent should use")
|
||||
.requiredOption("--effort <effort>", "Reasoning effort the agent should use")
|
||||
@@ -171,6 +175,7 @@ export async function main() {
|
||||
outputSchemaFile: string;
|
||||
outputSchema: string;
|
||||
sandbox: string;
|
||||
permissionProfile: string;
|
||||
model: string;
|
||||
effort: string;
|
||||
safetyStrategy: string;
|
||||
@@ -186,6 +191,7 @@ export async function main() {
|
||||
outputSchema,
|
||||
outputSchemaFile,
|
||||
sandbox,
|
||||
permissionProfile,
|
||||
model,
|
||||
effort,
|
||||
safetyStrategy,
|
||||
@@ -245,7 +251,8 @@ export async function main() {
|
||||
extraArgs,
|
||||
explicitOutputFile: emptyAsNull(outputFile),
|
||||
outputSchema: outputSchemaSource,
|
||||
sandbox: toSandboxMode(sandbox),
|
||||
sandbox: toOptionalSandboxMode(sandbox),
|
||||
permissionProfile: emptyAsNull(permissionProfile),
|
||||
model: emptyAsNull(model),
|
||||
effort: emptyAsNull(effort),
|
||||
safetyStrategy: toSafetyStrategy(safetyStrategy),
|
||||
@@ -341,15 +348,18 @@ function toSafetyStrategy(value: string): SafetyStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
function toSandboxMode(value: string): SandboxMode {
|
||||
switch (value) {
|
||||
function toOptionalSandboxMode(value: string): SandboxMode | null {
|
||||
const normalized = emptyAsNull(value);
|
||||
switch (normalized) {
|
||||
case null:
|
||||
return null;
|
||||
case "read-only":
|
||||
case "workspace-write":
|
||||
case "danger-full-access":
|
||||
return value;
|
||||
return normalized;
|
||||
default:
|
||||
throw new Error(
|
||||
`Invalid sandbox: ${value}. Must be one of 'read-only', 'workspace-write', or 'danger-full-access'.`
|
||||
`Invalid sandbox: ${normalized}. Must be one of 'read-only', 'workspace-write', or 'danger-full-access'.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+86
-10
@@ -26,6 +26,10 @@ export type SandboxMode =
|
||||
| "workspace-write"
|
||||
| "danger-full-access";
|
||||
|
||||
type PermissionSelection =
|
||||
| { type: "sandbox"; mode: SandboxMode }
|
||||
| { type: "profile"; name: string };
|
||||
|
||||
export type OutputSchemaSource =
|
||||
| {
|
||||
type: "file";
|
||||
@@ -36,6 +40,15 @@ export type OutputSchemaSource =
|
||||
content: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds and runs a `codex exec` command, writes the prompt to its standard input, and publishes
|
||||
* the command's final message as the action output.
|
||||
*
|
||||
* Authentication is intentionally outside this function. The composite action starts or reuses
|
||||
* the Responses API proxy and writes the corresponding Codex configuration before invoking this
|
||||
* command. Keeping that setup separate also lets tests put a fake `codex` executable on `PATH` to
|
||||
* verify command construction and output handling without an API key or network request.
|
||||
*/
|
||||
export async function runCodexExec({
|
||||
prompt,
|
||||
codexHome,
|
||||
@@ -48,6 +61,7 @@ export async function runCodexExec({
|
||||
safetyStrategy,
|
||||
codexUser,
|
||||
sandbox,
|
||||
permissionProfile,
|
||||
}: {
|
||||
prompt: PromptSource;
|
||||
codexHome: string | null;
|
||||
@@ -59,7 +73,8 @@ export async function runCodexExec({
|
||||
effort: string | null;
|
||||
safetyStrategy: SafetyStrategy;
|
||||
codexUser: string | null;
|
||||
sandbox: SandboxMode;
|
||||
sandbox: SandboxMode | null;
|
||||
permissionProfile: string | null;
|
||||
}): Promise<void> {
|
||||
let input: string;
|
||||
switch (prompt.type) {
|
||||
@@ -85,9 +100,11 @@ export async function runCodexExec({
|
||||
outputSchema,
|
||||
runAsUser
|
||||
);
|
||||
const sandboxMode = await determineSandboxMode({
|
||||
const permissionSelection = determinePermissionSelection({
|
||||
safetyStrategy,
|
||||
requestedSandbox: sandbox,
|
||||
permissionProfile,
|
||||
extraArgs,
|
||||
});
|
||||
|
||||
const command: Array<string> = [];
|
||||
@@ -143,7 +160,17 @@ export async function runCodexExec({
|
||||
|
||||
command.push(...extraArgs);
|
||||
|
||||
command.push("--sandbox", sandboxMode);
|
||||
switch (permissionSelection.type) {
|
||||
case "sandbox":
|
||||
command.push("--sandbox", permissionSelection.mode);
|
||||
break;
|
||||
case "profile":
|
||||
command.push(
|
||||
"--config",
|
||||
`default_permissions=${JSON.stringify(permissionSelection.name)}`
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
const env = { ...process.env };
|
||||
if (!env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE) {
|
||||
@@ -323,16 +350,65 @@ async function createTempDir(
|
||||
}
|
||||
}
|
||||
|
||||
async function determineSandboxMode({
|
||||
function determinePermissionSelection({
|
||||
safetyStrategy,
|
||||
requestedSandbox,
|
||||
permissionProfile,
|
||||
extraArgs,
|
||||
}: {
|
||||
safetyStrategy: SafetyStrategy;
|
||||
requestedSandbox: SandboxMode;
|
||||
}): Promise<SandboxMode> {
|
||||
if (safetyStrategy === "read-only") {
|
||||
return "read-only";
|
||||
} else {
|
||||
return requestedSandbox;
|
||||
requestedSandbox: SandboxMode | null;
|
||||
permissionProfile: string | null;
|
||||
extraArgs: Array<string>;
|
||||
}): PermissionSelection {
|
||||
if (permissionProfile != null && requestedSandbox != null) {
|
||||
throw new Error(
|
||||
"`permission-profile` and `sandbox` are mutually exclusive. Permission profiles do not compose with legacy sandbox settings."
|
||||
);
|
||||
}
|
||||
if (permissionProfile != null && safetyStrategy === "read-only") {
|
||||
throw new Error(
|
||||
"`permission-profile` cannot be combined with the `read-only` safety strategy because that strategy forces the legacy read-only sandbox."
|
||||
);
|
||||
}
|
||||
if (permissionProfile != null && extraArgsSelectSandbox(extraArgs)) {
|
||||
throw new Error(
|
||||
"`permission-profile` cannot be combined with a sandbox override in `codex-args`."
|
||||
);
|
||||
}
|
||||
if (safetyStrategy === "read-only") {
|
||||
return { type: "sandbox", mode: "read-only" };
|
||||
}
|
||||
if (permissionProfile != null) {
|
||||
return { type: "profile", name: permissionProfile };
|
||||
}
|
||||
return { type: "sandbox", mode: requestedSandbox ?? "workspace-write" };
|
||||
}
|
||||
|
||||
function extraArgsSelectSandbox(args: Array<string>): boolean {
|
||||
return args.some((arg, index) => {
|
||||
if (
|
||||
arg === "--sandbox" ||
|
||||
arg.startsWith("--sandbox=") ||
|
||||
arg === "-s" ||
|
||||
arg.startsWith("-s=")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (arg === "--config" || arg === "-c") {
|
||||
return configOverrideSelectsSandbox(args[index + 1]);
|
||||
}
|
||||
if (arg.startsWith("--config=")) {
|
||||
return configOverrideSelectsSandbox(arg.slice("--config=".length));
|
||||
}
|
||||
if (arg.startsWith("-c=")) {
|
||||
return configOverrideSelectsSandbox(arg.slice("-c=".length));
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function configOverrideSelectsSandbox(override: string | undefined): boolean {
|
||||
const key = override?.trimStart().split(/[=.]/, 1)[0];
|
||||
return key === "sandbox_mode" || key === "sandbox_workspace_write";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
chmodSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { test } from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const mainPath = fileURLToPath(new URL("../dist/main.js", import.meta.url));
|
||||
|
||||
/**
|
||||
* Runs the bundled `run-codex-exec` command with a generated fake `codex` executable first on
|
||||
* `PATH`. The fake captures its arguments and writes a final-message file without making an API
|
||||
* request, so successful cases exercise command construction, process spawning, and output
|
||||
* handling without an API key. Validation-error cases assert that the fake was never spawned.
|
||||
*/
|
||||
function runCodexExecWithFakeCodex({
|
||||
sandbox = "",
|
||||
permissionProfile = "",
|
||||
extraArgs = "",
|
||||
safetyStrategy = "drop-sudo",
|
||||
} = {}) {
|
||||
const tempDir = mkdtempSync(path.join(tmpdir(), "codex-action-permissions-"));
|
||||
const capturePath = path.join(tempDir, "args.json");
|
||||
const outputPath = path.join(tempDir, "output.txt");
|
||||
const fakeCodexPath = path.join(tempDir, "codex.mjs");
|
||||
writeFileSync(
|
||||
fakeCodexPath,
|
||||
`import { writeFileSync } from "node:fs";
|
||||
const args = process.argv.slice(2);
|
||||
writeFileSync(process.env.CODEX_CAPTURE_ARGS, JSON.stringify(args));
|
||||
const outputIndex = args.indexOf("--output-last-message");
|
||||
if (outputIndex < 0 || outputIndex + 1 >= args.length) {
|
||||
throw new Error("missing --output-last-message");
|
||||
}
|
||||
writeFileSync(args[outputIndex + 1], "fake final message\\n");
|
||||
`,
|
||||
"utf8"
|
||||
);
|
||||
const posixLauncher = path.join(tempDir, "codex");
|
||||
writeFileSync(
|
||||
posixLauncher,
|
||||
`#!/bin/sh\nexec node "${fakeCodexPath}" "$@"\n`,
|
||||
"utf8"
|
||||
);
|
||||
chmodSync(posixLauncher, 0o755);
|
||||
writeFileSync(
|
||||
path.join(tempDir, "codex.cmd"),
|
||||
`@node "${fakeCodexPath}" %*\r\n`,
|
||||
"utf8"
|
||||
);
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
mainPath,
|
||||
"run-codex-exec",
|
||||
"--prompt",
|
||||
"test prompt",
|
||||
"--prompt-file",
|
||||
"",
|
||||
"--codex-home",
|
||||
"",
|
||||
"--cd",
|
||||
tempDir,
|
||||
"--extra-args",
|
||||
extraArgs,
|
||||
"--output-file",
|
||||
outputPath,
|
||||
"--output-schema-file",
|
||||
"",
|
||||
"--output-schema",
|
||||
"",
|
||||
"--sandbox",
|
||||
sandbox,
|
||||
"--permission-profile",
|
||||
permissionProfile,
|
||||
"--model",
|
||||
"",
|
||||
"--effort",
|
||||
"",
|
||||
"--safety-strategy",
|
||||
safetyStrategy,
|
||||
"--codex-user",
|
||||
"",
|
||||
],
|
||||
{
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${tempDir}${path.delimiter}${process.env.PATH ?? ""}`,
|
||||
CODEX_CAPTURE_ARGS: capturePath,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
let capturedArgs = null;
|
||||
try {
|
||||
capturedArgs = JSON.parse(readFileSync(capturePath, "utf8"));
|
||||
} catch {
|
||||
// Expected when argument validation rejects the invocation before spawning Codex.
|
||||
}
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
return { result, capturedArgs };
|
||||
}
|
||||
|
||||
test("preserves workspace-write as the default legacy sandbox", () => {
|
||||
const { result, capturedArgs } = runCodexExecWithFakeCodex();
|
||||
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.deepEqual(capturedArgs.slice(-2), ["--sandbox", "workspace-write"]);
|
||||
});
|
||||
|
||||
test("selects a permission profile without passing --sandbox", () => {
|
||||
const { result, capturedArgs } = runCodexExecWithFakeCodex({
|
||||
permissionProfile: "public-review",
|
||||
});
|
||||
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(capturedArgs.includes("--sandbox"), false);
|
||||
assert.deepEqual(capturedArgs.slice(-2), [
|
||||
"--config",
|
||||
'default_permissions="public-review"',
|
||||
]);
|
||||
});
|
||||
|
||||
test("rejects permission-profile with sandbox", () => {
|
||||
const { result, capturedArgs } = runCodexExecWithFakeCodex({
|
||||
permissionProfile: "public-review",
|
||||
sandbox: "read-only",
|
||||
});
|
||||
|
||||
assert.notEqual(result.status, 0);
|
||||
assert.equal(capturedArgs, null);
|
||||
assert.match(result.stderr, /mutually exclusive/);
|
||||
});
|
||||
|
||||
test("rejects permission-profile with the read-only safety strategy", () => {
|
||||
const { result, capturedArgs } = runCodexExecWithFakeCodex({
|
||||
permissionProfile: "public-review",
|
||||
safetyStrategy: "read-only",
|
||||
});
|
||||
|
||||
assert.notEqual(result.status, 0);
|
||||
assert.equal(capturedArgs, null);
|
||||
assert.match(result.stderr, /forces the legacy read-only sandbox/);
|
||||
});
|
||||
|
||||
test("rejects permission-profile with a sandbox in codex-args", () => {
|
||||
const { result, capturedArgs } = runCodexExecWithFakeCodex({
|
||||
permissionProfile: "public-review",
|
||||
extraArgs: '["--sandbox", "read-only"]',
|
||||
});
|
||||
|
||||
assert.notEqual(result.status, 0);
|
||||
assert.equal(capturedArgs, null);
|
||||
assert.match(result.stderr, /sandbox override in `codex-args`/);
|
||||
});
|
||||
|
||||
for (const extraArgs of [
|
||||
'["--config", "sandbox_workspace_write.network_access=true"]',
|
||||
'["--config=sandbox_workspace_write.network_access=true"]',
|
||||
]) {
|
||||
test(`rejects permission-profile with ${extraArgs} in codex-args`, () => {
|
||||
const { result, capturedArgs } = runCodexExecWithFakeCodex({
|
||||
permissionProfile: "public-review",
|
||||
extraArgs,
|
||||
});
|
||||
|
||||
assert.notEqual(result.status, 0);
|
||||
assert.equal(capturedArgs, null);
|
||||
assert.match(result.stderr, /sandbox override in `codex-args`/);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user