@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
|
@@ -34,15 +34,31 @@
|
|
|
34
34
|
* Every candidate is collected and validated BEFORE one is chosen. A first-match loop would make
|
|
35
35
|
* the answer depend on which pid or backend happened to be read first; here lookup order carries
|
|
36
36
|
* no meaning, and two live identities are a refusal rather than a race.
|
|
37
|
+
*
|
|
38
|
+
* A SECOND RAIL LIVES BELOW THE MARKER RAIL, and it does not share this one's join key.
|
|
39
|
+
* Codex names its caller on EVERY `tools/call` (`_meta.threadId` plus an
|
|
40
|
+
* `x-codex-turn-metadata.{session_id,thread_id}` block), and its parent pid cannot separate
|
|
41
|
+
* citizens at all: in the delivery-capable launch mode every hook and every MCP child of N
|
|
42
|
+
* live threads resolves to the ONE app-server pid, so a pid marker would be one marker for N
|
|
43
|
+
* citizens — exactly what the store's `nativeSessionId` uniqueness forbids (measured
|
|
44
|
+
* 2026-09-08, `scripts/raw-codex-measure/README.md` S1b-C/S1b-D). So the codex resolver is
|
|
45
|
+
* REQUEST-scoped, not process-scoped: see {@link resolveCodexRequestSenderIdentity}. The two
|
|
46
|
+
* rails never merge — `codex` is deliberately absent from {@link META_SENDER_BACKENDS}, since
|
|
47
|
+
* a backend listed there is one whose hook writes a pid marker, and codex birth writes none.
|
|
37
48
|
*/
|
|
38
49
|
|
|
39
50
|
import {
|
|
51
|
+
defaultMetaSessionsDir,
|
|
52
|
+
describe,
|
|
40
53
|
isPlausibleOwnerPid,
|
|
54
|
+
listAllMetaIdentities,
|
|
41
55
|
type MetaBackend,
|
|
42
56
|
type MetaIdentity,
|
|
43
57
|
type MetaSenderMarker,
|
|
58
|
+
makeStoreRecordReader,
|
|
44
59
|
metaRecordExistsByGardenId,
|
|
45
60
|
parentPid,
|
|
61
|
+
readActiveStoreEntries,
|
|
46
62
|
readMetaIdentityByGardenId,
|
|
47
63
|
readMetaSenderMarker,
|
|
48
64
|
} from "./meta-session.ts";
|
|
@@ -211,3 +227,292 @@ export async function probeNativeSenderAlive(
|
|
|
211
227
|
const probe = await resolveAdapter(identity.backend).probe(identity.nativeSessionId);
|
|
212
228
|
return probe.status === "alive";
|
|
213
229
|
}
|
|
230
|
+
|
|
231
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
232
|
+
// REQUEST-SCOPED SENDER IDENTITY — codex
|
|
233
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The provenance label the MANAGED codex MCP entry carries (`ENTWURF_BRIDGE_NATIVE_HOST=codex`
|
|
237
|
+
* under `[mcp_servers.entwurf-bridge]` in the codex config the entwurf config writer owns).
|
|
238
|
+
*
|
|
239
|
+
* It is an OWNERSHIP/DRIFT atom, not authentication: any process able to write that config, or
|
|
240
|
+
* to set that env on a child, can spell it. What it buys is the thing a codex bridge child
|
|
241
|
+
* genuinely cannot observe otherwise — that THIS child was launched from an entwurf-managed
|
|
242
|
+
* entry, so the request-scoped rail below is admitted deliberately rather than by sniffing.
|
|
243
|
+
* The value travels to this resolver as an EXPLICIT ARGUMENT: nothing here reads the
|
|
244
|
+
* environment, and in particular nothing here reads `ENTWURF_BRIDGE_EXTERNAL_AGENT_ID`, which
|
|
245
|
+
* is the omp root-policy label with its own policy attached (`applyOmpBridgeChildRootPolicy`)
|
|
246
|
+
* and must never double as a codex admission.
|
|
247
|
+
*/
|
|
248
|
+
export const CODEX_BRIDGE_PROVENANCE_LABEL = "codex";
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* `clientInfo.name` every codex MCP client sends, and it is a FIXED VENDOR LITERAL —
|
|
252
|
+
* `Implementation::new("codex-mcp-client", CARGO_PKG_VERSION).with_title("Codex")`
|
|
253
|
+
* (`codex-mcp/src/rmcp_client.rs:1035-1039`, read at 0.153.4). `initialize` carries no
|
|
254
|
+
* identity at all and is byte-identical between two children of two different threads, so this
|
|
255
|
+
* name is a HOST-KIND gate and never an identity: it says "the peer on this stdio is a codex
|
|
256
|
+
* client", which is precisely as much as startup can honestly say.
|
|
257
|
+
*/
|
|
258
|
+
export const CODEX_MCP_CLIENT_NAME = "codex-mcp-client";
|
|
259
|
+
|
|
260
|
+
/** The `_meta` member carrying the vendor turn-metadata block (`core/src/mcp_tool_call.rs`
|
|
261
|
+
* `build_mcp_tool_call_request_meta`, :1238-1263). A dash-cased key, hence the string form. */
|
|
262
|
+
const CODEX_TURN_METADATA_KEY = "x-codex-turn-metadata";
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* The codex caller's identity metadata is PRESENT but does not hold together: a `threadId` that
|
|
266
|
+
* is not a nonempty string, a missing/foreign turn-metadata block, or three ids that disagree.
|
|
267
|
+
*
|
|
268
|
+
* WHY THIS IS LOUD AND NOT `null`. `null` on this rail means "no identity claim reached me" —
|
|
269
|
+
* the bridge then refuses the send as anonymous, and the prescription is "open the session
|
|
270
|
+
* through the installed codex birth hook". A HALF claim is a different fact with a different
|
|
271
|
+
* cause (a vendor `_meta` shape that moved, or something imitating the codex client), and both
|
|
272
|
+
* ids exist precisely so neither has to be trusted alone: `threadId` and
|
|
273
|
+
* `x-codex-turn-metadata.session_id` are built by two different vendor call sites, and the
|
|
274
|
+
* hook's `session_id` is byte-identical to both (measured 2026-09-08, S1b-D/D3). Selecting a
|
|
275
|
+
* record from one field while its corroborator is absent or disagrees would address a citizen on
|
|
276
|
+
* an unverified id — the SE-1 shape — so the disagreement is reported instead.
|
|
277
|
+
*/
|
|
278
|
+
export class EntwurfCodexIdentityMetadataError extends Error {
|
|
279
|
+
constructor(reason: string) {
|
|
280
|
+
super(
|
|
281
|
+
"entwurf-bridge refused: this codex tool call carries identity metadata that does not hold " +
|
|
282
|
+
`together — ${reason} A codex caller names itself on every call with THREE ids that must be one ` +
|
|
283
|
+
`string: \`_meta.threadId\`, \`_meta.${CODEX_TURN_METADATA_KEY}.session_id\` and \`.thread_id\`. ` +
|
|
284
|
+
"Selecting a garden citizen from a partial or self-contradicting claim would send under an " +
|
|
285
|
+
"identity nothing vouched for. Report it; do not work around it — if the vendor `_meta` shape " +
|
|
286
|
+
"moved, the measurement in scripts/raw-codex-measure/README.md (S1b-D) must be re-taken.",
|
|
287
|
+
);
|
|
288
|
+
this.name = "EntwurfCodexIdentityMetadataError";
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* The codex `threadId` selected exactly one V3 record — and that record is NOT a codex citizen.
|
|
294
|
+
*
|
|
295
|
+
* This is corruption of the native→garden mapping, never a miss: the store says some other
|
|
296
|
+
* backend's session carries this exact native id. Returning `null` would report "no record for
|
|
297
|
+
* this thread" and prescribe re-opening the session, which would mint a SECOND record for the
|
|
298
|
+
* same `nativeSessionId` and turn a readable defect into the duplicate the certification
|
|
299
|
+
* refuses. Sending under the foreign citizen is worse still — it would attribute a codex
|
|
300
|
+
* caller's message to a claude-code/agy/pi session.
|
|
301
|
+
*/
|
|
302
|
+
export class EntwurfCodexIdentityBackendError extends Error {
|
|
303
|
+
readonly gardenId: string;
|
|
304
|
+
constructor(identity: MetaIdentity) {
|
|
305
|
+
super(
|
|
306
|
+
`entwurf-bridge refused: codex thread ${identity.nativeSessionId} selected meta-record ` +
|
|
307
|
+
`${identity.gardenId}, but that citizen's backend is "${identity.backend}", not "codex". One ` +
|
|
308
|
+
"native id is bound to one backend for the life of the record, so this store cannot say who the " +
|
|
309
|
+
"caller is — and neither re-opening the codex session nor sending under the foreign citizen is a " +
|
|
310
|
+
"fix. The record must be corrected (a fresh generation) before this thread can send.",
|
|
311
|
+
);
|
|
312
|
+
this.name = "EntwurfCodexIdentityBackendError";
|
|
313
|
+
this.gardenId = identity.gardenId;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** A JSON OBJECT, as a narrowing guard — `typeof value === "object"` alone admits `null` and
|
|
318
|
+
* arrays, and every codex identity field is read through a member access that both would
|
|
319
|
+
* survive silently (`[]._meta` is `undefined`, not a type error). */
|
|
320
|
+
function isJsonObject(value: unknown): value is Record<string, unknown> {
|
|
321
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** What the resolver is handed. Every field is UNKNOWN on purpose: three of them come off the
|
|
325
|
+
* wire (an MCP peer may send any JSON) and the fourth is a config-written label, so none of
|
|
326
|
+
* them may be typed as trusted at the boundary. */
|
|
327
|
+
export interface ResolveCodexRequestSenderOptions {
|
|
328
|
+
/** The managed-entry provenance, supplied EXPLICITLY by the bridge (never read here). */
|
|
329
|
+
provenance?: unknown;
|
|
330
|
+
/** The peer's `initialize` `clientInfo` — a host-kind gate, never an identity. */
|
|
331
|
+
clientInfo?: unknown;
|
|
332
|
+
/** THIS request's `params._meta`. Per-call, because identity on codex is per-call. */
|
|
333
|
+
requestMeta?: unknown;
|
|
334
|
+
/** Record store override (gates isolate it; production reads the SSOT default). */
|
|
335
|
+
sessionsDir?: string;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* The one codex caller a request names, as the RECORD describes it.
|
|
340
|
+
*
|
|
341
|
+
* `identity` is the whole answer: gardenId, cwd, model, transcriptPath and backend all come from
|
|
342
|
+
* the record, never from the wire. `_meta` carries a `workspaces` map and a `model` field, and
|
|
343
|
+
* both are IGNORED here — a caller that can name its own cwd/model can relabel a citizen by
|
|
344
|
+
* asking, which is a write disguised as a read. `threadId` is retained only because it is the
|
|
345
|
+
* SELECTOR that was proven equal to `identity.nativeSessionId`; it adds no new authority.
|
|
346
|
+
*/
|
|
347
|
+
export interface CodexRequestSender {
|
|
348
|
+
identity: MetaIdentity;
|
|
349
|
+
/** The wire id that selected the record; `=== identity.nativeSessionId` by construction. */
|
|
350
|
+
threadId: string;
|
|
351
|
+
/** The provenance that admitted this rail — always the codex label past the gate. */
|
|
352
|
+
provenance: typeof CODEX_BRIDGE_PROVENANCE_LABEL;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Read the caller's thread id out of ONE request, or refuse.
|
|
357
|
+
*
|
|
358
|
+
* `null` — this is not an admitted codex identity claim at all: no managed provenance, a peer
|
|
359
|
+
* that is not the codex client, no `_meta` object, or an `_meta` with no `threadId` member
|
|
360
|
+
* (`tools/list`, `initialize`, a non-turn call). All four are ordinary and carry no defect.
|
|
361
|
+
*
|
|
362
|
+
* THROWS {@link EntwurfCodexIdentityMetadataError} — a claim exists and is broken. The line
|
|
363
|
+
* between the two is the PRESENCE of `threadId`: absent is a call that never claimed identity,
|
|
364
|
+
* present-but-unusable is a claim that failed, and collapsing them would report "no identity"
|
|
365
|
+
* for a caller that named itself.
|
|
366
|
+
*
|
|
367
|
+
* Pure: no fs, no env, no clock. The store lookup is the next function's job on purpose — this
|
|
368
|
+
* half is the wire contract and can be pinned without a store at all.
|
|
369
|
+
*/
|
|
370
|
+
export function readCodexRequestThreadId(
|
|
371
|
+
opts: Pick<ResolveCodexRequestSenderOptions, "provenance" | "clientInfo" | "requestMeta">,
|
|
372
|
+
): string | null {
|
|
373
|
+
if (opts.provenance !== CODEX_BRIDGE_PROVENANCE_LABEL) return null;
|
|
374
|
+
if (!isJsonObject(opts.clientInfo) || opts.clientInfo.name !== CODEX_MCP_CLIENT_NAME) return null;
|
|
375
|
+
const meta = opts.requestMeta;
|
|
376
|
+
if (!isJsonObject(meta)) return null;
|
|
377
|
+
if (!("threadId" in meta)) return null;
|
|
378
|
+
|
|
379
|
+
const threadId = meta.threadId;
|
|
380
|
+
if (typeof threadId !== "string" || threadId.length === 0) {
|
|
381
|
+
throw new EntwurfCodexIdentityMetadataError(
|
|
382
|
+
`\`_meta.threadId\` is present but is not a nonempty string (got ${describe(threadId)}).`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
const turn = meta[CODEX_TURN_METADATA_KEY];
|
|
386
|
+
if (!isJsonObject(turn)) {
|
|
387
|
+
throw new EntwurfCodexIdentityMetadataError(
|
|
388
|
+
`\`_meta.threadId\` names ${threadId} but \`_meta.${CODEX_TURN_METADATA_KEY}\` is ${describe(turn)}, not an ` +
|
|
389
|
+
"object, so the second, independently-built id that corroborates it is missing.",
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
const sessionId = turn.session_id;
|
|
393
|
+
const turnThreadId = turn.thread_id;
|
|
394
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
395
|
+
throw new EntwurfCodexIdentityMetadataError(
|
|
396
|
+
`\`_meta.${CODEX_TURN_METADATA_KEY}.session_id\` is not a nonempty string (got ${describe(sessionId)}) — ` +
|
|
397
|
+
"that field is the one the birth hook also sees, so without it nothing joins the wire to a record.",
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
if (typeof turnThreadId !== "string" || turnThreadId.length === 0) {
|
|
401
|
+
throw new EntwurfCodexIdentityMetadataError(
|
|
402
|
+
`\`_meta.${CODEX_TURN_METADATA_KEY}.thread_id\` is not a nonempty string (got ${describe(turnThreadId)}).`,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
if (sessionId !== threadId || turnThreadId !== threadId) {
|
|
406
|
+
throw new EntwurfCodexIdentityMetadataError(
|
|
407
|
+
`the three ids disagree (threadId=${threadId}, session_id=${sessionId}, thread_id=${turnThreadId}); ` +
|
|
408
|
+
"on a healthy codex turn they are one byte-identical string.",
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
return threadId;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Resolve the ONE codex citizen a request names, or refuse.
|
|
416
|
+
*
|
|
417
|
+
* `null` — no admitted claim ({@link readCodexRequestThreadId}), or no record holds that
|
|
418
|
+
* `nativeSessionId`. The second case is the ordinary pre-birth window: codex birth is
|
|
419
|
+
* first-turn, not window-open, so a thread whose hook has not minted a record yet simply has no
|
|
420
|
+
* citizen, and the bridge's default anonymous refusal already names the fix.
|
|
421
|
+
*
|
|
422
|
+
* THROWS — every knowable defect, unfolded: a broken claim
|
|
423
|
+
* ({@link EntwurfCodexIdentityMetadataError}), a foreign-backend record
|
|
424
|
+
* ({@link EntwurfCodexIdentityBackendError}), or a store that cannot be certified
|
|
425
|
+
* (`MetaRecordError` from the strict listing — an unreadable/previous-generation record
|
|
426
|
+
* anywhere in the store, a duplicated `nativeSessionId`, or a store this process cannot read).
|
|
427
|
+
*
|
|
428
|
+
* THE LOOKUP IS THE STRICT STORE-WIDE LISTING, NOT A NARROW SCAN. `nativeSessionId` → garden id
|
|
429
|
+
* is the store's own authority (there is no index, by design), and the same reason
|
|
430
|
+
* `upsertMetaSession` certifies the WHOLE store before writing applies to a read that turns an
|
|
431
|
+
* id into an ADDRESS: a narrow "find my thread" pass would answer happily beside a duplicate,
|
|
432
|
+
* drifted or symlinked neighbour that the doctor refuses, so the runtime would hold the weaker
|
|
433
|
+
* contract exactly where a message gets attributed. `mode: "strict"` is what makes an
|
|
434
|
+
* unreadable record a refusal instead of an invisible one — a duplicate of THIS thread's id
|
|
435
|
+
* would otherwise be dropped from `identities` and read as "no record".
|
|
436
|
+
*/
|
|
437
|
+
export function resolveCodexRequestSenderIdentity(
|
|
438
|
+
opts: ResolveCodexRequestSenderOptions = {},
|
|
439
|
+
): CodexRequestSender | null {
|
|
440
|
+
const threadId = readCodexRequestThreadId(opts);
|
|
441
|
+
if (threadId === null) return null;
|
|
442
|
+
|
|
443
|
+
const dir = opts.sessionsDir ?? defaultMetaSessionsDir();
|
|
444
|
+
const { identities } = listAllMetaIdentities(readActiveStoreEntries(dir), makeStoreRecordReader(dir), {
|
|
445
|
+
mode: "strict",
|
|
446
|
+
});
|
|
447
|
+
const identity = identities.find((record) => record.nativeSessionId === threadId);
|
|
448
|
+
if (!identity) return null;
|
|
449
|
+
if (identity.backend !== "codex") throw new EntwurfCodexIdentityBackendError(identity);
|
|
450
|
+
return { identity, threadId, provenance: CODEX_BRIDGE_PROVENANCE_LABEL };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* The three rails an entwurf-bridge child can learn WHO is calling from. Named as a type so a
|
|
455
|
+
* reconciliation can report which rails spoke without any of them being privileged by position.
|
|
456
|
+
*/
|
|
457
|
+
export type SenderIdentityRail = "pi-session" | "meta-sender-marker" | "codex-request";
|
|
458
|
+
|
|
459
|
+
/** One rail's answer: the address it names (a pi session id, or a garden id). */
|
|
460
|
+
export interface SenderIdentityClaim {
|
|
461
|
+
rail: SenderIdentityRail;
|
|
462
|
+
id: string;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** One address, plus every rail that named it. Rails are sorted and de-duplicated, so this
|
|
466
|
+
* answer never depends on the order the caller collected its claims in. */
|
|
467
|
+
export interface ReconciledSenderIdentity {
|
|
468
|
+
id: string;
|
|
469
|
+
rails: SenderIdentityRail[];
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Two rails claim this MCP process for DIFFERENT addresses. Sibling of
|
|
474
|
+
* {@link EntwurfSenderIdentityAmbiguityError} and deliberately not the same error: that one is
|
|
475
|
+
* about two pid MARKERS on one owner pid, while this is about rails that do not even share a
|
|
476
|
+
* join key — a planted `PI_SESSION_ID` in a native host's environment, a pid marker inherited
|
|
477
|
+
* from a host whose child we are not, a codex `_meta` naming a thread while a marker names
|
|
478
|
+
* someone else. Which is right is unknowable here, and both are visible, so neither is used.
|
|
479
|
+
*/
|
|
480
|
+
export class EntwurfSenderIdentityConflictError extends Error {
|
|
481
|
+
readonly claims: SenderIdentityClaim[];
|
|
482
|
+
constructor(claims: readonly SenderIdentityClaim[]) {
|
|
483
|
+
super(
|
|
484
|
+
"entwurf-bridge refused: conflicting sender identity. This MCP process holds authoritative " +
|
|
485
|
+
`identity claims from more than one rail, naming DIFFERENT addresses (${claims
|
|
486
|
+
.map((claim) => `${claim.rail}=${claim.id}`)
|
|
487
|
+
.join(", ")}), so which one is calling cannot be determined. A send under the wrong identity ` +
|
|
488
|
+
"is worse than no send. Report it; do not work around it.",
|
|
489
|
+
);
|
|
490
|
+
this.name = "EntwurfSenderIdentityConflictError";
|
|
491
|
+
this.claims = [...claims];
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Fold every rail's answer into ONE address, or refuse.
|
|
497
|
+
*
|
|
498
|
+
* 0 claims → `null` (anonymous; the bridge's default refusal owns that outcome).
|
|
499
|
+
* 1+ claims that AGREE on the id → that address, carrying every rail that named it. Agreement is
|
|
500
|
+
* the normal shape of an overlap, not a conflict: a codex citizen's request-scoped claim and a
|
|
501
|
+
* pid marker for the same garden id are two views of one identity.
|
|
502
|
+
* claims that DISAGREE → throw {@link EntwurfSenderIdentityConflictError}.
|
|
503
|
+
*
|
|
504
|
+
* Every claim is collected and compared BEFORE one is chosen — the same rule the marker rail
|
|
505
|
+
* holds one layer up. A rail-priority order would answer even when the truth is unknown, which
|
|
506
|
+
* is the failure this exists to prevent, so callers must pass ALL rails they resolved rather
|
|
507
|
+
* than short-circuiting on the first one that answered.
|
|
508
|
+
*/
|
|
509
|
+
export function reconcileSenderIdentityClaims(claims: readonly SenderIdentityClaim[]): ReconciledSenderIdentity | null {
|
|
510
|
+
if (claims.length === 0) return null;
|
|
511
|
+
const ids = new Set(claims.map((claim) => claim.id));
|
|
512
|
+
if (ids.size > 1) throw new EntwurfSenderIdentityConflictError(claims);
|
|
513
|
+
const [id] = ids;
|
|
514
|
+
return {
|
|
515
|
+
id,
|
|
516
|
+
rails: [...new Set(claims.map((claim) => claim.rail))].sort(),
|
|
517
|
+
};
|
|
518
|
+
}
|