codex-channel-molecule/README.md
Hongming Wang 0f194d4507 v0.1.2: codex CLI subcommand shape + inbox poller activation + launchd PATH
Three production bugs caught by the codex agent live-testing the daemon
end-to-end against codex-cli 0.128 + a real LaunchAgent install:

1. Codex CLI 0.6+ moved `--resume` from a flag on `exec` to a
   `resume` subcommand. The daemon was sending
   `codex exec --skip-git-repo-check --resume <sid> <prompt>`, which
   parses on 0.5.x but fails on 0.6.x+. Fixed to:
     fresh:  codex exec --skip-git-repo-check <prompt>
     resume: codex exec resume --skip-git-repo-check <sid> <prompt>
   Verified on codex-cli 0.128 with a live binary; the 0.5.x behavior
   is preserved by the fake-codex test fixture, which now SystemExits
   if it sees the legacy `--resume` flag (regression gate).

2. wait_for_message() never returned anything because nothing in the
   daemon ever called molecule_runtime.inbox.activate() or started
   the poller thread. The wheel ships those primitives but expects
   the embedding runtime to wire them up — the workspace runtime
   does this in start.py, but a standalone daemon embedding the
   tools must do it itself. Added the activation + poller-thread
   start in _RealTools.__init__.

3. The codex binary is a `#!/usr/bin/env node` shim. Under launchd /
   stripped systemd units, the parent process PATH is `/usr/bin:/bin`
   and `env node` 127s out silently. CodexRunner now prepends the
   directory of the resolved codex binary to the subprocess PATH at
   spawn time — Node lives next to codex under nvm / brew /
   pnpm-global, so this restores the discovery without operators
   having to thread PATH through their LaunchAgent / unit file.
   README updated with a note on the launchd/systemd interaction.

Test:
- 31 passed (was 28). Three new regression gates: subcommand-shape,
  PATH-prepend (launchd-default PATH stripped), and PATH-no-double
  (idempotent when codex_bin_dir already present). Verified the two
  behavioural new tests FAIL on the old codex_runner.py by stashing
  the source change only and re-running.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 21:21:44 -07:00

5.3 KiB

codex-channel-molecule

Bridge daemon — gives codex CLI push parity with the Molecule AI platform's other external runtimes.

The Molecule platform's hermes-channel-molecule plugin gives hermes-agent true push delivery — peer agents and canvas-user messages land mid-session as conversation turns. Codex CLI has no plugin API today and its MCP runtime drops inbound notifications, so this daemon is the equivalent push surface — built outside the codex process.

How it works

canvas user / peer agent ──► molecule platform inbox
                                       │
                              wait_for_message (long-poll)
                                       │
                                       ▼
                       codex-channel-molecule daemon
                                       │
                              codex exec --resume <sid> "<msg>"
                                       │
                              capture stdout
                                       │
                              send_message_to_user / delegate_task
                                       │
                              inbox_pop(activity_id)
                                       │
                                       ▼
                         canvas chat / peer workspace

Per chat thread (one canvas-user thread or one peer-workspace thread) gets its own codex session_id, persisted to ~/.codex-channel-molecule/sessions.json so daemon restarts don't lose conversation context. Set CODEX_CHANNEL_MOLECULE_STATE_DIR to override the default location (e.g. when running under systemd with a per-instance state dir).

When to use this vs. the codex tab in the External Connect modal

The codex tab wires the molecule MCP server into ~/.codex/config.toml so codex can call platform tools (list_peers, delegate_task, send_message_to_user, commit_memory, etc.). That's outbound — codex calls out to the platform.

This daemon is the inbound counterpart — the platform pushes to codex. Run both for full bidirectional integration.

Install

npm install -g @openai/codex@^0.57
pip install codex-channel-molecule

Configure + run

The same env-var contract as hermes-channel-molecule's outbound MCP path (WORKSPACE_ID, PLATFORM_URL, MOLECULE_WORKSPACE_TOKEN):

export WORKSPACE_ID=<uuid from External Connect modal>
export PLATFORM_URL=https://<your-tenant>.moleculesai.app
export MOLECULE_WORKSPACE_TOKEN=<bearer token from External Connect modal>

codex-channel-molecule

The daemon runs in the foreground; logs go to stderr. For systemd hosts, register a unit; for one-off use, nohup ... & plus a log file works.

Running under launchd / systemd (Node-on-PATH note)

The codex binary is a #!/usr/bin/env node shim, so spawning it requires node to be discoverable on PATH. Under an interactive shell that's automatic; under launchd (macOS) and stripped-down systemd units PATH defaults to /usr/bin:/bin, and env node will 127-out silently.

Since 0.1.2 the daemon resolves codex to its absolute path and prepends that directory to the subprocess PATH automatically — Node lives next to codex under nvm / brew / pnpm-global, so the shim's env node finds it. Operators typically don't need any PATH plumbing in their LaunchAgent / unit file beyond the three required env vars.

If you're on an unusual install layout where codex and node live in different directories, set the LaunchAgent / unit PATH explicitly to include both.

Deprecation path

When openai/codex#17543 lands upstream — a generic path for handling MCP custom notifications in codex and forwarding them into the active session as user submissions — this daemon becomes redundant. Codex itself will accept inbound molecule messages as Op::UserInput directly through the MCP server already wired in ~/.codex/config.toml. Until then, this is the operator-facing answer.

Development

git clone https://github.com/Molecule-AI/codex-channel-molecule
cd codex-channel-molecule
pip install -e ".[test]"
pytest -q

Tests are entirely real-subprocess (no mocking the spawn boundary) so the boot path is covered the same way the daemon runs in production.

Releasing

Tag-on-push triggers publish.yml which builds + publishes to PyPI via OIDC trusted publishing (no API token needed).

# Bump pyproject.toml `version`, commit, then:
git tag v0.1.1 && git push origin v0.1.1

The workflow refuses to publish if the tag doesn't match pyproject.toml's version — keeps PyPI versions and git tags in lockstep.

One-time PyPI setup (before the first release):

  1. Create the project on PyPI by uploading the first wheel manually, OR
  2. Pre-register the project on PyPI under a "Pending publisher" config so the first tagged push creates it.

Either way, on the project's PyPI page → "Manage" → "Publishing" → "Add a new publisher", configure:

  • Owner: Molecule-AI
  • Repository: codex-channel-molecule
  • Workflow filename: publish.yml
  • Environment name: pypi

After this, every git push origin v*.*.* ships the wheel to PyPI without any further intervention.

License

Apache-2.0