mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +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
@@ -480,7 +480,11 @@ const capabilitiesSchema = z
|
||||
launchDefaults: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.enum(['codexModel', 'codexReasoningEffort'] as const satisfies readonly LaunchDefaultSettingKey[])
|
||||
z.enum([
|
||||
'codexModel',
|
||||
'codexReasoningEffort',
|
||||
'codexNetworkAccess',
|
||||
] as const satisfies readonly LaunchDefaultSettingKey[])
|
||||
)
|
||||
.refine((v) => Object.keys(v).length >= 1 && Object.keys(v).length <= 8, 'launchDefaults takes 1 to 8 params')
|
||||
.optional(),
|
||||
|
||||
@@ -627,6 +627,7 @@ const CODEX: CliEntry = {
|
||||
launch: {
|
||||
params: {
|
||||
bypassApprovals: { type: 'bool' },
|
||||
networkAccess: { type: 'bool' },
|
||||
animations: { type: 'bool' },
|
||||
model: { type: 'token', pattern: 'model' },
|
||||
reasoningEffort: { type: 'enum', values: [...CODEX_REASONING_EFFORTS] },
|
||||
@@ -637,7 +638,33 @@ const CODEX: CliEntry = {
|
||||
id: 'default',
|
||||
args: [
|
||||
{ lit: 'codex' },
|
||||
// Codex 0.162 hands every client's tool commands to ONE shared background
|
||||
// app-server (`codex app-server --managed-daemon`), started by whichever client
|
||||
// came first. Two things break inside a Codeman pane, both measured 2026-11-01 on
|
||||
// 0.162.0: the commands run with the STARTER's environment, so
|
||||
// `$CODEMAN_SESSION_ID`/`$CODEMAN_API_URL` belong to another session (the codeman
|
||||
// skill's self-check 404s and its delete guard protects the wrong id), and a daemon
|
||||
// of another version (a 0.166 one left by a later install) stops every launch on an
|
||||
// "incompatible feature settings" dialog before the composer appears. `--no-daemon`
|
||||
// runs this session's own in-process server, which is what a pane per session
|
||||
// means anyway. Gated at the version it was measured on; an older or unknown
|
||||
// codex gets nothing (fail-closed), since an unknown flag makes codex exit.
|
||||
{ flag: '--no-daemon', when: { capabilityGate: 'noDaemonFlag' } },
|
||||
{ flag: '--dangerously-bypass-approvals-and-sandbox', when: { param: 'bypassApprovals', is: true } },
|
||||
// Lets commands in codex's workspace-write sandbox open network connections,
|
||||
// Codeman's own API on loopback included (the codeman skill's curl calls fail
|
||||
// with "couldn't connect" without it). Narrower than the bypass above: the
|
||||
// sandbox and the approval prompts stay. Measured on 0.162.0 (`codex sandbox` and
|
||||
// a live pane): loopback stays blocked under workspace-write until this is set.
|
||||
// ⚠️ Workspace-write ONLY: a folder codex runs read-only (one it does not trust,
|
||||
// such as a fresh non-git case) keeps the network closed and asks per command.
|
||||
// Deliberately not paired with `--sandbox workspace-write`, which would also
|
||||
// grant write access to a folder codex itself chose not to trust.
|
||||
{
|
||||
flag: '--config',
|
||||
value: 'sandbox_workspace_write.network_access=true',
|
||||
when: { param: 'networkAccess', is: true },
|
||||
},
|
||||
{ flag: '--config', value: 'tui.animations=true', when: { param: 'animations', is: true } },
|
||||
{ flag: '--config', value: 'tui.animations=false', when: { param: 'animations', is: false } },
|
||||
{ flag: '--model', valueFrom: 'model', when: { param: 'model', state: 'set' } },
|
||||
@@ -803,7 +830,14 @@ const CODEX: CliEntry = {
|
||||
// App Settings → Codex model / reasoning effort (synced), filled into a LOCAL launch's
|
||||
// codexConfig wherever the caller left the field unset. Launch-only: nothing writes
|
||||
// codex's own config.toml. Read by applyLaunchDefaults() in src/web/launch-defaults.ts.
|
||||
launchDefaults: { model: 'codexModel', reasoningEffort: 'codexReasoningEffort' },
|
||||
// `networkAccess` (App Settings → Codex → Allow network access) rides the same path, so
|
||||
// a codex session started by the codeman skill, `codeman agent spawn` or the MCP server
|
||||
// gets it as well as one started from the browser. Off (the default) fills nothing.
|
||||
launchDefaults: {
|
||||
model: 'codexModel',
|
||||
reasoningEffort: 'codexReasoningEffort',
|
||||
networkAccess: 'codexNetworkAccess',
|
||||
},
|
||||
// Two columns, like claude's, measured on a live 0.154.0 answer: the `•`/`›`/`⚠`
|
||||
// markers sit in the gutter, prose continuations sit at 2, and a nested YAML block
|
||||
// the model wrote rendered at 2/4/6/8 for its own 0/2/4/6. Replayed at 100, 120,
|
||||
@@ -828,7 +862,19 @@ const CODEX: CliEntry = {
|
||||
// does. codex is the entry where the two names differ (`bypassApprovals` here,
|
||||
// `dangerouslyBypassApprovals` on the wire), so it is the one that would have caught a
|
||||
// regression; `schema.ts` now rejects a name that is not a declared param.
|
||||
privilegedParams: [{ param: 'bypassApprovals', clampTo: false }],
|
||||
//
|
||||
// `networkAccess` widens the sandbox too (any host, not just Codeman's loopback API), so
|
||||
// a non-granted owner gets it forced off like the bypass. Only-if-sent is enough here as
|
||||
// well: an absent value leaves codex's own no-network default, and the launch default
|
||||
// above is filled BEFORE the clamp runs, so a filled `true` is still clamped.
|
||||
privilegedParams: [
|
||||
{ param: 'bypassApprovals', clampTo: false },
|
||||
{ param: 'networkAccess', clampTo: false },
|
||||
],
|
||||
// `--no-daemon` above. Evaluated against codex's OWN version (resolveSessionCliVersion).
|
||||
gates: {
|
||||
noDaemonFlag: { minVersion: '0.162.0', failClosed: true },
|
||||
},
|
||||
mcpConfig: {
|
||||
path: '.codex/config.toml',
|
||||
format: 'codex-toml',
|
||||
|
||||
@@ -110,7 +110,7 @@ export type ModelConfigResolverName = 'deepseek-route';
|
||||
* arbitrary setting onto a command line; each name must also be a `SettingsUpdateSchema`
|
||||
* key, which the resolver's typing enforces.
|
||||
*/
|
||||
export type LaunchDefaultSettingKey = 'codexModel' | 'codexReasoningEffort';
|
||||
export type LaunchDefaultSettingKey = 'codexModel' | 'codexReasoningEffort' | 'codexNetworkAccess';
|
||||
|
||||
/** The MCP config dialects `src/mcp-sync.ts` has an adapter for. */
|
||||
export type McpConfigFormat =
|
||||
|
||||
@@ -22,7 +22,7 @@ import { renderLaunch, type EngineValues, type ParamValues } from './config/cli-
|
||||
import { matchesPattern } from './config/cli-registry/patterns.js';
|
||||
import { buildAdvisorSettings, buildEffortCliArgs, sanitizeCliSessionName } from './session-cli-builder.js';
|
||||
import { compareVersions } from './utils/dependency-checker.js';
|
||||
import { getClaudeCliVersion } from './utils/claude-cli-resolver.js';
|
||||
import { resolveSessionCliVersion } from './utils/cli-resolver.js';
|
||||
import { launcherDefaultTarget } from './utils/cli-launcher.js';
|
||||
import { getCli } from './config/cli-registry/registry.js';
|
||||
import type {
|
||||
@@ -183,9 +183,19 @@ function resolveGatesPassed(entry: CliEntry, resolveVersion: () => string | null
|
||||
export function launchGatePasses(mode: string, gate: string, claudeCliVersion?: string | null): boolean {
|
||||
const entry = getCli(mode);
|
||||
if (!entry?.capabilities.gates[gate]) return false;
|
||||
return resolveGatesPassed(entry, () =>
|
||||
claudeCliVersion !== undefined ? claudeCliVersion : getClaudeCliVersion()
|
||||
).has(gate);
|
||||
return resolveGatesPassed(entry, () => gateVersion(entry, claudeCliVersion)).has(gate);
|
||||
}
|
||||
|
||||
/**
|
||||
* The version a gate of `entry` is measured against: the caller's explicit value (tests, or
|
||||
* null for "known unknown"), else THIS entry's own local CLI via resolveSessionCliVersion.
|
||||
* ⚠️ Never `getClaudeCliVersion()` for every entry, which is what this used to do: codex's
|
||||
* `noDaemonFlag` gate (0.162.0) would then have passed on any claude 2.x and handed an older
|
||||
* codex a flag it exits on. resolveSessionCliVersion still routes claude to its own probe
|
||||
* policy (`retryOnTransientFailure`), so a claude launch is unchanged.
|
||||
*/
|
||||
function gateVersion(entry: CliEntry, explicit: string | null | undefined): string | null {
|
||||
return explicit !== undefined ? explicit : resolveSessionCliVersion(entry.id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -248,14 +258,12 @@ export function buildSpawnCommandFromRegistry(entry: CliEntry, options: SpawnBri
|
||||
engineValues.effortSettingsJson = JSON.stringify(settingsObj);
|
||||
}
|
||||
|
||||
// Preserves buildSpawnCommand's original fallback exactly: an EXPLICIT `undefined` probes
|
||||
// the local claude CLI (getClaudeCliVersion, null under vitest); an explicit `null` means
|
||||
// "known to be unresolvable" and must not probe. The probe only ever runs from
|
||||
// resolveGatesPassed, and only for an entry that actually declares a gate, so this stays
|
||||
// generic without spawning a stray `claude --version` for every other CLI's launch.
|
||||
const gatesPassed = resolveGatesPassed(entry, () =>
|
||||
options.claudeCliVersion !== undefined ? options.claudeCliVersion : getClaudeCliVersion()
|
||||
);
|
||||
// Preserves buildSpawnCommand's original fallback: an EXPLICIT `undefined` probes the
|
||||
// entry's own local CLI (gateVersion: claude's probe for claude, the registry resolver for
|
||||
// the rest, both null under vitest); an explicit `null` means "known to be unresolvable"
|
||||
// and must not probe. The probe only ever runs from resolveGatesPassed, and only for an
|
||||
// entry that actually declares a gate, so a CLI with none never spawns a `--version`.
|
||||
const gatesPassed = resolveGatesPassed(entry, () => gateVersion(entry, options.claudeCliVersion));
|
||||
|
||||
return renderLaunch(entry.launch, params, engineValues, gatesPassed);
|
||||
}
|
||||
+3
-2
@@ -1163,8 +1163,9 @@ export function buildSpawnCommand(options: {
|
||||
/** Name pinned on claude as `--name` (version-gated, sanitized; local spawns only). By default only a user-chosen name; `pinCliSessionName` pins every tab name: see `Session.cliPinnedName`. */
|
||||
sessionName?: string;
|
||||
/**
|
||||
* Claude CLI version for the `--name` gate. Omitted = probe the local CLI
|
||||
* (getClaudeCliVersion; null under vitest). Tests inject a value here; the
|
||||
* CLI version for the launch template's version gates (claude's `--name`, codex's
|
||||
* `--no-daemon`). Omitted = probe the mode's own local CLI (resolveSessionCliVersion;
|
||||
* null under vitest). The name predates the second gated CLI. Tests inject a value here; the
|
||||
* docker/remote paths never see this builder's output, which is what keeps the
|
||||
* gate measuring the RIGHT binary, the local one.
|
||||
*/
|
||||
|
||||
@@ -437,6 +437,12 @@ export interface CodexConfig {
|
||||
resumeSessionId?: string;
|
||||
/** Bypass approval prompts (passes --dangerously-bypass-approvals-and-sandbox) */
|
||||
dangerouslyBypassApprovals?: boolean;
|
||||
/**
|
||||
* Let commands in codex's workspace-write sandbox use the network, Codeman's loopback API
|
||||
* included (passes --config sandbox_workspace_write.network_access=true). Filled from the
|
||||
* synced `codexNetworkAccess` setting when unset; clamped off for a non-granted owner.
|
||||
*/
|
||||
networkAccess?: boolean;
|
||||
/** Enable Codex's decorative TUI animations. Disable to reduce remote terminal redraws. */
|
||||
animations?: boolean;
|
||||
/** Browser rendering strategy for Codex sessions. Hybrid TUI is the only supported mode. */
|
||||
|
||||
@@ -3003,6 +3003,13 @@
|
||||
</div>
|
||||
<label class="switch switch-sm"><input type="checkbox" id="appSettingsCodexDangerouslyBypassApprovals"><span class="slider"></span></label>
|
||||
</div>
|
||||
<div class="set-row" data-search="codex network access sandbox loopback codeman skill orchestrate spawn">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">Allow network access</span>
|
||||
<span class="set-row-desc">Lets commands in Codex's workspace-write sandbox use the network, which the Codeman skill needs to reach this server. A folder Codex runs read-only (one it does not trust) still asks per command. The sandbox and approvals stay on. Applies to new Codex sessions.</span>
|
||||
</div>
|
||||
<label class="switch switch-sm"><input type="checkbox" id="appSettingsCodexNetworkAccess"><span class="slider"></span></label>
|
||||
</div>
|
||||
<div class="set-row" data-search="codex animations status effects">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">Animated status effects</span>
|
||||
|
||||
@@ -697,6 +697,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
// that does have it.
|
||||
document.getElementById('appSettingsCodexDangerouslyBypassApprovals').checked =
|
||||
settings.codexDangerouslyBypassApprovals ?? false;
|
||||
document.getElementById('appSettingsCodexNetworkAccess').checked = settings.codexNetworkAccess ?? false;
|
||||
document.getElementById('appSettingsCodexAnimations').checked =
|
||||
settings.codexAnimationsEnabled ?? false;
|
||||
document.getElementById('appSettingsCodexModel').value = settings.codexModel ?? '';
|
||||
@@ -3072,6 +3073,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
codexModel: document.getElementById('appSettingsCodexModel').value.trim(),
|
||||
codexReasoningEffort: document.getElementById('appSettingsCodexReasoningEffort').value,
|
||||
codexDangerouslyBypassApprovals: document.getElementById('appSettingsCodexDangerouslyBypassApprovals').checked,
|
||||
codexNetworkAccess: document.getElementById('appSettingsCodexNetworkAccess').checked,
|
||||
codexAnimationsEnabled: document.getElementById('appSettingsCodexAnimations').checked,
|
||||
// Claude Permissions settings
|
||||
agentTeamsEnabled: document.getElementById('appSettingsAgentTeams').checked,
|
||||
|
||||
@@ -341,6 +341,7 @@ const CodexConfigSchema = z
|
||||
.regex(/^[a-zA-Z0-9_-]+$/)
|
||||
.optional(),
|
||||
dangerouslyBypassApprovals: z.boolean().optional(),
|
||||
networkAccess: z.boolean().optional(),
|
||||
animations: z.boolean().optional(),
|
||||
renderMode: z
|
||||
.enum(['scrollback', 'hybrid'])
|
||||
@@ -1685,6 +1686,7 @@ export const SettingsUpdateSchema = z
|
||||
.optional(),
|
||||
codexReasoningEffort: z.enum(['', ...CODEX_REASONING_EFFORTS]).optional(),
|
||||
codexDangerouslyBypassApprovals: z.boolean().optional(),
|
||||
codexNetworkAccess: z.boolean().optional(),
|
||||
codexAnimationsEnabled: z.boolean().optional(),
|
||||
// Terminal history and retention
|
||||
terminalScrollbackLines: z
|
||||
|
||||
Reference in new issue
Block a user