Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/codex-skill-network.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"aicodeman": minor
---

**Codex sessions can drive other sessions through the Codeman skill.** A new opt-in Codex setting, **Allow network access** (App Settings, Codex, off by default), starts Codex with `sandbox_workspace_write.network_access=true`, so the skill's calls to this server are no longer blocked by Codex's sandbox; the sandbox and approval prompts stay on, it applies where Codex runs the folder workspace-write, and it is forced off for users without the privileged-commands grant in multi-user mode. Codex 0.162 and later now also starts with `--no-daemon`, so each session runs its own Codex app server: a shared background server (another version's included) no longer stops a launch on an "incompatible feature settings" dialog, and commands inside Codex see their own session's `CODEMAN_*` variables. Version gates in the CLI registry are now measured against that CLI's own version instead of Claude's. The skill falls back to its `curl` path when `codeman` is not on PATH, and asks Codex to re-run a blocked call with approval instead of giving up.
6 changes: 5 additions & 1 deletion docs/cli-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ interface CliEntry {
// (where this CLI's own chrome names the model it runs: SessionState.displayModel)
// .launchDefaults?: { [launchParam]: settingsKey }
// (synced App Settings that seed a LOCAL launch's params the caller left unset;
// codex's model and reasoning effort, via src/web/launch-defaults.ts)
// codex's model, reasoning effort and network access, via src/web/launch-defaults.ts)
// .gates: { [name]: { minVersion, failClosed } }
// (version gates a `capabilityGate` launch condition names: claude's `--name`,
// codex's `--no-daemon`. Measured against THAT CLI's own version, never claude's:
// resolveSessionCliVersion, so an unknown version passes no fail-closed gate)
overlays: CliOverlays; // remote-SSH / Docker pane commands, credential store
}
```
Expand Down
18 changes: 14 additions & 4 deletions docs/wiki/Codeman-Skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,22 @@ message a worker directly instead of typing into its terminal.

Two things to know:

- Codex's default sandbox blocks network access, even to this machine, so the skill needs
**Bypass approvals and sandbox** (App Settings, Codex).
- Codex's default sandbox blocks network access, even to this machine, so turn on
**Allow network access** (App Settings, Codex) for a Codex session that should start and
drive others. It applies to Codex sessions started after you switch it on, and it keeps
Codex's sandbox and approval prompts. It opens the network only where Codex runs the
folder in its workspace-write sandbox, which is what Codex does for a project you
trusted. In a folder Codex runs read-only (a new case that is not a git repository, for
one), Codex asks you to approve each call to Codeman instead; `/permissions` in Codex
switches that session to workspace-write. **Bypass approvals and sandbox** works
everywhere, but removes the sandbox and the approval prompts.
- Codex 0.162 runs its commands in a shared background process, whose Codeman variables can
belong to the session that started it.
belong to the session that started it. Codeman starts Codex 0.162 and later with
`--no-daemon`, so each session runs its own; with an older Codex the skill notices and
says so instead of acting.

The skill checks for both and says so instead of acting.
OpenCode and DeepSeek Harness need nothing extra: in their default setup neither keeps its
shell commands off the network, so the skill works there as soon as it is injected.

## What is in the skill

Expand Down
1 change: 1 addition & 0 deletions docs/wiki/Settings-Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ instead of its native cloud backend. See [Custom Model Endpoints](Custom-Model-E
| Default Codex model | Model for new local Codex sessions; empty uses Codex's own config. Letters, digits, `.` `_` `-` `/` only. |
| Default Codex reasoning effort | Reasoning level for new local Codex sessions; empty uses Codex's own config. |
| Bypass approvals and sandbox | Starts new Codex sessions with `--dangerously-bypass-approvals-and-sandbox`. Read [Agent CLIs](Agent-CLIs) before enabling. |
| Allow network access | Starts new Codex sessions with `sandbox_workspace_write.network_access=true`, so commands in Codex's workspace-write sandbox can use the network. The [Codeman Skill](Codeman-Skill) needs it to reach this server from a Codex session. It applies where Codex runs the folder in workspace-write (a project you trusted in Codex); in a folder Codex runs read-only, Codex asks for approval per command instead. The sandbox and approval prompts stay on. Synced, off by default; forced off for users without the privileged-commands grant in multi-user mode. |
| Animated status effects | Cosmetic. |
| MCP server sync | Copies the MCP servers each installed, enabled CLI (Claude, Codex, Gemini, OpenCode, Antigravity) and GitHub Copilot CLI has into the others' own config files. Synced, off by default, admin only in multi-user mode. Turn it on and **Apply** or **Save** (Apply keeps Settings open), then **Preview** shows what would change and **Sync now** applies it. It only adds missing servers, keeps the previous file as `.codeman-bak`, and leaves a file that receives env values or headers readable by you only. A config dir moved by `CODEX_HOME`, `CLAUDE_CONFIG_DIR`, `XDG_CONFIG_HOME`, `GEMINI_CLI_HOME` or `COPILOT_HOME` in Codeman's own environment is followed. |

Expand Down
20 changes: 16 additions & 4 deletions plugins/codeman/skills/codeman/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ land in `~/.claude`; pass `--working-dir ~` to mean it). Directory launches keep
mark the project as agent scratch. If startup fails or is ambiguous, the CLI
reports the exact created ID: inspect it before retrying or cleaning up.

**`codeman: command not found` is not a dead end.** Some installs (a server run from a
source checkout, for one) put no `codeman` on PATH. Do not stop there: §0 and §1 below
are the same verbs over `curl`, and need nothing but `curl` and `jq`.

## 0. Guard and bootstrap

For live API operations, if `CODEMAN_MUX` is not `1`, **stop and say so**.
Expand All @@ -68,13 +72,21 @@ Do not guess an API URL; a server you are not part of is not yours to drive.
Two failures come from your CLI, not from Codeman: name them to the user instead of
retrying.

- **Every call fails at once with "couldn't connect" or "connection refused".** Your
CLI's sandbox blocks network access, even to this machine. Codex's default sandbox
does; it needs **Bypass approvals and sandbox** (Codeman App Settings, Codex).
- **Every call fails at once with "couldn't connect", "connection refused" or `connect
EPERM`.** Your CLI's sandbox blocks network access, even to this machine. If your CLI
can ask the user to run a command outside its sandbox (Codex can), re-run **the same
command** once that way, never a different one; if the user declines, stop. For lasting
access in Codex, the user turns on **Allow network access** (Codeman App Settings,
Codex) and starts a new Codex session: it opens the network wherever Codex runs the
folder workspace-write, and keeps the sandbox and approvals. A folder Codex runs
read-only still needs the per-command approval (or `/permissions`). **Bypass approvals
and sandbox** works everywhere but drops the sandbox and approvals.
- **`"${CURL[@]}" -o /dev/null -w '%{http_code}' "$API/api/v1/sessions/$SELF"` prints
`404`.** The commands you run do not carry this session's environment: Codex can run them in a shared background
process another session started, so `$CODEMAN_SESSION_ID` (and with it `$SELF` and
the delete guard) is that session's. Delete nothing; tell the user.
the delete guard) is that session's. Delete nothing; tell the user. Codeman starts
Codex 0.162 and later with `--no-daemon` to prevent this, so seeing it means an older
Codex or a server that predates that.

⚠️ **Your shell state does not survive between tool calls.** Each Bash call starts a
fresh shell, so `$API`, `$SELF`, the `CURL` array and `delete_session` are all gone by
Expand Down
20 changes: 16 additions & 4 deletions skills/codeman/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ land in `~/.claude`; pass `--working-dir ~` to mean it). Directory launches keep
mark the project as agent scratch. If startup fails or is ambiguous, the CLI
reports the exact created ID: inspect it before retrying or cleaning up.

**`codeman: command not found` is not a dead end.** Some installs (a server run from a
source checkout, for one) put no `codeman` on PATH. Do not stop there: §0 and §1 below
are the same verbs over `curl`, and need nothing but `curl` and `jq`.

## 0. Guard and bootstrap

For live API operations, if `CODEMAN_MUX` is not `1`, **stop and say so**.
Expand All @@ -68,13 +72,21 @@ Do not guess an API URL; a server you are not part of is not yours to drive.
Two failures come from your CLI, not from Codeman: name them to the user instead of
retrying.

- **Every call fails at once with "couldn't connect" or "connection refused".** Your
CLI's sandbox blocks network access, even to this machine. Codex's default sandbox
does; it needs **Bypass approvals and sandbox** (Codeman App Settings, Codex).
- **Every call fails at once with "couldn't connect", "connection refused" or `connect
EPERM`.** Your CLI's sandbox blocks network access, even to this machine. If your CLI
can ask the user to run a command outside its sandbox (Codex can), re-run **the same
command** once that way, never a different one; if the user declines, stop. For lasting
access in Codex, the user turns on **Allow network access** (Codeman App Settings,
Codex) and starts a new Codex session: it opens the network wherever Codex runs the
folder workspace-write, and keeps the sandbox and approvals. A folder Codex runs
read-only still needs the per-command approval (or `/permissions`). **Bypass approvals
and sandbox** works everywhere but drops the sandbox and approvals.
- **`"${CURL[@]}" -o /dev/null -w '%{http_code}' "$API/api/v1/sessions/$SELF"` prints
`404`.** The commands you run do not carry this session's environment: Codex can run them in a shared background
process another session started, so `$CODEMAN_SESSION_ID` (and with it `$SELF` and
the delete guard) is that session's. Delete nothing; tell the user.
the delete guard) is that session's. Delete nothing; tell the user. Codeman starts
Codex 0.162 and later with `--no-daemon` to prevent this, so seeing it means an older
Codex or a server that predates that.

⚠️ **Your shell state does not survive between tool calls.** Each Bash call starts a
fresh shell, so `$API`, `$SELF`, the `CURL` array and `delete_session` are all gone by
Expand Down
6 changes: 5 additions & 1 deletion src/config/cli-registry/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
50 changes: 48 additions & 2 deletions src/config/cli-registry/stock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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] },
Expand All @@ -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' } },
Expand Down Expand Up @@ -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,
Expand All @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion src/config/cli-registry/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
32 changes: 20 additions & 12 deletions src/session-cli-registry-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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);
}
Loading
Loading