Skip to content

Commit 5f5bfb5

Browse files
committed
refactor(codex): wire bundled plugin via marketplace + per-spawn MCP overrides
Codex consumes the same plugin tree as Claude (resources/plugins/ harness-status/), but the integration path is different in three fundamental ways the implementation had to discover empirically: 1. Codex does NOT interpolate `${VAR}` in .mcp.json — not user env (${HARNESS_NODE_EXEC}), not its own injected vars (${CLAUDE_PLUGIN_ROOT}). Templates are passed verbatim to execve, and the plugin's source .mcp.json (which Claude expands correctly via --plugin-dir) would fail with "MCP startup failed: No such file or directory" if Codex tried to spawn it. We neutralize the cached .mcp.json post-install to `{"mcpServers": {}}` and register harness-control per-Codex-spawn via `-c mcp_servers.harness-control.command="..."` literal overrides. 2. Codex spawns MCP subprocesses with a stripped env — only HOME/PATH/PWD/USER/LANG inherit. The HARNESS_* vars set on the agent PTY don't reach the bridge. The per-spawn `-c` override bakes literal values directly into the MCP env block. 3. Codex's `~` resolution uses getpwuid_r (system passwd database) rather than $HOME on macOS, so an isolated $HOME doesn't reach the Codex binary. We set process.env.CODEX_HOME = $HOME/.codex at boot when not already set — idempotent for the common case, fixes isolation for dev sessions, propagates to all child processes. Install path: - `hooks:accept` IPC runs `codex plugin marketplace add <bundled-root>` + `codex plugin add harness-status@harness`. Both write to $CODEX_HOME/config.toml; cleanly reversible via the matching remove commands. - Boot logic re-runs the install every launch when consent === 'accepted'. Codex's plugin cache never auto-refreshes, so re-running is how we propagate new Harness releases to the cache. - Spawn args: agents/codex.ts buildSpawnArgs appends -c overrides for command, args, and env when opts.harnessControl is set. Verification (Settings → Codex → "Harness status plugin"): - Plugin enabled in Codex (`codex plugin list --marketplace harness`) - Hooks file present in cache - Hooks trusted by Codex (key-based presence check — `[hooks.state."harness-status@harness:hooks/hooks.json:<event>:N:M"]` with non-empty trusted_hash for every Codex-recognized event from hooks.json; Notification is filtered out as Claude-only). Trust: - We do NOT auto-trust. Codex's trust prompt is TUI-only; there's no CLI command or flag to grant trust non-interactively. The Settings card surfaces inline guidance when hooks aren't yet trusted: open a Codex tab, accept the "Hooks need review / Trust all and continue" prompt, then Re-check. Uninstall: - `codex plugin remove` + `codex plugin marketplace remove` - Strip [hooks.state] entries from $CODEX_HOME/config.toml by `harness-status@harness:` key prefix (Codex's `trusted_hash` is over a normalized form we can't reproduce, so key-based matching is the reliable signal). A reinstall re-prompts for trust review rather than silently inheriting prior consent via content-hash match. UI: - Install prompt lives as an in-tab overlay in XTerminal.tsx (`agentKind === 'codex' && hooksConsent === 'pending'`), mirroring the Claude "Switch to Chat mode" hint. No top-of-app banner — Claude-only users never see it. - Settings card shows the actual CLI commands Harness runs, the verification panel, and a Re-check button. - Card title: "Harness status plugin" (it's a plugin Harness ships for Codex to call back into Harness, not a Codex-authored plugin). Codex agent module (agents/codex.ts): - Removed installHooks/uninstallHooks/hooksInstalled (~150 lines of dead code from the pre-plugin ~/.codex/hooks.json install path). stripHooksFromWorktree reduced to a one-line delegate satisfying the AgentModule contract; effectively dead post-migration. - Sessions dir lookup honors CODEX_HOME. Migration: - `codexPluginMigrated` flag gates a one-shot strip of legacy ~/.codex/hooks.json + per-worktree .codex/hooks.json Harness entries on first boot post-upgrade. Inferred consent (positive legacy strip → 'accepted') so prior consenters don't get re-prompted.
1 parent 7541332 commit 5f5bfb5

16 files changed

Lines changed: 847 additions & 445 deletions

File tree

CLAUDE.md

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -303,14 +303,23 @@ when it spawns each tab), so sessions outside Harness no-op cleanly.
303303
A drift-detection test in `src/main/claude-plugin.test.ts` asserts the
304304
shipped `hooks.json` matches what `makeHookCommand()` would generate
305305
today so the two can't silently diverge.
306-
- **Codex** — still installed at user scope (`~/.codex/hooks.json`),
307-
gated by the consent banner / Settings card. Codex has no plugin
308-
system; users have to opt in once. The `hooks/consent` slice +
309-
`hooks:accept|decline|uninstall` IPC handlers exist for this path
310-
only. A one-shot boot migration (gated by
311-
`config.hooksMigratedToPlugin`) strips any legacy Harness entries from
312-
the old global Claude install + per-worktree
313-
`.claude/settings.local.json` files.
306+
- **Codex** — Codex 0.133+ reads the **same** plugin tree as Claude.
307+
No `--plugin-dir` flag exists for Codex, so we register the bundled
308+
directory as a local marketplace via `codex plugin marketplace add
309+
<bundled-root>` and enable the plugin with `codex plugin add
310+
harness-status@harness`. Both commands write entries to
311+
`~/.codex/config.toml`, which is why this path is still gated by the
312+
consent banner / Settings card. The `hooks/consent` slice +
313+
`hooks:accept|decline|uninstall` IPC handlers wire the user choice;
314+
see `src/main/codex-plugin.ts` for the install / uninstall helpers.
315+
Two one-shot boot migrations sweep dead state from prior installs:
316+
`config.hooksMigratedToPlugin` strips legacy Claude entries from
317+
`~/.claude/settings.json` + per-worktree `.claude/settings.local.json`;
318+
`config.codexPluginMigrated` strips legacy Harness entries from
319+
`~/.codex/hooks.json` + per-worktree `.codex/hooks.json`.
320+
After acceptance, `installCodexPlugin()` runs on every boot —
321+
idempotent, and the only way to force Codex's plugin cache to pick up
322+
a new Harness release (the cache never refreshes automatically).
314323

315324
## How performance debugging works
316325

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"name": "harness-status",
33
"version": "1.0.0",
4-
"description": "Harness status detection hooks. Generated; see scripts/generate-plugin-hooks.mjs."
4+
"description": "Integrate with Harness for up-to-date status"
55
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { describe, it, expect } from 'vitest'
2+
import { buildSpawnArgs } from './codex'
3+
4+
describe('codex buildSpawnArgs with harnessControl', () => {
5+
it('emits -c mcp_servers.harness-control.* with literal values', () => {
6+
const cmd = buildSpawnArgs({
7+
command: 'codex',
8+
cwd: '/wt',
9+
harnessControl: {
10+
execPath: '/abs/Electron',
11+
bridgePath: '/abs/bridge.js',
12+
port: 9999,
13+
token: 'secret',
14+
terminalId: 'term-1',
15+
workspaceId: '/wt',
16+
repoRoot: '/repo',
17+
isMain: true
18+
}
19+
})
20+
console.log('SPAWN:', cmd)
21+
expect(cmd).toContain('-c')
22+
expect(cmd).toContain('mcp_servers.harness-control.command')
23+
expect(cmd).toContain('"/abs/Electron"')
24+
expect(cmd).toContain('"/abs/bridge.js"')
25+
expect(cmd).toContain('HARNESS_PORT="9999"')
26+
expect(cmd).toContain('HARNESS_TOKEN="secret"')
27+
expect(cmd).toContain('HARNESS_TERMINAL_ID="term-1"')
28+
expect(cmd).toContain('HARNESS_IS_MAIN="1"')
29+
})
30+
})

src/main/agents/codex.test.ts

Lines changed: 0 additions & 153 deletions
This file was deleted.

0 commit comments

Comments
 (0)