@junghanacs/entwurf 0.35.1 → 0.36.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 (42) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +209 -0
  3. package/CONTRIBUTING.md +1 -1
  4. package/DELIVERY.md +15 -0
  5. package/FAQ.md +3 -3
  6. package/README.md +30 -16
  7. package/VERIFY.md +9 -7
  8. package/demo/README.md +1 -1
  9. package/docs/acp-backend-rail.md +1 -1
  10. package/docs/setup-clean-host.md +30 -22
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +4 -3
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +99 -18
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +3 -3
  14. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +59 -14
  15. package/mcp/entwurf-bridge/dist/scripts/codex-socket-classify.js +44 -0
  16. package/mcp/entwurf-bridge/tsconfig.build.json +5 -0
  17. package/package.json +19 -14
  18. package/pi/pi-durable/bootstrap.mjs +12 -8
  19. package/pi/pi-durable/carrier/runtime.js +52 -2
  20. package/pi/pi-durable/carrier/subagent.js +6 -1
  21. package/pi/pi-durable/carrier/tui.js +31 -4
  22. package/pi/pi-durable/carrier-relocation.mjs +2 -2
  23. package/pi/pi-durable/overlay/upstream-pin.json +79 -36
  24. package/pi-extensions/lib/codex-fresh-preflight.ts +4 -3
  25. package/pi-extensions/lib/native-push/codex-ws-client.ts +129 -18
  26. package/pi-extensions/lib/pi-durable-runtime.ts +4 -4
  27. package/pi-extensions/meta-bridge-pi-durable.ts +77 -21
  28. package/run.sh +112 -38
  29. package/scripts/check-codex-app-server-launch.ts +55 -12
  30. package/scripts/check-codex-native-push.ts +231 -12
  31. package/scripts/check-gate-qualification.ts +3 -3
  32. package/scripts/codex-app-server-launch.sh +54 -29
  33. package/scripts/codex-socket-classify.ts +46 -0
  34. package/scripts/emit-pi-durable-carrier.ts +159 -168
  35. package/scripts/mutants/codex-app-server-launch.json +39 -12
  36. package/scripts/mutants/codex-native.json +15 -0
  37. package/scripts/mutants/control-send-receipt.json +1 -1
  38. package/scripts/mutants/pack-install.json +3 -3
  39. package/scripts/mutants/pi-durable.json +165 -16
  40. package/scripts/mutants/pi-floor.json +5 -5
  41. package/scripts/pi-durable-contact-host.mjs +1 -1
  42. package/pi/pi-durable/overlay/runtime-contacts.patch +0 -283
@@ -1,35 +1,36 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * emit-pi-durable-carrier — the MAINTAINER's one path from upstream source to the pi-durable
4
- * carrier (#130). An operator never runs it: an installed package carries the emitted JS and reads
5
- * no upstream source.
3
+ * emit-pi-durable-carrier — the MAINTAINER's one path from the owned pi-durable source to the carrier
4
+ * (#130, #141). An operator never runs it: an installed package carries the emitted JS and reads no
5
+ * TypeScript.
6
6
  *
7
- * node scripts/emit-pi-durable-carrier.ts --source <git repository holding the pinned commit> --write
8
- * node scripts/emit-pi-durable-carrier.ts --source <git repository holding the pinned commit> --check
7
+ * node scripts/emit-pi-durable-carrier.ts --check
8
+ * node scripts/emit-pi-durable-carrier.ts --write
9
+ * node scripts/emit-pi-durable-carrier.ts --origin <git repository holding origin.commit>
9
10
  *
10
- * Inputs, and nothing else: the pinned commit's OBJECTS (each declared file and the license, read as
11
- * `<commit>:<path>` — never a worktree, so the repository's checkout state cannot enter), the packaged
12
- * overlay patch, and this Node's `stripTypeScriptTypes` in strip mode (upstream runs these exact
13
- * sources under Node's strip-only TypeScript). The one patch carries exactly one section per file the
14
- * pin declares `patched`, and no other section (#134). Each section is applied by git's own engine to a
15
- * private scratch copy of its file; the base blob and the patched result must carry the blob ids that
16
- * section's `index` line names. Each stripped file is then relocated by pi/pi-durable/carrier-relocation.mjs, and the targets
17
- * the six files import must equal the pin's declared `distTargets` exactly.
11
+ * Inputs, and nothing else: the in-repo source the pin's `carrier.files[].source` and
12
+ * `carrier.license.source` name (pi/pi-durable/src, Entwurf's canonical thin composition), and this
13
+ * Node's `stripTypeScriptTypes` in strip mode. Each stripped file is relocated by
14
+ * pi/pi-durable/carrier-relocation.mjs, and the targets the files import must equal the pin's declared
15
+ * `distTargets` exactly. A missing source file is a named refusal, never a skipped rebuild.
18
16
  *
19
- * `--write` emits pi/pi-durable/carrier/{<files>,LICENSE} and fills the pin's generated fields
20
- * (source blob ids, emitted sha256s, the Node version). `--check` re-emits in memory and compares:
21
- * equal is the rebuild acceptance; any difference, another Node version, or an undeclared file in the
22
- * carrier directory is a named FAIL. `--check` without a source holding the commit is a named SKIP —
23
- * an unavailable observation, never rebuild acceptance. Read-only against the source repository.
17
+ * `--check` re-emits in memory and compares. The byte proof (carrier == relocate(strip(source)), with the
18
+ * pin's blob ids and digests) is unconditional. The emitter's Node identity is a separate fact: under
19
+ * another Node the bytes are still compared, but equal bytes are NOT a rebuild PASS — the identity is a
20
+ * named SKIP, red under ENTWURF_REQUIRE_EMITTER_NODE=1 (the CI check job, which runs the pinned Node).
21
+ * `--write` emits pi/pi-durable/carrier/{<files>,LICENSE} and fills the pin's generated fields (source blob
22
+ * ids, emitted sha256s, the Node version), printing every generated field it changes. `--origin` is
23
+ * provenance only: it checks the frozen `origin` record against the upstream objects it names, read as
24
+ * `<commit>:<path>` with `--no-replace-objects`; it never feeds a rebuild.
24
25
  *
25
- * Exit: 0 written / equal, 1 named refusal or drift, 2 usage, 97 skip (no source).
26
+ * Exit: 0 written / equal / origin matches, 1 named refusal or drift, 2 usage, 97 skip (Node identity
27
+ * unverified, or no repository holding the origin commit).
26
28
  */
27
29
 
28
30
  import { spawnSync } from "node:child_process";
29
31
  import { createHash } from "node:crypto";
30
32
  import * as fs from "node:fs";
31
33
  import { stripTypeScriptTypes } from "node:module";
32
- import * as os from "node:os";
33
34
  import * as path from "node:path";
34
35
  import { relocateCarrierSource } from "../pi/pi-durable/carrier-relocation.mjs";
35
36
  import { skipLive } from "./lib/live-skip.ts";
@@ -38,6 +39,7 @@ const ROOT = path.resolve(import.meta.dirname, "..");
38
39
  const DURABLE = path.join(ROOT, "pi", "pi-durable");
39
40
  const PIN_PATH = path.join(DURABLE, "overlay", "upstream-pin.json");
40
41
  const CARRIER_DIR = path.join(DURABLE, "carrier");
42
+ const SOURCE_ROOT = "pi/pi-durable/src/";
41
43
  const LICENSE_NAME = "LICENSE";
42
44
  const EMITTER_TOOL = "node:module.stripTypeScriptTypes";
43
45
  const EMITTER_MODE = "strip";
@@ -46,15 +48,19 @@ const GIT_TIMEOUT_MS = 30_000;
46
48
  interface CarrierFile {
47
49
  name: string;
48
50
  source: string;
49
- patched: boolean;
50
51
  blob: string;
51
52
  sha256: string;
52
53
  }
53
54
 
54
- interface CarrierPin {
55
+ export interface CarrierPin {
55
56
  schemaVersion: number;
56
- commit: string;
57
- patches: string[];
57
+ origin: {
58
+ repository: string;
59
+ tag: string;
60
+ commit: string;
61
+ license: { path: string; blob: string };
62
+ files: { path: string; blob: string }[];
63
+ };
58
64
  carrier: {
59
65
  emitter: { tool: string; mode: string; node: string };
60
66
  license: { source: string; blob: string; sha256: string };
@@ -67,28 +73,28 @@ class Refusal extends Error {}
67
73
  const refuse = (reason: string, detail: string): Refusal => new Refusal(`${reason}: ${detail}`);
68
74
 
69
75
  const sha256 = (data: Buffer): string => createHash("sha256").update(data).digest("hex");
70
- /** git's blob id, computed here so no object is ever written into the source repository. */
76
+ /** git's blob id, computed here so no object is ever written into any repository. */
71
77
  const blobId = (data: Buffer): string =>
72
78
  createHash("sha1")
73
79
  .update(Buffer.concat([Buffer.from(`blob ${data.length}\0`), data]))
74
80
  .digest("hex");
75
81
 
76
82
  /** The environment every git child gets: no inherited repository, index or work-tree redirection. */
77
- function gitEnv(extra: Record<string, string> = {}): NodeJS.ProcessEnv {
78
- const env: NodeJS.ProcessEnv = { GIT_OPTIONAL_LOCKS: "0", ...extra };
83
+ function gitEnv(): NodeJS.ProcessEnv {
84
+ const env: NodeJS.ProcessEnv = { GIT_OPTIONAL_LOCKS: "0" };
79
85
  for (const [key, value] of Object.entries(process.env)) {
80
86
  if (!key.startsWith("GIT_") && env[key] === undefined) env[key] = value;
81
87
  }
82
88
  return env;
83
89
  }
84
90
 
85
- /** Every read of the source repository: `--no-replace-objects`, so a `refs/replace/*` entry (or a
86
- * replacement reached through alternates) can never stand in for a pinned object. */
91
+ /** Every read of the origin repository: `--no-replace-objects`, so a `refs/replace/*` entry (or a
92
+ * replacement reached through alternates) can never stand in for a recorded object. */
87
93
  export function sourceGitArgs(source: string, args: readonly string[]): string[] {
88
94
  return ["--no-replace-objects", "-C", source, ...args];
89
95
  }
90
96
 
91
- /** One pinned object's bytes, `<commit>:<path>`, as the source repository stores it. */
97
+ /** One recorded object's bytes, `<commit>:<path>`, as the origin repository stores it. */
92
98
  export function readSourceObject(source: string, rev: string): Buffer {
93
99
  return git(source, ["cat-file", "blob", rev]);
94
100
  }
@@ -101,17 +107,21 @@ function git(source: string, args: string[]): Buffer {
101
107
  });
102
108
  if (run.status !== 0) {
103
109
  throw refuse(
104
- "carrier-source-unreadable",
110
+ "carrier-origin-unreadable",
105
111
  `git ${args.join(" ")} in ${source}: ${run.stderr?.toString().trim() || run.error?.message || `status ${run.status}`}`,
106
112
  );
107
113
  }
108
114
  return run.stdout;
109
115
  }
110
116
 
111
- function readPin(): CarrierPin {
112
- const pin = JSON.parse(fs.readFileSync(PIN_PATH, "utf8")) as CarrierPin;
113
- if (pin.schemaVersion !== 2 || !/^[0-9a-f]{40}$/.test(pin.commit) || pin.patches?.length !== 1) {
114
- throw refuse("carrier-pin-malformed", `${PIN_PATH}: schemaVersion 2, a full commit and one patch are required`);
117
+ /** A pin-declared source path: a plain relative file under pi/pi-durable/src. */
118
+ const ownedSource = (rel: unknown): rel is string =>
119
+ typeof rel === "string" && rel.startsWith(SOURCE_ROOT) && !rel.split("/").includes("..") && !rel.includes("\\");
120
+
121
+ export function readPin(file = PIN_PATH): CarrierPin {
122
+ const pin = JSON.parse(fs.readFileSync(file, "utf8")) as CarrierPin;
123
+ if (pin.schemaVersion !== 3 || !/^[0-9a-f]{40}$/.test(pin.origin?.commit ?? "")) {
124
+ throw refuse("carrier-pin-malformed", `${file}: schemaVersion 3 with a full origin commit is required`);
115
125
  }
116
126
  if (pin.carrier.emitter.tool !== EMITTER_TOOL || pin.carrier.emitter.mode !== EMITTER_MODE) {
117
127
  throw refuse(
@@ -119,86 +129,29 @@ function readPin(): CarrierPin {
119
129
  `the emitter is ${EMITTER_TOOL} in ${EMITTER_MODE} mode, not ${JSON.stringify(pin.carrier.emitter)}`,
120
130
  );
121
131
  }
122
- if (!pin.carrier.files.some((f) => f.patched)) {
123
- throw refuse("carrier-pin-malformed", "at least one carrier file carries the overlay patch");
132
+ const sources = [...pin.carrier.files.map((f) => f.source), pin.carrier.license.source];
133
+ const outside = sources.filter((s) => !ownedSource(s));
134
+ if (outside.length > 0) {
135
+ throw refuse("carrier-pin-malformed", `carrier sources must be files under ${SOURCE_ROOT}: ${outside.join(", ")}`);
124
136
  }
125
137
  return pin;
126
138
  }
127
139
 
128
- /** One file's section of the overlay patch: its own bytes and the two blob ids its `index` line names. */
129
- export interface PatchSection {
130
- bytes: Buffer;
131
- base: string;
132
- result: string;
133
- }
134
-
135
- /**
136
- * The overlay patch split into its per-file sections, keyed by the existing file each one changes, and
137
- * matched against the source paths the pin declares `patched`: exactly one section per declared file and
138
- * no section for any other file. Anything else is the patch's shape, refused by name.
139
- */
140
- export function patchSections(
141
- text: string,
142
- patched: readonly string[],
143
- name = "the overlay patch",
144
- ): Map<string, PatchSection> {
145
- const starts = [...text.matchAll(/^diff --git /gm)].map((m) => m.index);
146
- if (starts.length === 0 || starts[0] !== 0) {
147
- throw refuse("carrier-patch-shape", `${name} must start with its first file section`);
148
- }
149
- const sections = new Map<string, PatchSection>();
150
- for (const [at, start] of starts.entries()) {
151
- const chunk = text.slice(start, starts[at + 1] ?? text.length);
152
- const files = [...chunk.matchAll(/^diff --git a\/(\S+) b\/(\S+)$/gm)];
153
- const index = [...chunk.matchAll(/^index ([0-9a-f]+)\.\.([0-9a-f]+) 100644$/gm)];
154
- if (files.length !== 1 || files[0][1] !== files[0][2] || index.length !== 1) {
155
- throw refuse("carrier-patch-shape", `every section of ${name} changes exactly one existing file`);
156
- }
157
- const file = files[0][1];
158
- if (sections.has(file)) throw refuse("carrier-patch-shape", `${name} changes ${file} in two sections`);
159
- sections.set(file, { bytes: Buffer.from(chunk, "utf8"), base: index[0][1], result: index[0][2] });
160
- }
161
- const undeclared = [...sections.keys()].filter((file) => !patched.includes(file));
162
- const missing = patched.filter((file) => !sections.has(file));
163
- if (undeclared.length > 0 || missing.length > 0) {
164
- throw refuse(
165
- "carrier-patch-shape",
166
- `${name} must change exactly the files the pin declares patched (undeclared: ${undeclared.join(", ") || "none"}; missing: ${missing.join(", ") || "none"})`,
167
- );
168
- }
169
- return sections;
170
- }
171
-
172
- function readPatch(pin: CarrierPin): Map<string, PatchSection> {
173
- const text = fs.readFileSync(path.join(DURABLE, "overlay", pin.patches[0]), "utf8");
174
- const patched = pin.carrier.files.filter((f) => f.patched).map((f) => f.source);
175
- return patchSections(text, patched, pin.patches[0]);
176
- }
177
-
178
- /** `base` with the patch applied by git, in a private scratch tree outside any repository. */
179
- function applyPatch(file: string, base: Buffer, patch: Buffer): Buffer {
180
- const scratch = fs.mkdtempSync(path.join(os.tmpdir(), "pi-durable-carrier-"));
140
+ /** An owned source file's bytes. Absent or not a regular file is a named refusal: the source is in this
141
+ * repository, so there is no "unavailable source" to skip over. */
142
+ function readOwnedSource(rel: string): Buffer {
143
+ const at = path.join(ROOT, rel);
144
+ let regular = false;
181
145
  try {
182
- const target = path.join(scratch, file);
183
- fs.mkdirSync(path.dirname(target), { recursive: true });
184
- fs.writeFileSync(target, base);
185
- const patchFile = path.join(scratch, "overlay.patch");
186
- fs.writeFileSync(patchFile, patch);
187
- const run = spawnSync("git", ["apply", "--whitespace=nowarn", "overlay.patch"], {
188
- cwd: scratch,
189
- env: gitEnv({ GIT_CEILING_DIRECTORIES: path.dirname(scratch) }),
190
- timeout: GIT_TIMEOUT_MS,
191
- });
192
- if (run.status !== 0) {
193
- throw refuse("carrier-patch-rejected", run.stderr?.toString().trim() || `git apply status ${run.status}`);
194
- }
195
- return fs.readFileSync(target);
196
- } finally {
197
- fs.rmSync(scratch, { recursive: true, force: true });
146
+ regular = fs.lstatSync(at).isFile();
147
+ } catch {
148
+ // Bounded environment probe: an absent path is the refusal below.
198
149
  }
150
+ if (!regular) throw refuse("carrier-source-missing", `${rel} is not a regular file in this checkout`);
151
+ return fs.readFileSync(at);
199
152
  }
200
153
 
201
- interface Emission {
154
+ export interface Emission {
202
155
  files: Map<string, Buffer>;
203
156
  generated: {
204
157
  node: string;
@@ -207,11 +160,7 @@ interface Emission {
207
160
  };
208
161
  }
209
162
 
210
- function emit(source: string, pin: CarrierPin): Emission {
211
- if (git(source, ["cat-file", "-t", pin.commit]).toString().trim() !== "commit") {
212
- throw refuse("carrier-source-unreadable", `${pin.commit} is not a commit in ${source}`);
213
- }
214
- const sections = readPatch(pin);
163
+ export function emitCarrier(pin: CarrierPin, read: (rel: string) => Buffer = readOwnedSource): Emission {
215
164
  const names = pin.carrier.files.map((f) => f.name);
216
165
  const targets = pin.carrier.distTargets;
217
166
  if (new Set(targets).size !== targets.length) throw refuse("carrier-pin-malformed", "distTargets repeats a path");
@@ -219,35 +168,17 @@ function emit(source: string, pin: CarrierPin): Emission {
219
168
  const generatedFiles = new Map<string, { blob: string; sha256: string }>();
220
169
  const used = new Set<string>();
221
170
  for (const declared of pin.carrier.files) {
222
- const original = readSourceObject(source, `${pin.commit}:${declared.source}`);
223
- const blob = blobId(original);
224
- let text = original;
225
- const section = declared.patched ? sections.get(declared.source) : undefined;
226
- if (section !== undefined) {
227
- if (!blob.startsWith(section.base)) {
228
- throw refuse(
229
- "carrier-patch-base",
230
- `the patch changes ${declared.source} from ${section.base}; the pinned object is ${blob}`,
231
- );
232
- }
233
- text = applyPatch(declared.source, original, section.bytes);
234
- if (!blobId(text).startsWith(section.result)) {
235
- throw refuse(
236
- "carrier-patch-result",
237
- `patched ${declared.source} is ${blobId(text)}, the patch names ${section.result}`,
238
- );
239
- }
240
- }
241
- const stripped = stripTypeScriptTypes(text.toString("utf8"), { mode: EMITTER_MODE });
171
+ const source = read(declared.source);
172
+ const stripped = stripTypeScriptTypes(source.toString("utf8"), { mode: EMITTER_MODE });
242
173
  const relocated = relocateCarrierSource(declared.name, stripped, { siblings: names, targets });
243
174
  for (const target of relocated.used) used.add(target);
244
175
  const bytes = Buffer.from(relocated.code, "utf8");
245
176
  files.set(declared.name, bytes);
246
- generatedFiles.set(declared.name, { blob, sha256: sha256(bytes) });
177
+ generatedFiles.set(declared.name, { blob: blobId(source), sha256: sha256(bytes) });
247
178
  }
248
179
  const unused = targets.filter((t) => !used.has(t));
249
180
  if (unused.length > 0) throw refuse("carrier-targets-differ", `declared but not imported: ${unused.join(", ")}`);
250
- const license = readSourceObject(source, `${pin.commit}:${pin.carrier.license.source}`);
181
+ const license = read(pin.carrier.license.source);
251
182
  files.set(LICENSE_NAME, license);
252
183
  return {
253
184
  files,
@@ -260,15 +191,30 @@ function emit(source: string, pin: CarrierPin): Emission {
260
191
  }
261
192
 
262
193
  /** Files in the carrier directory that the pin does not declare. */
263
- function undeclared(pin: CarrierPin): string[] {
194
+ function undeclared(pin: CarrierPin, carrierDir: string): string[] {
264
195
  const declared = new Set([...pin.carrier.files.map((f) => f.name), LICENSE_NAME]);
265
- const present = fs.existsSync(CARRIER_DIR) ? fs.readdirSync(CARRIER_DIR) : [];
196
+ const present = fs.existsSync(carrierDir) ? fs.readdirSync(carrierDir) : [];
266
197
  return present.filter((name) => !declared.has(name)).sort();
267
198
  }
268
199
 
269
- function write(pin: CarrierPin, emission: Emission): void {
270
- const extra = undeclared(pin);
200
+ /** The pin's generated fields as `field value` pairs, for a write that names what it changed. */
201
+ function generatedFields(pin: CarrierPin): Map<string, string> {
202
+ const fields = new Map<string, string>([
203
+ ["carrier.emitter.node", pin.carrier.emitter.node],
204
+ ["carrier.license.blob", pin.carrier.license.blob],
205
+ ["carrier.license.sha256", pin.carrier.license.sha256],
206
+ ]);
207
+ for (const f of pin.carrier.files) {
208
+ fields.set(`${f.name} blob`, f.blob);
209
+ fields.set(`${f.name} sha256`, f.sha256);
210
+ }
211
+ return fields;
212
+ }
213
+
214
+ function write(pin: CarrierPin, emission: Emission): string[] {
215
+ const extra = undeclared(pin, CARRIER_DIR);
271
216
  if (extra.length > 0) throw refuse("carrier-undeclared-file", `remove ${extra.join(", ")} from ${CARRIER_DIR} first`);
217
+ const before = generatedFields(pin);
272
218
  fs.mkdirSync(CARRIER_DIR, { recursive: true });
273
219
  for (const [name, bytes] of emission.files) fs.writeFileSync(path.join(CARRIER_DIR, name), bytes);
274
220
  pin.carrier.emitter.node = emission.generated.node;
@@ -276,6 +222,11 @@ function write(pin: CarrierPin, emission: Emission): void {
276
222
  pin.carrier.license.sha256 = emission.generated.license.sha256;
277
223
  for (const file of pin.carrier.files) Object.assign(file, emission.generated.files.get(file.name));
278
224
  fs.writeFileSync(PIN_PATH, formatPin(pin));
225
+ const changed: string[] = [];
226
+ for (const [field, value] of generatedFields(pin)) {
227
+ if (before.get(field) !== value) changed.push(`${field}: ${before.get(field) || "(none)"} -> ${value}`);
228
+ }
229
+ return changed;
279
230
  }
280
231
 
281
232
  /** The pin as the repository formatter leaves it (biome, tabs, width 120): JSON.stringify, with an array
@@ -294,14 +245,10 @@ function formatPin(pin: CarrierPin): string {
294
245
  return `${text}\n`;
295
246
  }
296
247
 
297
- /** Every difference between the committed carrier + pin and a fresh emission. */
298
- function compare(pin: CarrierPin, emission: Emission): string[] {
248
+ /** Every byte difference between the committed carrier + pin and a fresh emission. The Node that emitted
249
+ * is not one of them: that is emitterNodeDrift's fact, kept apart so it can never be read as bytes. */
250
+ export function compareCarrier(pin: CarrierPin, emission: Emission, carrierDir = CARRIER_DIR): string[] {
299
251
  const drift: string[] = [];
300
- if (pin.carrier.emitter.node !== emission.generated.node) {
301
- drift.push(
302
- `carrier-emitter-node-mismatch: the carrier was emitted by Node ${pin.carrier.emitter.node || "(none)"}, this is ${emission.generated.node}`,
303
- );
304
- }
305
252
  const license = emission.generated.license;
306
253
  if (pin.carrier.license.blob !== license.blob || pin.carrier.license.sha256 !== license.sha256) {
307
254
  drift.push(`carrier-pin-drift: license is ${license.blob}/${license.sha256}`);
@@ -315,65 +262,109 @@ function compare(pin: CarrierPin, emission: Emission): string[] {
315
262
  }
316
263
  }
317
264
  for (const [name, bytes] of emission.files) {
318
- const onDisk = path.join(CARRIER_DIR, name);
265
+ const onDisk = path.join(carrierDir, name);
319
266
  if (!fs.existsSync(onDisk)) drift.push(`carrier-file-missing: ${name}`);
320
267
  else if (!fs.readFileSync(onDisk).equals(bytes))
321
268
  drift.push(`carrier-file-drift: ${name} differs from its emission`);
322
269
  }
323
- for (const name of undeclared(pin)) drift.push(`carrier-undeclared-file: ${name}`);
270
+ for (const name of undeclared(pin, carrierDir)) drift.push(`carrier-undeclared-file: ${name}`);
271
+ return drift;
272
+ }
273
+
274
+ /** The emitter identity: the carrier's recorded Node against the one running now, exactly. */
275
+ export function emitterNodeDrift(pinned: string, running: string): string | undefined {
276
+ if (pinned !== running) {
277
+ return `carrier-emitter-node-mismatch: the carrier was emitted by Node ${pinned || "(none)"}, this is ${running}`;
278
+ }
279
+ return undefined;
280
+ }
281
+
282
+ /** The frozen origin record against the upstream objects it names. Provenance, never rebuild input. */
283
+ function checkOrigin(repo: string, pin: CarrierPin): string[] {
284
+ const { origin } = pin;
285
+ if (git(repo, ["cat-file", "-t", origin.commit]).toString().trim() !== "commit") {
286
+ throw refuse("carrier-origin-unreadable", `${origin.commit} is not a commit in ${repo}`);
287
+ }
288
+ const drift: string[] = [];
289
+ for (const recorded of [...origin.files, origin.license]) {
290
+ const blob = blobId(readSourceObject(repo, `${origin.commit}:${recorded.path}`));
291
+ if (blob !== recorded.blob) {
292
+ drift.push(
293
+ `carrier-origin-drift: ${recorded.path} is ${blob} at ${origin.commit}, the pin records ${recorded.blob}`,
294
+ );
295
+ }
296
+ }
324
297
  return drift;
325
298
  }
326
299
 
327
300
  function main(argv: readonly string[]): number {
328
- const sourceAt = argv.indexOf("--source");
329
- const source = sourceAt === -1 ? undefined : argv[sourceAt + 1];
330
- const mode = argv.includes("--write") ? "write" : argv.includes("--check") ? "check" : undefined;
331
- if (mode === undefined || argv.length !== (source === undefined ? 1 : 3) || (sourceAt !== -1 && !source)) {
301
+ const mode = argv[0];
302
+ const repo = argv[1];
303
+ if (
304
+ !((mode === "--check" || mode === "--write") && argv.length === 1) &&
305
+ !(mode === "--origin" && argv.length === 2 && repo)
306
+ ) {
332
307
  console.error(
333
- "usage: emit-pi-durable-carrier.ts --source <git repository holding the pinned commit> (--write | --check)",
308
+ "usage: emit-pi-durable-carrier.ts (--check | --write | --origin <git repository holding origin.commit>)",
334
309
  );
335
310
  return 2;
336
311
  }
337
312
  const pin = readPin();
338
- if (mode === "check") {
313
+ if (mode === "--origin") {
339
314
  const held =
340
- source !== undefined &&
341
- spawnSync("git", sourceGitArgs(source, ["cat-file", "-e", `${pin.commit}^{commit}`]), {
315
+ spawnSync("git", sourceGitArgs(path.resolve(repo), ["cat-file", "-e", `${pin.origin.commit}^{commit}`]), {
342
316
  env: gitEnv(),
343
317
  timeout: GIT_TIMEOUT_MS,
344
318
  }).status === 0;
345
319
  if (!held) {
346
320
  skipLive(
347
- "emit-pi-durable-carrier --check",
348
- `no --source repository holding ${pin.commit} — the rebuild was NOT checked; pass a clone of ${(pin as { repository?: string }).repository ?? "upstream"} that contains it`,
321
+ "emit-pi-durable-carrier --origin",
322
+ `${repo} does not hold ${pin.origin.commit} — provenance was NOT checked; pass a clone of ${pin.origin.repository} that contains it`,
349
323
  );
350
324
  }
325
+ const drift = checkOrigin(path.resolve(repo), pin);
326
+ if (drift.length > 0) {
327
+ for (const line of drift) console.error(`[emit-pi-durable-carrier] ${line}`);
328
+ return 1;
329
+ }
330
+ console.log(
331
+ `[emit-pi-durable-carrier] origin ok — ${pin.origin.files.length} files and the license are the recorded objects of ${pin.origin.commit} (provenance only, not a rebuild)`,
332
+ );
333
+ return 0;
351
334
  }
352
- if (source === undefined) {
353
- console.error("emit-pi-durable-carrier --write needs --source");
354
- return 2;
355
- }
356
- const emission = emit(path.resolve(source), pin);
357
- if (mode === "write") {
358
- write(pin, emission);
335
+ const emission = emitCarrier(pin);
336
+ if (mode === "--write") {
337
+ const changed = write(pin, emission);
338
+ for (const line of changed) console.error(`[emit-pi-durable-carrier] changed ${line}`);
359
339
  console.log(
360
- `[emit-pi-durable-carrier] wrote ${emission.files.size} files to ${path.relative(ROOT, CARRIER_DIR)} from ${pin.commit} (${emission.generated.node})`,
340
+ `[emit-pi-durable-carrier] wrote ${emission.files.size} files to ${path.relative(ROOT, CARRIER_DIR)} from ${SOURCE_ROOT} (${emission.generated.node}); ${changed.length} generated field(s) changed`,
361
341
  );
362
342
  return 0;
363
343
  }
364
- const drift = compare(pin, emission);
344
+ const drift = compareCarrier(pin, emission);
365
345
  if (drift.length > 0) {
366
346
  for (const line of drift) console.error(`[emit-pi-durable-carrier] ${line}`);
367
347
  return 1;
368
348
  }
349
+ const node = emitterNodeDrift(pin.carrier.emitter.node, emission.generated.node);
350
+ if (node !== undefined) {
351
+ if (process.env.ENTWURF_REQUIRE_EMITTER_NODE === "1") {
352
+ console.error(`[emit-pi-durable-carrier] ${node} (ENTWURF_REQUIRE_EMITTER_NODE=1)`);
353
+ return 1;
354
+ }
355
+ skipLive(
356
+ "emit-pi-durable-carrier --check",
357
+ `the carrier bytes equal their emission from ${SOURCE_ROOT}, but ${node} — the emitter identity was NOT checked, so this is no rebuild PASS; run it under ${pin.carrier.emitter.node}`,
358
+ );
359
+ }
369
360
  console.log(
370
- `[emit-pi-durable-carrier] ok — the carrier is the emission of ${pin.commit} (${emission.generated.node})`,
361
+ `[emit-pi-durable-carrier] ok — the carrier is the emission of ${SOURCE_ROOT} (${emission.generated.node})`,
371
362
  );
372
363
  return 0;
373
364
  }
374
365
 
375
366
  // Run only when this file is the process entry (argv realpath, as the shipped entries decide it);
376
- // the beside test imports the source-read seam above without emitting anything.
367
+ // the beside test imports the seams above without emitting anything.
377
368
  const invokedDirectly = (() => {
378
369
  const entry = process.argv[1];
379
370
  if (typeof entry !== "string" || entry.length === 0) return false;
@@ -20,7 +20,8 @@
20
20
  "gate": ["bash", "run.sh", "check-codex-app-server-launch"],
21
21
  "timeoutSeconds": 300,
22
22
  "signature": "[QK:CODEX-APP-SERVER-ADDRESS-MATCHES-PRODUCT-LEAF]",
23
- "signatureSource": "scripts/check-codex-app-server-launch.ts"
23
+ "signatureSource": "scripts/check-codex-app-server-launch.ts",
24
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
24
25
  },
25
26
  {
26
27
  "claim": "CODEX-APP-SERVER-EXECS-NOT-FORKS",
@@ -31,7 +32,8 @@
31
32
  "gate": ["bash", "run.sh", "check-codex-app-server-launch"],
32
33
  "timeoutSeconds": 300,
33
34
  "signature": "[QK:CODEX-APP-SERVER-EXECS-NOT-FORKS]",
34
- "signatureSource": "scripts/check-codex-app-server-launch.ts"
35
+ "signatureSource": "scripts/check-codex-app-server-launch.ts",
36
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
35
37
  },
36
38
  {
37
39
  "claim": "CODEX-APP-SERVER-REFUSES-LIVE-SOCKET",
@@ -42,18 +44,20 @@
42
44
  "gate": ["bash", "run.sh", "check-codex-app-server-launch"],
43
45
  "timeoutSeconds": 300,
44
46
  "signature": "[QK:CODEX-APP-SERVER-REFUSES-LIVE-SOCKET]",
45
- "signatureSource": "scripts/check-codex-app-server-launch.ts"
47
+ "signatureSource": "scripts/check-codex-app-server-launch.ts",
48
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
46
49
  },
47
50
  {
48
51
  "claim": "CODEX-APP-SERVER-REFUSES-INDETERMINATE-SOCKET",
49
- "title": "reading an unidentifiable path (symlink, foreign owner, not a socket) as an absent one lets the launch clobber something it could not identify \u2014 the same classification the delivery rail refuses on, silently downgraded at the one place that would overwrite it",
52
+ "title": "reading the classifier's indeterminate verdict (a symlink that is not Codex's exact alias, a foreign owner, not a socket) as an absent path lets the launch clobber something it could not identify \u2014 the same classification the delivery rail refuses on, silently downgraded at the one place that would overwrite it",
50
53
  "subject": "scripts/codex-app-server-launch.sh",
51
- "find": ["\tprint(\"indeterminate\\tthe path is a symlink\")"],
52
- "replace": ["\tprint(\"absent\")"],
54
+ "find": ["\tindeterminate) probe=\"indeterminate\t$classified_detail\" ;;"],
55
+ "replace": ["\tindeterminate) probe=\"absent\" ;;"],
53
56
  "gate": ["bash", "run.sh", "check-codex-app-server-launch"],
54
57
  "timeoutSeconds": 300,
55
58
  "signature": "[QK:CODEX-APP-SERVER-REFUSES-INDETERMINATE-SOCKET]",
56
- "signatureSource": "scripts/check-codex-app-server-launch.ts"
59
+ "signatureSource": "scripts/check-codex-app-server-launch.ts",
60
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
57
61
  },
58
62
  {
59
63
  "claim": "CODEX-APP-SERVER-REFUSES-SECOND-LISTEN",
@@ -72,7 +76,8 @@
72
76
  "gate": ["bash", "run.sh", "check-codex-app-server-launch"],
73
77
  "timeoutSeconds": 300,
74
78
  "signature": "[QK:CODEX-APP-SERVER-REFUSES-SECOND-LISTEN]",
75
- "signatureSource": "scripts/check-codex-app-server-launch.ts"
79
+ "signatureSource": "scripts/check-codex-app-server-launch.ts",
80
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
76
81
  },
77
82
  {
78
83
  "claim": "CODEX-APP-SERVER-READS-PROC-HOLDER",
@@ -94,7 +99,8 @@
94
99
  "gate": ["bash", "run.sh", "check-codex-app-server-launch"],
95
100
  "timeoutSeconds": 300,
96
101
  "signature": "[QK:CODEX-APP-SERVER-READS-PROC-HOLDER]",
97
- "signatureSource": "scripts/check-codex-app-server-launch.ts"
102
+ "signatureSource": "scripts/check-codex-app-server-launch.ts",
103
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
98
104
  },
99
105
  {
100
106
  "claim": "CODEX-APP-SERVER-REFUSES-UNRECOGNISED-PROBE",
@@ -115,7 +121,26 @@
115
121
  "gate": ["bash", "run.sh", "check-codex-app-server-launch"],
116
122
  "timeoutSeconds": 300,
117
123
  "signature": "[QK:CODEX-APP-SERVER-REFUSES-UNRECOGNISED-PROBE]",
118
- "signatureSource": "scripts/check-codex-app-server-launch.ts"
124
+ "signatureSource": "scripts/check-codex-app-server-launch.ts",
125
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
126
+ },
127
+ {
128
+ "claim": "CODEX-APP-SERVER-REFUSES-UNRECOGNISED-CLASSIFIER",
129
+ "title": "passing an unrecognised classifier word through to the probe verdicts lets a word the classifier never gives \u2014 'stale' \u2014 be read as a dead socket file and launched over, so a classifier that answers something nobody wrote decides that this launch may clobber the endpoint",
130
+ "subject": "scripts/codex-app-server-launch.sh",
131
+ "find": [
132
+ "\t*)",
133
+ "\t\tfail \"codex-app-server-socket-probe-unrecognised: the socket classifier answered",
134
+ " '$classified_word', which is not one of socket/absent/dangling/indeterminate. Refusing to launch on a",
135
+ " reading nothing understands.\"",
136
+ "\t\t;;"
137
+ ],
138
+ "replace": ["\t*) probe=\"$classified_word\" ;;"],
139
+ "gate": ["bash", "run.sh", "check-codex-app-server-launch"],
140
+ "timeoutSeconds": 300,
141
+ "signature": "[QK:CODEX-APP-SERVER-REFUSES-UNRECOGNISED-CLASSIFIER]",
142
+ "signatureSource": "scripts/check-codex-app-server-launch.ts",
143
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
119
144
  },
120
145
  {
121
146
  "claim": "CODEX-APP-SERVER-REFUSES-RELATIVE-SOCKET",
@@ -135,7 +160,8 @@
135
160
  "gate": ["bash", "run.sh", "check-codex-app-server-launch"],
136
161
  "timeoutSeconds": 300,
137
162
  "signature": "[QK:CODEX-APP-SERVER-REFUSES-RELATIVE-SOCKET]",
138
- "signatureSource": "scripts/check-codex-app-server-launch.ts"
163
+ "signatureSource": "scripts/check-codex-app-server-launch.ts",
164
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
139
165
  },
140
166
  {
141
167
  "claim": "CODEX-APP-SERVER-REFUSES-CONTROL-CHAR-SOCKET",
@@ -151,7 +177,8 @@
151
177
  "gate": ["bash", "run.sh", "check-codex-app-server-launch"],
152
178
  "timeoutSeconds": 300,
153
179
  "signature": "[QK:CODEX-APP-SERVER-REFUSES-CONTROL-CHAR-SOCKET]",
154
- "signatureSource": "scripts/check-codex-app-server-launch.ts"
180
+ "signatureSource": "scripts/check-codex-app-server-launch.ts",
181
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
155
182
  }
156
183
  ]
157
184
  }
@@ -28,6 +28,21 @@
28
28
  "signature": "[QK:CODEX-NATIVE-LOADED-PROBE]",
29
29
  "signatureSource": "scripts/check-codex-native-push.ts"
30
30
  },
31
+ {
32
+ "claim": "CODEX-SOCKET-ALIAS-EXACT",
33
+ "title": "accepting any symlink into Codex's socket directory instead of the one target Codex derives for THIS advertised path lets a retargeted alias hand delivery, preflight and the launcher a socket that belongs to a different listen address \u2014 the exact-target equality is the whole of what makes the 0.162.1 alias trustworthy",
34
+ "subject": "pi-extensions/lib/native-push/codex-ws-client.ts",
35
+ "find": [
36
+ "\tif (target !== expected) {",
37
+ "\t\treturn refuse(`app-server socket is a symlink that is not Codex's rendezvous alias: ${socketPath} -> ${target}`);",
38
+ "\t}"
39
+ ],
40
+ "replace": ["\t// any target inside Codex's socket directory is good enough"],
41
+ "gate": ["bash", "run.sh", "check-codex-native-push"],
42
+ "timeoutSeconds": 120,
43
+ "signature": "[QK:CODEX-SOCKET-ALIAS-EXACT]",
44
+ "signatureSource": "scripts/check-codex-native-push.ts"
45
+ },
31
46
  {
32
47
  "claim": "CODEX-NATIVE-VENDOR-QUEUE",
33
48
  "title": "native delivery invokes a different Codex subcommand instead of the measured one-shot queue surface",