mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
feat(codex): let Codex sessions drive other sessions through the codeman skill
Measured on 1.47.0 with the same "spawn a shell worker, echo, read, delete" task: DeepSeek Harness and OpenCode already pass end to end; Codex 0.162 failed three ways, fixed here. - Start Codex 0.162+ with --no-daemon (new noDaemonFlag gate, fail-closed). The shared app-server daemon stopped every launch on an "incompatible feature settings" dialog when another version's daemon was running, and ran commands with another session's CODEMAN_* environment. - Evaluate registry version gates against the gated CLI's own version (resolveSessionCliVersion) instead of claude's for every entry. - Add the opt-in synced setting App Settings > Codex > Allow network access (codexNetworkAccess, default off): --config sandbox_workspace_write.network_access=true, filled as a launch default and clamped off for non-granted owners like the bypass switch. - Skill: carry on with the curl path when `codeman` is not on PATH, re-run a sandbox-blocked call once through the CLI's approval path, name the new setting. Wiki and registry docs updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f1f7eec4d6
commit
87c656283d
20 files changed
+284
-34
No files matched your search
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @fileoverview A launch gate is measured against its OWN CLI's version.
|
||||
*
|
||||
* `capabilities.gates` used to be resolved with `getClaudeCliVersion()` for every entry,
|
||||
* because claude was the only CLI with a gate. Codex's `noDaemonFlag` (0.162.0) would then
|
||||
* have passed on any claude 2.x and handed an older codex `--no-daemon`, which makes codex
|
||||
* exit on launch. The bridge now asks `resolveSessionCliVersion(entry.id)`; this pins that
|
||||
* with both resolvers mocked to DIFFERENT versions.
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const versions: Record<string, string | null> = {};
|
||||
|
||||
vi.mock('../src/utils/cli-resolver.js', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../src/utils/cli-resolver.js')>();
|
||||
return { ...actual, resolveSessionCliVersion: vi.fn((mode: string) => versions[mode] ?? null) };
|
||||
});
|
||||
vi.mock('../src/utils/claude-cli-resolver.js', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../src/utils/claude-cli-resolver.js')>();
|
||||
return { ...actual, getClaudeCliVersion: vi.fn(() => '2.1.316') };
|
||||
});
|
||||
|
||||
const { getCli } = await import('../src/config/cli-registry/registry.js');
|
||||
const { buildSpawnCommandFromRegistry, launchGatePasses } = await import('../src/session-cli-registry-bridge.js');
|
||||
|
||||
const SID = '00000000-0000-4000-8000-000000000000';
|
||||
|
||||
function codexCommand(): string | undefined {
|
||||
return buildSpawnCommandFromRegistry(getCli('codex')!, { mode: 'codex', sessionId: SID });
|
||||
}
|
||||
|
||||
describe('launch gates read the gated CLI version', () => {
|
||||
beforeEach(() => {
|
||||
for (const key of Object.keys(versions)) delete versions[key];
|
||||
});
|
||||
|
||||
it("does not pass codex's gate on claude's version", () => {
|
||||
versions.claude = '2.1.316';
|
||||
versions.codex = '0.161.0';
|
||||
expect(codexCommand()).toBe('codex');
|
||||
expect(launchGatePasses('codex', 'noDaemonFlag')).toBe(false);
|
||||
});
|
||||
|
||||
it("passes codex's gate on codex's own version", () => {
|
||||
versions.codex = '0.162.0';
|
||||
expect(codexCommand()).toBe('codex --no-daemon');
|
||||
expect(launchGatePasses('codex', 'noDaemonFlag')).toBe(true);
|
||||
});
|
||||
|
||||
it('fails closed when the codex version is unknown', () => {
|
||||
versions.claude = '2.1.316';
|
||||
expect(codexCommand()).toBe('codex');
|
||||
});
|
||||
|
||||
it("still measures claude's gates on claude's version", () => {
|
||||
versions.claude = '2.1.316';
|
||||
versions.codex = '0.1.0';
|
||||
expect(launchGatePasses('claude', 'nameFlag')).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -391,7 +391,7 @@ describe('cross-field integrity', () => {
|
||||
// shipped entry must be on the param side of that line.
|
||||
const codex = STOCK_CLIS.find((e) => (e.id as string) === 'codex');
|
||||
expect(codex).toBeDefined();
|
||||
expect(codex!.capabilities.privilegedParams.map((c) => c.param)).toEqual(['bypassApprovals']);
|
||||
expect(codex!.capabilities.privilegedParams.map((c) => c.param)).toEqual(['bypassApprovals', 'networkAccess']);
|
||||
expect(codex!.launch.legacyConfigAliases?.bypassApprovals).toBe('dangerouslyBypassApprovals');
|
||||
|
||||
const wrong = baseEntry('codex');
|
||||
|
||||
@@ -182,6 +182,30 @@ describe('codex', () => {
|
||||
'codex --model gpt-5 --config model_reasoning_effort=high resume roll_42'
|
||||
);
|
||||
});
|
||||
|
||||
it('emits the sandbox network-access config only when asked', () => {
|
||||
expect(cx({ networkAccess: true })).toBe('codex --config sandbox_workspace_write.network_access=true');
|
||||
expect(cx({ networkAccess: false })).toBe('codex');
|
||||
});
|
||||
|
||||
// The `claudeCliVersion` option is the version every gate of THIS entry is measured
|
||||
// against (the name predates a second gated CLI); undefined probes codex itself.
|
||||
const cxv = (version: string | null, codexConfig?: SpawnBridgeOptions['codexConfig']) =>
|
||||
render({ mode: 'codex', sessionId: SID, codexConfig, claudeCliVersion: version });
|
||||
|
||||
it('runs without the shared app-server daemon from 0.162.0, and fails closed below or unknown', () => {
|
||||
expect(cxv('0.162.0')).toBe('codex --no-daemon');
|
||||
expect(cxv('0.166.0')).toBe('codex --no-daemon');
|
||||
expect(cxv('0.161.9')).toBe('codex');
|
||||
expect(cxv(null)).toBe('codex');
|
||||
});
|
||||
|
||||
it('keeps every flag ahead of the resume subcommand with the daemon flag on', () => {
|
||||
expect(cxv('0.162.0', { dangerouslyBypassApprovals: true, networkAccess: true, resumeSessionId: 'roll_42' })).toBe(
|
||||
'codex --no-daemon --dangerously-bypass-approvals-and-sandbox ' +
|
||||
'--config sandbox_workspace_write.network_access=true resume roll_42'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gemini', () => {
|
||||
|
||||
@@ -99,7 +99,7 @@ describe('clampExternalCliBypassForOwner — multi-user mode', () => {
|
||||
undefined,
|
||||
{ alwaysApprove: true, model: 'grok-4.5' }
|
||||
);
|
||||
expect(out.codexConfig).toEqual({ dangerouslyBypassApprovals: false, model: 'gpt-5' });
|
||||
expect(out.codexConfig).toEqual({ dangerouslyBypassApprovals: false, networkAccess: false, model: 'gpt-5' });
|
||||
expect(out.antigravityConfig).toEqual({ dangerouslySkipPermissions: false, model: 'gemini-3-pro' });
|
||||
expect(out.grokConfig).toEqual({ alwaysApprove: false, model: 'grok-4.5' });
|
||||
});
|
||||
@@ -134,7 +134,26 @@ describe('clampExternalCliBypassForOwner — multi-user mode', () => {
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
expect(out.codexConfig).toEqual({ dangerouslyBypassApprovals: false, reasoningEffort: 'xhigh' });
|
||||
expect(out.codexConfig).toEqual({
|
||||
dangerouslyBypassApprovals: false,
|
||||
networkAccess: false,
|
||||
reasoningEffort: 'xhigh',
|
||||
});
|
||||
});
|
||||
|
||||
it("forces a non-granted owner's codex network access off, as when the launch default filled it", async () => {
|
||||
// applyLaunchDefaults runs BEFORE this clamp, so App Settings → Allow network access
|
||||
// arrives here as an ordinary `networkAccess: true` in a sent config. It widens the
|
||||
// sandbox to every host, so a non-granted owner must not get it.
|
||||
const out = await _clampExternalCliBypassForOwner(
|
||||
'peon',
|
||||
{ networkAccess: true },
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
expect(out.codexConfig).toEqual({ dangerouslyBypassApprovals: false, networkAccess: false });
|
||||
});
|
||||
|
||||
it('leaves codex/antigravity/grok absent when nothing was sent (they already spawn safe)', async () => {
|
||||
|
||||
@@ -89,6 +89,32 @@ describe('Codex launch defaults', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('fills Allow network access into every local codex launch, and an explicit value wins', async () => {
|
||||
await writeFile(SETTINGS_PATH, JSON.stringify({ codexNetworkAccess: true }));
|
||||
expect(await resolveCodexDefaults(undefined)).toEqual({ networkAccess: true });
|
||||
expect(buildCodexCommand(await resolveCodexDefaults(undefined))).toBe(
|
||||
'codex --config sandbox_workspace_write.network_access=true'
|
||||
);
|
||||
// The browser sends a codexConfig without the field, so the default still applies.
|
||||
expect(await resolveCodexDefaults({ dangerouslyBypassApprovals: false })).toEqual({
|
||||
dangerouslyBypassApprovals: false,
|
||||
networkAccess: true,
|
||||
});
|
||||
expect(await resolveCodexDefaults({ networkAccess: false })).toEqual({ networkAccess: false });
|
||||
// Off (and a malformed value) fills nothing, so the launch stays byte-identical.
|
||||
for (const value of [false, 'yes']) {
|
||||
await writeFile(SETTINGS_PATH, JSON.stringify({ codexNetworkAccess: value }));
|
||||
expect(await resolveCodexDefaults(undefined)).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
for (const url of ['/api/sessions', '/api/quick-start']) {
|
||||
it(`records Allow network access on a codex session created at ${url}`, async () => {
|
||||
await writeFile(SETTINGS_PATH, JSON.stringify({ codexNetworkAccess: true }));
|
||||
expect(await createdConfig(url)).toMatchObject({ networkAccess: true });
|
||||
});
|
||||
}
|
||||
|
||||
it('keeps defaults out of custom endpoint launches', async () => {
|
||||
const config = { model: 'local-model', animations: false };
|
||||
expect(await resolveCodexDefaults(config, true)).toBe(config);
|
||||
@@ -100,6 +126,7 @@ describe('Codex launch defaults', () => {
|
||||
expect(getCli('codex')!.capabilities.launchDefaults).toEqual({
|
||||
model: 'codexModel',
|
||||
reasoningEffort: 'codexReasoningEffort',
|
||||
networkAccess: 'codexNetworkAccess',
|
||||
});
|
||||
for (const entry of STOCK_CLIS) {
|
||||
if ((entry.id as string) === 'codex') continue;
|
||||
|
||||
Reference in new issue
Block a user