Skip to content

Commit d93969e

Browse files
committed
fix: harden shared app-server socket lifecycle
Serialize authority ownership, handle child and transport failures, and document the remote SSH boot-skip contract.
1 parent 8976c11 commit d93969e

3 files changed

Lines changed: 565 additions & 13 deletions

File tree

linux-features/shared-app-server-socket/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ Keep the socket in a directory accessible only to the owning user. It is a local
2626
control endpoint and must not be exposed directly over TCP or forwarded as a
2727
network service.
2828

29+
Authority startup is serialized by an owner-only lock next to the socket. The
30+
feature fails closed if either path already exists; it never guesses that an
31+
existing socket or lock is stale. After an abnormal Desktop termination, verify
32+
that no authority still owns the configured endpoint before removing stale
33+
paths and restarting Desktop.
34+
2935
## SSH setup
3036

3137
Use a stable socket path when the Desktop instance will be reached over SSH:
@@ -52,11 +58,26 @@ fi
5258
exec "$real_codex" "$@"
5359
```
5460

61+
The upstream SSH transport normally starts its own authority before invoking
62+
the proxy. Configure the **remote account's login-shell environment** to skip
63+
that bootstrap when this wrapper is used:
64+
65+
```bash
66+
export CODEX_SSH_SKIP_APP_SERVER_BOOT=true
67+
```
68+
69+
Put that export in the startup file read by the account's SSH login shell (for
70+
example `~/.profile` when that is the active login profile). This is remote
71+
account configuration; setting it only in the local Desktop launcher does not
72+
propagate it through SSH. Use it only for an account whose wrapper is dedicated
73+
to this Desktop-owned socket.
74+
5575
Make the wrapper executable and verify that non-interactive SSH resolves it:
5676

5777
```bash
5878
chmod 0755 "$HOME/.local/bin/codex"
5979
ssh host 'command -v codex'
80+
ssh host 'printf "%s\n" "$CODEX_SSH_SKIP_APP_SERVER_BOOT"'
6081
```
6182

6283
Codex SSH clients can then connect normally; no client-side protocol option or

linux-features/shared-app-server-socket/patch.js

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,24 @@ function findTransportSymbols(source) {
2929
};
3030
}
3131

32+
function sharedTransportClassSource(symbols) {
33+
return (
34+
"class CodexLinuxSharedAppServerSocketTransport{" +
35+
"kind=`websocket`;proxyStreams=new Set;authority=null;authorityError=null;authorityReady=null;lockIdentity=null;socketIdentity=null;" +
36+
"constructor(e){this.socketPath=e;this.lockPath=`${e}.lock`}" +
37+
"supportsReconnect(){return!0}" +
38+
"sameIdentity(e,t){return e!=null&&t.dev===e.dev&&t.ino===e.ino}" +
39+
"releaseOwnedPaths(e=!1){let t=require(`node:fs`),n=[];if(this.socketIdentity)try{let e=t.lstatSync(this.socketPath);this.sameIdentity(this.socketIdentity,e)&&t.unlinkSync(this.socketPath),this.socketIdentity=null}catch(e){e?.code===`ENOENT`?this.socketIdentity=null:n.push(e)}if(this.lockIdentity)try{let e=t.lstatSync(this.lockPath);this.sameIdentity(this.lockIdentity,e)&&t.unlinkSync(this.lockPath),this.lockIdentity=null}catch(e){e?.code===`ENOENT`?this.lockIdentity=null:n.push(e)}if(n.length&&!e)throw n[0];n.length&&console.warn(`WARN: shared app-server socket cleanup failed: ${n[0].message}`)}" +
40+
"dispose(){for(let e of this.proxyStreams)e.destroy();this.proxyStreams.clear();let e=this.authority;this.authority=null;if(e&&e.exitCode==null&&e.signalCode==null){let t=()=>this.releaseOwnedPaths(!0);e.once(`close`,t);try{e.kill()}catch(e){console.warn(`WARN: shared app-server authority stop failed: ${e.message}`)}}else this.releaseOwnedPaths(!0)}" +
41+
"acquireOwnership(){let e=require(`node:fs`),t=require(`node:path`);e.mkdirSync(t.dirname(this.socketPath),{recursive:!0,mode:448});let n;try{n=e.openSync(this.lockPath,`wx`,384),this.lockIdentity=e.fstatSync(n)}catch(e){if(e?.code===`EEXIST`)throw Error(`shared app-server socket is already owned: ${this.socketPath}`);throw e}finally{n!=null&&e.closeSync(n)}try{e.lstatSync(this.socketPath);throw Error(`shared app-server socket path already exists: ${this.socketPath}`)}catch(e){if(e?.code!==`ENOENT`){this.releaseOwnedPaths();throw e}}}" +
42+
"stopAuthority(e){return new Promise(t=>{if(!e||e.exitCode!=null||e.signalCode!=null)return t(!0);let n=!1,r=i=>{if(n)return;n=!0,clearTimeout(a),e.off(`close`,o),e.off(`exit`,o),e.off(`error`,s),t(i)},o=()=>r(!0),s=e=>{this.authorityError??=e},a=setTimeout(()=>r(!1),2e3);a.unref?.(),e.once(`close`,o),e.once(`exit`,o),e.on(`error`,s);try{e.kill()}catch(e){this.authorityError??=e,r(!1)}})}" +
43+
"async ensureAuthority(){if(this.authorityReady)return this.authorityReady;if(this.authority&&this.authority.exitCode==null&&this.authority.signalCode==null){if(this.authorityError)throw this.authorityError;return}let e=this.startAuthority();this.authorityReady=e;try{return await e}finally{this.authorityReady===e&&(this.authorityReady=null)}}" +
44+
"async startAuthority(){let e=process.env.CODEX_CLI_PATH;if(!e)throw Error(`shared app-server socket requires CODEX_CLI_PATH`);this.authorityError=null,this.acquireOwnership();let t=require(`node:fs`),n;try{n=require(`node:child_process`).spawn(e,[`app-server`,`--listen`,`unix://${this.socketPath}`],{env:process.env,stdio:`ignore`}),this.authority=n}catch(e){this.releaseOwnedPaths();throw e}try{await new Promise((e,r)=>{let i=!1,a,o=()=>{clearTimeout(a),clearTimeout(u),n.off(`error`,s),n.off(`exit`,l),n.off(`close`,l)},c=(t,u)=>{if(i)return;i=!0,o(),t?r(t):e(u)},s=e=>{this.authorityError=e,c(e)},l=()=>c(Error(`shared app-server authority exited before socket creation`)),h=()=>{if(i)return;try{let e=t.lstatSync(this.socketPath);if(e.isSocket()){if(typeof process.getuid==`function`&&e.uid!==process.getuid())return c(Error(`shared app-server socket has unexpected owner`));this.socketIdentity={dev:e.dev,ino:e.ino};return c(null)}}catch(e){if(e?.code!==`ENOENT`)return c(e)}a=setTimeout(h,100),a.unref?.()},u=setTimeout(()=>c(Error(`shared app-server socket creation timed out`)),1e4);n.once(`error`,s),n.once(`exit`,l),n.once(`close`,l),h(),u.unref?.()}),n.on(`error`,e=>{this.authorityError=e;for(let t of this.proxyStreams)t.destroy(e)}),n.once(`exit`,()=>{this.authority===n&&(this.authority=null,this.releaseOwnedPaths(!0))})}catch(e){this.authority=null;(await this.stopAuthority(n))&&this.releaseOwnedPaths();throw e}}" +
45+
"createProxyStream(){let c=process.env.CODEX_CLI_PATH;if(!c)throw Error(`shared app-server socket requires CODEX_CLI_PATH`);let e=require(`node:child_process`).spawn(c,[`app-server`,`proxy`,`--sock`,this.socketPath],{env:process.env,stdio:[`pipe`,`pipe`,`pipe`]}),t=e.stdin,n=e.stdout,r=e.stderr;if(t==null||n==null||r==null)throw e.kill(),Error(`shared app-server proxy stdio was unavailable`);let i=``;r.on(`data`,e=>{i=`${i}${e.toString(`utf8`)}`.slice(-4000)});let a=new(require(`node:stream`).Duplex)({read(){n.resume()},write(e,n,r){t.write(e,n,r)},final(e){t.end(),e()},destroy(t,n){e.kill(),n(t)}});Object.assign(a,{setKeepAlive:()=>a,setNoDelay:()=>a,setTimeout:()=>a});let o=e=>a.destroy(e);t.on(`error`,o),n.on(`data`,e=>{a.push(e)||n.pause()}),n.on(`end`,()=>a.push(null)),e.on(`error`,o),e.on(`close`,(e,n)=>{t.removeListener(`error`,o),e===0?a.push(null):a.destroy(Error(`shared app-server proxy exited (${e??n??`unknown`}): ${i.trim()}`))}),this.proxyStreams.add(a),a.once(`close`,()=>this.proxyStreams.delete(a));return a}" +
46+
`async connect(){await this.ensureAuthority();let e={current:null},t=new ${symbols.namespace}.${symbols.webSocketClass}(${symbols.webSocketUrl},{perMessageDeflate:!1,createConnection:()=>(e.current=this.createProxyStream(),e.current)});t.once(\`close\`,()=>e.current?.destroy());try{await new Promise((n,r)=>{let i=setTimeout(()=>o(Error(\`shared app-server websocket open timed out\`)),3e4);i.unref();let a=()=>{clearTimeout(i),t.off(\`error\`,o),t.off(\`close\`,s)},o=e=>{a(),r(e)},s=()=>o(Error(\`shared app-server websocket closed before opening\`));t.once(\`open\`,()=>{a(),n()}),t.once(\`error\`,o),t.once(\`close\`,s)})}catch(n){e.current?.destroy(),t.terminate(),await new Promise(e=>setTimeout(e,0));throw n}${symbols.namespace}.${symbols.keepAlive}(t,{onPongTimeout:()=>t.terminate()});return new ${symbols.namespace}.${symbols.adapterClass}(t)}}`
47+
);
48+
}
49+
3250
function applySharedAppServerSocketPatch(source) {
3351
if (source.includes("class CodexLinuxSharedAppServerSocketTransport")) return source;
3452

@@ -55,15 +73,7 @@ function applySharedAppServerSocketPatch(source) {
5573
return source;
5674
}
5775

58-
const classSource =
59-
"class CodexLinuxSharedAppServerSocketTransport{" +
60-
"kind=`websocket`;proxyStreams=new Set;authority=null;" +
61-
"constructor(e){this.socketPath=e}" +
62-
"supportsReconnect(){return!0}" +
63-
"dispose(){for(let e of this.proxyStreams)e.destroy();this.proxyStreams.clear();this.authority?.kill();this.authority=null;try{require(`node:fs`).unlinkSync(this.socketPath)}catch(e){if(e?.code!==`ENOENT`)throw e}}" +
64-
"async ensureAuthority(){if(this.authority&&this.authority.exitCode==null)return;let e=process.env.CODEX_CLI_PATH;if(!e)throw Error(`shared app-server socket requires CODEX_CLI_PATH`);let t=require(`node:fs`),n=require(`node:path`);t.mkdirSync(n.dirname(this.socketPath),{recursive:!0,mode:448});try{t.unlinkSync(this.socketPath)}catch(e){if(e?.code!==`ENOENT`)throw e}this.authority=require(`node:child_process`).spawn(e,[`app-server`,`--listen`,`unix://${this.socketPath}`],{env:process.env,stdio:`ignore`});for(let e=0;e<100;e++){if(this.authority.exitCode!=null)throw Error(`shared app-server authority exited before socket creation`);try{if(t.statSync(this.socketPath).isSocket())return}catch(e){if(e?.code!==`ENOENT`)throw e}await new Promise(e=>setTimeout(e,100))}this.authority.kill();this.authority=null;throw Error(`shared app-server socket creation timed out`)}" +
65-
"createProxyStream(){let c=process.env.CODEX_CLI_PATH;if(!c)throw Error(`shared app-server socket requires CODEX_CLI_PATH`);let e=require(`node:child_process`).spawn(c,[`app-server`,`proxy`,`--sock`,this.socketPath],{env:process.env,stdio:[`pipe`,`pipe`,`pipe`]}),t=e.stdin,n=e.stdout,r=e.stderr;if(t==null||n==null||r==null)throw e.kill(),Error(`shared app-server proxy stdio was unavailable`);let i=``;r.on(`data`,e=>{i=`${i}${e.toString(`utf8`)}`.slice(-4000)});let a=new(require(`node:stream`).Duplex)({read(){n.resume()},write(e,n,r){t.write(e,n,r)},final(e){t.end(),e()},destroy(t,n){e.kill(),n(t)}});Object.assign(a,{setKeepAlive:()=>a,setNoDelay:()=>a,setTimeout:()=>a});let o=e=>a.destroy(e);t.on(`error`,o),n.on(`data`,e=>{a.push(e)||n.pause()}),n.on(`end`,()=>a.push(null)),e.on(`error`,o),e.on(`close`,(e,n)=>{t.removeListener(`error`,o),e===0?a.push(null):a.destroy(Error(`shared app-server proxy exited (${e??n??`unknown`}): ${i.trim()}`))}),this.proxyStreams.add(a),a.once(`close`,()=>this.proxyStreams.delete(a));return a}" +
66-
`async connect(){await this.ensureAuthority();let e={current:null},t=new ${symbols.namespace}.${symbols.webSocketClass}(${symbols.webSocketUrl},{perMessageDeflate:!1,createConnection:()=>(e.current=this.createProxyStream(),e.current)});t.once(\`close\`,()=>e.current?.destroy());await new Promise((n,r)=>{let i=setTimeout(()=>{r(Error(\`shared app-server websocket open timed out\`))},3e4);i.unref();let a=()=>{clearTimeout(i),t.off(\`error\`,o),t.off(\`close\`,s)},o=e=>{a(),r(e)},s=()=>{a(),r(Error(\`shared app-server websocket closed before opening\`))};t.once(\`open\`,()=>{a(),n()}),t.once(\`error\`,o),t.once(\`close\`,s)});${symbols.namespace}.${symbols.keepAlive}(t,{onPongTimeout:()=>t.terminate()});return new ${symbols.namespace}.${symbols.adapterClass}(t)}}`;
76+
const classSource = sharedTransportClassSource(symbols);
6777

6878
const patchedFactory = factorySource.replace(
6979
localFallbackPattern,
@@ -86,4 +96,5 @@ module.exports = {
8696
applySharedAppServerSocketPatch,
8797
descriptors,
8898
findTransportSymbols,
99+
sharedTransportClassSource,
89100
};

0 commit comments

Comments
 (0)