Make WSL agent setup unambiguous for MCP installs (#4655)

Windows users running agent CLIs inside WSL were landing on the native Windows guide and copying the README MCP command directly, which left Linux /usr/bin/od shadowing, daemon origin, Node ABI, and Codex config parse failures unexplained. Add a WSL2 guide and teach the Codex config normalizer to drop nested feature tables that current Codex parses as maps instead of boolean flags.

Constraint: WSL agent CLIs need the same Linux environment for the wrapper, daemon, Node modules, and credentials

Rejected: Docs-only workaround | would leave daemon-launched Codex runs failing on nested features tables

Confidence: high

Scope-risk: narrow

Directive: Keep WSL-specific guidance separate from native Windows PowerShell troubleshooting

Tested: pnpm --filter @open-design/daemon exec vitest run -c vitest.config.ts tests/codex-config-normalize.test.ts

Tested: pnpm --filter @open-design/daemon typecheck

Tested: pnpm guard

Tested: git diff --check

Not-tested: Manual WSL2 end-to-end MCP install on a Windows host

Related: Fixes #4648
This commit is contained in:
Sid
2026-06-23 17:56:22 +08:00
committed by GitHub
parent 3467bb5830
commit 48695090f8
6 changed files with 318 additions and 7 deletions

View File

@@ -8,7 +8,7 @@ Run the full product locally.
- **Node.js:** `~24` (Node 24.x). The repo enforces this through `package.json#engines`.
- **pnpm:** `10.33.x`. The repo pins `pnpm@10.33.2` through `packageManager`; use Corepack so the pinned version is selected automatically.
- **OS:** macOS, Linux, and WSL2 are the primary paths. Windows native is supported; see [`docs/windows-troubleshooting.md`](docs/windows-troubleshooting.md) for common setup gotchas.
- **OS:** macOS, Linux, and WSL2 are the primary paths. If your agent CLIs run inside WSL2, use the [`WSL2 setup guide`](docs/wsl-setup.md). Windows native is supported; see [`docs/windows-troubleshooting.md`](docs/windows-troubleshooting.md) for common PowerShell setup gotchas.
- **Optional local agent CLI:** Claude Code, Codex, Devin for Terminal, Gemini CLI, OpenCode, Cursor Agent, Qwen, Qoder CLI, GitHub Copilot CLI, etc. If none are installed, use the BYOK API mode from Settings.
### Local agent CLI and PATH

View File

@@ -307,6 +307,10 @@ od mcp install <agent>
# | pi | vibe | hermes | cline | kimi | trae | opencode
```
> **WSL2 users:** If your coding-agent CLIs run inside WSL2, follow the
> [`WSL2 setup guide`](docs/wsl-setup.md) first. Linux's `/usr/bin/od` can
> shadow Open Design's `od` command.
Then, inside the agent:
```
@@ -343,7 +347,7 @@ corepack enable && pnpm install
pnpm tools-dev run web
```
Node `~24`, pnpm `10.33.x`. Windows users, see [`docs/windows-troubleshooting.md`](docs/windows-troubleshooting.md). Full quickstart, env vars, Nix flake, and packaged build flow → [`QUICKSTART.md`](QUICKSTART.md).
Node `~24`, pnpm `10.33.x`. WSL2 users, see [`docs/wsl-setup.md`](docs/wsl-setup.md); native Windows users, see [`docs/windows-troubleshooting.md`](docs/windows-troubleshooting.md). Full quickstart, env vars, Nix flake, and packaged build flow → [`QUICKSTART.md`](QUICKSTART.md).
### A full workflow — from brief to artifact

View File

@@ -24,8 +24,14 @@
// intentionally scoped: only standalone `service_tier` key lines are touched;
// everything else in config.toml is preserved verbatim.
//
// The normalization is idempotent: if the file is absent or every service_tier
// value is already valid, it is left unchanged.
// The normalizer also removes nested `[features.*]` tables. Current Codex CLI
// configs model `[features]` as a map of boolean flags; a nested table makes a
// flag value a TOML map and the CLI exits with:
//
// invalid type: map, expected a boolean in `features`
//
// The normalization is idempotent: if the file is absent, or if no invalid
// service_tier value or nested features table is present, it is left unchanged.
import { randomBytes } from 'node:crypto';
import { rename, readFile, unlink, writeFile } from 'node:fs/promises';
@@ -61,6 +67,51 @@ export function resolveCodexConfigPath(
*/
const VALID_SERVICE_TIERS = new Set(['fast', 'flex']);
function splitLinesPreservingEndings(content: string): string[] {
const lines = content.match(/[^\r\n]*(?:\r\n|\n|\r|$)/g) ?? [];
if (lines.at(-1) === '') lines.pop();
return lines;
}
function tableHeaderName(line: string): string | null {
const withoutLineEnding = line.replace(/\r\n$|\n$|\r$/, '');
const trimmed = withoutLineEnding.trim();
const arrayHeader = trimmed.match(/^\[\[([^\]\r\n]+)\]\][^\S\r\n]*(?:#.*)?$/);
const arrayHeaderName = arrayHeader?.[1];
if (arrayHeaderName) return arrayHeaderName.trim();
const tableHeader = trimmed.match(/^\[([^\]\r\n]+)\][^\S\r\n]*(?:#.*)?$/);
const tableName = tableHeader?.[1];
if (tableName) return tableName.trim();
return null;
}
function removeNestedFeaturesTables(content: string): string | null {
const lines = splitLinesPreservingEndings(content);
let changed = false;
let droppingNestedFeaturesTable = false;
const kept: string[] = [];
for (const line of lines) {
const headerName = tableHeaderName(line);
if (headerName) {
droppingNestedFeaturesTable = headerName.startsWith('features.');
if (droppingNestedFeaturesTable) {
changed = true;
continue;
}
}
if (droppingNestedFeaturesTable) {
changed = true;
continue;
}
kept.push(line);
}
return changed ? kept.join('') : null;
}
/**
* Normalize the `service_tier` field in a config.toml string.
*
@@ -68,6 +119,9 @@ const VALID_SERVICE_TIERS = new Set(['fast', 'flex']);
* {@link VALID_SERVICE_TIERS} has its entire line removed (so the Codex CLI
* uses its built-in default tier). Valid values are left verbatim.
*
* Any nested `[features.*]` table is also removed because current Codex CLI
* configs expect `[features]` entries to be booleans, not maps.
*
* Returns `null` when nothing needed to change, otherwise the patched content.
*/
export function normalizeCodexConfigContent(content: string): string | null {
@@ -94,7 +148,7 @@ export function normalizeCodexConfigContent(content: string): string | null {
/^([^\S\r\n]*)service_tier([^\S\r\n]*=[^\S\r\n]*)(["'])([^"'\r\n]*)\3([^\S\r\n]*(?:#[^\r\n]*)?)(\r?\n|$)/gm;
let changed = false;
const patched = content.replace(
const serviceTierPatched = content.replace(
pattern,
(match: string, _indent, _eq, _quote, value: string) => {
if (VALID_SERVICE_TIERS.has(value)) {
@@ -107,7 +161,12 @@ export function normalizeCodexConfigContent(content: string): string | null {
},
);
return changed ? patched : null;
const featuresPatched = removeNestedFeaturesTables(serviceTierPatched);
if (featuresPatched !== null) {
changed = true;
}
return changed ? (featuresPatched ?? serviceTierPatched) : null;
}
/**

View File

@@ -195,6 +195,49 @@ describe('normalizeCodexConfigContent', () => {
expect(normalizeCodexConfigContent(input)).toBeNull();
});
it('removes nested features tables that make Codex parse features as maps (#4648)', () => {
const input = [
'[features]',
'hide_spawn_agent_metadata = false',
'[features.multi_agent_v2]',
'hide_spawn_agent_metadata = false',
'max_concurrent_threads_per_session = 10000',
'enabled = false',
'',
'[model]',
'model = "gpt-5.5"',
].join('\n');
const result = normalizeCodexConfigContent(input);
expect(result).toBe([
'[features]',
'hide_spawn_agent_metadata = false',
'[model]',
'model = "gpt-5.5"',
].join('\n'));
});
it('preserves unrelated dotted tables while removing nested features tables', () => {
const input = [
'[profiles.default]',
'model = "gpt-5.5"',
'[features.multi_agent_v2]',
'enabled = false',
'[projects."/tmp/open-design"]',
'trust_level = "trusted"',
].join('\n');
const result = normalizeCodexConfigContent(input);
expect(result).toBe([
'[profiles.default]',
'model = "gpt-5.5"',
'[projects."/tmp/open-design"]',
'trust_level = "trusted"',
].join('\n'));
});
// -------------------------------------------------------------------------
// CRLF regression: config.toml files written by the Codex app on Windows use
// CRLF endings. Removing the stale line must preserve ALL surrounding \r\n
@@ -216,6 +259,17 @@ describe('normalizeCodexConfigContent', () => {
// No naked LF introduced.
expect(result).not.toMatch(/(?<!\r)\n/);
});
it('CRLF regression: removes nested features tables while preserving surrounding \\r\\n endings', () => {
const crlfContent = '[features]\r\nhide_spawn_agent_metadata = false\r\n[features.multi_agent_v2]\r\nenabled = false\r\n[model]\r\nmodel = "gpt-5.5"\r\n';
const result = normalizeCodexConfigContent(crlfContent);
expect(result).not.toBeNull();
expect(result).not.toContain('[features.multi_agent_v2]');
expect(result).toBe('[features]\r\nhide_spawn_agent_metadata = false\r\n[model]\r\nmodel = "gpt-5.5"\r\n');
expect(result).not.toMatch(/\r(?!\n)/);
expect(result).not.toMatch(/(?<!\r)\n/);
});
});
// ---------------------------------------------------------------------------
@@ -284,6 +338,34 @@ describe('normalizeCodexConfigFile', () => {
expect(after).toContain('model = "gpt-5.5"');
});
it('removes nested features tables from config.toml (#4648 regression)', async () => {
const configPath = join(tmpDir, 'config.toml');
writeFileSync(
configPath,
[
'[features]',
'hide_spawn_agent_metadata = false',
'[features.multi_agent_v2]',
'hide_spawn_agent_metadata = false',
'max_concurrent_threads_per_session = 10000',
'enabled = false',
'[model]',
'model = "gpt-5.5"',
].join('\n'),
'utf8',
);
await normalizeCodexConfigFile({ CODEX_HOME: tmpDir });
const after = readFileSync(configPath, 'utf8');
expect(after).toContain('[features]');
expect(after).toContain('hide_spawn_agent_metadata = false');
expect(after).not.toContain('[features.multi_agent_v2]');
expect(after).not.toContain('max_concurrent_threads_per_session');
expect(after).toContain('[model]');
expect(after).toContain('model = "gpt-5.5"');
});
it('does not modify config.toml when service_tier is already valid', async () => {
const configPath = join(tmpDir, 'config.toml');
const original = `service_tier = "fast"\n`;

View File

@@ -2,7 +2,7 @@
Open Design runs on Windows natively, but the path is less travelled than macOS, Linux, or WSL2. This guide covers the most common errors you will hit on a fresh Windows machine and the exact fix for each.
> **Tip:** If you already have WSL2 set up, that is the smoothest path on Windows. This guide is for native Windows (PowerShell).
> **Tip:** If your coding-agent CLIs run inside WSL2, use the dedicated [`WSL2 setup guide`](wsl-setup.md). This guide is for native Windows (PowerShell).
---

166
docs/wsl-setup.md Normal file
View File

@@ -0,0 +1,166 @@
# WSL2 Setup Guide
Use this guide when your coding-agent CLIs run inside WSL2. In that setup,
install and run Open Design from WSL as well so the agent CLI, `od` command,
daemon, Node modules, and credentials all come from the same Linux environment.
For native Windows PowerShell setup, use
[`docs/windows-troubleshooting.md`](windows-troubleshooting.md) instead.
## Recommended shape
- Clone Open Design inside WSL2.
- Install Node `~24` and the repo-pinned pnpm (`10.33.2`) inside WSL2.
- Put a WSL-native `od` wrapper before `/usr/bin` on `PATH`.
- Start the daemon from WSL with `od --no-open`.
- Install MCP entries from the same WSL shell.
Do not assume the Windows desktop app's daemon is the right daemon for WSL
agent clients. WSL2 networking and Windows credential stores can make that path
ambiguous. A WSL-started daemon keeps the MCP clients and Open Design in the
same environment.
## 1. Install from source in WSL
```bash
git clone https://github.com/nexu-io/open-design.git ~/tools/open-design
cd ~/tools/open-design
node --version # should print v24.x.x
corepack enable
corepack pnpm --version # should print 10.33.2
pnpm install
```
If you use `mise`, trust and install the repo toolchain before `pnpm install`:
```bash
mise trust
mise install
```
## 2. Fix the `od` command collision
Linux already ships `/usr/bin/od` (octal dump). If that binary wins on `PATH`,
commands such as `od mcp install claude` fail with file-not-found messages for
`mcp`, `install`, and the agent name.
Check what your shell resolves:
```bash
type -a od
```
If `/usr/bin/od` appears before Open Design, create a wrapper in `~/.local/bin`
and make sure that directory is first on `PATH`:
```bash
mkdir -p ~/.local/bin
cat > ~/.local/bin/od <<'EOF'
#!/usr/bin/env bash
repo="$HOME/tools/open-design"
cd "$repo" || exit 127
if command -v mise >/dev/null 2>&1; then
exec mise exec -- pnpm exec od "$@"
fi
exec corepack pnpm exec od "$@"
EOF
chmod +x ~/.local/bin/od
export PATH="$HOME/.local/bin:$PATH"
hash -r
type -a od
```
Expected first result:
```text
od is /home/<user>/.local/bin/od
```
`od.exe` is not a reliable workaround from WSL. It may resolve to a Windows
coreutils binary instead of Open Design, especially on machines with Windows
coreutils installed.
## 3. Start the daemon from WSL
Run the daemon from the same WSL environment that your agent CLIs use:
```bash
cd ~/tools/open-design
od --no-open
```
In another WSL terminal, verify it is reachable:
```bash
curl -sSf http://127.0.0.1:7456 >/dev/null && echo "Open Design daemon is reachable"
```
Leave the daemon terminal running while using MCP integrations.
## 4. Install MCP entries
From WSL, run the installer for each agent CLI you use:
```bash
od mcp install claude
od mcp install opencode
od mcp install codex
od mcp install antigravity
od mcp install copilot
```
The installer writes to the agent config locations for the current WSL user,
for example `~/.claude.json`, `~/.config/opencode/opencode.json`,
`~/.codex/config.toml`, `~/.gemini/antigravity/mcp_config.json`, and
`~/.copilot/mcp-config.json`.
## Native module mismatch after changing Node versions
If dependencies were installed under Node 22 and Open Design later runs under
Node 24, native modules such as `better-sqlite3` can fail with a
`NODE_MODULE_VERSION` mismatch.
Reinstall under the active Node 24 runtime:
```bash
cd ~/tools/open-design
rm -rf node_modules
pnpm store prune
pnpm install
```
Then verify the native module loads:
```bash
pnpm --filter @open-design/daemon exec node -e "require('better-sqlite3')"
```
## Codex config parse failures
If Codex fails before MCP install or a direct `codex` run with:
```text
invalid type: map, expected a boolean
in `features`
```
check `~/.codex/config.toml` for nested feature tables such as:
```toml
[features.multi_agent_v2]
hide_spawn_agent_metadata = false
max_concurrent_threads_per_session = 10000
enabled = false
```
Current Codex CLI versions expect `[features]` values to be booleans. Remove or
comment out the nested `[features.*]` block, then retry the command.
Open Design also normalizes this shape before daemon-launched Codex runs, but
manual cleanup may still be needed when Codex itself is invoked directly before
Open Design gets a chance to patch the config.