cross_yapper
README.md
# cross-yapper
A small personal bridge that lets Codex agents discover projects, read existing
conversations, and send messages to specific conversations on trusted machines.
Keep using ordinary Codex conversations. No dashboard, hosted service, separate
agent framework, or cloud dependency.
**Status:** the macOS desktop adapter has passed local same-conversation and
installed-MCP round trips. Linux's attached app-server adapter and actual
cross-machine delivery still need qualification. Windows installation is also
unverified. This is an experimental personal tool, not a hardened public service.
## Download and check
Version 0.2 uses Rust for the service, CLI and MCP protocol. Desktop integration
retains a tiny launcher running in Codex's bundled Node: the installed Mac app
rejects an unsigned Rust process at its private pipe. Each desktop MCP client
has a launcher; one elected launcher shares a persistent desktop connection.
Linux owner mode needs no Node runtime.
Download the matching archive from [Releases](https://github.com/adrianvalentim/cross-yapper/releases)
and extract it into a permanent directory. Keep the executable and both `.mjs`
files together. No Rust compiler or npm installation is required for releases.
On Windows run `cross-yapper.exe`; on Unix run `./cross-yapper`.
For source development (Git and Rust 1.95):
```sh
git clone https://github.com/adrianvalentim/cross-yapper.git
cd cross-yapper
cargo test --locked
./scripts/build.sh
./bin/cross-yapper help
```
Source examples below use `./bin/cross-yapper`. For a release installation,
substitute the downloaded executable. See [Agent setup](docs/AGENT_SETUP.md).
## How it connects
```text
Codex conversation -> cross_yapper MCP -> local bridge service
|
SSH tunnel + peer token
|
remote bridge service
|
remote conversation's actual owner
```
Only narrow bridge requests cross machines. Raw Codex endpoints stay local.
Your OpenAI account supplies Codex access; separate random bridge tokens identify
trusted machines and authorize specific logical projects.
On macOS the desktop app owns its conversations. The service handles HTTP,
authentication and durable queues; a Codex-launched MCP process supplies the
local desktop adapter. A standalone launchd process was rejected by the desktop
pipe's signature authorization, even using the bundled runtime. Do not disable
that check: keep the app and its MCP adapter running.
On Linux the optional owner adapter attaches to an existing app-server through
`codex app-server proxy`. Its terminal client must connect to that **same** server.
Launching another worker or resuming history in a different process does not
prove communication with the original conversation.
## Initial configuration
Run setup as the user who runs Codex. Choose a unique machine ID; `mac` is
already used by the original installation. Examples: `linux-lab`, `mac-studio`.
```sh
./bin/cross-yapper init linux-lab
./bin/cross-yapper project-add cross-yapper "$PWD" "Codex conversation bridge"
```
On macOS, run `init` **from an ordinary Codex desktop conversation** so it can
capture `CODEX_APP_TOOLS_PIPE_PATH` and `CODEX_THREAD_ID`. For Linux, follow the
owner configuration below before starting the bridge.
Config: `~/.config/cross-yapper/config.json`, or the absolute path specified by
`CROSS_YAPPER_CONFIG`. Tokens and durable state live beside it. `init` refuses to
overwrite an existing config. Set `codex` to the actual absolute CLI executable.
Never commit or paste token values, account credentials, or conversation dumps.
### macOS
Run from an ordinary Codex desktop conversation. Set `codex` in the private
config to the installed absolute Codex CLI path. `init` captures the current
pipe and conversation binding. For an existing config use `bind-desktop`.
```sh
./bin/cross-yapper install
./bin/cross-yapper start
./bin/cross-yapper status
```
`install` creates a user LaunchAgent, backs up its prior definition, and replaces
only MCP `cross_yapper`, preserving unrelated Codex settings. It registers the
bundled Node launcher with the native MCP child. Set `CROSS_YAPPER_NODE` to the
actual bundled runtime when it is not at the default ChatGPT.app location.
Keep Codex open with this MCP loaded. The service alone cannot access the app.
`stop`, `start`, and `status` control the service. After an app update/restart,
if the pipe changes, run `bind-desktop`, restart the bridge and reload its MCP.
`adapter` runs a foreground diagnostic worker through the source wrapper; it
must be launched from Codex. Release equivalent: bundled Node with
`desktop-launcher.mjs --adapter-only`.
### Linux: attach to the same owner
Inspect the installed Codex help/schema before relying on these experimental
commands. No existing conversation should be moved or resumed behind its UI.
```sh
# Terminal 1: local control endpoint only.
codex app-server --listen unix://
# Terminal 2: ordinary terminal client connected to that exact owner.
codex --remote unix://
```
Keep both running. In the bridge config, set `codex` to the absolute Codex path
and add `owner` with the same executable:
```json
"owner": {
"command": "/absolute/path/to/codex",
"args": ["app-server", "proxy"]
}
```
This overrides the desktop adapter; `desktop` can remain unused. The bridge does
not launch or resume target conversations. Open a disposable conversation in
that attached client; it must be loaded in that owner.
Register MCP before creating the test conversation:
```sh
./bin/cross-yapper install
./bin/cross-yapper start
```
On Linux `install` creates a systemd user service; `start`/`stop` use systemctl.
For machines without systemd, register `codex mcp add cross_yapper --
/absolute/path/to/cross-yapper mcp` and run `cross-yapper serve` in the foreground.
Keep the owner process running separately. The adapter requires
`thread/turns/list`; missing capabilities fail explicitly. Linux live-owner
integration remains unverified.
### Windows
Use the native Windows executable and Codex installation, never WSL for native
work. Set `CROSS_YAPPER_NODE` to Codex's bundled Windows Node, then run `install`
for MCP registration and `serve` in the logged-in user's session. Windows does
not yet have automatic service lifecycle setup; a user Task Scheduler entry can
run `serve`. Protect config/token files with the user's Windows ACL. Native
Windows desktop pipe compatibility and installation remain unverified.
## Projects and conversations
Each machine has a small explicit registry. The same logical project `id`, such
as `porto`, maps to different local `paths`. Display names do not establish
identity. `nativeProjectId` can record a desktop project ID for reference.
All reads and sends check the target thread's exact normalized cwd against the
selected project's paths, including another check before queued delivery.
Register each relevant worktree and session subdirectory explicitly. Inspect
`git worktree list --porcelain`; do not automatically authorize every descendant
folder. Symlinks resolve where possible; Windows comparisons ignore case.
When Codex supplies sender thread metadata, its cwd resolves the local logical
project. That same ID is the default project on a selected remote machine.
CLI callers outside Codex must specify a project and are labeled as CLI senders.
Some agent-created desktop conversations were omitted by native thread listing
on the tested version. Known IDs may be registered in the project's `threads`
array; they appear as `configuredThreads` after a membership check. Native lists
remain bounded and paginated. Metadata-reader `notLoaded` describes that reader
process, not the desktop's live status; use `read_thread` for live context.
## MCP and CLI
MCP server name: `cross_yapper`.
| Tool | Purpose |
| --- | --- |
| `list_machines()` | Configured machines, reachability, actual OS/architecture and WSL flag |
| `list_projects(machine?)` | Explicit logical project mappings |
| `read_project(machine?, project?)` | Mapping and short thread overview |
| `list_threads(machine?, project?, cursor?, limit?)` | Up to 50 metadata summaries; continuation cursor |
| `read_thread(machine?, project?, thread, cursor?, limit?)` | Bounded history; default 3 turns, maximum 10 |
| `send_message(machine?, project?, thread, text, correlationId, mode?, requirePlatform?, replyTo?)` | Durable exact-thread queue; only `mode="queue"` |
| `read_receipt(machine?, project?, correlationId, senderMachine?)` | State and associated reply |
Machine defaults to local; project defaults to the sender's mapped project when
available. Text is bounded; desktop reads exclude tool outputs. Example:
```sh
./bin/cross-yapper status
./bin/cross-yapper call list_projects '{}'
./bin/cross-yapper call list_threads '{"project":"cross-yapper","limit":10}'
```
Select a real test ID from your own installation. Read it, send a unique
correlation ID, then poll `read_receipt` with the same project and ID. Reuse that
ID and identical content when retrying; changed content is rejected.
`accepted` means durably queued; `delivered` means the owner acknowledged the
send; `completed` means the observed associated turn finished. `failed`,
`interrupted`, and `uncertain` are distinct. Crashes/timeouts during dispatch
become uncertain and are never automatically resent. Inspect the target before
using a fresh ID.
Messages preserve sender machine/project/thread and correlation ID. The service
never sends automatic replies. An explicitly requested reply is a new send to
the recorded origin, with a fresh ID and `replyTo` naming the original ID.
## Pair machines over SSH
Use a suitable key-authenticated SSH connection into the recipient's native
user environment. Verify host keys; do not disable checking. Do not modify an
unrelated restricted compute/WSL endpoint to make it fit this bridge.
On the receiving bridge, authorize only the required logical projects:
```sh
./bin/cross-yapper allow-peer mac cross-yapper porto
```
This prints a newly generated **credential file path**, never its secret.
Securely transfer the token file to the sender using SSH/SCP. On Unix, keep the
credential directory at 0700 and tokens at 0600. Restart the receiver after
changing config. On the sender:
```sh
ssh -NT -o ExitOnForwardFailure=yes -o StrictHostKeyChecking=yes \
-L 127.0.0.1:47832:127.0.0.1:47831 receiver-ssh-alias
./bin/cross-yapper peer-add linux-lab 47832 /absolute/path/to/received.token
```
Replace the SSH alias, machine ID and token path with verified local values.
Restart the sender bridge. Forward only bridge port 47831; raw Codex endpoints
stay local. Configure the reverse direction separately if explicit replies are
needed. The bridge rejects peer transit and remote adapter-control requests.
For Windows-native work, send `requirePlatform: "win32"`; WSL does not satisfy
that requirement. The platform label is not a substitute for a real native
reproduction test.
## Tests, limitations and troubleshooting
`cargo test --locked` covers membership/cursor rejection, busy/idle queueing, retry
idempotency, uncertain dispatch across restart, and native-platform/steering
rejection. Local tests do not establish remote delivery.
On the original Mac, app-owned reads/sends, installed MCP delivery, actual busy
queueing, service restart/deduplication and HTTP authentication/project ACL checks
passed. Private evidence and machine-specific installation records stay out of
this public repository. No screenshot-level verification is claimed.
The desktop adapter uses the installed app's private app-tools pipe and catalog.
It is version-sensitive. Its send operation has no atomic idle precondition or
native turn ID in the acknowledgment. The bridge serializes its own traffic;
simultaneous human/other-client sends can still race it and make attribution
uncertain. Avoid concurrent manual sends during delivery. Steering is not
exposed. No model, permissions, approvals or sandbox overrides are sent.
A separate app-server was able to read metadata but could not start/steer the
existing desktop test thread; resume returned `already has an active writer`.
The bridge never injects messages by editing Codex databases or transcripts.
If the adapter is offline, check the desktop app, MCP loading, bundled runtime,
and current pipe binding. If a peer is unreachable, check its service and SSH
tunnel; retry ambiguous sends only with their original correlation ID.
## Performance and migration
See [benchmark results and reproduction](docs/BENCHMARK.md). The benchmark
compares the complete old Node and Rust process trees, including every desktop
launcher. It uses disposable synthetic endpoints and never sends real messages.
Existing JSON config, project IDs, tool arguments, cursor handling and receipt
fingerprints remain compatible. Stop the old service, back up the config/state,
build or extract the release, run `install`, then `start`. Reload MCP in Codex;
already running old MCP processes keep their old code until reloaded. The old
implementation remains available at Git commit `f9d1f04` for rollback and benchmarks.
No Codex transcript or internal database is changed.
Protocol references: [Codex app-server](https://learn.chatgpt.com/docs/app-server)
and [Remote connections](https://learn.chatgpt.com/docs/remote-connections).
The initial Mac inspection used CLI 0.153.4. Generate version-matched schemas
locally with `codex app-server generate-ts --out /temporary/schema-directory`.
To uninstall, stop only this service, run `codex mcp remove cross_yapper`, and
remove its service definition. Optionally archive its private config directory.
Never restore a whole old Codex configuration over later unrelated changes.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues