See proposal.md for motivation. Stock codex-acp currently starts the executable selected by CODEX_PATH with the single app-server argument and exchanges newline-delimited JSON-RPC over that child process's stdin and stdout. A separately managed Codex App Server already listens on ws://127.0.0.1:4500 and exposes the same JSON-RPC protocol as one JSON message per WebSocket text frame.
The solution spans a new standalone package repository and a later consumer configuration change in the user's dotgit-managed Emacs initialization. The shared server owns authentication and lifecycle; the bridge and agent-shell must remain clients only.
Emacs agent-shell
|
v
stock codex-acp
| spawn: $CODEX_PATH app-server
| stdio JSONL
v
codex-app-server-bridge
| one text frame per JSONL record
| ws://127.0.0.1:4500
v
shared Codex App Server
Goals:
codex-acp while substituting only the executable behind its existing CODEX_PATH contract.Non-Goals:
codex-acp, agent-shell, or Codex.CODEX_PATH as the integration seamcodex-acp will remain unchanged. Emacs will resolve the globally installed codex-app-server-bridge executable and pass its absolute path as CODEX_PATH; codex-acp will therefore spawn codex-app-server-bridge app-server using its normal code path.
Alternatives considered:
codex-acp to add native WebSocket support. This removes the extra process but introduces an upstream fork and version coupling for a transport adaptation that can be isolated.CODEX_PATH at a wrapper that delegates other Codex commands. This creates recursion and command-routing risks without a requirement; the bridge instead accepts exactly app-server.Each bridge instance owns one WebSocket connection for the lifetime of one codex-acp child. After validating arguments and configuration, it starts the connection with a five-second deadline. It does not begin consuming stdin until the socket is open, leaving early parent writes in the operating-system pipe rather than maintaining a custom startup queue.
After connection, a line reader maps each nonblank stdin record to one text frame. Incoming text frames are written to stdout with a newline delimiter. Payloads are not parsed, so method semantics—including account/logout—remain owned by the endpoints. Binary frames are fatal protocol errors. A dropped connection terminates the bridge and lets codex-acp surface failure; reconnecting would require replay and session-recovery semantics that version 1 intentionally excludes.
Alternative considered: an RPC-aware proxy that validates, filters, or rewrites methods. It could block globally scoped operations such as logout, but it would duplicate protocol knowledge, risk incompatibility with App Server evolution, and violate the selected transparent transport boundary.
CODEX_APP_SERVER_URL is mandatory and has no default. URL validation admits only ws://127.0.0.1:<port> and ws://[::1]:<port> at the root path, without credentials, query, or fragment. localhost is rejected to avoid name-resolution ambiguity. There is no private-server fallback.
Alternative considered: accept arbitrary ws:// or wss:// endpoints. That would require a broader network threat model, TLS trust configuration, credential handling, and remote failure policy that are unnecessary for the confirmed local shared-server use case.
The implementation uses strict TypeScript and Node's native WebSocket client plus core stream/process APIs, so the package has no runtime dependencies. tsconfig.json provides strict no-emit checking, while a dedicated build configuration reproducibly emits ESM JavaScript for Node 24 into ignored dist/ output.
The npm package declares engines.node >=24, version 0.1.0, private: true, and a bin entry. A minimal JavaScript bin bootstrap checks the Node major version before importing the compiled entry point so an unsupported runtime fails intentionally. npm pack runs the build and includes dist/ but excludes TypeScript source and tests. The generated JavaScript and tarball remain uncommitted. Pinned typescript and @types/node are the only development dependencies.
Alternatives considered:
node_modules, including npm's global package store.tsx or ts-node. These add runtime dependencies solely to execute source after installation.The supported version 1 flow is npm ci, typecheck, build, test, npm pack, and global installation from the resulting .tgz. Packing rebuilds the ignored dist/ directory, and the tarball includes only the compiled runtime plus its bootstrap and documentation. Both generated output forms remain uncommitted. Marking the package private prevents accidental registry publication but does not prevent packing or local installation.
Alternative considered: install directly from the working directory or a Git URL. Both can work, but a packed tarball validates the actual installation unit and avoids requiring a registry or a remote repository.
The existing CLI tool-auth helper remains responsible for launching the shared server with its authentication environment and independently configured listen URL. The agent-shell environment function explicitly injects CODEX_APP_SERVER_URL=ws://127.0.0.1:4500; it does not inherit the value from Emacs's parent environment and does not share an Emacs variable with the server launcher in version 1.
For CODEX_PATH, the launcher calls executable-find and injects the resolved absolute path. Missing installation is reported before codex-acp starts. The normal C-c p x binding remains. The capital C-c p X binding and its agent-shell-specific privileged launcher are removed, while Pi and Claude bindings and the tool-auth CLI helper remain unchanged.
Alternative considered: define one Emacs variable for both endpoints. This reduces duplication but couples two independently managed concerns; the confirmed version 1 choice keeps them separate.
The bridge emits no payload or routine relay logs. Configuration, timeout, connection, protocol, and close failures go to stderr with a sanitized endpoint and available lifecycle reason. Stdin EOF, SIGINT, and SIGTERM initiate WebSocket closure and return success; failures and unexpected remote closure return nonzero.
A debug mode is excluded from version 1 because prompt and tool traffic can contain sensitive data. If later diagnostics require payload visibility, that must be a separately designed opt-in capability.
Automated tests use node:test against TypeScript test files. A minimal local WebSocket test peer built from Node core APIs exercises handshake, text and binary framing, startup timeout, shutdown, and error paths without requiring Codex, credentials, or network access. Pure URL and invocation validation are tested directly.
The release validation sequence separately typechecks, runs hermetic tests, inspects npm pack contents, installs the tarball, and performs a live smoke check against an already-running loopback App Server. The live check verifies initialization and a normal request, confirms the shared server survives bridge exit, confirms no private codex app-server child is spawned, and confirms the resulting Emacs-owned thread is visible from the shared server.
Alternative considered: make the real Codex App Server part of npm test. That would couple normal tests to local credentials and lifecycle state and make failures nondeterministic.
codex-acp changes its CODEX_PATH app-server invocation contract] -> Pin and document the tested adapter version, cover the invocation with a packaged smoke test, and update the narrow bridge contract when needed.npm pack, keep dist/ uncommitted, and validate the installed tarball rather than a stale working-tree build.0.1.0 tarball and install it globally; verify Emacs can resolve the executable.ws://127.0.0.1:4500 with the intended tool-auth environment.Rollback consists of reverting the Emacs dotfiles change and restarting affected agent-shell buffers, then uninstalling the bridge package if desired. The shared App Server requires no rollback because the bridge does not mutate its lifecycle or persistent configuration.