CodeWide is an Android-first agentic IDE for working with Codex on remote development machines.
- Expo 57 / React Native 0.86 adaptive phone, tablet, and unfolded-fold UI.
- Slack/Telegram Desktop-style fold workspace: server rail, selected-server threads, selected thread.
- Typed domain identities that isolate equal remote thread IDs across servers.
- Rich renderer registry with bounded fallback for unknown future items.
- Live structured plans, turn diffs, token usage and bounded MCP progress.
- Durable SQLite cache/outbox/drafts/pending approvals with batched delta commits.
- Native Android foreground WebSocket service with replay journal, network callbacks, and voice dictation bridge.
- Authenticated loopback companion bridging WebSocket frames to the managed App Server over its Unix WebSocket socket.
- One-time device pairing/revocation, authenticated file transfer, and bounded localhost previews plus native phone-local TCP forwarding for HTTP, WebSocket and HMR.
- Companion-owned durable prompt queue with reconnect reconciliation and delivery receipts, plus goals/progress, review, compact, steer and boundary-aware fork.
- A full-width composer with a left control menu for model/effort, files, skills, permissions and delivery mode; no filter tabs below thread search.
- Typed file/image/audio inputs with persisted attachment drafts and authenticated inline previews for remote files. Arbitrary external image URLs still require the authenticated media-proxy gate.
- Bounded, schema-valid per-server outboxes with exactly-once reconciliation and explicit backpressure instead of invisible overflow.
- Deterministic protocol fixtures for rendering, synchronization, and performance tests.
CodeWide is distributed under the MIT License.
pnpm install
pnpm typecheck
pnpm test
pnpm bench:fixtures
pnpm test:e2e
pnpm security:scan-artifacts
pnpm security:scan-secrets
pnpm --filter @codewide/android build
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo run -p codewide-companion -- --helppnpm security:scan-secrets runs pinned Gitleaks against both reachable Git
history and the tracked/untracked non-ignored working tree. The verified binary
is cached outside the repository; set CODEWIDE_GITLEAKS_BIN to use an existing
installation instead.
Serve every locally available Android APK from a compact build shelf with a stable latest-build marker and resumable byte-range downloads:
python3 scripts/build-shelf-server.py --host 127.0.0.1 --port 4190The shelf scans Gradle APK outputs and archived test builds on every request.
While it is running, each new release/app-release.apk is also copied once into
builds/android/, with its version metadata and SHA-256, so rebuilding does not
erase the recent download history. The archive keeps the eight newest APKs and
their metadata. /latest.apk always resolves to the highest release version
code; /api/builds exposes the current catalog; /healthz is the lightweight
tunnel probe.
Run the logical multi-server recovery soak (24 hours by default):
pnpm soak:syncIt writes a private mode-0600 progress/result artifact under
test-results/soak/. This exercises replay, network drops and JS runtime
recreation; it does not replace the physical Android Doze/device gate.
Run the UI in a browser during renderer development:
pnpm --filter @codewide/android webFor Android JS/TS Fast Refresh, install the debug APK once. Keep Metro on the
workstation loopback and provide an operator-owned HTTPS endpoint through
CODEWIDE_METRO_URL; the phone then needs neither USB nor ADB and may use any
network:
pnpm dev:androidThe debug application id is dev.codexremote.app.dev, so it can remain
installed beside the signed release app. Open the codewide:// URL printed
by the helper once; subsequent JS/TS edits arrive through Fast Refresh. Metro
signs its development manifest with the same local OTA key. On Linux it also needs a sufficiently large
inotify budget (fs.inotify.max_user_watches=524288 and
fs.inotify.max_user_instances=1024).
The dev.codexremote.app package identity and related legacy storage, service,
and deep-link aliases are retained intentionally. They let existing installs
upgrade to CodeWide without losing local state or breaking saved links.
For the signed release app, publish JS/TS, message-renderer, and asset changes without rebuilding or reinstalling the APK:
./scripts/release-otaThe one-shot command discovers the private OTA key and public update endpoint,
runs the Android release checks, publishes atomically, verifies the public
manifest and bundle hash, and prints the update ID as JSON. Use --dry-run to
validate release configuration without publishing.
The build shelf serves the signed Expo Updates protocol at /api/updates.
A release app checks once when its UI opens and at most once every 30 minutes
while it remains active, downloads a new compatible bundle, and immediately
recreates its JS runtime with that bundle. The Android process does not need to
be force-stopped; transient UI state may be reset. The signing key defaults to
the user data directory under codewide/ota/private-key.pem and can be
overridden with CODEWIDE_OTA_PRIVATE_KEY. Only the public certificate is
checked in.
Changes to Kotlin, the Android manifest, native libraries, or native dependency
versions still require a new APK and a new runtimeVersion; the updater rejects
bundles built for a different native runtime.
The composer records mono PCM16 at Android's native routed sample rate. Native
callbacks use roughly 100 ms capture frames, but those frames are not network
requests: the client coalesces them into ordered one-second batches and keeps
exactly one companion/dictation/appendBatch RPC in flight. Stop flushes the
partial batch before companion/dictation/finish, so a long recording cannot
be reordered and a short recording cannot lose its tail.
The companion writes the private PCM stream in capture order, wraps bounded WAV segments, and calls the ChatGPT transcription endpoint only after Stop. Codex OAuth stays on the host; microphone audio and credentials are never persisted by the Android client. Retryable rate limits retain the host-side recording only until explicit retry/cancel or idle cleanup.
Build and install the companion as a user service:
pnpm build:companion
target/release/codewide-companion create-token
apps/companion/deploy/install.sh
codewide-companion pairThe packaged service exposes an HTTP carrier. Put it behind any ordinary
public-CA TLS ingress, Tailscale, ngrok, or another relay. Application traffic
uses a separate TLS 1.3 connection inside the blind WebSocket carrier, so the
relay cannot read it. Tell the CLI which public wss:// URL belongs in the QR:
set -lx CODEWIDE_PUBLIC_ENDPOINT wss://host.example/v1/sync
set -lx CODEWIDE_SERVER_NAME 'Home workstation'
set -lx CODEWIDE_SERVER_EMOJI '🏠'
codewide-companion pairpair reads the Companion identity through the private control socket and puts
its SHA-256 SPKI pin in the QR. A current Android build establishes pinned inner
TLS before it sends the one-time pairing token, public device key, or proof. The
initial challenge/session exchange and all later private traffic stay inside it.
The public carrier exposes no authentication or application data routes; it
accepts only the blind /v1/e2ee-tunnel plus Build Shelf / update downloads.
Secure transport is mandatory. Pairing payloads and saved profiles without a Companion identity pin are rejected and must be paired again. There is no preview switch, legacy transport, automatic trust-on-current-carrier upgrade, or downgrade path.
Scan the terminal QR from Add server, or enter the one-time token manually.
The CLI selects compact Unicode, ANSI, or a private SVG fallback with
--qr auto; use --qr unicode|ansi|svg to force a renderer.
The QR expires after five minutes and contains a secret: do not paste it into
logs or share a screenshot. Remote endpoints must use wss://; plain ws://
is accepted only for loopback, the Android emulator host alias, or an SSH
forward terminating on the device. serve --insecure-http is retained only as
a hidden command-line compatibility no-op; it no longer disables or changes
inner TLS.
Build Shelf / OTA and Expo Updates intentionally remain carrier-level public traffic. They do not pass through the native inner proxy because an app must be able to download its own update before the application transport is available.
Inspect the public identity without exposing its private key:
codewide-companion identityThe private identity key is stored through the Companion Secure Store. It uses the platform credential store when available (Keychain, Credential Manager, or Linux keyring plus Secret Service) and records that backend durably. Headless systems fall back to a mode-0600 file. A temporarily unavailable platform store never causes silent key regeneration or a weaker fallback; a later successful load may migrate the file-backed key upward.
Identity rotation is an explicit recovery event, not silent renewal. Stop the service, rotate with acknowledgement, restart it, then scan a fresh QR on every device. The previous private key is deleted and old pins fail closed:
systemctl --user stop codewide-companion.service
codewide-companion rotate-identity --confirm-device-repair
systemctl --user start codewide-companion.service
codewide-companion pairThe phone stores only its revocable per-device capability in Android
Keystore-backed SecureStore; the host persists only its SHA-256 hash and the
installation's public P-256 key. Before every socket reconnect,
the foreground service signs a one-use 60-second challenge with the
non-exportable Android Keystore key, then receives a non-chainable 15-minute
in-memory session token. A stolen bearer without that key cannot mint a session;
a long-lived device capability cannot open /v1/sync; and a paired device can
never open the raw /v1/app-server bridge. Every successfully authenticated
paired session receives the complete Companion data-plane grant.
The companion exposes registration and session minting through the single
public /v1/auth endpoint. Registration signs the one-time pairing token and
device metadata with the non-exportable Android Keystore key. The server derives
deviceId from that public key; the id is an identifier, not a secret or a
second factor. Copying either the id or the long-lived capability is insufficient
to mint a session without the private key. Pairing creation, device management,
health checks and the raw App Server bridge exist only on the OS-local control
endpoint and are not routed by the public server at all.
Inspect and revoke devices:
codewide-companion devices
codewide-companion revoke DEVICE_IDAndroid sends bounded structured telemetry batches to its own companion. The
schema contains timestamps, event names, numeric measurements, short tags and
explicit identifiers only; message text, prompts, protocol payloads and raw
state are rejected by both client and server. The companion stores at most
200,000 events for seven days in a separate telemetry.redb, deduplicated by
device and event id, so diagnostics cannot enlarge or lock the canonical thread
index.
Telemetry ingestion is session-authorized on the public endpoint. Reading it is available only through the OS-local administrator control endpoint:
codewide-companion telemetry query --session-id THREAD_ID --descending
codewide-companion telemetry query --request-id REQUEST_ID
codewide-companion telemetry query --from-unix-ms 1787240000000 --to-unix-ms 1787240300000 --name stream.react_commitTime filters use the companion receipt time, which remains useful when a phone
clock is skewed. Queries also accept --device-id, --connection-id,
--batch-id, --client-session-id, --event-id, --thread-id, --turn-id,
--item-id, exact --tag-name/--tag-value pairs and --limit.
The Android thread menu can open a full-screen terminal workspace rooted at
that thread's working directory. Each Codex thread owns its own terminal tabs;
there is no global terminal. The companion owns every pseudo-terminal and sends
ordered binary input/output over a dedicated authenticated WebSocket at
/v1/terminals; Android owns the certificate-pinned socket, while
expo-libghostty provides VT parsing, rendering, selection, IME input and the
terminal accessory keyboard. The down-arrow minimizes the full-screen workspace
without stopping its shells. A Terminals · N chip beside the thread resources
restores it; only closing an individual tab terminates that shell. Android keeps
the ordered ANSI transcript in its cache so a recreated renderer can recover
after minimization without keeping hidden terminal views mounted. Transcripts
are deleted with their tabs and bounded to 128 MiB each. A connection shutdown,
thread deletion, or companion shutdown also closes the associated terminals.
The client and companion each permit at most eight concurrent sessions.
Remote shell access is part of the full grant issued to every authenticated paired session. Revoke the device to remove that access.
The current expo-libghostty React Native seam carries base64 strings across
the JS/native boundary. That encoding is confined to the renderer adapter; the
phone-to-companion transport remains binary, so replacing the seam later does
not change the terminal protocol. The native dependency ships 64-bit Android
artifacts only, so Android builds target arm64-v8a devices and x86_64
emulators rather than advertising a 32-bit terminal that cannot load.
The Android foreground service can expose a remote-machine loopback service as
127.0.0.1:<phone-port> on the phone. Each current forward maps one remote
127.0.0.1:<remote-port> to an automatically selected or explicitly preferred
phone port. Current forwards survive JS runtime recreation, but are never
restored from saved profiles. The native service polls current listeners while
the connection is enabled and reconstructs forwards from discovery after restart.
Only explicit include/exclude policies persist; React renders the native projection.
The companion lazily inventories listeners when the selected server is active.
It applies the same conservative recognition policy as Doma for Docker Compose,
Minikube, Kubernetes port-forwards, Hermes, Vite, Bun/Node, Python, zrok,
user processes and system-owned services. Stable service keys preserve an
include/exclude choice across ephemeral port changes. Known developer services
start automatically; system services and unknown ephemeral listeners stay in
Available until explicitly included. The Ports screen groups recognized
services, filters by name/category/port, and separates Active, Available
and Excluded entries.
A confirmed-live service opens as a full-screen app route, not inside the Ports sheet. Its browser can dock the bundled Chromium DevTools Frontend below the page and connects it to the inspected WebView through the real Chrome DevTools Protocol. Elements, Console, Sources, Network, Application and the Performance panel therefore use WebView's native CDP implementation rather than an injected JavaScript console. A separate Android WebView trace control records all native tracing categories as a fallback. Browser-shell features that Android WebView does not implement remain unavailable; this is the maximum page-level surface, not an emulation of desktop Chrome.
Each accepted phone TCP connection gets one authenticated binary WebSocket at
/v1/port-forwards/<remote-port>. The companion opens only its own
127.0.0.1:<remote-port> and forwards opaque bytes, so HTTP keep-alive,
WebSocket upgrades and dev-server HMR work without protocol rewriting. The
endpoint requires a short-lived authenticated paired session, rejects
browser-origin upgrades, expires streams with their session, and never accepts
an arbitrary target host. The older bounded path-based preview endpoint remains
available for compatibility.
A successful discovery removes a vanished service and closes its phone listener
and active streams. An explicit exclusion remains a policy, not a stale row;
it applies again if the service returns. Failed scans do not masquerade as an
empty inventory. There is no Saved ports group. Confirmed-live HTTP services
open in CodeWide's built-in browser, including its bundled developer tools.
The phone listener is loopback-only, but another local app that discovers its port could connect to it. Prefer automatic high ports and stop profiles that are not in use; do not forward an unauthenticated administrative service.
Pairings created before device-proof support or before the Companion identity pin became mandatory must be paired again.
The production companion binds its blind HTTP carrier to 127.0.0.1:8766 and a
process-private TLS 1.3 listener to a random loopback port. Operator commands
such as pairing and device management use
the same CLI over a private local IPC endpoint ($XDG_RUNTIME_DIR/codewide/companion-control.sock
on Linux), protected by directory mode 0700, endpoint mode 0600 and the
administrator capability. There is no administrative TCP listener to forward by
mistake. A reverse proxy, SSH forward, overlay, or relay sees only opaque inner
TLS records after the WebSocket tunnel is established.
The installed companion binary is also the headless operator CLI: create-token,
pair, devices and revoke call the running process through that
local endpoint.
A durable deployment runs codewide-companion.service behind an operator-owned
blind relay or private overlay network. Hostnames, tunnel credentials and
provider-specific configuration are machine-local and must not be committed.
Set the Android build's update endpoint explicitly:
export CODEWIDE_UPDATE_URL=https://updates.example.com/api/updatesWireless development similarly requires an operator-owned HTTPS Metro endpoint
through CODEWIDE_METRO_URL; the repository has no default public tunnel.
The Android system picker uploads composer attachments into the companion-owned
private attachments root. Named roots provide stable bases for relative paths;
authenticated paired sessions may also read or write explicit absolute host paths:
set -lx CODEWIDE_FILE_ROOTS '{"project":"/absolute/path/to/project"}'
systemctl --user restart codewide-companion.serviceUploads require X-Content-SHA256 and use bounded resumable chunks. Downloads resume through byte ranges;
both directions verify the final SHA-256 before publishing the destination.
Release builds never fall back to the debug signing key. The normal release path is one command:
./scripts/release-apkIt discovers the private signing material, increments the patch version,
version code and native runtime together, runs the release checks, builds and
verifies the signed arm64 APK, publishes it to the build shelf, downloads it
through the public endpoint, verifies its SHA-256, and prints the release data
as JSON. Source versions roll back if the build fails before publication. Use
--dry-run to inspect the next version and validate credentials without
changing files or publishing. --version X.Y.Z overrides the automatic patch
version while the version code still increments by one.
The low-level Gradle/signing variables below remain available for debugging or nonstandard builds:
set -lx CODEWIDE_RELEASE_STORE_FILE /absolute/path/to/release.keystore
set -lx CODEWIDE_RELEASE_STORE_PASSWORD '...'
set -lx CODEWIDE_RELEASE_KEY_ALIAS codex-remote-v1
set -lx CODEWIDE_RELEASE_KEY_PASSWORD '...'The Android project also contains a :baselineprofile Macrobenchmark module.
It generates a real startup/ART profile from an API 33+ connected device and
compares cold startup with no compilation against the required profile. Normal
release builds do not silently launch instrumentation; generate and then build
explicitly:
pnpm android:gradle -- :app:generateReleaseBaselineProfile
pnpm android:gradle -- :baselineprofile:connectedBenchmarkReleaseAndroidTest
pnpm android:gradle -- :app:assembleReleaseThe wrapper uses explicit JAVA_HOME / ANDROID_HOME when provided and falls
back to the repository's known Gradle-managed JDK and Android SDK locations.
This keeps CLI builds deterministic even when java is not exported in the
interactive shell.
Generation on an emulator is useful for correctness. Treat benchmark numbers as release evidence only when captured on physical mid-range hardware.
Run the release/device evidence gate against one explicitly selected ADB target:
pnpm test:android-device -- \
--serial emulator-5554 \
--apk apps/android/android/app/build/outputs/apk/release/app-release.apk \
--suite smoke,lifecycle,layout \
--expected-server-count 4 \
--capture-screenshotAn AVD can be paired without a camera or a token in shell history. Write the five-minute one-time payload to a private file, then pass only its path:
mkdir -p test-results/private-runtime/android
chmod 700 test-results/private-runtime/android
set -lx CODEWIDE_PUBLIC_ENDPOINT wss://10.0.2.2:8766/v1/sync
set -lx CODEWIDE_SERVER_NAME 'AVD test server'
codewide-companion pair --json \
> test-results/private-runtime/android/pairing.json
chmod 600 test-results/private-runtime/android/pairing.json
pnpm test:android-device -- \
--serial emulator-5554 \
--apk apps/android/android/app/build/outputs/apk/release/app-release.apk \
--pairing-file test-results/private-runtime/android/pairing.jsonThe runner accepts only a mode-0600, unexpired pairing payload with shell-safe
automation fields. It never writes the one-time token or resulting capability
into evidence.
The runner installs with adb install -r, exercises sleep/wake, forced Doze,
App Standby and process recreation, checks the real accessibility geometry for
server rail -> threads -> thread and Menu -> wide input -> voice -> send,
and writes a mode-0600 evidence bundle under ignored test-results/. An
upgrade run additionally requires --suite upgrade --previous-apk <old.apk> and
fails if the package UID or first-install identity changes.
Run the complete Android-to-Observer E2E suite with one command:
pnpm test:android:e2eThe runner uses Appium with the pinned UiAutomator2 driver. It builds an
isolated dev.codexremote.app.e2e APK, starts Metro and an isolated Companion,
pairs through the real UI, creates a real Observer thread, sends a foreground
message, verifies a mobile-originated turn while Android is backgrounded,
injects another turn directly through the App Server while Android is
backgrounded, and verifies process-death recovery. It records every scenario
and writes videos, Appium/Metro/Companion logs, screenshots on failure, and
evidence.json under test-results/android-e2e/<run-id>/. Open the generated
report.html to play every recording inline without downloading the MP4 files.
For the V2 command/reinitialize race, the runner builds only its isolated
Companion with the non-default e2e-command-fault Cargo feature. It arms the
one-shot controller through the mode-0600 private Unix control socket, performs
one real Appium activation, waits until the authentic SourceGap has been sent
and the next live boundary is held, then releases it. The evidence records
monotonic milestones and requires exactly one structured Companion admission
with the recovered operation ID before the App Server result is accepted.
Production Android code has no corresponding fault hook, and an ordinary
Companion build does not contain the controller or its private routes.
The command requires a running Codex App Server control socket. It uses the
single connected emulator, or starts the first configured AVD when none is
running. Set CODEWIDE_E2E_SERIAL, CODEWIDE_E2E_AVD, or
CODEWIDE_E2E_APP_SERVER_SOCKET to select those resources explicitly. Pass
--skip-build only while iterating on an already built E2E APK and Companion.