@junghanacs/entwurf 0.36.0 → 0.36.1

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.
@@ -1014,6 +1014,58 @@
1014
1014
  "timeoutSeconds": 300,
1015
1015
  "signature": "[QK:CODEX-BIRTH-INVERSE-SOLE-OWNER]",
1016
1016
  "signatureSource": "scripts/smoke-codex-birth.sh"
1017
+ },
1018
+ {
1019
+ "claim": "CODEX-SOCKET-OWNER-HELD-INODE",
1020
+ "title": "the named pid counts as the socket's holder for being named, so a wrapper whose subtree holds no fd on the LISTEN inode reads as owning it again",
1021
+ "subject": "scripts/lib/codex-socket-listener.ts",
1022
+ "find": ["\tconst holders = o.nodes.filter((node) => node.holdingFds.length > 0);"],
1023
+ "replace": [
1024
+ "\tconst holders = o.nodes.filter((node) => node.holdingFds.length > 0 || node.pid === o.namedPid); // mutant: named means holding"
1025
+ ],
1026
+ "gate": ["bash", "run.sh", "check-mux-fresh-call"],
1027
+ "timeoutSeconds": 180,
1028
+ "signature": "[QK:CODEX-SOCKET-OWNER-HELD-INODE]",
1029
+ "signatureSource": "test/codex-socket-listener.test.ts"
1030
+ },
1031
+ {
1032
+ "claim": "CODEX-SOCKET-OWNER-PID-REUSE",
1033
+ "title": "only a vanished pid counts as moved, so a pid reused by another process between the two reads keeps the first process's ownership",
1034
+ "subject": "scripts/lib/codex-socket-listener.ts",
1035
+ "find": [
1036
+ "\tconst moved = o.nodes.filter((node) => node.after?.startTime !== node.startTime || node.after?.ppid !== node.ppid);"
1037
+ ],
1038
+ "replace": [
1039
+ "\tconst moved = o.nodes.filter((node) => node.after === null || node.after.ppid !== node.ppid); // mutant: reuse is not movement"
1040
+ ],
1041
+ "gate": ["bash", "run.sh", "check-mux-fresh-call"],
1042
+ "timeoutSeconds": 180,
1043
+ "signature": "[QK:CODEX-SOCKET-OWNER-PID-REUSE]",
1044
+ "signatureSource": "test/codex-socket-listener.test.ts"
1045
+ },
1046
+ {
1047
+ "claim": "CODEX-SOCKET-OWNER-FD-REREAD",
1048
+ "title": "the first fd read stands in for the second, so a holder that closed or handed off the LISTEN fd while another process kept the inode still reads as owning it",
1049
+ "subject": "scripts/lib/codex-socket-listener.ts",
1050
+ "find": ["\tconst heldAfter = listener.after?.holdingFds ?? [];"],
1051
+ "replace": [
1052
+ "\tconst heldAfter = listener.after?.holdingFds?.length ? listener.after.holdingFds : listener.holdingFds; // mutant: the first read stands in"
1053
+ ],
1054
+ "gate": ["bash", "run.sh", "check-mux-fresh-call"],
1055
+ "timeoutSeconds": 180,
1056
+ "signature": "[QK:CODEX-SOCKET-OWNER-FD-REREAD]",
1057
+ "signatureSource": "test/codex-socket-listener.test.ts"
1058
+ },
1059
+ {
1060
+ "claim": "CODEX-SOCKET-LISTEN-ROW-ONLY",
1061
+ "title": "an accepted connection, which carries the listener's path without ACCEPTCON, is read as a second LISTEN row, so any attached client turns ownership indeterminate",
1062
+ "subject": "scripts/lib/codex-socket-listener.ts",
1063
+ "find": ["\t\tif ((Number.parseInt(flags ?? \"\", 16) & SO_ACCEPTCON) === 0) continue;"],
1064
+ "replace": ["\t\t// mutant: every bound row at the path counts, listening or not"],
1065
+ "gate": ["bash", "run.sh", "check-mux-fresh-call"],
1066
+ "timeoutSeconds": 180,
1067
+ "signature": "[QK:CODEX-SOCKET-LISTEN-ROW-ONLY]",
1068
+ "signatureSource": "test/codex-socket-listener.test.ts"
1017
1069
  }
1018
1070
  ]
1019
1071
  }
@@ -8,7 +8,7 @@
8
8
  "subject": "scripts/check-herdr-sandbox.ts",
9
9
  "find": ["const SUPPLY_MANIFEST = \"scripts/fixtures/herdr-supply.json\";"],
10
10
  "replace": [
11
- "const SUPPLY_MANIFEST = \"scripts/fixtures/herdr-supply.json\";\nconst PINNED_FALLBACK = \"0.9.3\";\nvoid PINNED_FALLBACK;"
11
+ "const SUPPLY_MANIFEST = \"scripts/fixtures/herdr-supply.json\";\nconst PINNED_FALLBACK = \"0.9.4\";\nvoid PINNED_FALLBACK;"
12
12
  ],
13
13
  "gate": ["bash", "run.sh", "check-herdr-supply"],
14
14
  "timeoutSeconds": 120,
@@ -19,8 +19,8 @@
19
19
  "claim": "HSUP-DIGEST-SHAPE",
20
20
  "title": "a pinned digest is truncated, so the check that is supposed to catch a swapped binary can no longer fail",
21
21
  "subject": "scripts/fixtures/herdr-supply.json",
22
- "find": ["\"sha256\": \"18a8dc65f1c2fa485884344356dea1cfd911c6f06cf46fa78e193f4087f4dba7\""],
23
- "replace": ["\"sha256\": \"18a8dc65f1c2fa485884344356de\""],
22
+ "find": ["\"sha256\": \"493304fbefb0ed0ea0455f373b47f48d24e8774bc7974f4c916ad2e937675b23\""],
23
+ "replace": ["\"sha256\": \"493304fbefb0ed0ea0455f373b47\""],
24
24
  "gate": ["bash", "run.sh", "check-herdr-supply"],
25
25
  "timeoutSeconds": 120,
26
26
  "signature": "[QK:HSUP-DIGEST-SHAPE]",
@@ -119,14 +119,14 @@
119
119
  "signatureSource": "scripts/check-herdr-supply.ts"
120
120
  },
121
121
  {
122
- "claim": "HSUP-X86-UNEXECUTED",
123
- "title": "the x86_64 asset is recorded as executed although no cell of this rail has ever run on it",
122
+ "claim": "HSUP-EXECUTION-RECEIPT-BOUND",
123
+ "title": "the x86_64 asset is recorded as executed with no receipt bound to this pin's version and its own full digest",
124
124
  "subject": "scripts/fixtures/herdr-supply.json",
125
125
  "find": ["\"executed\": false"],
126
126
  "replace": ["\"executed\": true"],
127
127
  "gate": ["bash", "run.sh", "check-herdr-supply"],
128
128
  "timeoutSeconds": 120,
129
- "signature": "[QK:HSUP-X86-UNEXECUTED]",
129
+ "signature": "[QK:HSUP-EXECUTION-RECEIPT-BOUND]",
130
130
  "signatureSource": "scripts/check-herdr-supply.ts"
131
131
  }
132
132
  ]
@@ -68,6 +68,7 @@ import {
68
68
  } from "../pi-extensions/lib/meta-session.ts";
69
69
  import { FRESH_CALL_BACKENDS } from "../pi-extensions/lib/mux-fresh-call.ts";
70
70
  import {
71
+ checkCodexSocketFile,
71
72
  readCodexThread,
72
73
  realCodexProtocolOpener,
73
74
  resolveCodexDefaultSocketPath,
@@ -95,6 +96,12 @@ import {
95
96
  sourceCleanupWindows,
96
97
  validateFinalMailboxBody,
97
98
  } from "./lib/codex-fresh-source-receipts.ts";
99
+ import {
100
+ describeCodexSocketOwnership,
101
+ judgeCodexSocketOwnership,
102
+ observeCodexSocketSubtree,
103
+ parseProcStat,
104
+ } from "./lib/codex-socket-listener.ts";
98
105
  import { skipLive } from "./lib/live-skip.ts";
99
106
  import { parseTmuxRow, TMUX_COORDINATE_FIELDS, TMUX_COORDINATE_FORMAT } from "./lib/tmux-coordinate-row.ts";
100
107
 
@@ -697,7 +704,8 @@ async function run(): Promise<void> {
697
704
  // `--cut` still refuses it as BLOCKED (MUST SKIP). Nothing is
698
705
  // excused — a cut without this receipt remains impossible.
699
706
  // PRESENT BUT WRONG -> FAIL, unchanged: a malformed or dead PID, a process whose
700
- // argv is not a Codex app-server, a foreign uid, A == S. The
707
+ // argv is not a Codex app-server, a foreign uid, a subtree that
708
+ // does not hold the endpoint's LISTEN socket, A == S. The
701
709
  // operator answered, and the answer is a defect.
702
710
  // Nothing is inferred either way, and no default is invented for a value the operator
703
711
  // did not give. The reproducible green is the invocation VERIFY and the release skill's
@@ -747,6 +755,10 @@ async function run(): Promise<void> {
747
755
  fs.existsSync(procDir),
748
756
  `missing ${procDir}`,
749
757
  );
758
+ // Read BEFORE the uid/argv/environment checks below and handed to the socket-ownership
759
+ // observer, so the process those checks read and the process that owns the socket are the
760
+ // same one: a pid reused in between changes this start time and fails closed (#128).
761
+ const namedStartTime = parseProcStat(fs.readFileSync(path.join(procDir, "stat"), "utf8")).startTime;
750
762
  const procStat = fs.statSync(procDir);
751
763
  if (typeof process.getuid === "function") {
752
764
  ok("the named app-server process belongs to this operator", procStat.uid === process.getuid());
@@ -774,15 +786,27 @@ async function run(): Promise<void> {
774
786
  // cannot be borrowed into a claim about what a Codex MCP child inherits.
775
787
  appServerEnv = { ...procEnv };
776
788
  codexSocketPath = resolveCodexDefaultSocketPath(appServerEnv);
777
- const expectedListen = `unix://${codexSocketPath}`;
778
- const listensOnDefaultSocket = argv.some(
779
- (arg, index) => (arg === "--listen" && argv[index + 1] === expectedListen) || arg === `--listen=${expectedListen}`,
789
+ // Ownership is read from the kernel, not from argv (#128). The endpoint is the one delivery
790
+ // itself connects to — the production classifier's verified physical socket — and the claim
791
+ // is that the named pid's OWN subtree holds its LISTEN inode. The argv `--listen` spelling
792
+ // is recorded in the receipt below and decides nothing: a wrapper whose child listens and a
793
+ // listener started with the vendor-default `--listen unix://` are both read as what they are.
794
+ const socketFile = checkCodexSocketFile(codexSocketPath);
795
+ ok(
796
+ "the default control path is a live socket or Codex's exact rendezvous alias — the endpoint delivery connects to",
797
+ socketFile.ok,
798
+ socketFile.ok ? "" : `${codexSocketPath}: ${socketFile.reason}`,
799
+ );
800
+ const socketOwnership = judgeCodexSocketOwnership(
801
+ observeCodexSocketSubtree(appServerPid, socketFile.connectPath, namedStartTime),
780
802
  );
781
803
  ok(
782
- "the explicit app-server PID owns the same default UDS the fresh Codex runtime will use",
783
- listensOnDefaultSocket,
784
- `expected --listen ${expectedListen}; argv=${JSON.stringify(argv)}`,
804
+ "the explicit app-server PID's own process subtree holds the LISTEN socket at that verified endpoint",
805
+ socketOwnership.verdict === "owned",
806
+ `${describeCodexSocketOwnership(socketOwnership)}; argv=${JSON.stringify(argv)}`,
785
807
  );
808
+ receipts["1a-codex-socket-ownership"] =
809
+ `advertised=${codexSocketPath}\nendpoint=${socketFile.connectPath}\n${describeCodexSocketOwnership(socketOwnership)}`;
786
810
  const appServerCoordinate = inspectTmuxCoordinate("Codex app-server/MCP inherited seat", appServerEnv);
787
811
  const fixtureCoordinate = inspectTmuxCoordinate("fixture/initial-Pi caller seat", process.env);
788
812
  ok(
@@ -538,9 +538,29 @@ ok(
538
538
  fs.existsSync(liveDb),
539
539
  liveDb,
540
540
  );
541
+ const sqlite = await import("node:sqlite");
542
+ // A doorbell input stays `placed` while the run that consumed it is active and turns `done` only when that run
543
+ // ends (pi-durable's SubmissionRecord). Pi's relay can land before pd's final generation commits, so the snapshot
544
+ // waits for THIS message's submission to settle (`done` or `unanswered`); the assertions below judge what settled.
545
+ const msgName = /meta-mailbox → enqueued \(([^)]+)\)/.exec(piToPd.text)?.[1] ?? "";
546
+ const doorbellRequestId = `entwurf-doorbell:${pdGid}:${msgName}`;
547
+ if (msgName !== "") {
548
+ receipts["6-native-doorbell-settled"] = await until(`pd's doorbell submission to settle`, TURN_WAIT_MS, () => {
549
+ const live = new sqlite.DatabaseSync(liveDb, { readOnly: true });
550
+ try {
551
+ const rows = live.prepare("SELECT status, record FROM submissions").all() as Array<{
552
+ status: string;
553
+ record: string;
554
+ }>;
555
+ const hit = rows.find((r) => (JSON.parse(r.record) as { requestId?: unknown }).requestId === doorbellRequestId);
556
+ return hit?.status === "done" || hit?.status === "unanswered" ? hit.status : null;
557
+ } finally {
558
+ live.close();
559
+ }
560
+ });
561
+ }
541
562
  const snapshot = path.join(receiptsDir, "pd-session.snapshot.sqlite");
542
563
  fs.mkdirSync(receiptsDir, { recursive: true });
543
- const sqlite = await import("node:sqlite");
544
564
  {
545
565
  const source = new sqlite.DatabaseSync(liveDb, { readOnly: true });
546
566
  try {
@@ -599,8 +619,7 @@ ok(
599
619
  JSON.stringify(pdToPi),
600
620
  );
601
621
  receipts["6-pd-to-pi-settled"] = JSON.stringify(pdToPi);
602
- const msgName = /meta-mailbox → enqueued \(([^)]+)\)/.exec(piToPd.text)?.[1] ?? "";
603
- const doorbell = pdDoorbellSubmission(nativeSubmissions, rootId, `entwurf-doorbell:${pdGid}:${msgName}`);
622
+ const doorbell = pdDoorbellSubmission(nativeSubmissions, rootId, doorbellRequestId);
604
623
  receipts["6-native-doorbell-submission"] = JSON.stringify(doorbell);
605
624
  ok(
606
625
  "the doorbell admitted THIS message's notice to the root conversation and the submission is done",