@junghanacs/entwurf 0.20.0 → 0.21.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.
Files changed (112) hide show
  1. package/AGENTS.md +85 -190
  2. package/BASELINE.md +6 -5
  3. package/CHANGELOG.md +220 -14
  4. package/CONTRIBUTING.md +1 -1
  5. package/DELIVERY.md +202 -60
  6. package/README.md +54 -26
  7. package/VERIFY.md +37 -7
  8. package/docs/acp-backend-rail.md +31 -15
  9. package/docs/external-mcp-host.md +58 -35
  10. package/docs/fresh-cut-policy.md +3 -2
  11. package/docs/setup-clean-host.md +110 -17
  12. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +101 -109
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +6 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +326 -0
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +22 -2
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +3 -1
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -7
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +30 -17
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +7 -1
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +7 -4
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +20 -11
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +227 -1
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +50 -15
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +10 -7
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +403 -0
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +4 -4
  28. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook-codex.js +325 -0
  29. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +6 -1
  30. package/mcp/entwurf-bridge/src/index.ts +115 -111
  31. package/mcp/entwurf-bridge/tsconfig.build.json +1 -0
  32. package/package.json +14 -5
  33. package/pi-extensions/entwurf-control.ts +71 -19
  34. package/pi-extensions/lib/acp/acp-client.ts +3 -3
  35. package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
  36. package/pi-extensions/lib/acp/backend.ts +3 -3
  37. package/pi-extensions/lib/acp/event-mapper.ts +4 -4
  38. package/pi-extensions/lib/acp/tool-surface.ts +6 -0
  39. package/pi-extensions/lib/codex-fresh-preflight.ts +363 -0
  40. package/pi-extensions/lib/compaction-send-guard.ts +80 -0
  41. package/pi-extensions/lib/entwurf-fact-provider.ts +29 -3
  42. package/pi-extensions/lib/entwurf-peers-render.ts +4 -1
  43. package/pi-extensions/lib/entwurf-v2-contract.ts +6 -7
  44. package/pi-extensions/lib/entwurf-v2-native-push.ts +35 -18
  45. package/pi-extensions/lib/entwurf-v2-production.ts +10 -3
  46. package/pi-extensions/lib/entwurf-v2-surface.ts +7 -4
  47. package/pi-extensions/lib/meta-mailbox-body.ts +22 -13
  48. package/pi-extensions/lib/meta-sender-identity.ts +305 -0
  49. package/pi-extensions/lib/mux-fresh-call.ts +64 -19
  50. package/pi-extensions/lib/native-push/adapter.ts +21 -24
  51. package/pi-extensions/lib/native-push/codex-ws-client.ts +506 -0
  52. package/pi-extensions/lib/native-push/register.ts +7 -9
  53. package/pi-extensions/meta-bridge-hook-codex.ts +371 -0
  54. package/run.sh +251 -35
  55. package/scripts/check-acp-usage-accounting.ts +9 -9
  56. package/scripts/check-agy-sender-identity.ts +1 -1
  57. package/scripts/check-codex-birth-hook.ts +264 -0
  58. package/scripts/check-codex-bridge-identity.ts +179 -0
  59. package/scripts/check-codex-native-push.ts +386 -0
  60. package/scripts/check-codex-sender-identity.ts +495 -0
  61. package/scripts/check-compaction-send-guard.ts +130 -0
  62. package/scripts/check-copilot-receive-arm.ts +4 -1
  63. package/scripts/check-entwurf-fact-provider.ts +38 -0
  64. package/scripts/check-entwurf-peers-surface.ts +13 -1
  65. package/scripts/check-entwurf-self-address.ts +15 -16
  66. package/scripts/check-entwurf-v2-contract.ts +4 -3
  67. package/scripts/check-entwurf-v2-decider.ts +7 -5
  68. package/scripts/check-entwurf-v2-native-push.ts +35 -7
  69. package/scripts/check-entwurf-v2-production.ts +203 -11
  70. package/scripts/check-entwurf-v2-runner.ts +1 -1
  71. package/scripts/check-entwurf-v2-surface.ts +1 -1
  72. package/scripts/check-gate-qualification.ts +7 -4
  73. package/scripts/check-harness-admission-parity.ts +0 -1
  74. package/scripts/check-install-surface.ts +23 -7
  75. package/scripts/check-mux-launch-tmux.ts +47 -2
  76. package/scripts/check-native-push-adapter.ts +20 -16
  77. package/scripts/check-native-push-register.ts +5 -1
  78. package/scripts/check-release-gate-outcomes.ts +47 -1
  79. package/scripts/check-setup-qualification.sh +3 -1
  80. package/scripts/codex-birth-doctor.sh +276 -0
  81. package/scripts/codex-birth-install.sh +414 -0
  82. package/scripts/codex-birth-uninstall.sh +170 -0
  83. package/scripts/codex-mcp-config.py +435 -0
  84. package/scripts/codex-statusline-config.py +434 -0
  85. package/scripts/codex_toml_io.py +532 -0
  86. package/scripts/lib/codex-fresh-live-protocol.ts +113 -0
  87. package/scripts/lib/codex-fresh-source-receipts.ts +399 -0
  88. package/scripts/lib/launch-receipt-windows.ts +46 -0
  89. package/scripts/lib/tmux-coordinate-row.ts +58 -0
  90. package/scripts/meta-bridge-fresh-cut.ts +6 -1
  91. package/scripts/mutants/codex-native.json +838 -0
  92. package/scripts/mutants/compaction-send-guard.json +103 -0
  93. package/scripts/mutants/entwurf-peers.json +19 -0
  94. package/scripts/mutants/mux-fresh-call.json +19 -8
  95. package/scripts/mutants/omp-fresh.json +6 -4
  96. package/scripts/mutants/pi-package-ownership.json +26 -0
  97. package/scripts/mutants/release-gate.json +13 -0
  98. package/scripts/mutants/v2-surface.json +53 -1
  99. package/scripts/pi_settings_io.py +3 -1
  100. package/scripts/raw-async-delivery/README.md +2 -1
  101. package/scripts/raw-codex-measure/README.md +114 -46
  102. package/scripts/register-pi-package.py +38 -30
  103. package/scripts/register-pi-provider.py +3 -2
  104. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  105. package/scripts/smoke-agy-native-push-live.ts +3 -1
  106. package/scripts/smoke-codex-birth.sh +347 -0
  107. package/scripts/smoke-codex-config-state.sh +511 -0
  108. package/scripts/smoke-codex-fresh-live.ts +1186 -0
  109. package/scripts/smoke-codex-native-push-live.ts +75 -0
  110. package/scripts/smoke-setup-verdict.sh +123 -10
  111. package/scripts/smoke-user-scope-citizen.sh +199 -71
  112. package/scripts/tsconfig.json +1 -0
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * native-push adapter rail — the transport LEAF by which a native-push backend
3
- * (antigravity, the first) is (1) probed for a LIVE conversation and (2) direct-injected
4
- * with a message. Mirrors the ACP backend-adapter rail (acp/backend-adapter.ts §ADAPTERS
3
+ * is (1) probed for a LIVE native target and (2) direct-injected with a message.
4
+ * Mirrors the ACP backend-adapter rail (acp/backend-adapter.ts §ADAPTERS
5
5
  * /resolveAcpBackendAdapter): one interface, one registry, a fail-fast resolver.
6
6
  *
7
7
  * Purity contract (봉인 3):
@@ -26,6 +26,7 @@ import * as os from "node:os";
26
26
  import * as path from "node:path";
27
27
 
28
28
  import type { NativePushBackend } from "../entwurf-v2-contract.ts";
29
+ import { codexNativePushAdapter } from "./codex-ws-client.ts";
29
30
 
30
31
  // ── runner seam (injectable process exec) ───────────────────────────────────
31
32
 
@@ -85,14 +86,13 @@ export const realNativePushRunner: NativePushRunner = {
85
86
  // ── route + probe result ────────────────────────────────────────────────────
86
87
 
87
88
  /**
88
- * A VOLATILE native-push route the live LS address (`127.0.0.1:PORT`) a probe found
89
- * serving the conversation. MUST NOT be stored (봉인 3): it is re-derived every dispatch
90
- * by a fresh probe (the LS port is per-process and shifts). Carried only within a single
91
- * `send` call, handed straight from a fresh `probe`.
89
+ * A VOLATILE native-push route. Antigravity carries its live LS address; Codex carries
90
+ * the one app-server UDS that proved the target thread loaded. Routes are never stored:
91
+ * every dispatch derives one from a fresh probe and hands it straight to `send`.
92
92
  */
93
- export interface NativePushRoute {
94
- readonly lsAddress: string;
95
- }
93
+ export type NativePushRoute =
94
+ | { readonly backend: "antigravity"; readonly lsAddress: string }
95
+ | { readonly backend: "codex"; readonly socketPath: string };
96
96
 
97
97
  /**
98
98
  * A probe outcome. `alive` carries the volatile route; `dead`/`indeterminate` carry a
@@ -108,17 +108,12 @@ export type NativePushProbeResult =
108
108
  export interface NativePushAdapter {
109
109
  /** Backend discriminator (a member of NATIVE_PUSH_BACKENDS). */
110
110
  readonly id: NativePushBackend;
111
- /**
112
- * Full-scan probe: find a LIVE route serving `nativeSessionId`, else report
113
- * dead/indeterminate. Scans EVERY host pid (never `head -1`) and re-discovers the
114
- * route on every call (no cache — volatile-route discipline, 봉인 3).
115
- */
111
+ /** Whether the executor may re-probe and retry once after an ambiguous send failure.
112
+ * Codex is false: `codex queue` may accept before its receipt fails, so retry can duplicate. */
113
+ readonly retriable: boolean;
114
+ /** Probe the backend's current native route for `nativeSessionId`. */
116
115
  probe(nativeSessionId: string): Promise<NativePushProbeResult>;
117
- /**
118
- * Direct-inject `content` into the conversation over `route`. Single attempt: throws
119
- * on failure (fail-loud). Does NOT probe and does NOT retry — the executor hand owns
120
- * re-probe/retry (봉인 3), so the adapter can never silently paper over a dead route.
121
- */
116
+ /** Direct-inject once through the just-probed route. Throws on failure. */
122
117
  send(route: NativePushRoute, nativeSessionId: string, content: string): Promise<void>;
123
118
  }
124
119
 
@@ -211,6 +206,7 @@ export function createAntigravityAdapter(deps: AntigravityAdapterDeps): NativePu
211
206
 
212
207
  return {
213
208
  id: "antigravity",
209
+ retriable: true,
214
210
 
215
211
  async probe(nativeSessionId) {
216
212
  let pids: number[];
@@ -237,7 +233,7 @@ export function createAntigravityAdapter(deps: AntigravityAdapterDeps): NativePu
237
233
  for (const pid of pids) {
238
234
  for (const lsAddress of portsByPid.get(pid) ?? []) {
239
235
  if (await servesConversation(lsAddress, nativeSessionId)) {
240
- return { status: "alive", route: { lsAddress } };
236
+ return { status: "alive", route: { backend: "antigravity", lsAddress } };
241
237
  }
242
238
  }
243
239
  }
@@ -251,12 +247,13 @@ export function createAntigravityAdapter(deps: AntigravityAdapterDeps): NativePu
251
247
  },
252
248
 
253
249
  async send(route, nativeSessionId, content) {
250
+ if (route.backend !== "antigravity") {
251
+ throw new Error(`antigravity adapter received ${route.backend} route`);
252
+ }
254
253
  const r = await runner.exec([binary, "agentapi", "send-message", nativeSessionId, content], {
255
254
  env: { ANTIGRAVITY_LS_ADDRESS: route.lsAddress },
256
255
  timeoutMs: AGY_SEND_TIMEOUT_MS,
257
256
  });
258
- // A non-zero code — including a timeout kill (124) on a stalled route — THROWS
259
- // (fail-loud); the executor hand owns the 1-shot re-probe→re-send on that throw.
260
257
  if (r.code !== 0) {
261
258
  throw new Error(
262
259
  `native-push send failed (agentapi send-message exit ${r.code}) via ${route.lsAddress}: ${
@@ -268,12 +265,12 @@ export function createAntigravityAdapter(deps: AntigravityAdapterDeps): NativePu
268
265
  };
269
266
  }
270
267
 
271
- /** The production antigravity adapter (real runner + env-resolved binary). */
268
+ /** Production adapters. */
272
269
  export const antigravityAdapter: NativePushAdapter = createAntigravityAdapter({ runner: realNativePushRunner });
273
270
 
274
271
  // ── registry + fail-fast resolver (mirror resolveAcpBackendAdapter) ──────────
275
272
 
276
- const ADAPTERS: readonly NativePushAdapter[] = [antigravityAdapter];
273
+ const ADAPTERS: readonly NativePushAdapter[] = [antigravityAdapter, codexNativePushAdapter];
277
274
 
278
275
  /**
279
276
  * Resolve the native-push adapter that owns backend `id`. Fail-fast, like
@@ -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 native conversation (antigravity) as a garden
4
- * citizen; it does NOT spawn one (that is the deferred v2 fresh-mint capability — kept
5
- * distinct so a caller never confuses "bind an existing conversation" with "create a new
6
- * sibling").
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
- /** The native backend hosting the conversation. Only antigravity is registerable on this
32
- * lane (codex is a separate lane). */
33
- backend: NativePushBackend;
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). */