@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.
- package/CHANGELOG.md +194 -0
- package/DELIVERY.md +2 -0
- package/VERIFY.md +3 -3
- package/docs/acp-backend-rail.md +69 -5
- package/package.json +3 -3
- package/run.sh +4 -1
- package/scripts/check-acp-prompt-lifecycle.ts +4 -2
- package/scripts/check-acp-session-reuse.ts +4 -2
- package/scripts/check-acp-stop-reason.ts +4 -2
- package/scripts/check-acp-stream-hooks.ts +4 -2
- package/scripts/check-acp-usage-accounting.ts +4 -2
- package/scripts/check-gate-qualification.ts +2 -2
- package/scripts/check-herdr-supply.ts +32 -7
- package/scripts/fixtures/herdr-supply.json +10 -9
- package/scripts/lib/acp-gate-home.ts +75 -0
- package/scripts/lib/codex-socket-listener.ts +429 -0
- package/scripts/mutants/acp-stop-reason.json +11 -0
- package/scripts/mutants/codex-native.json +52 -0
- package/scripts/mutants/herdr-supply.json +6 -6
- package/scripts/smoke-codex-fresh-live.ts +31 -7
- package/scripts/smoke-pi-durable-fresh-live.ts +22 -3
|
@@ -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.
|
|
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\": \"
|
|
23
|
-
"replace": ["\"sha256\": \"
|
|
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-
|
|
123
|
-
"title": "the x86_64 asset is recorded as executed
|
|
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-
|
|
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,
|
|
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
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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
|
|
783
|
-
|
|
784
|
-
|
|
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
|
|
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",
|