diff --git a/docs/README.md b/docs/README.md index 6d202ac0..0889d43b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -51,6 +51,8 @@ Use this index to choose the smallest document that matches your goal. model for scans and reviews. - [Security Backlog](maintainers/security-backlog.md) points to security backlog issues and routes supply-chain review through `@codex-security`. +- [Remote Mobile Host Boundary Review](maintainers/remote-mobile-host-boundary-review.md) + records the host-state matrix for remote-control and Codex mobile review. - [Agentic Maintenance Policy](policies/agentic-maintenance.md) explains what belongs in tracked docs, what belongs in agent policy, and what should remain local session evidence. diff --git a/docs/maintainers/remote-mobile-host-boundary-review.md b/docs/maintainers/remote-mobile-host-boundary-review.md new file mode 100644 index 00000000..b2d56885 --- /dev/null +++ b/docs/maintainers/remote-mobile-host-boundary-review.md @@ -0,0 +1,82 @@ +# Remote Mobile Host Boundary Review + +This review note records the current review gate for the `remote-control-ui` +and `remote-mobile-control` port integrations. It is issue #59's durable +evidence surface for the local host boundary; GitHub comments should point back +here instead of carrying the only copy of the matrix. + +## Scope + +The review covers fork-owned Linux behavior around: + +- generated bundle patch descriptors for remote-control and Codex mobile + surfaces; +- app-server or managed remote-control daemon startup and config preservation; +- the Linux software device-key provider under XDG config; +- host identity selection for remote-control auto-connect; +- docs and review evidence that distinguish local fork behavior from + OpenAI-hosted account, enrollment, MFA, mobile-client authorization, and + remote-access policy. + +Generated app output, local state, and mobile-client behavior are inspection +evidence. Durable changes belong in source patchers, port integration tests, +maintainer docs, or the issue/PR evidence trail. + +## Host-State Matrix + +| Review row | Repo-local evidence | Live evidence required before issue closure | +| --- | --- | --- | +| Enrollment state shown in UI | `PRODUCT.md` and `DESIGN.md` require that connected-looking UI not imply unverified enrollment, host liveness, thread visibility, authorization, remote environment state, or service availability. | A live account/mobile check records the enrollment state shown in the generated app and confirms it matches current account/mobile enrollment state. | +| App-server or daemon liveness | `port-integrations/remote-mobile-control/cold-start-hook.sh` starts the managed daemon only when the Desktop app-server does not own remote-control, and tests cover the ownership marker and stale daemon PID cleanup. | A live run records the intended Desktop app-server or managed daemon process as alive and reachable. | +| Linux device-key store | `port-integrations/remote-mobile-control/test.js` covers key creation, signing, deletion, and `0600` mode for `${XDG_CONFIG_HOME:-$HOME/.config}/codex-app/remote-control-device-keys-v1.json`. | A live run records the configured key path and owner-only mode without exposing key material. | +| Mobile side sees intended thread/session | Fork-side tests cannot prove OpenAI-hosted account/mobile discovery semantics. | The mobile side records the intended host thread/session as visible. | +| First mobile action reaches intended host thread/session | Fork-side tests cannot prove mobile action routing through OpenAI-hosted services. | A first mobile action or message is applied to the intended live host thread/session. | +| Stale, revoked, unauthorized, or mismatched hosts are rejected | `port-integrations/remote-mobile-control/test.js` covers auto-connecting only the local installation, leaving hosts disconnected when no local identity is available, and refreshing empty connection snapshots before selecting the intended host. | A live run records stale, revoked, unauthorized, or mismatched hosts rejected instead of displayed as connected. | + +## Scoped Security Review Evidence + +- `remote-control-ui` patches expose Linux remote-control UI surfaces and Linux + copy. They do not authorize a host, mint device keys, or prove account/mobile + enrollment. +- `remote-mobile-control` keeps OpenAI-hosted account, enrollment, step-up, MFA, + mobile-client authorization, and remote-access decisions outside the local + fork. Local patches preserve those checks and only adapt Linux host plumbing. +- Linux device keys are exportable software keys. The provider stores them under + per-user XDG config, writes the key store with `0600` mode, uses a `0700` lock + directory, and fails when no user config root can be resolved. +- The Desktop app-server path owns remote-control when the generated app carries + the ownership marker. The standalone daemon path is a local fallback and must + not imply OpenAI-hosted enrollment or mobile reachability. +- Auto-connect is limited to `remote-control:` host records whose installation + id matches the local `electron-local-remote-control-installation-id`. Empty + connection snapshots are refreshed before selection, and missing local + identity leaves every remote host disconnected. + +## Review Rules + +- Do not treat a connected-looking local UI as proof of account/mobile + authorization, host liveness, or thread/session reachability. +- Do not claim general-ready status until `@codex-security` review evidence and + the host-state matrix are both recorded. +- Keep local fork behavior distinct from OpenAI-hosted services in docs, PR + text, and issue closure comments. +- Do not persist screenshots, key material, private account identifiers, + private hostnames, private paths, tokens, or mobile-client state that is not + necessary for review. + +## Local Validation + +Run these local checks after source or review-doc changes in this area: + +```bash +node --test port-integrations/remote-control-ui/test.js +node --test port-integrations/remote-mobile-control/test.js +``` + +If shared patching behavior changes, also run: + +```bash +node --test scripts/patch-linux-window-ui.test.js +``` + +If shell hooks change, run `bash -n` on the touched shell files. diff --git a/docs/maintainers/security-backlog.md b/docs/maintainers/security-backlog.md index 311bcb57..ac0bbc9c 100644 --- a/docs/maintainers/security-backlog.md +++ b/docs/maintainers/security-backlog.md @@ -45,7 +45,9 @@ Medium priority: - [Review generated-app Electron IPC and file-manager handling](https://github.com/nisavid/codex-app-linux/issues/57) - [Review Linux Computer Use desktop-control boundary](https://github.com/nisavid/codex-app-linux/issues/58) -- [Review experimental remote-control and Codex mobile host boundary](https://github.com/nisavid/codex-app-linux/issues/59) +- [Review experimental remote-control and Codex mobile host boundary](https://github.com/nisavid/codex-app-linux/issues/59): + see [Remote Mobile Host Boundary Review](remote-mobile-host-boundary-review.md) + for the host-state matrix and repo-local evidence. - [Review bundled browser and Chrome native-host boundary](https://github.com/nisavid/codex-app-linux/issues/60) - [Require trusted metadata for non-default DMG sources](https://github.com/nisavid/codex-app-linux/issues/61) - [Pin executable build inputs outside the Nix path](https://github.com/nisavid/codex-app-linux/issues/62) diff --git a/docs/maintainers/threat-model.md b/docs/maintainers/threat-model.md index fb967ada..1491ca10 100644 --- a/docs/maintainers/threat-model.md +++ b/docs/maintainers/threat-model.md @@ -456,12 +456,17 @@ end-to-end authorization path. plumbing without fabricating OpenAI enrollment, connected-client, or MFA state; patches are descriptor-scoped and fail soft; Linux device keys are stored in a per-user XDG config file with `0600` mode; and tests cover key creation, -signing, deletion, visibility gating, and Linux-specific copy. +signing, deletion, visibility gating, local host auto-connect selection, +missing local host identity, refreshed connection snapshots, and Linux-specific +copy. **Gaps:** Linux keys are software-only and same-user readable; fork-side tests -cannot prove OpenAI account/mobile authorization semantics; remote-control -patches need fresh security review before being treated as general-ready -functionality. +cannot prove OpenAI account/mobile authorization semantics; connected-looking +UI is not proof that the intended live host, app-server or managed daemon, and +thread/session are current, reachable, and authorized; remote-control patches +need fresh security review and the host-state matrix in +[Remote Mobile Host Boundary Review](remote-mobile-host-boundary-review.md) +before being treated as general-ready functionality. **Priority:** High when touching remote-control/mobile behavior; Medium otherwise. @@ -584,7 +589,9 @@ still contain arbitrary sensitive values. - `port-integrations/remote-control-ui/` and `port-integrations/remote-mobile-control/`: port integrations for remote-control/mobile UI gates, app-server config preservation, Linux - device-key storage, and generated-copy patches. + device-key storage, generated-copy patches, and host-state evidence. See + [Remote Mobile Host Boundary Review](remote-mobile-host-boundary-review.md) + for the host-state matrix. - `scripts/lib/dmg.sh`: installer DMG download and version extraction. - `scripts/lib/native-modules.sh`: native dependency version floors and Electron-specific temporary source compatibility patches. diff --git a/port-integrations/remote-mobile-control/test.js b/port-integrations/remote-mobile-control/test.js index 7bc467be..8c166279 100644 --- a/port-integrations/remote-mobile-control/test.js +++ b/port-integrations/remote-mobile-control/test.js @@ -1188,6 +1188,107 @@ test("Linux remote-control enablement bridge auto-connects only this Desktop hos assert.equal(calls[3].params.autoConnect, false); }); +test("Linux remote-control enablement bridge leaves remote hosts disconnected without a local host identity", async () => { + const source = syntheticAppMainEnablementBridgeBundle(); + const patched = applyLinuxRemoteControlEnablementBridgePatch(source); + + const calls = []; + const context = { + DF: "[remote-connections/slingshot-gate-bridge]", + navigator: { userAgent: "X11; Linux x86_64" }, + Promise, + q: { warning() {} }, + Q: { + useEffect(callback) { + callback(); + }, + }, + sc: () => false, + Z: { c: () => [] }, + $o: (method, { params }) => { + calls.push({ method, params }); + if (method === "set-remote-control-connections-enabled") { + return Promise.resolve({ + remoteControlConnections: [ + { hostId: "remote-control:env_one", installationId: "install_one" }, + { hostId: "remote-control:env_two", installationId: "install_two" }, + ], + }); + } + if (method === "get-global-state") { + return Promise.resolve({ value: null }); + } + return Promise.resolve({}); + }, + }; + vm.runInNewContext(`${patched};OF();`, context); + await new Promise((resolve) => setImmediate(resolve)); + + assert.equal(calls.length, 4); + assert.equal(calls[0].method, "set-remote-control-connections-enabled"); + assert.equal(calls[0].params.enabled, true); + assert.equal(calls[1].method, "get-global-state"); + assert.equal(calls[1].params.key, "electron-local-remote-control-installation-id"); + assert.equal(calls[2].method, "set-remote-connection-auto-connect"); + assert.equal(calls[2].params.hostId, "remote-control:env_one"); + assert.equal(calls[2].params.autoConnect, false); + assert.equal(calls[3].method, "set-remote-connection-auto-connect"); + assert.equal(calls[3].params.hostId, "remote-control:env_two"); + assert.equal(calls[3].params.autoConnect, false); +}); + +test("Linux remote-control enablement bridge refreshes empty connection snapshots before auto-connect", async () => { + const source = syntheticAppMainEnablementBridgeBundle(); + const patched = applyLinuxRemoteControlEnablementBridgePatch(source); + + const calls = []; + const context = { + DF: "[remote-connections/slingshot-gate-bridge]", + navigator: { userAgent: "X11; Linux x86_64" }, + Promise, + q: { warning() {} }, + Q: { + useEffect(callback) { + callback(); + }, + }, + sc: () => false, + Z: { c: () => [] }, + $o: (method, { params }) => { + calls.push({ method, params }); + if (method === "set-remote-control-connections-enabled") { + return Promise.resolve({ remoteControlConnections: [] }); + } + if (method === "refresh-remote-control-connections") { + return Promise.resolve({ + sharedObjects: { + local_remote_control_installation_id: "install_local", + remote_control_connections: [ + { hostId: "remote-control:env_local", installation_id: "install_local" }, + { hostId: "remote-control:env_stale", installation_id: "install_stale" }, + ], + }, + }); + } + return Promise.resolve({}); + }, + }; + vm.runInNewContext(`${patched};OF();`, context); + await new Promise((resolve) => setImmediate(resolve)); + + assert.equal(calls.length, 4); + assert.equal(calls[0].method, "set-remote-control-connections-enabled"); + assert.equal(calls[0].params.enabled, true); + assert.equal(calls[1].method, "refresh-remote-control-connections"); + assert.deepEqual(structuredClone(calls[1].params), {}); + assert.equal(calls[2].method, "set-remote-connection-auto-connect"); + assert.equal(calls[2].params.hostId, "remote-control:env_local"); + assert.equal(calls[2].params.autoConnect, true); + assert.equal(calls[3].method, "set-remote-connection-auto-connect"); + assert.equal(calls[3].params.hostId, "remote-control:env_stale"); + assert.equal(calls[3].params.autoConnect, false); +}); + test("patched Linux device-key provider can create, sign with, and delete a key", async () => { const configHome = fs.mkdtempSync(path.join(os.tmpdir(), "codex-remote-mobile-key-store-")); try {