Skip to content

Commit d2cb909

Browse files
PythonLuvrclaude
andcommitted
v0.7.1: Codex CLI registry entry + hand-rolled TOML serializer
Codex CLI joins Claude Code and Gemini CLI in v0.7.0's MCP-server-mode infrastructure. A Windows / macOS / Linux operator with Codex installed can now run cli-bridge against codex and have the bridged session call OpenWar's eight native tools through MCP, same as Claude Code and Gemini CLI in v0.7.0. Brought up as v0.7.1 because Codex reads MCP config from ~/.codex/config.toml. v0.7.0's registry only knew how to write JSON. v0.7.1 adds a small hand-rolled TOML serializer covering only what MCP config needs (string values, string array values, dotted section headers, every TOML 1.0 basic-string escape rule). Zero new runtime dependencies; the alternative was shipping a TOML library and crossing the framework's no-deps line. Phase 0 design pick: append vs overwrite for ~/.codex/config.toml is read-modify-write (option a in the brief). Operators with hand-edited Codex configs (model preferences, history settings, etc) keep those sections intact across OpenWar runs. The merge is text-boundary based: find [mcp_servers.openwar] at column 0, replace its body up to the next column-0 section header or EOF, append if absent. No TOML parser required (and none added). BridgedCliStrategy interface gains three optional fields, all back-compat additive: serializeConfig (default JSON.stringify), mergeIntoExisting (default false), mergeSectionHeader (default "mcp_servers.<serverName>"). Claude Code and Gemini CLI entries unchanged. 35 new tests. 25 in tests/mcp/toml-writer.test.ts (every escape rule individually, Windows backslash paths, dotted headers, string arrays, empty arrays, canonical MCP shape, plus 6 upsertTomlSection cases for append / replace / final-section / prefix-collision / CRLF normalization). 4 in tests/mcp/cli-bridge-wiring-codex.test.ts (end-to-end against a tmp HOME: TOML written, operator [user] + [history] sections survive merge, stale openwar block gets replaced, cli.mcp_forward: false opt-out short-circuits setup). 7 extended in tests/mcp/bridged-cli-registry.test.ts covering Codex resolution + configPath + serializer dispatch + merge flags + the updated listKnownBridgedClis output. Known smoke-gap: real Codex CLI session against an OpenWar brief is deferred (operator's Codex install is the gate). Ship on unit-test strength per the brief's deferral clause. Smoke verification target moves to v0.7.2. 413/413 tests pass (was 378 on v0.7.0). Lint clean, build clean. Cross-platform CI matrix tests the home-dir expansion on both POSIX and Windows via the existing infrastructure. Zero new runtime dependencies. Out of scope, deferred to later: aider registry entry (no native MCP support yet), TOML parsing (only writes), general-purpose TOML (integers, floats, booleans, dates, inline tables, multi-line strings all unsupported by design). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ae4ed4e commit d2cb909

10 files changed

Lines changed: 729 additions & 5 deletions

‎CHANGELOG.md‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,52 @@
11
# Changelog
22

3+
## 0.7.1
4+
5+
Codex CLI joins Claude Code and Gemini CLI in the v0.7.0 MCP-server-mode infrastructure. Brought up as v0.7.1 because Codex's TOML config format required a small hand-rolled serializer; deferred from v0.7.0 to keep that release focused on the architectural foundation.
6+
7+
A Windows / macOS / Linux operator with Codex CLI installed can now run:
8+
9+
```bash
10+
npx @pythonluvr/openwar run examples/cli-bridge-mcp-memory-brief.md \
11+
--adapter cli-bridge \
12+
--cli-binary codex
13+
```
14+
15+
The runner writes `~/.codex/config.toml` with the OpenWar MCP server config, Codex discovers and consumes it, the bridged Codex session can call OpenWar's eight native tools through MCP. Same operator experience as Claude Code and Gemini CLI in v0.7.0.
16+
17+
### Added
18+
19+
- **`src/mcp/toml-writer.ts`**: hand-rolled TOML serializer scoped to MCP config shape only. Supports dotted section headers, string values, string array values. Escape rules cover every TOML 1.0 basic-string escape (`\b`, `\t`, `\n`, `\f`, `\r`, `\"`, `\\`, `\uXXXX` for non-printable Unicode). Public API is `writeTomlConfig(config: TomlConfig): string` plus `TomlConfig` type plus `upsertTomlSection(existing, header, body): string` for the read-modify-write merge path. Deliberately not a general-purpose TOML library: no integers, floats, booleans, dates, inline tables, multi-line strings, parsing.
20+
- **Codex CLI registry entry**: `binary: "codex"` resolves to the Codex strategy. `configPath` returns `~/.codex/config.toml` via `os.homedir()`. Serializes the standard `McpConfigFileContent` to TOML via the new writer. No CLI flag injection (Codex auto-discovers `~/.codex/config.toml`). `cleanupConfigFile: false` consistent with Gemini (operators who wire Codex MCP typically want the wiring sticky). `mergeIntoExisting: true` so existing operator-edited sections survive.
21+
- **`BridgedCliStrategy` interface gains three optional fields** (back-compat additive): `serializeConfig?(content): string` (default `JSON.stringify(content, null, 2)`), `mergeIntoExisting?: boolean` (default `false`), `mergeSectionHeader?: string` (default `"mcp_servers.<serverName>"`). Claude Code and Gemini CLI entries unchanged; the JSON default + overwrite behavior matches v0.7.0 exactly.
22+
- **Runner-side read-modify-write** in `src/mcp/cli-bridge-wiring.ts`. When a strategy sets `mergeIntoExisting: true` and the config file exists, the runner reads the existing content, calls `upsertTomlSection` to replace or append just the OpenWar section, writes the merged result. When the file doesn't exist, behavior is unchanged (write the serialized output verbatim).
23+
- **Tests**: `tests/mcp/toml-writer.test.ts` (25 cases covering basic strings, every TOML 1.0 escape individually, Windows backslash paths, paths with spaces, dotted section headers, string arrays, empty arrays, the canonical MCP config shape, and 6 cases on `upsertTomlSection` for append / replace / final-section / prefix-collision / CRLF normalization). `tests/mcp/cli-bridge-wiring-codex.test.ts` (4 end-to-end cases against a tmp HOME: writes TOML to `~/.codex/config.toml`, merge preserves operator `[user]` + `[history]` sections, merge replaces stale `[mcp_servers.openwar]` block, `cli.mcp_forward: false` opt-out short-circuits the whole setup). `tests/mcp/bridged-cli-registry.test.ts` extended with 7 Codex resolution + behavior cases.
24+
- **`docs/adapters.md`**: cli-bridge section gains a "MCP-server-mode and the bridged-CLI registry" subsection with a table of all three registered CLIs (Claude Code, Gemini CLI, Codex CLI) and their config strategy.
25+
26+
### Changed
27+
28+
- The cli-bridge MCP forwarding now serializes the config file through the strategy's `serializeConfig` hook (default JSON, Codex TOML). Behavior for Claude Code and Gemini CLI is byte-identical to v0.7.0.
29+
30+
### Phase 0 design pick
31+
32+
**Append vs overwrite for `~/.codex/config.toml`: read-modify-write.** Builder picked option (a) from the v0.7.1 brief for safety. Operators with hand-edited Codex configs (model preferences, history settings) keep those sections intact across OpenWar runs. The merge is text-boundary based: find `[mcp_servers.openwar]` at column 0, replace its body up to the next column-0 section header or EOF, append if absent. No TOML parser required.
33+
34+
### Known smoke-gap
35+
36+
The brief allowed deferring the operator smoke test if Codex CLI isn't available locally. This release ships on the strength of 35 new unit tests (TOML writer + Codex registry + end-to-end wiring against a tmp HOME), validated cross-platform via the existing CI matrix. A real bridged-Codex session against an OpenWar brief is the v0.7.2 smoke-verification target once the operator has Codex installed.
37+
38+
### Out of scope (deferred or skipped)
39+
40+
- **aider registry entry.** Aider has no native MCP server support today. Defer to v0.7.2 or whenever aider ships first-class MCP support.
41+
- **TOML parsing.** OpenWar only writes TOML. No parser is added.
42+
- **General-purpose TOML support.** The serializer covers only what MCP config requires. Scope creep is the failure mode for v0.7.1; integers, floats, booleans, dates, inline tables, multi-line strings, tables of tables are all deliberately unsupported.
43+
44+
### Notes for forkers and War Room integrators
45+
46+
- Zero new runtime dependencies. The TOML writer is ~150 lines of hand-rolled Node stdlib.
47+
- No state schema bump. No brief format change. Drop-in compatible with v0.7.0.
48+
- The `BridgedCliStrategy` interface additions are optional fields; forker-defined custom registry entries that don't set them get the v0.7.0 JSON + overwrite behavior unchanged.
49+
350
## 0.7.0
451

552
MCP-server-mode for cli-bridge. v0.5.0's cli-bridge adapter let OpenWar coordinate a CLI agent (Claude Code, etc), but the bridged CLI could not call OpenWar's native tools because its tool registry was its own. v0.7 closes the gap: OpenWar exposes its native tools as an MCP server, the bridged CLI consumes them through standard MCP, and the runtime threads OpenWar's authorization gates across the bridge. Operators can now run a brief that asks the bridged Claude Code to call `write_project_memory` (or any of the eight native tools) and have it actually work.

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ System prompts cost nothing to install and work with any agent. The runtime is h
144144

145145
## Versioning
146146

147-
Current: **v0.7.0**. See [CHANGELOG.md](./CHANGELOG.md) for full release notes.
147+
Current: **v0.7.1**. See [CHANGELOG.md](./CHANGELOG.md) for full release notes.
148148

149149
- v0.1: framework doc only (single markdown file).
150150
- v0.2: runtime, CLI, BYOK adapters.

‎docs/adapters.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,22 @@ cli:
8282

8383
Set `framework_prefix: false` (or pass `--cli-no-framework`) when the CLI already has OpenWar in its own system prompt. For Claude Code with `openwar.md` already in CLAUDE.md, the prepend is redundant and wasteful.
8484

85+
### MCP-server-mode and the bridged-CLI registry (v0.7+)
86+
87+
When a brief uses cli-bridge, OpenWar additionally exposes its eight native tools (`read_file`, `write_file`, `list_dir`, `shell_exec`, `http_fetch`, `apply_patch`, `read_project_memory`, `write_project_memory`) to the bridged CLI through standard MCP. The bridged agent can call any of them as `openwar:<tool_name>` and the runtime authorizes every call through the brief's `authorized_costs`.
88+
89+
The runtime knows how to wire MCP forwarding for these bridged CLIs out of the box:
90+
91+
| Binary basename | CLI | Config file | Wiring | Persists |
92+
|---|---|---|---|---|
93+
| `claude` | Claude Code | `<temp>/openwar-mcp-config-*.json` | `--mcp-config <path>` flag injection | cleaned up at session end |
94+
| `gemini` | Gemini CLI | `<workdir>/.gemini/settings.json` | auto-discovered | yes (no cleanup) |
95+
| `codex` | Codex CLI (v0.7.1+) | `~/.codex/config.toml` | auto-discovered, merged into existing TOML | yes (no cleanup) |
96+
97+
Unknown binaries (aider, custom tools without native MCP support) hit a fallback: the OpenWar MCP config is still written to a temp JSON file, but no CLI args are injected. The runtime emits a startup warning so the operator can wire MCP manually for that CLI, or set `cli.mcp_forward: false` in the brief frontmatter to disable forwarding entirely and run the bridged CLI in its own tool sandbox.
98+
99+
The Codex entry preserves operator hand-edits to other sections of `~/.codex/config.toml` (it reads the file, replaces or appends the `[mcp_servers.openwar]` block, leaves everything else untouched). Same model is expected to extend to other TOML-config CLIs in v0.7.2+.
100+
85101
### When to use cli-bridge vs an API adapter
86102

87103
Use `cli-bridge` when:

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@pythonluvr/openwar",
3-
"version": "0.7.0",
3+
"version": "0.7.1",
44
"description": "Runtime that enforces the OpenWar framework: phase-aware agent loop with confirmation, blocker, and destructive-action gates.",
55
"license": "MIT",
66
"type": "module",

‎src/mcp/bridged-cli-registry.ts‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
// --cli-binary claude, --cli-binary claude.cmd, and absolute paths.
2525

2626
import { join } from "node:path";
27+
import { homedir } from "node:os";
28+
import { writeTomlConfig, type TomlConfig } from "./toml-writer.js";
2729

2830
export interface ConfigPathContext {
2931
workdir: string;
@@ -57,6 +59,19 @@ export interface BridgedCliStrategy {
5759
// temp paths; false for known-location overrides where the operator may
5860
// want the config to persist for future runs.
5961
cleanupConfigFile?: boolean;
62+
// v0.7.1: serialize the MCP config to the on-disk format the bridged CLI
63+
// expects. Default (undefined) is JSON.stringify with two-space indent.
64+
// Codex overrides to TOML because its config.toml is, well, TOML.
65+
serializeConfig?(content: McpConfigFileContent): string;
66+
// v0.7.1: when true, the runner reads the existing config file and merges
67+
// the OpenWar MCP server section in (preserving other operator-authored
68+
// sections) instead of overwriting. Used by Codex because operators may
69+
// hand-edit other parts of ~/.codex/config.toml. Strategies that own a
70+
// temp path or a dedicated subdir keep this false.
71+
mergeIntoExisting?: boolean;
72+
// v0.7.1: the merge step needs to know the section header to replace.
73+
// Defaults to "mcp_servers.<serverName>" when omitted.
74+
mergeSectionHeader?: string;
6075
}
6176

6277
// Map of binary basename → strategy. Match is case-insensitive; .cmd / .bat /
@@ -81,8 +96,46 @@ const STRATEGIES = new Map<string, BridgedCliStrategy>([
8196
configPath: ({ workdir }) => join(workdir, ".gemini", "settings.json"),
8297
buildArgs: () => [],
8398
}],
99+
// v0.7.1: Codex CLI. Reads MCP server config from ~/.codex/config.toml.
100+
// TOML format requires the v0.7.1 hand-rolled serializer. The runner
101+
// merges the [mcp_servers.openwar] section into any existing file rather
102+
// than overwriting so operator hand-edits to other sections survive
103+
// (Phase 0 pick (a) in the v0.7.1 brief). Codex auto-discovers; no CLI
104+
// args needed. File persists across runs like Gemini.
105+
["codex", {
106+
display_name: "Codex CLI",
107+
mcp_supported: true,
108+
writeConfigFile: true,
109+
cleanupConfigFile: false,
110+
configPath: () => join(homedir(), ".codex", "config.toml"),
111+
buildArgs: () => [],
112+
serializeConfig: (content) => writeTomlConfig(mcpConfigToToml(content)),
113+
mergeIntoExisting: true,
114+
mergeSectionHeader: "mcp_servers.openwar",
115+
}],
84116
]);
85117

118+
// Convert the standard McpConfigFileContent (JSON-shaped, used by Claude
119+
// Code + Gemini) into the TomlConfig shape the writer accepts. v0.7.1 only
120+
// emits one section per call (the OpenWar entry under mcp_servers) because
121+
// that is the only section the strategy owns.
122+
function mcpConfigToToml(content: McpConfigFileContent): TomlConfig {
123+
const sections: TomlConfig["sections"] = [];
124+
for (const [serverName, server] of Object.entries(content.mcpServers)) {
125+
const fields: TomlConfig["sections"][number]["fields"] = [
126+
{ key: "command", value: server.command },
127+
{ key: "args", value: server.args },
128+
];
129+
if (server.env) {
130+
for (const [k, v] of Object.entries(server.env)) {
131+
fields.push({ key: `env.${k}`, value: v });
132+
}
133+
}
134+
sections.push({ header: `mcp_servers.${serverName}`, fields });
135+
}
136+
return { sections };
137+
}
138+
86139
// Fallback used when the binary is unknown. Writes the config file, does not
87140
// inject CLI args (we don't know how this CLI consumes config). Caller emits
88141
// a startup warning so the operator can wire the config manually.

‎src/mcp/cli-bridge-wiring.ts‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { fileURLToPath } from "node:url";
1919
import type { Brief, RunnerIO, ToolCallRecord } from "../types.js";
2020
import type { CliBridgeAdapter } from "../adapters/cli-bridge.js";
2121
import { resolveBridgedCliStrategy, buildMcpConfigFile } from "./bridged-cli-registry.js";
22+
import { upsertTomlSection } from "./toml-writer.js";
2223

2324
export interface CliBridgeMcpSetup {
2425
enabled: boolean;
@@ -114,7 +115,27 @@ export async function setupCliBridgeMcpForwarding(
114115
serverArgs: [openwarBin, ...serverArgs],
115116
});
116117
await mkdir(dirname(configPath), { recursive: true });
117-
await writeFile(configPath, JSON.stringify(content, null, 2), "utf8");
118+
// v0.7.1: per-strategy serializer (default JSON). Codex returns TOML.
119+
const serialized = strategy.serializeConfig
120+
? strategy.serializeConfig(content)
121+
: JSON.stringify(content, null, 2);
122+
// v0.7.1: optional merge-into-existing. Codex sets this true so we
123+
// don't clobber operator hand-edits to other sections of config.toml.
124+
if (strategy.mergeIntoExisting && existsSync(configPath)) {
125+
const existing = await readFile(configPath, "utf8");
126+
const sectionHeader = strategy.mergeSectionHeader ?? "mcp_servers.openwar";
127+
// The serialized output is one [section]\nkey=val\n... block. Strip
128+
// the leading "[header]\n" so upsertTomlSection's body argument is
129+
// just the field lines; the helper re-adds the header verbatim.
130+
const headerLine = `[${sectionHeader}]\n`;
131+
const body = serialized.startsWith(headerLine)
132+
? serialized.slice(headerLine.length).replace(/\n+$/, "")
133+
: serialized.replace(/\n+$/, "");
134+
const merged = upsertTomlSection(existing, sectionHeader, body);
135+
await writeFile(configPath, merged, "utf8");
136+
} else {
137+
await writeFile(configPath, serialized, "utf8");
138+
}
118139
}
119140

120141
const cliArgs = strategy.buildArgs({

0 commit comments

Comments
 (0)