Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,24 @@ To use Claude Code's full functionality, you'll need to manually enable tools:

**Recommended approach**: Start with basic tools enabled and add more as needed. You can always adjust these settings later.

### Codex Permission Defaults

Codex chat supports three permission modes:

- `default` — uses Codex's `workspace-write` sandbox with the `untrusted` approval policy.
- `acceptEdits` — uses the `workspace-write` sandbox with approval disabled, so Codex can apply workspace edits without prompting while staying sandboxed.
- `bypassPermissions` — uses `danger-full-access` with no approval. Use it only in trusted local or sandboxed workspaces.

The UI sends the selected mode with each Codex request. For self-hosted deployments that need a server-side fallback when a client does not send a mode, set `CLOUDCLI_CODEX_PERMISSION_MODE` to one of those values. If unset or invalid, CloudCLI uses `default`.

Examples:

```bash
CLOUDCLI_CODEX_PERMISSION_MODE=default npx @cloudcli-ai/cloudcli
CLOUDCLI_CODEX_PERMISSION_MODE=acceptEdits npx @cloudcli-ai/cloudcli
CLOUDCLI_CODEX_PERMISSION_MODE=bypassPermissions npx @cloudcli-ai/cloudcli
```

---

## Plugins
Expand Down
52 changes: 52 additions & 0 deletions server/codex-permission-mode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export const CODEX_PERMISSION_MODE_ENV = 'CLOUDCLI_CODEX_PERMISSION_MODE';

const CODEX_PERMISSION_MODES = new Set(['default', 'acceptEdits', 'bypassPermissions']);

function formatInvalidPermissionMode(value) {
return JSON.stringify(String(value).trim());
}

function warnInvalidPermissionMode(logger, message) {
if (logger && typeof logger.warn === 'function') {
logger.warn(message);
}
}

export function getConfiguredCodexPermissionMode(env = process.env, logger = console) {
const configuredPermissionMode = env[CODEX_PERMISSION_MODE_ENV];
if (configuredPermissionMode == null || String(configuredPermissionMode).trim() === '') {
return 'default';
}

const normalizedPermissionMode = String(configuredPermissionMode).trim();
if (CODEX_PERMISSION_MODES.has(normalizedPermissionMode)) {
return normalizedPermissionMode;
}

warnInvalidPermissionMode(
logger,
`[Codex] Invalid ${CODEX_PERMISSION_MODE_ENV}=${formatInvalidPermissionMode(normalizedPermissionMode)}; falling back to default`,
);
return 'default';
}

export function resolveCodexPermissionMode(
permissionMode,
hasExplicitPermissionMode,
{ env = process.env, logger = console } = {},
) {
if (!hasExplicitPermissionMode) {
return getConfiguredCodexPermissionMode(env, logger);
}

const normalizedPermissionMode = String(permissionMode).trim();
if (CODEX_PERMISSION_MODES.has(normalizedPermissionMode)) {
return normalizedPermissionMode;
}

warnInvalidPermissionMode(
logger,
`[Codex] Invalid request permission mode=${formatInvalidPermissionMode(normalizedPermissionMode)}; falling back to default`,
);
return 'default';
}
105 changes: 105 additions & 0 deletions server/codex-permission-mode.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import assert from 'node:assert/strict';
import test from 'node:test';

import {
CODEX_PERMISSION_MODE_ENV,
getConfiguredCodexPermissionMode,
resolveCodexPermissionMode,
} from './codex-permission-mode.js';

function createLogger() {
const warnings = [];
return {
warnings,
warn(message) {
warnings.push(message);
},
};
}

test('resolveCodexPermissionMode preserves an explicit request mode when env is unset', () => {
const logger = createLogger();

const resolved = resolveCodexPermissionMode('acceptEdits', true, {
env: {},
logger,
});

assert.equal(resolved, 'acceptEdits');
assert.deepEqual(logger.warnings, []);
});

test('resolveCodexPermissionMode lets explicit request mode override env fallback', () => {
const logger = createLogger();

const resolved = resolveCodexPermissionMode('default', true, {
env: { [CODEX_PERMISSION_MODE_ENV]: 'bypassPermissions' },
logger,
});

assert.equal(resolved, 'default');
assert.deepEqual(logger.warnings, []);
});

test('resolveCodexPermissionMode defaults omitted mode to default when env is unset', () => {
const logger = createLogger();

const resolved = resolveCodexPermissionMode(undefined, false, {
env: {},
logger,
});

assert.equal(resolved, 'default');
assert.deepEqual(logger.warnings, []);
});

test('resolveCodexPermissionMode uses acceptEdits env fallback when request omits mode', () => {
const logger = createLogger();

const resolved = resolveCodexPermissionMode(undefined, false, {
env: { [CODEX_PERMISSION_MODE_ENV]: 'acceptEdits' },
logger,
});

assert.equal(resolved, 'acceptEdits');
assert.deepEqual(logger.warnings, []);
});

test('resolveCodexPermissionMode uses bypassPermissions env fallback when request omits mode', () => {
const logger = createLogger();

const resolved = resolveCodexPermissionMode(undefined, false, {
env: { [CODEX_PERMISSION_MODE_ENV]: 'bypassPermissions' },
logger,
});

assert.equal(resolved, 'bypassPermissions');
assert.deepEqual(logger.warnings, []);
});

test('getConfiguredCodexPermissionMode warns and falls back to default for invalid env values', () => {
const logger = createLogger();

const resolved = getConfiguredCodexPermissionMode({
[CODEX_PERMISSION_MODE_ENV]: 'bad\nmode',
}, logger);

assert.equal(resolved, 'default');
assert.deepEqual(logger.warnings, [
`[Codex] Invalid ${CODEX_PERMISSION_MODE_ENV}="bad\\nmode"; falling back to default`,
]);
});

test('resolveCodexPermissionMode warns and falls back to default for invalid request values', () => {
const logger = createLogger();

const resolved = resolveCodexPermissionMode('bad\nmode', true, {
env: { [CODEX_PERMISSION_MODE_ENV]: 'acceptEdits' },
logger,
});

assert.equal(resolved, 'default');
assert.deepEqual(logger.warnings, [
'[Codex] Invalid request permission mode="bad\\nmode"; falling back to default',
]);
});
8 changes: 6 additions & 2 deletions server/openai-codex.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
*/

import { Codex } from '@openai/codex-sdk';

import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
import { sessionsService } from './modules/providers/services/sessions.service.js';
import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
import { providerModelsService } from './modules/providers/services/provider-models.service.js';
import { resolveCodexPermissionMode } from './codex-permission-mode.js';
import { createNormalizedMessage } from './shared/utils.js';

// Track active sessions
Expand Down Expand Up @@ -228,7 +230,7 @@ export async function queryCodex(command, options = {}, ws) {
cwd,
projectPath,
model,
permissionMode = 'default'
permissionMode
} = options;

const resolvedModel = await providerModelsService.resolveResumeModel(
Expand All @@ -237,8 +239,10 @@ export async function queryCodex(command, options = {}, ws) {
model,
);

const hasExplicitPermissionMode = Object.prototype.hasOwnProperty.call(options, 'permissionMode');
const effectivePermissionMode = resolveCodexPermissionMode(permissionMode, hasExplicitPermissionMode);
const workingDirectory = cwd || projectPath || process.cwd();
const { sandboxMode, approvalPolicy } = mapPermissionModeToCodexOptions(permissionMode);
const { sandboxMode, approvalPolicy } = mapPermissionModeToCodexOptions(effectivePermissionMode);

let codex;
let thread;
Expand Down