@junghanacs/entwurf 0.20.1 → 0.22.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.
- package/AGENTS.md +85 -190
- package/BASELINE.md +6 -3
- package/CHANGELOG.md +473 -14
- package/CONTRIBUTING.md +1 -1
- package/DELIVERY.md +332 -60
- package/README.md +100 -22
- package/VERIFY.md +93 -7
- package/docs/acp-backend-rail.md +0 -1
- package/docs/external-mcp-host.md +64 -33
- package/docs/setup-clean-host.md +151 -17
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +130 -114
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-caller-seat.js +174 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +519 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +22 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +30 -17
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +8 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +7 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +20 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +227 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +189 -19
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +10 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +403 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook-codex.js +325 -0
- package/mcp/entwurf-bridge/dist/scripts/codex-socket-path.js +30 -0
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +6 -1
- package/mcp/entwurf-bridge/src/index.ts +154 -116
- package/mcp/entwurf-bridge/tsconfig.build.json +10 -0
- package/package.json +13 -4
- package/pi-extensions/entwurf-control.ts +71 -19
- package/pi-extensions/lib/codex-caller-seat.ts +204 -0
- package/pi-extensions/lib/codex-fresh-preflight.ts +580 -0
- package/pi-extensions/lib/compaction-send-guard.ts +80 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +29 -3
- package/pi-extensions/lib/entwurf-peers-render.ts +4 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +6 -7
- package/pi-extensions/lib/entwurf-v2-native-push.ts +35 -18
- package/pi-extensions/lib/entwurf-v2-production.ts +10 -3
- package/pi-extensions/lib/entwurf-v2-runner.ts +3 -2
- package/pi-extensions/lib/entwurf-v2-send.ts +16 -11
- package/pi-extensions/lib/entwurf-v2-surface.ts +7 -4
- package/pi-extensions/lib/meta-mailbox-body.ts +22 -13
- package/pi-extensions/lib/meta-sender-identity.ts +305 -0
- package/pi-extensions/lib/mux-fresh-call.ts +233 -29
- package/pi-extensions/lib/native-push/adapter.ts +21 -24
- package/pi-extensions/lib/native-push/codex-ws-client.ts +506 -0
- package/pi-extensions/lib/native-push/register.ts +7 -9
- package/pi-extensions/meta-bridge-hook-codex.ts +371 -0
- package/run.sh +342 -28
- package/scripts/check-agy-sender-identity.ts +1 -1
- package/scripts/check-codex-app-server-launch.ts +445 -0
- package/scripts/check-codex-birth-hook.ts +264 -0
- package/scripts/check-codex-bridge-identity.ts +179 -0
- package/scripts/check-codex-native-push.ts +386 -0
- package/scripts/check-codex-sender-identity.ts +495 -0
- package/scripts/check-compaction-send-guard.ts +130 -0
- package/scripts/check-copilot-receive-arm.ts +4 -1
- package/scripts/check-entwurf-fact-provider.ts +38 -0
- package/scripts/check-entwurf-peers-surface.ts +13 -1
- package/scripts/check-entwurf-self-address.ts +15 -16
- package/scripts/check-entwurf-v2-contract.ts +4 -3
- package/scripts/check-entwurf-v2-decider.ts +7 -5
- package/scripts/check-entwurf-v2-native-push.ts +35 -7
- package/scripts/check-entwurf-v2-production.ts +245 -12
- package/scripts/check-entwurf-v2-runner.ts +1 -1
- package/scripts/check-entwurf-v2-send.ts +26 -7
- package/scripts/check-entwurf-v2-surface.ts +1 -1
- package/scripts/check-gate-qualification.ts +8 -3
- package/scripts/check-harness-admission-parity.ts +0 -1
- package/scripts/check-mux-launch-tmux.ts +345 -4
- package/scripts/check-native-push-adapter.ts +20 -16
- package/scripts/check-native-push-register.ts +5 -1
- package/scripts/check-release-gate-outcomes.ts +47 -1
- package/scripts/check-setup-qualification.sh +3 -1
- package/scripts/codex-app-server-launch.sh +275 -0
- package/scripts/codex-birth-doctor.sh +276 -0
- package/scripts/codex-birth-install.sh +414 -0
- package/scripts/codex-birth-uninstall.sh +170 -0
- package/scripts/codex-mcp-config.py +435 -0
- package/scripts/codex-socket-path.ts +33 -0
- package/scripts/codex-statusline-config.py +434 -0
- package/scripts/codex-terminal-title-config.py +500 -0
- package/scripts/codex_toml_io.py +653 -0
- package/scripts/lib/codex-fresh-live-protocol.ts +121 -0
- package/scripts/lib/codex-fresh-source-receipts.ts +426 -0
- package/scripts/lib/launch-receipt-windows.ts +46 -0
- package/scripts/lib/tmux-coordinate-row.ts +58 -0
- package/scripts/meta-bridge-fresh-cut.ts +6 -1
- package/scripts/mutants/codex-app-server-launch.json +157 -0
- package/scripts/mutants/codex-caller-seat.json +336 -0
- package/scripts/mutants/codex-native.json +838 -0
- package/scripts/mutants/compaction-send-guard.json +103 -0
- package/scripts/mutants/entwurf-peers.json +19 -0
- package/scripts/mutants/mux-fresh-call.json +93 -10
- package/scripts/mutants/omp-fresh.json +6 -4
- package/scripts/mutants/release-gate.json +13 -0
- package/scripts/mutants/v2-surface.json +75 -1
- package/scripts/raw-async-delivery/README.md +2 -1
- package/scripts/raw-codex-measure/README.md +114 -46
- package/scripts/smoke-agy-native-push-live.ts +3 -1
- package/scripts/smoke-codex-birth.sh +347 -0
- package/scripts/smoke-codex-config-state.sh +700 -0
- package/scripts/smoke-codex-fresh-live.ts +1426 -0
- package/scripts/smoke-codex-native-push-live.ts +75 -0
- package/scripts/smoke-entwurf-chain-live.ts +50 -0
- package/scripts/smoke-setup-verdict.sh +125 -10
- package/scripts/tsconfig.json +1 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import * as crypto from "node:crypto";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as net from "node:net";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
|
|
7
|
+
import type { NativePushBackend } from "../entwurf-v2-contract.js";
|
|
8
|
+
|
|
9
|
+
export interface CodexSocketEnv {
|
|
10
|
+
HOME?: string;
|
|
11
|
+
CODEX_HOME?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveCodexHome(env: CodexSocketEnv): string {
|
|
15
|
+
const explicit = env.CODEX_HOME?.trim();
|
|
16
|
+
if (explicit) return explicit;
|
|
17
|
+
const home = env.HOME?.trim();
|
|
18
|
+
if (!home) throw new Error("codex app-server: neither CODEX_HOME nor HOME is available");
|
|
19
|
+
return path.join(home, ".codex");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveCodexDefaultSocketPath(env: CodexSocketEnv): string {
|
|
23
|
+
return path.join(resolveCodexHome(env), "app-server-control", "app-server-control.sock");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type CodexSocketFileCheck = { ok: true } | { ok: false; status: "dead" | "indeterminate"; reason: string };
|
|
27
|
+
|
|
28
|
+
export function checkCodexSocketFile(socketPath: string): CodexSocketFileCheck {
|
|
29
|
+
try {
|
|
30
|
+
const stat = fs.lstatSync(socketPath);
|
|
31
|
+
if (stat.isSymbolicLink()) {
|
|
32
|
+
return { ok: false, status: "indeterminate", reason: `app-server socket is a symlink: ${socketPath}` };
|
|
33
|
+
}
|
|
34
|
+
if (!stat.isSocket()) {
|
|
35
|
+
return { ok: false, status: "indeterminate", reason: `app-server path is not a socket: ${socketPath}` };
|
|
36
|
+
}
|
|
37
|
+
if (typeof process.getuid === "function" && stat.uid !== process.getuid()) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
status: "indeterminate",
|
|
41
|
+
reason: `app-server socket is owned by uid ${stat.uid}, not ${process.getuid()}`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return { ok: true };
|
|
45
|
+
} catch (error) {
|
|
46
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
47
|
+
if (code === "ENOENT") return { ok: false, status: "dead", reason: `app-server socket is absent: ${socketPath}` };
|
|
48
|
+
return { ok: false, status: "indeterminate", reason: error instanceof Error ? error.message : String(error) };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface CodexRpcProtocol {
|
|
53
|
+
request(method: string, params: Record<string, unknown>): Promise<unknown>;
|
|
54
|
+
notify(method: string, params?: Record<string, unknown>): void;
|
|
55
|
+
close(): void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface CodexProtocolOpener {
|
|
59
|
+
open(socketPath: string): Promise<CodexRpcProtocol>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function promiseResolvers<T>(): {
|
|
63
|
+
promise: Promise<T>;
|
|
64
|
+
resolve: (value: T | PromiseLike<T>) => void;
|
|
65
|
+
reject: (reason?: unknown) => void;
|
|
66
|
+
} {
|
|
67
|
+
return (
|
|
68
|
+
Promise as unknown as {
|
|
69
|
+
withResolvers<U>(): {
|
|
70
|
+
promise: Promise<U>;
|
|
71
|
+
resolve: (value: U | PromiseLike<U>) => void;
|
|
72
|
+
reject: (reason?: unknown) => void;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
).withResolvers<T>();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
79
|
+
const REQUEST_TIMEOUT_MS = 5_000;
|
|
80
|
+
const MAX_MESSAGE_BYTES = 1024 * 1024;
|
|
81
|
+
|
|
82
|
+
function clientFrame(opcode: number, payload: Buffer): Buffer {
|
|
83
|
+
const mask = crypto.randomBytes(4);
|
|
84
|
+
let header: Buffer;
|
|
85
|
+
if (payload.length < 126) {
|
|
86
|
+
header = Buffer.from([0x80 | opcode, 0x80 | payload.length]);
|
|
87
|
+
} else if (payload.length <= 0xffff) {
|
|
88
|
+
header = Buffer.allocUnsafe(4);
|
|
89
|
+
header[0] = 0x80 | opcode;
|
|
90
|
+
header[1] = 0x80 | 126;
|
|
91
|
+
header.writeUInt16BE(payload.length, 2);
|
|
92
|
+
} else {
|
|
93
|
+
header = Buffer.allocUnsafe(10);
|
|
94
|
+
header[0] = 0x80 | opcode;
|
|
95
|
+
header[1] = 0x80 | 127;
|
|
96
|
+
header.writeBigUInt64BE(BigInt(payload.length), 2);
|
|
97
|
+
}
|
|
98
|
+
const masked = Buffer.allocUnsafe(payload.length);
|
|
99
|
+
for (let index = 0; index < payload.length; index++) masked[index] = payload[index]! ^ mask[index % 4]!;
|
|
100
|
+
return Buffer.concat([header, mask, masked]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
class CodexWebSocketRpc implements CodexRpcProtocol {
|
|
104
|
+
private nextId = 1;
|
|
105
|
+
private buffer = Buffer.alloc(0);
|
|
106
|
+
private fragmentOpcode: number | null = null;
|
|
107
|
+
private fragments: Buffer[] = [];
|
|
108
|
+
private fragmentBytes = 0;
|
|
109
|
+
private terminalError: Error | null = null;
|
|
110
|
+
private readonly pending = new Map<
|
|
111
|
+
number,
|
|
112
|
+
{ resolve: (value: unknown) => void; reject: (error: Error) => void; timer: NodeJS.Timeout }
|
|
113
|
+
>();
|
|
114
|
+
|
|
115
|
+
private readonly socket: net.Socket;
|
|
116
|
+
|
|
117
|
+
private constructor(socket: net.Socket) {
|
|
118
|
+
this.socket = socket;
|
|
119
|
+
socket.on("data", (chunk) => this.feed(chunk));
|
|
120
|
+
socket.on("error", (error) => this.failAll(error));
|
|
121
|
+
socket.on("close", () => this.failAll(new Error("codex app-server WebSocket closed")));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
static open(socketPath: string): Promise<CodexWebSocketRpc> {
|
|
125
|
+
const { promise, resolve, reject } = promiseResolvers<CodexWebSocketRpc>();
|
|
126
|
+
const socket = net.createConnection(socketPath);
|
|
127
|
+
const key = crypto.randomBytes(16).toString("base64");
|
|
128
|
+
const expectedAccept = crypto
|
|
129
|
+
.createHash("sha1")
|
|
130
|
+
.update(key + WS_GUID)
|
|
131
|
+
.digest("base64");
|
|
132
|
+
let header = Buffer.alloc(0);
|
|
133
|
+
const timer = setTimeout(() => {
|
|
134
|
+
const error = Object.assign(new Error("codex app-server WebSocket handshake timed out"), { code: "ETIMEDOUT" });
|
|
135
|
+
socket.destroy(error);
|
|
136
|
+
}, REQUEST_TIMEOUT_MS);
|
|
137
|
+
const onError = (error: Error): void => {
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
reject(error);
|
|
140
|
+
};
|
|
141
|
+
const onData = (chunk: Buffer): void => {
|
|
142
|
+
header = Buffer.concat([header, chunk]);
|
|
143
|
+
if (header.length > 16 * 1024) {
|
|
144
|
+
socket.destroy(new Error("codex app-server WebSocket handshake exceeded 16 KiB"));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const boundary = header.indexOf("\r\n\r\n");
|
|
148
|
+
if (boundary < 0) return;
|
|
149
|
+
clearTimeout(timer);
|
|
150
|
+
const lines = header.subarray(0, boundary).toString("utf8").split("\r\n");
|
|
151
|
+
const headers = new Map<string, string>();
|
|
152
|
+
for (const line of lines.slice(1)) {
|
|
153
|
+
const colon = line.indexOf(":");
|
|
154
|
+
if (colon > 0) headers.set(line.slice(0, colon).trim().toLowerCase(), line.slice(colon + 1).trim());
|
|
155
|
+
}
|
|
156
|
+
if (lines[0] !== "HTTP/1.1 101 Switching Protocols" || headers.get("sec-websocket-accept") !== expectedAccept) {
|
|
157
|
+
socket.destroy(new Error("codex app-server refused the WebSocket upgrade"));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
socket.off("error", onError);
|
|
161
|
+
socket.off("data", onData);
|
|
162
|
+
const rpc = new CodexWebSocketRpc(socket);
|
|
163
|
+
const remainder = header.subarray(boundary + 4);
|
|
164
|
+
if (remainder.length > 0) rpc.feed(remainder);
|
|
165
|
+
resolve(rpc);
|
|
166
|
+
};
|
|
167
|
+
socket.once("error", onError);
|
|
168
|
+
socket.on("data", onData);
|
|
169
|
+
socket.once("connect", () => {
|
|
170
|
+
socket.write(
|
|
171
|
+
`GET / HTTP/1.1\r\nHost: localhost\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: ${key}\r\nSec-WebSocket-Version: 13\r\n\r\n`,
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
return promise;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
request(method: string, params: Record<string, unknown>): Promise<unknown> {
|
|
178
|
+
if (this.terminalError) return Promise.reject(this.terminalError);
|
|
179
|
+
const id = this.nextId++;
|
|
180
|
+
const { promise, resolve, reject } = promiseResolvers<unknown>();
|
|
181
|
+
const timer = setTimeout(() => {
|
|
182
|
+
this.pending.delete(id);
|
|
183
|
+
reject(new Error(`codex app-server request timed out: ${method}`));
|
|
184
|
+
}, REQUEST_TIMEOUT_MS);
|
|
185
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
186
|
+
this.socket.write(clientFrame(0x1, Buffer.from(JSON.stringify({ id, method, params }))));
|
|
187
|
+
return promise;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
notify(method: string, params: Record<string, unknown> = {}): void {
|
|
191
|
+
if (this.terminalError) return;
|
|
192
|
+
this.socket.write(clientFrame(0x1, Buffer.from(JSON.stringify({ method, params }))));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
close(): void {
|
|
196
|
+
if (!this.socket.destroyed) this.socket.destroy();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private feed(chunk: Buffer): void {
|
|
200
|
+
if (this.terminalError) return;
|
|
201
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
202
|
+
while (!this.terminalError && this.takeFrame()) {
|
|
203
|
+
// Drain every complete frame already buffered.
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private takeFrame(): boolean {
|
|
208
|
+
if (this.buffer.length < 2) return false;
|
|
209
|
+
const first = this.buffer[0]!;
|
|
210
|
+
const second = this.buffer[1]!;
|
|
211
|
+
if ((second & 0x80) !== 0) return this.protocolFailure("server sent a masked WebSocket frame");
|
|
212
|
+
let length = second & 0x7f;
|
|
213
|
+
let offset = 2;
|
|
214
|
+
if (length === 126) {
|
|
215
|
+
if (this.buffer.length < 4) return false;
|
|
216
|
+
length = this.buffer.readUInt16BE(2);
|
|
217
|
+
offset = 4;
|
|
218
|
+
} else if (length === 127) {
|
|
219
|
+
if (this.buffer.length < 10) return false;
|
|
220
|
+
const wide = this.buffer.readBigUInt64BE(2);
|
|
221
|
+
if (wide > BigInt(MAX_MESSAGE_BYTES)) return this.protocolFailure("WebSocket frame exceeds 1 MiB");
|
|
222
|
+
length = Number(wide);
|
|
223
|
+
offset = 10;
|
|
224
|
+
}
|
|
225
|
+
if (length > MAX_MESSAGE_BYTES) return this.protocolFailure("WebSocket frame exceeds 1 MiB");
|
|
226
|
+
if (this.buffer.length < offset + length) return false;
|
|
227
|
+
const payload = this.buffer.subarray(offset, offset + length);
|
|
228
|
+
this.buffer = this.buffer.subarray(offset + length);
|
|
229
|
+
const opcode = first & 0x0f;
|
|
230
|
+
const final = (first & 0x80) !== 0;
|
|
231
|
+
if (opcode === 0x9) {
|
|
232
|
+
this.socket.write(clientFrame(0xa, payload));
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
if (opcode === 0x8) return this.protocolFailure("codex app-server closed the WebSocket");
|
|
236
|
+
if (opcode === 0x1 && final) return this.receiveJson(payload);
|
|
237
|
+
if (opcode === 0x1 && !final && this.fragmentOpcode === null) {
|
|
238
|
+
this.fragmentOpcode = opcode;
|
|
239
|
+
this.fragments = [payload];
|
|
240
|
+
this.fragmentBytes = payload.length;
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
if (opcode === 0x0 && this.fragmentOpcode === 0x1) {
|
|
244
|
+
this.fragmentBytes += payload.length;
|
|
245
|
+
if (this.fragmentBytes > MAX_MESSAGE_BYTES) return this.protocolFailure("fragmented message exceeds 1 MiB");
|
|
246
|
+
this.fragments.push(payload);
|
|
247
|
+
if (final) {
|
|
248
|
+
const message = Buffer.concat(this.fragments, this.fragmentBytes);
|
|
249
|
+
this.fragmentOpcode = null;
|
|
250
|
+
this.fragments = [];
|
|
251
|
+
this.fragmentBytes = 0;
|
|
252
|
+
return this.receiveJson(message);
|
|
253
|
+
}
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
return this.protocolFailure(`unsupported WebSocket opcode ${opcode}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private receiveJson(payload: Buffer): boolean {
|
|
260
|
+
let parsed: unknown;
|
|
261
|
+
try {
|
|
262
|
+
parsed = JSON.parse(payload.toString("utf8"));
|
|
263
|
+
} catch {
|
|
264
|
+
return this.protocolFailure("codex app-server returned invalid JSON");
|
|
265
|
+
}
|
|
266
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
267
|
+
return this.protocolFailure("codex app-server returned a non-object JSON message");
|
|
268
|
+
const message = parsed as { id?: unknown; result?: unknown; error?: unknown };
|
|
269
|
+
if (typeof message.id !== "number") return true;
|
|
270
|
+
const entry = this.pending.get(message.id);
|
|
271
|
+
if (!entry) return true;
|
|
272
|
+
clearTimeout(entry.timer);
|
|
273
|
+
this.pending.delete(message.id);
|
|
274
|
+
if (message.error !== undefined)
|
|
275
|
+
entry.reject(new Error(`codex app-server error: ${JSON.stringify(message.error)}`));
|
|
276
|
+
else entry.resolve(message.result);
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private protocolFailure(message: string): false {
|
|
281
|
+
if (this.terminalError) return false;
|
|
282
|
+
const error = new Error(message);
|
|
283
|
+
this.terminalError = error;
|
|
284
|
+
this.buffer = Buffer.alloc(0);
|
|
285
|
+
this.fragmentOpcode = null;
|
|
286
|
+
this.fragments = [];
|
|
287
|
+
this.fragmentBytes = 0;
|
|
288
|
+
this.failAll(error);
|
|
289
|
+
if (!this.socket.destroyed) this.socket.destroy(error);
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private failAll(error: Error): void {
|
|
294
|
+
for (const entry of this.pending.values()) {
|
|
295
|
+
clearTimeout(entry.timer);
|
|
296
|
+
entry.reject(error);
|
|
297
|
+
}
|
|
298
|
+
this.pending.clear();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export const realCodexProtocolOpener: CodexProtocolOpener = {
|
|
303
|
+
open: (socketPath) => CodexWebSocketRpc.open(socketPath),
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const CODEX_CLIENT_INFO = { name: "entwurf-native-push/codex", title: "entwurf-native-push/codex", version: "0" };
|
|
307
|
+
|
|
308
|
+
async function initialize(protocol: CodexRpcProtocol): Promise<void> {
|
|
309
|
+
await protocol.request("initialize", {
|
|
310
|
+
clientInfo: CODEX_CLIENT_INFO,
|
|
311
|
+
capabilities: { experimentalApi: true, requestAttestation: false },
|
|
312
|
+
});
|
|
313
|
+
protocol.notify("initialized");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export interface CodexRoute {
|
|
317
|
+
readonly backend: "codex";
|
|
318
|
+
readonly socketPath: string;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export type CodexProbeResult =
|
|
322
|
+
| { status: "alive"; route: CodexRoute }
|
|
323
|
+
| { status: "dead" | "indeterminate"; reason: string };
|
|
324
|
+
|
|
325
|
+
export interface CodexExecResult {
|
|
326
|
+
code: number;
|
|
327
|
+
stdout: string;
|
|
328
|
+
stderr: string;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export interface CodexExecRunner {
|
|
332
|
+
exec(argv: readonly string[], timeoutMs: number): Promise<CodexExecResult>;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const realCodexExecRunner: CodexExecRunner = {
|
|
336
|
+
exec(argv, timeoutMs) {
|
|
337
|
+
const { promise, resolve } = promiseResolvers<CodexExecResult>();
|
|
338
|
+
const [command, ...args] = argv;
|
|
339
|
+
execFile(command ?? "", args, { maxBuffer: MAX_MESSAGE_BYTES, timeout: timeoutMs }, (error, stdout, stderr) => {
|
|
340
|
+
const failure = error as (NodeJS.ErrnoException & { killed?: boolean }) | null;
|
|
341
|
+
resolve({
|
|
342
|
+
code: failure == null ? 0 : failure.killed ? 124 : typeof failure.code === "number" ? failure.code : 127,
|
|
343
|
+
stdout: stdout ?? "",
|
|
344
|
+
stderr: stderr ?? "",
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
return promise;
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
export interface CodexAdapterDeps {
|
|
352
|
+
env?: CodexSocketEnv;
|
|
353
|
+
openProtocol?: CodexProtocolOpener;
|
|
354
|
+
checkSocket?: (socketPath: string) => CodexSocketFileCheck;
|
|
355
|
+
runner?: CodexExecRunner;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export async function probeCodexThread(
|
|
359
|
+
nativeSessionId: string,
|
|
360
|
+
deps: CodexAdapterDeps = {},
|
|
361
|
+
): Promise<CodexProbeResult> {
|
|
362
|
+
const socketPath = resolveCodexDefaultSocketPath(deps.env ?? process.env);
|
|
363
|
+
const socketCheck = (deps.checkSocket ?? checkCodexSocketFile)(socketPath);
|
|
364
|
+
if (!socketCheck.ok) return { status: socketCheck.status, reason: socketCheck.reason };
|
|
365
|
+
let protocol: CodexRpcProtocol;
|
|
366
|
+
try {
|
|
367
|
+
protocol = await (deps.openProtocol ?? realCodexProtocolOpener).open(socketPath);
|
|
368
|
+
} catch (error) {
|
|
369
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
370
|
+
return {
|
|
371
|
+
status: code === "ENOENT" || code === "ECONNREFUSED" ? "dead" : "indeterminate",
|
|
372
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
await initialize(protocol);
|
|
377
|
+
let cursor: string | undefined;
|
|
378
|
+
for (let page = 0; page < 64; page++) {
|
|
379
|
+
const result = (await protocol.request("thread/loaded/list", cursor ? { cursor } : {})) as {
|
|
380
|
+
data?: unknown;
|
|
381
|
+
nextCursor?: unknown;
|
|
382
|
+
};
|
|
383
|
+
if (!Array.isArray(result?.data) || !result.data.every((item) => typeof item === "string")) {
|
|
384
|
+
throw new Error("codex app-server returned an invalid thread/loaded/list page");
|
|
385
|
+
}
|
|
386
|
+
if (result.data.includes(nativeSessionId)) return { status: "alive", route: { backend: "codex", socketPath } };
|
|
387
|
+
if (result.nextCursor == null)
|
|
388
|
+
return { status: "dead", reason: `Codex app-server has no loaded thread ${nativeSessionId}` };
|
|
389
|
+
if (typeof result.nextCursor !== "string" || result.nextCursor === cursor) {
|
|
390
|
+
throw new Error("codex app-server returned a non-advancing thread cursor");
|
|
391
|
+
}
|
|
392
|
+
cursor = result.nextCursor;
|
|
393
|
+
}
|
|
394
|
+
return { status: "indeterminate", reason: "Codex loaded-thread inventory exceeded 64 pages" };
|
|
395
|
+
} catch (error) {
|
|
396
|
+
return { status: "indeterminate", reason: error instanceof Error ? error.message : String(error) };
|
|
397
|
+
} finally {
|
|
398
|
+
protocol.close();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const QUEUE_TIMEOUT_MS = 8_000;
|
|
403
|
+
const QUEUE_ACK = /^Queued message (\S+) for thread (\S+)\.$/;
|
|
404
|
+
|
|
405
|
+
export async function sendViaCodexQueue(
|
|
406
|
+
route: CodexRoute,
|
|
407
|
+
nativeSessionId: string,
|
|
408
|
+
content: string,
|
|
409
|
+
runner: CodexExecRunner = realCodexExecRunner,
|
|
410
|
+
): Promise<void> {
|
|
411
|
+
const result = await runner.exec(
|
|
412
|
+
["codex", "queue", "--remote", `unix://${route.socketPath}`, "--thread", nativeSessionId, "--message", content],
|
|
413
|
+
QUEUE_TIMEOUT_MS,
|
|
414
|
+
);
|
|
415
|
+
if (result.code !== 0) {
|
|
416
|
+
throw new Error(
|
|
417
|
+
`codex queue exited ${result.code}: ${result.stderr.trim() || result.stdout.trim() || "no output"}`,
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
const match = QUEUE_ACK.exec(result.stdout.trim());
|
|
421
|
+
if (!match || match[2] !== nativeSessionId) {
|
|
422
|
+
throw new Error(`codex queue returned an invalid acknowledgement: ${JSON.stringify(result.stdout.trim())}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export async function setCodexThreadName(
|
|
427
|
+
socketPath: string,
|
|
428
|
+
threadId: string,
|
|
429
|
+
name: string,
|
|
430
|
+
opener: CodexProtocolOpener = realCodexProtocolOpener,
|
|
431
|
+
): Promise<void> {
|
|
432
|
+
const protocol = await opener.open(socketPath);
|
|
433
|
+
try {
|
|
434
|
+
await initialize(protocol);
|
|
435
|
+
const result = await protocol.request("thread/name/set", { threadId, name });
|
|
436
|
+
if (result == null || typeof result !== "object" || Array.isArray(result) || Object.keys(result).length !== 0) {
|
|
437
|
+
throw new Error(
|
|
438
|
+
`codex app-server returned an invalid thread/name/set acknowledgement: ${JSON.stringify(result)}`,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
} finally {
|
|
442
|
+
protocol.close();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export interface CodexThreadItem {
|
|
447
|
+
type?: string;
|
|
448
|
+
tool?: string;
|
|
449
|
+
server?: string;
|
|
450
|
+
result?: { content?: unknown[] } | null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export interface CodexThreadView {
|
|
454
|
+
id?: string;
|
|
455
|
+
name?: string | null;
|
|
456
|
+
turns?: Array<{ items?: CodexThreadItem[] }>;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Read one thread's app-server view. `[source]` rust-v0.153.4
|
|
461
|
+
* `app-server-protocol/src/protocol/v2/thread.rs:1656-1671` — params `{ threadId, includeTurns? }`,
|
|
462
|
+
* response `{ thread: Thread }`; `thread_data.rs:203-281` — `name: Option<String>` is the visible
|
|
463
|
+
* title this unit arms at birth, and `turns` is populated ONLY when `includeTurns` is true.
|
|
464
|
+
*
|
|
465
|
+
* This is a READ, so it is not part of the delivery rail: it carries no retry and no liveness
|
|
466
|
+
* meaning. `probeCodexThread` remains the only reachability answer.
|
|
467
|
+
*/
|
|
468
|
+
export async function readCodexThread(
|
|
469
|
+
socketPath: string,
|
|
470
|
+
threadId: string,
|
|
471
|
+
includeTurns = false,
|
|
472
|
+
opener: CodexProtocolOpener = realCodexProtocolOpener,
|
|
473
|
+
): Promise<CodexThreadView> {
|
|
474
|
+
const protocol = await opener.open(socketPath);
|
|
475
|
+
try {
|
|
476
|
+
await initialize(protocol);
|
|
477
|
+
const result = (await protocol.request("thread/read", { threadId, includeTurns })) as {
|
|
478
|
+
thread?: unknown;
|
|
479
|
+
} | null;
|
|
480
|
+
const thread = result?.thread;
|
|
481
|
+
if (thread == null || typeof thread !== "object" || Array.isArray(thread)) {
|
|
482
|
+
throw new Error(`codex app-server returned an invalid thread/read response: ${JSON.stringify(result)}`);
|
|
483
|
+
}
|
|
484
|
+
return thread as CodexThreadView;
|
|
485
|
+
} finally {
|
|
486
|
+
protocol.close();
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export interface CodexNativePushAdapter {
|
|
491
|
+
readonly id: NativePushBackend;
|
|
492
|
+
readonly retriable: false;
|
|
493
|
+
probe(nativeSessionId: string): Promise<CodexProbeResult>;
|
|
494
|
+
send(route: CodexRoute, nativeSessionId: string, content: string): Promise<void>;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export function createCodexNativePushAdapter(deps: CodexAdapterDeps = {}): CodexNativePushAdapter {
|
|
498
|
+
return {
|
|
499
|
+
id: "codex",
|
|
500
|
+
retriable: false,
|
|
501
|
+
probe: (nativeSessionId) => probeCodexThread(nativeSessionId, deps),
|
|
502
|
+
send: (route, nativeSessionId, content) => sendViaCodexQueue(route, nativeSessionId, content, deps.runner),
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export const codexNativePushAdapter = createCodexNativePushAdapter();
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* native-push/register — the pure-ish core of the `entwurf_register_native` MCP tool
|
|
3
|
-
* (봉인 5). It REGISTERS an already-running
|
|
4
|
-
* citizen; it does NOT spawn one
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* (봉인 5). It REGISTERS an already-running Antigravity conversation as a garden
|
|
4
|
+
* citizen; it does NOT spawn one. Codex owns automatic birth from its system hook,
|
|
5
|
+
* so widening the native-push backend registry does not widen this manual registration
|
|
6
|
+
* authority.
|
|
7
7
|
*
|
|
8
8
|
* Flow (봉인 5):
|
|
9
9
|
* 1. Resolve the native-push adapter for the backend and PROBE the conversation. Only a
|
|
@@ -23,15 +23,13 @@
|
|
|
23
23
|
* upsert (pi-free), so the harness-neutral MCP bridge can reach it at boot.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import type { NativePushBackend } from "../entwurf-v2-contract.ts";
|
|
27
26
|
import { type MetaCitizenBackend, type UpsertAction, upsertMetaSession } from "../meta-session.ts";
|
|
28
27
|
import { type NativePushAdapter, resolveNativePushAdapter } from "./adapter.ts";
|
|
29
28
|
|
|
30
29
|
export interface RegisterNativeConversationInput {
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
/** The backend's native conversation id (antigravity conversationId). */
|
|
30
|
+
/** Codex is born automatically from its system hook; this manual lane stays Antigravity-only. */
|
|
31
|
+
backend: "antigravity";
|
|
32
|
+
/** The Antigravity conversation id. */
|
|
35
33
|
nativeSessionId: string;
|
|
36
34
|
/** The cwd to record for this citizen — REQUIRED: a native conversation's metadata cannot
|
|
37
35
|
* confirm it, so the caller must state it (봉인 5). */
|