@modelprofile.com/mcp-crossharness 5.4.0 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@modelprofile.com/mcp-crossharness',
6
- version: '5.4.0',
6
+ version: '5.5.0',
7
7
  description: 'Connection-first MCP server for explicit OpenCode HTTP and Codex WebSocket session servers'
8
8
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSxvQ0FBb0M7SUFDMUMsT0FBTyxFQUFFLE9BQU87SUFDaEIsV0FBVyxFQUFFLDRGQUE0RjtDQUMxRyxDQUFBIn0=
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Codex's `--listen unix://PATH` contract since Codex 0.156.0 (codex-rs/app-server-transport,
3
+ * `protected_socket_path`): the socket is bound in the fixed per-user directory
4
+ * `<realpath /tmp>/codex-daemon-<euid>` under the SHA-256 of PATH with its parent canonicalised,
5
+ * and PATH itself is only a symlink to it. Earlier releases bound PATH directly; that layout is
6
+ * not this contract and is refused like any other foreign entry.
7
+ */
8
+ export interface ICodexUnixSocketOptions {
9
+ /**
10
+ * Codex's per-user socket directory. Defaults to `<realpath /tmp>/codex-daemon-<euid>`, which
11
+ * Codex derives without HOME, TMPDIR or CODEX_HOME; override it only for fixtures.
12
+ */
13
+ daemonDirectory?: string;
14
+ }
15
+ /** `absent`: nothing exists at the path. `published`: Codex's own alias to a private socket. */
16
+ export type TCodexUnixSocketPublication = 'absent' | 'published';
17
+ /** Where Codex binds the socket it publishes at `socketPathArg`. The parent directory must exist. */
18
+ export declare const codexUnixSocketTarget: (socketPathArg: string, optionsArg?: ICodexUnixSocketOptions) => Promise<string>;
19
+ /**
20
+ * Reports whether Codex has published an app-server socket at `socketPathArg`. Anything present
21
+ * must be exactly Codex's own same-user link to its target, in a parent other users cannot
22
+ * rewrite (Codex's own startup condition), pointing at a same-user socket in a 0700 same-user
23
+ * directory; every other entry throws instead of being connected to.
24
+ */
25
+ export declare const observeCodexUnixSocket: (socketPathArg: string, optionsArg?: ICodexUnixSocketOptions) => Promise<TCodexUnixSocketPublication>;
@@ -0,0 +1,94 @@
1
+ import * as plugins from './plugins.js';
2
+ import * as helpers from './helpers.js';
3
+ const assertNormalizedAbsolute = (pathArg, messageArg) => {
4
+ if (typeof pathArg !== 'string' || !plugins.path.isAbsolute(pathArg) || pathArg.includes('\0')
5
+ || plugins.path.normalize(pathArg) !== pathArg) {
6
+ throw new Error(messageArg);
7
+ }
8
+ };
9
+ /**
10
+ * Codex splits PATH into parent and file name with Rust's `Path`, which drops `.` components and
11
+ * has no file name for `..`; a normalized path without a trailing separator splits identically here.
12
+ */
13
+ const assertSocketPath = (socketPathArg) => {
14
+ const message = 'Codex Unix socket path must be a normalized absolute file path';
15
+ assertNormalizedAbsolute(socketPathArg, message);
16
+ if (socketPathArg.endsWith(plugins.path.sep))
17
+ throw new Error(message);
18
+ };
19
+ /** Codex keys its directory and checks every entry by the effective uid, so this module does too. */
20
+ const effectiveUserId = () => {
21
+ const euid = process.geteuid?.();
22
+ if (euid === undefined)
23
+ throw new Error('Codex Unix socket inspection requires a POSIX effective user id');
24
+ return euid;
25
+ };
26
+ /** A malformed root is a caller error, so it throws whatever exists at the socket path. */
27
+ const assertOptions = (optionsArg) => {
28
+ if (optionsArg.daemonDirectory !== undefined) {
29
+ assertNormalizedAbsolute(optionsArg.daemonDirectory, 'Codex daemon socket directory must be a normalized absolute path');
30
+ }
31
+ };
32
+ const daemonDirectoryFor = async (optionsArg, euidArg) => {
33
+ if (optionsArg.daemonDirectory !== undefined)
34
+ return optionsArg.daemonDirectory;
35
+ return plugins.path.join(await plugins.fs.promises.realpath('/tmp'), `codex-daemon-${euidArg}`);
36
+ };
37
+ const isMissing = (errorArg) => helpers.isRecord(errorArg) && errorArg.code === 'ENOENT';
38
+ /** Where Codex binds the socket it publishes at `socketPathArg`. The parent directory must exist. */
39
+ export const codexUnixSocketTarget = async (socketPathArg, optionsArg = {}) => {
40
+ assertSocketPath(socketPathArg);
41
+ assertOptions(optionsArg);
42
+ const euid = effectiveUserId();
43
+ const directory = await daemonDirectoryFor(optionsArg, euid);
44
+ const canonical = plugins.path.join(await plugins.fs.promises.realpath(plugins.path.dirname(socketPathArg)), plugins.path.basename(socketPathArg));
45
+ return plugins.path.join(directory, plugins.crypto.createHash('sha256').update(canonical).digest('hex'));
46
+ };
47
+ /**
48
+ * Reports whether Codex has published an app-server socket at `socketPathArg`. Anything present
49
+ * must be exactly Codex's own same-user link to its target, in a parent other users cannot
50
+ * rewrite (Codex's own startup condition), pointing at a same-user socket in a 0700 same-user
51
+ * directory; every other entry throws instead of being connected to.
52
+ */
53
+ export const observeCodexUnixSocket = async (socketPathArg, optionsArg = {}) => {
54
+ assertSocketPath(socketPathArg);
55
+ assertOptions(optionsArg);
56
+ const euid = effectiveUserId();
57
+ let alias;
58
+ try {
59
+ alias = await plugins.fs.promises.lstat(socketPathArg);
60
+ }
61
+ catch (error) {
62
+ if (isMissing(error))
63
+ return 'absent';
64
+ throw error;
65
+ }
66
+ const target = await codexUnixSocketTarget(socketPathArg, optionsArg);
67
+ if (!alias.isSymbolicLink() || alias.uid !== euid || await plugins.fs.promises.readlink(socketPathArg) !== target) {
68
+ throw new Error('Codex Unix socket path is not the alias Codex publishes');
69
+ }
70
+ const parent = await plugins.fs.promises.stat(plugins.path.dirname(socketPathArg));
71
+ if (!parent.isDirectory() || (parent.uid !== 0 && parent.uid !== euid)
72
+ || ((parent.mode & 0o022) !== 0 && (parent.mode & 0o1000) === 0)) {
73
+ throw new Error('Codex Unix socket parent directory lets other users replace its entries');
74
+ }
75
+ let directory;
76
+ let socket;
77
+ try {
78
+ directory = await plugins.fs.promises.lstat(plugins.path.dirname(target));
79
+ socket = await plugins.fs.promises.lstat(target);
80
+ }
81
+ catch (error) {
82
+ if (isMissing(error))
83
+ throw new Error('Codex Unix socket alias has no socket behind it');
84
+ throw error;
85
+ }
86
+ if (!directory.isDirectory() || directory.uid !== euid || (directory.mode & 0o777) !== 0o700) {
87
+ throw new Error('Codex Unix socket directory is not private to the current user');
88
+ }
89
+ if (!socket.isSocket() || socket.uid !== euid) {
90
+ throw new Error('Codex Unix socket is not a socket owned by the current user');
91
+ }
92
+ return 'published';
93
+ };
94
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZnVuY3Rpb25zLmNvZGV4dW5peHNvY2tldC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL2Z1bmN0aW9ucy5jb2RleHVuaXhzb2NrZXQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSxjQUFjLENBQUM7QUFDeEMsT0FBTyxLQUFLLE9BQU8sTUFBTSxjQUFjLENBQUM7QUFvQnhDLE1BQU0sd0JBQXdCLEdBQUcsQ0FBQyxPQUFlLEVBQUUsVUFBa0IsRUFBUSxFQUFFO0lBQzdFLElBQUksT0FBTyxPQUFPLEtBQUssUUFBUSxJQUFJLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUMsT0FBTyxDQUFDLElBQUksT0FBTyxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUM7V0FDekYsT0FBTyxDQUFDLElBQUksQ0FBQyxTQUFTLENBQUMsT0FBTyxDQUFDLEtBQUssT0FBTyxFQUFFLENBQUM7UUFDakQsTUFBTSxJQUFJLEtBQUssQ0FBQyxVQUFVLENBQUMsQ0FBQztJQUM5QixDQUFDO0FBQ0gsQ0FBQyxDQUFDO0FBRUY7OztHQUdHO0FBQ0gsTUFBTSxnQkFBZ0IsR0FBRyxDQUFDLGFBQXFCLEVBQVEsRUFBRTtJQUN2RCxNQUFNLE9BQU8sR0FBRyxnRUFBZ0UsQ0FBQztJQUNqRix3QkFBd0IsQ0FBQyxhQUFhLEVBQUUsT0FBTyxDQUFDLENBQUM7SUFDakQsSUFBSSxhQUFhLENBQUMsUUFBUSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDO1FBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQyxPQUFPLENBQUMsQ0FBQztBQUN6RSxDQUFDLENBQUM7QUFFRixxR0FBcUc7QUFDckcsTUFBTSxlQUFlLEdBQUcsR0FBVyxFQUFFO0lBQ25DLE1BQU0sSUFBSSxHQUFHLE9BQU8sQ0FBQyxPQUFPLEVBQUUsRUFBRSxDQUFDO0lBQ2pDLElBQUksSUFBSSxLQUFLLFNBQVM7UUFBRSxNQUFNLElBQUksS0FB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
@@ -1,5 +1,6 @@
1
1
  export * from './interfaces.js';
2
2
  export * from './classes.codexappserverclient.js';
3
+ export * from './functions.codexunixsocket.js';
3
4
  export * from './classes.connectionregistry.js';
4
5
  export * from './classes.dispatchregistry.js';
5
6
  export * from './classes.mcptoolregistrar.js';
package/dist_ts/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './interfaces.js';
2
2
  export * from './classes.codexappserverclient.js';
3
+ export * from './functions.codexunixsocket.js';
3
4
  export * from './classes.connectionregistry.js';
4
5
  export * from './classes.dispatchregistry.js';
5
6
  export * from './classes.mcptoolregistrar.js';
@@ -8,4 +9,4 @@ import { CrossHarnessMcpServer } from './classes.mcpserver.js';
8
9
  export const runCli = async () => {
9
10
  await new CrossHarnessMcpServer().start();
10
11
  };
11
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjLGlCQUFpQixDQUFDO0FBQ2hDLGNBQWMsbUNBQW1DLENBQUM7QUFDbEQsY0FBYyxpQ0FBaUMsQ0FBQztBQUNoRCxjQUFjLCtCQUErQixDQUFDO0FBQzlDLGNBQWMsK0JBQStCLENBQUM7QUFDOUMsY0FBYyx3QkFBd0IsQ0FBQztBQUV2QyxPQUFPLEVBQUUscUJBQXFCLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQUUvRCxNQUFNLENBQUMsTUFBTSxNQUFNLEdBQUcsS0FBSyxJQUFtQixFQUFFO0lBQzlDLE1BQU0sSUFBSSxxQkFBcUIsRUFBRSxDQUFDLEtBQUssRUFBRSxDQUFDO0FBQzVDLENBQUMsQ0FBQyJ9
12
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjLGlCQUFpQixDQUFDO0FBQ2hDLGNBQWMsbUNBQW1DLENBQUM7QUFDbEQsY0FBYyxnQ0FBZ0MsQ0FBQztBQUMvQyxjQUFjLGlDQUFpQyxDQUFDO0FBQ2hELGNBQWMsK0JBQStCLENBQUM7QUFDOUMsY0FBYywrQkFBK0IsQ0FBQztBQUM5QyxjQUFjLHdCQUF3QixDQUFDO0FBRXZDLE9BQU8sRUFBRSxxQkFBcUIsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBRS9ELE1BQU0sQ0FBQyxNQUFNLE1BQU0sR0FBRyxLQUFLLElBQW1CLEVBQUU7SUFDOUMsTUFBTSxJQUFJLHFCQUFxQixFQUFFLENBQUMsS0FBSyxFQUFFLENBQUM7QUFDNUMsQ0FBQyxDQUFDIn0=
@@ -1,9 +1,11 @@
1
1
  import * as crypto from 'node:crypto';
2
+ import * as fs from 'node:fs';
2
3
  import * as http from 'node:http';
3
4
  import * as net from 'node:net';
5
+ import * as path from 'node:path';
4
6
  import * as stream from 'node:stream';
5
7
  import type { AddressInfo } from 'node:net';
6
- export { crypto, http, net, stream };
8
+ export { crypto, fs, http, net, path, stream };
7
9
  export type { AddressInfo };
8
10
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
11
  import type { RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js';
@@ -1,9 +1,11 @@
1
1
  // Node native modules
2
2
  import * as crypto from 'node:crypto';
3
+ import * as fs from 'node:fs';
3
4
  import * as http from 'node:http';
4
5
  import * as net from 'node:net';
6
+ import * as path from 'node:path';
5
7
  import * as stream from 'node:stream';
6
- export { crypto, http, net, stream };
8
+ export { crypto, fs, http, net, path, stream };
7
9
  // Third-party modules
8
10
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
11
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
@@ -12,4 +14,4 @@ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
12
14
  import { WebSocket as WsWebSocket, WebSocketServer } from 'ws';
13
15
  import { z } from 'zod';
14
16
  export { InMemoryTransport, McpClient, McpServer, StdioServerTransport, WebSocketServer, WsWebSocket, z, };
15
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3BsdWdpbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsc0JBQXNCO0FBQ3RCLE9BQU8sS0FBSyxNQUFNLE1BQU0sYUFBYSxDQUFDO0FBQ3RDLE9BQU8sS0FBSyxJQUFJLE1BQU0sV0FBVyxDQUFDO0FBQ2xDLE9BQU8sS0FBSyxHQUFHLE1BQU0sVUFBVSxDQUFDO0FBQ2hDLE9BQU8sS0FBSyxNQUFNLE1BQU0sYUFBYSxDQUFDO0FBR3RDLE9BQU8sRUFBRSxNQUFNLEVBQUUsSUFBSSxFQUFFLEdBQUcsRUFBRSxNQUFNLEVBQUUsQ0FBQztBQUdyQyxzQkFBc0I7QUFDdEIsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLHlDQUF5QyxDQUFDO0FBRXBFLE9BQU8sRUFBRSxvQkFBb0IsRUFBRSxNQUFNLDJDQUEyQyxDQUFDO0FBQ2pGLE9BQU8sRUFBRSxNQUFNLElBQUksU0FBUyxFQUFFLE1BQU0sMkNBQTJDLENBQUM7QUFDaEYsT0FBTyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sdUNBQXVDLENBQUM7QUFFMUUsT0FBTyxFQUFFLFNBQVMsSUFBSSxXQUFXLEVBQUUsZUFBZSxFQUFFLE1BQU0sSUFBSSxDQUFDO0FBRS9ELE9BQU8sRUFBRSxDQUFDLEVBQUUsTUFBTSxLQUFLLENBQUM7QUFFeEIsT0FBTyxFQUNMLGlCQUFpQixFQUNqQixTQUFTLEVBQ1QsU0FBUyxFQUNULG9CQUFvQixFQUNwQixlQUFlLEVBQ2YsV0FBVyxFQUNYLENBQUMsR0FDRixDQUFDIn0=
17
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3BsdWdpbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsc0JBQXNCO0FBQ3RCLE9BQU8sS0FBSyxNQUFNLE1BQU0sYUFBYSxDQUFDO0FBQ3RDLE9BQU8sS0FBSyxFQUFFLE1BQU0sU0FBUyxDQUFDO0FBQzlCLE9BQU8sS0FBSyxJQUFJLE1BQU0sV0FBVyxDQUFDO0FBQ2xDLE9BQU8sS0FBSyxHQUFHLE1BQU0sVUFBVSxDQUFDO0FBQ2hDLE9BQU8sS0FBSyxJQUFJLE1BQU0sV0FBVyxDQUFDO0FBQ2xDLE9BQU8sS0FBSyxNQUFNLE1BQU0sYUFBYSxDQUFDO0FBR3RDLE9BQU8sRUFBRSxNQUFNLEVBQUUsRUFBRSxFQUFFLElBQUksRUFBRSxHQUFHLEVBQUUsSUFBSSxFQUFFLE1BQU0sRUFBRSxDQUFDO0FBRy9DLHNCQUFzQjtBQUN0QixPQUFPLEVBQUUsU0FBUyxFQUFFLE1BQU0seUNBQXlDLENBQUM7QUFFcEUsT0FBTyxFQUFFLG9CQUFvQixFQUFFLE1BQU0sMkNBQTJDLENBQUM7QUFDakYsT0FBTyxFQUFFLE1BQU0sSUFBSSxTQUFTLEVBQUUsTUFBTSwyQ0FBMkMsQ0FBQztBQUNoRixPQUFPLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSx1Q0FBdUMsQ0FBQztBQUUxRSxPQUFPLEVBQUUsU0FBUyxJQUFJLFdBQVcsRUFBRSxlQUFlLEVBQUUsTUFBTSxJQUFJLENBQUM7QUFFL0QsT0FBTyxFQUFFLENBQUMsRUFBRSxNQUFNLEtBQUssQ0FBQztBQUV4QixPQUFPLEVBQ0wsaUJBQWlCLEVBQ2pCLFNBQVMsRUFDVCxTQUFTLEVBQ1Qsb0JBQW9CLEVBQ3BCLGVBQWUsRUFDZixXQUFXLEVBQ1gsQ0FBQyxHQUNGLENBQUMifQ==
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelprofile.com/mcp-crossharness",
3
- "version": "5.4.0",
3
+ "version": "5.5.0",
4
4
  "private": false,
5
5
  "description": "Connection-first MCP server for explicit OpenCode HTTP and Codex WebSocket session servers",
6
6
  "main": "dist_ts/index.js",
@@ -23,10 +23,10 @@
23
23
  "mcp-crossharness": "./cli.js"
24
24
  },
25
25
  "devDependencies": {
26
- "@git.zone/tsbuild": "^4.4.2",
27
- "@git.zone/tsrun": "^2.0.6",
28
- "@git.zone/tstest": "^4.0.0",
29
- "@types/node": "^26.4.1",
26
+ "@git.zone/tsbuild": "^5.0.0",
27
+ "@git.zone/tsrun": "^3.0.0",
28
+ "@git.zone/tstest": "^6.2.1",
29
+ "@types/node": "^26.6.1",
30
30
  "@types/ws": "^8.18.1"
31
31
  },
32
32
  "dependencies": {
package/readme.md CHANGED
@@ -197,6 +197,56 @@ client never grants or automatically declines a permission. `close()` ends the s
197
197
  stdio streams but never kills a subprocess. The existing MCP Codex adapter delegates to
198
198
  this client and retains its conservative automatic rejection policy.
199
199
 
200
+ ### Codex Unix socket alias
201
+
202
+ Since Codex 0.156.0, `codex app-server --listen unix://PATH` does not bind PATH. Codex binds
203
+ the socket in its fixed per-user directory, `<realpath /tmp>/codex-daemon-<euid>` (mode 0700),
204
+ under the SHA-256 of PATH with its parent directory canonicalised, and publishes PATH as a
205
+ symlink to it. Its shared local daemon socket, `$CODEX_HOME/app-server-control/app-server-control.sock`,
206
+ is published the same way. The unix transport connects through that link like any other
207
+ path; these two functions let a caller check, before connecting, that the entry really is
208
+ Codex's own:
209
+
210
+ ```typescript
211
+ import { CodexAppServerClient, codexUnixSocketTarget, observeCodexUnixSocket } from '@modelprofile.com/mcp-crossharness';
212
+
213
+ const target = await codexUnixSocketTarget(socketPath); // where Codex binds PATH
214
+ if (await observeCodexUnixSocket(socketPath) === 'published') {
215
+ const client = new CodexAppServerClient({
216
+ transport: { type: 'unix', socketPath },
217
+ clientInfo: { name: 'my-app', title: 'My App', version: '1.0.0' },
218
+ });
219
+ await client.connect();
220
+ }
221
+ ```
222
+
223
+ `observeCodexUnixSocket(socketPath)` returns `'absent'` when nothing exists at the path and
224
+ `'published'` only when the path is a link owned by the effective user whose text is exactly
225
+ the derived target, in a parent directory other users cannot rewrite (owned by the user or
226
+ root, and not group- or world-writable unless sticky, which is Codex's own startup condition),
227
+ pointing at a socket owned by the effective user in a directory owned by that user with mode
228
+ 0700. Every other entry throws, and nothing is created, removed or connected to. Ownership is
229
+ compared with the effective uid because Codex keys the directory by and checks against it.
230
+ A socket bound directly at the path, as Codex 0.155 and earlier did, is not this contract
231
+ and is refused like any other foreign entry.
232
+
233
+ Both functions take an optional `{ daemonDirectory }` that replaces the per-user directory for
234
+ fixtures; `codexUnixSocketTarget` needs the socket's parent directory to exist. They throw
235
+ plain `Error`s with these exact messages:
236
+
237
+ | Message | Cause |
238
+ | --- | --- |
239
+ | `Codex Unix socket path must be a normalized absolute file path` | relative, non-normalized, trailing-separator or NUL-containing path |
240
+ | `Codex daemon socket directory must be a normalized absolute path` | invalid `daemonDirectory` |
241
+ | `Codex Unix socket inspection requires a POSIX effective user id` | no `process.geteuid` (Windows) |
242
+ | `Codex Unix socket path is not the alias Codex publishes` | a regular file, a direct socket, another user's link, or a link to any other target |
243
+ | `Codex Unix socket parent directory lets other users replace its entries` | the path's parent is not safe for a published alias |
244
+ | `Codex Unix socket alias has no socket behind it` | Codex's own link whose target is gone (a stale publication) |
245
+ | `Codex Unix socket directory is not private to the current user` | the per-user directory is not the user's own 0700 directory |
246
+ | `Codex Unix socket is not a socket owned by the current user` | the target is not a socket, or is another user's |
247
+
248
+ Filesystem errors other than a missing path propagate unchanged.
249
+
200
250
  ### Composable tool registration
201
251
 
202
252
  `CrossHarnessMcpToolRegistrar` registers the same nine Crossharness tools used by the standalone
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@modelprofile.com/mcp-crossharness',
6
- version: '5.4.0',
6
+ version: '5.5.0',
7
7
  description: 'Connection-first MCP server for explicit OpenCode HTTP and Codex WebSocket session servers'
8
8
  }
@@ -0,0 +1,119 @@
1
+ import * as plugins from './plugins.js';
2
+ import * as helpers from './helpers.js';
3
+
4
+ /**
5
+ * Codex's `--listen unix://PATH` contract since Codex 0.156.0 (codex-rs/app-server-transport,
6
+ * `protected_socket_path`): the socket is bound in the fixed per-user directory
7
+ * `<realpath /tmp>/codex-daemon-<euid>` under the SHA-256 of PATH with its parent canonicalised,
8
+ * and PATH itself is only a symlink to it. Earlier releases bound PATH directly; that layout is
9
+ * not this contract and is refused like any other foreign entry.
10
+ */
11
+ export interface ICodexUnixSocketOptions {
12
+ /**
13
+ * Codex's per-user socket directory. Defaults to `<realpath /tmp>/codex-daemon-<euid>`, which
14
+ * Codex derives without HOME, TMPDIR or CODEX_HOME; override it only for fixtures.
15
+ */
16
+ daemonDirectory?: string;
17
+ }
18
+
19
+ /** `absent`: nothing exists at the path. `published`: Codex's own alias to a private socket. */
20
+ export type TCodexUnixSocketPublication = 'absent' | 'published';
21
+
22
+ const assertNormalizedAbsolute = (pathArg: string, messageArg: string): void => {
23
+ if (typeof pathArg !== 'string' || !plugins.path.isAbsolute(pathArg) || pathArg.includes('\0')
24
+ || plugins.path.normalize(pathArg) !== pathArg) {
25
+ throw new Error(messageArg);
26
+ }
27
+ };
28
+
29
+ /**
30
+ * Codex splits PATH into parent and file name with Rust's `Path`, which drops `.` components and
31
+ * has no file name for `..`; a normalized path without a trailing separator splits identically here.
32
+ */
33
+ const assertSocketPath = (socketPathArg: string): void => {
34
+ const message = 'Codex Unix socket path must be a normalized absolute file path';
35
+ assertNormalizedAbsolute(socketPathArg, message);
36
+ if (socketPathArg.endsWith(plugins.path.sep)) throw new Error(message);
37
+ };
38
+
39
+ /** Codex keys its directory and checks every entry by the effective uid, so this module does too. */
40
+ const effectiveUserId = (): number => {
41
+ const euid = process.geteuid?.();
42
+ if (euid === undefined) throw new Error('Codex Unix socket inspection requires a POSIX effective user id');
43
+ return euid;
44
+ };
45
+
46
+ /** A malformed root is a caller error, so it throws whatever exists at the socket path. */
47
+ const assertOptions = (optionsArg: ICodexUnixSocketOptions): void => {
48
+ if (optionsArg.daemonDirectory !== undefined) {
49
+ assertNormalizedAbsolute(optionsArg.daemonDirectory, 'Codex daemon socket directory must be a normalized absolute path');
50
+ }
51
+ };
52
+
53
+ const daemonDirectoryFor = async (optionsArg: ICodexUnixSocketOptions, euidArg: number): Promise<string> => {
54
+ if (optionsArg.daemonDirectory !== undefined) return optionsArg.daemonDirectory;
55
+ return plugins.path.join(await plugins.fs.promises.realpath('/tmp'), `codex-daemon-${euidArg}`);
56
+ };
57
+
58
+ const isMissing = (errorArg: unknown): boolean => helpers.isRecord(errorArg) && errorArg.code === 'ENOENT';
59
+
60
+ /** Where Codex binds the socket it publishes at `socketPathArg`. The parent directory must exist. */
61
+ export const codexUnixSocketTarget = async (
62
+ socketPathArg: string,
63
+ optionsArg: ICodexUnixSocketOptions = {},
64
+ ): Promise<string> => {
65
+ assertSocketPath(socketPathArg);
66
+ assertOptions(optionsArg);
67
+ const euid = effectiveUserId();
68
+ const directory = await daemonDirectoryFor(optionsArg, euid);
69
+ const canonical = plugins.path.join(await plugins.fs.promises.realpath(plugins.path.dirname(socketPathArg)),
70
+ plugins.path.basename(socketPathArg));
71
+ return plugins.path.join(directory, plugins.crypto.createHash('sha256').update(canonical).digest('hex'));
72
+ };
73
+
74
+ /**
75
+ * Reports whether Codex has published an app-server socket at `socketPathArg`. Anything present
76
+ * must be exactly Codex's own same-user link to its target, in a parent other users cannot
77
+ * rewrite (Codex's own startup condition), pointing at a same-user socket in a 0700 same-user
78
+ * directory; every other entry throws instead of being connected to.
79
+ */
80
+ export const observeCodexUnixSocket = async (
81
+ socketPathArg: string,
82
+ optionsArg: ICodexUnixSocketOptions = {},
83
+ ): Promise<TCodexUnixSocketPublication> => {
84
+ assertSocketPath(socketPathArg);
85
+ assertOptions(optionsArg);
86
+ const euid = effectiveUserId();
87
+ let alias: plugins.fs.Stats;
88
+ try {
89
+ alias = await plugins.fs.promises.lstat(socketPathArg);
90
+ } catch (error) {
91
+ if (isMissing(error)) return 'absent';
92
+ throw error;
93
+ }
94
+ const target = await codexUnixSocketTarget(socketPathArg, optionsArg);
95
+ if (!alias.isSymbolicLink() || alias.uid !== euid || await plugins.fs.promises.readlink(socketPathArg) !== target) {
96
+ throw new Error('Codex Unix socket path is not the alias Codex publishes');
97
+ }
98
+ const parent = await plugins.fs.promises.stat(plugins.path.dirname(socketPathArg));
99
+ if (!parent.isDirectory() || (parent.uid !== 0 && parent.uid !== euid)
100
+ || ((parent.mode & 0o022) !== 0 && (parent.mode & 0o1000) === 0)) {
101
+ throw new Error('Codex Unix socket parent directory lets other users replace its entries');
102
+ }
103
+ let directory: plugins.fs.Stats;
104
+ let socket: plugins.fs.Stats;
105
+ try {
106
+ directory = await plugins.fs.promises.lstat(plugins.path.dirname(target));
107
+ socket = await plugins.fs.promises.lstat(target);
108
+ } catch (error) {
109
+ if (isMissing(error)) throw new Error('Codex Unix socket alias has no socket behind it');
110
+ throw error;
111
+ }
112
+ if (!directory.isDirectory() || directory.uid !== euid || (directory.mode & 0o777) !== 0o700) {
113
+ throw new Error('Codex Unix socket directory is not private to the current user');
114
+ }
115
+ if (!socket.isSocket() || socket.uid !== euid) {
116
+ throw new Error('Codex Unix socket is not a socket owned by the current user');
117
+ }
118
+ return 'published';
119
+ };
package/ts/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './interfaces.js';
2
2
  export * from './classes.codexappserverclient.js';
3
+ export * from './functions.codexunixsocket.js';
3
4
  export * from './classes.connectionregistry.js';
4
5
  export * from './classes.dispatchregistry.js';
5
6
  export * from './classes.mcptoolregistrar.js';
package/ts/plugins.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  // Node native modules
2
2
  import * as crypto from 'node:crypto';
3
+ import * as fs from 'node:fs';
3
4
  import * as http from 'node:http';
4
5
  import * as net from 'node:net';
6
+ import * as path from 'node:path';
5
7
  import * as stream from 'node:stream';
6
8
  import type { AddressInfo } from 'node:net';
7
9
 
8
- export { crypto, http, net, stream };
10
+ export { crypto, fs, http, net, path, stream };
9
11
  export type { AddressInfo };
10
12
 
11
13
  // Third-party modules
package/readme.hints.md DELETED
@@ -1,52 +0,0 @@
1
- # readme.hints.md
2
-
3
- Implementation findings for mcp-crossharness.
4
-
5
- ## Server-only architecture
6
-
7
- - Every harness operation after connection setup is scoped to an opaque connection established from an explicit server origin and remote absolute directory; `list_connections` remains global.
8
- - Duplicate harness/origin/directory bindings are rejected and each registry is capped at 32 connections.
9
- - `CROSSHARNESS_ALLOWED_SERVER_URLS` is an exact-origin allowlist. Credentials are fixed environment variables, never tool inputs.
10
- - URLs reject user info, query strings, fragments, and non-root paths. OpenCode fetch calls use `redirect: 'error'`.
11
- - No production source reads harness stores, invokes harness CLIs, starts or stops processes, or discovers endpoints.
12
-
13
- ## OpenCode server API
14
-
15
- Verified against OpenCode 1.18.13 docs, source, and generated SDK.
16
-
17
- - Health: `GET /global/health`.
18
- - Session list: `GET /session?directory=...&scope=project&roots=true&limit=...`.
19
- - Session ownership: `GET /session/:id?directory=...`, then verify returned `directory` exactly.
20
- - Transcript: `GET /session/:id/message?directory=...&limit=...`.
21
- - Send: `POST /session/:id/message?directory=...` with a generated `messageID` and text part.
22
- - OpenCode owns queuing. Crossharness intentionally does not serialize HTTP sends, subscribe to permission events, or issue session-wide aborts.
23
- - Aborting a fetch does not prove that a queued/running server turn stopped, so timeout errors state the ambiguity and no retry occurs.
24
- - Successful JSON response bodies are capped at 16 MiB; error bodies are capped at 64 KiB and only the first 2,000 characters are surfaced.
25
-
26
- ## Codex app-server API
27
-
28
- Verified against Codex 0.146.0 using `codex app-server generate-ts` and official app-server documentation.
29
-
30
- - Explicit transports are `ws://` and `wss://`; one initialized WebSocket is owned per Crossharness connection.
31
- - Flow: `initialize` -> `initialized`; `thread/list`; `thread/read`; `thread/resume`; `turn/start`; correlated notifications; optional `turn/interrupt` on timeout.
32
- - `thread/list` supports exact `cwd` filtering. `thread/read` results are still verified against the connection directory before reading or sending.
33
- - Request responses are correlated by JSON-RPC id. Turn events are correlated by `threadId` and `turnId`; events can arrive before `turn/start` resolves, so one unbound turn per thread may claim the early id.
34
- - Sends to one thread are sequenced; different threads remain concurrent.
35
- - Codex send queues are bounded globally and per thread, outbound WebSocket buffering is capped at 4 MiB, and a send's timeout includes time spent waiting in the same-thread queue.
36
- - Turn output is bounded by text size and delta count, including empty streaming deltas.
37
- - Foreground MCP cancellation aborts the operation-scoped OpenCode request or safely interrupts the correlated Codex turn; asynchronous dispatches remain independent after acceptance.
38
- - A timed-out send keeps its thread queue locked until `turn/interrupt` is acknowledged and `turn/completed` reports terminal state. Missing turn correlation or incomplete interruption closes the connection fail-closed.
39
- - Socket close rejects all pending requests and turns. Disconnect removes the connection before closing so no new work can enter.
40
- - Stdio EOF closes the MCP transport and every active or handshaking connection.
41
- - Server requests are never ignored: command/file approvals decline, tool input returns empty answers, MCP elicitation declines, permission-profile requests grant no extra permissions for the turn, legacy approvals abort, and unknown requests receive `-32601`.
42
-
43
- ## Claude Code
44
-
45
- Claude Code 2.1.219 and the current Agent SDK expose local CLI/library interfaces, not a URL-addressable session server. `connect_harness` rejects `claudecode` until such a server exists.
46
-
47
- ## Testing
48
-
49
- - HTTP fixtures cover required Basic auth, allowlisting, redirect rejection, directory ownership, concurrent OpenCode sends, and disconnect state.
50
- - WebSocket fixtures cover initialization, list/read/send, interleaved turns, bounded deltas and send buffering, safe server-request responses, timeout interruption, directory ownership, and disconnect failure propagation.
51
- - Registry fixtures cover pending-connect shutdown and bounded dispatch admission before send invocation.
52
- - Timeout fixtures cover OpenCode validation, Codex same-thread queueing, interruption completion, malformed terminal events, and stdio EOF cleanup.