@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.
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +209 -0
- package/CONTRIBUTING.md +1 -1
- package/DELIVERY.md +15 -0
- package/FAQ.md +3 -3
- package/README.md +30 -16
- package/VERIFY.md +9 -7
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +1 -1
- package/docs/setup-clean-host.md +30 -22
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +4 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +99 -18
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +3 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +59 -14
- package/mcp/entwurf-bridge/dist/scripts/codex-socket-classify.js +44 -0
- package/mcp/entwurf-bridge/tsconfig.build.json +5 -0
- package/package.json +19 -14
- package/pi/pi-durable/bootstrap.mjs +12 -8
- package/pi/pi-durable/carrier/runtime.js +52 -2
- package/pi/pi-durable/carrier/subagent.js +6 -1
- package/pi/pi-durable/carrier/tui.js +31 -4
- package/pi/pi-durable/carrier-relocation.mjs +2 -2
- package/pi/pi-durable/overlay/upstream-pin.json +79 -36
- package/pi-extensions/lib/codex-fresh-preflight.ts +4 -3
- package/pi-extensions/lib/native-push/codex-ws-client.ts +129 -18
- package/pi-extensions/lib/pi-durable-runtime.ts +4 -4
- package/pi-extensions/meta-bridge-pi-durable.ts +77 -21
- package/run.sh +112 -38
- package/scripts/check-codex-app-server-launch.ts +55 -12
- package/scripts/check-codex-native-push.ts +231 -12
- package/scripts/check-gate-qualification.ts +3 -3
- package/scripts/codex-app-server-launch.sh +54 -29
- package/scripts/codex-socket-classify.ts +46 -0
- package/scripts/emit-pi-durable-carrier.ts +159 -168
- package/scripts/mutants/codex-app-server-launch.json +39 -12
- package/scripts/mutants/codex-native.json +15 -0
- package/scripts/mutants/control-send-receipt.json +1 -1
- package/scripts/mutants/pack-install.json +3 -3
- package/scripts/mutants/pi-durable.json +165 -16
- package/scripts/mutants/pi-floor.json +5 -5
- package/scripts/pi-durable-contact-host.mjs +1 -1
- 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
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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 --
|
|
8
|
-
* node scripts/emit-pi-durable-carrier.ts --
|
|
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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
* `--
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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 (
|
|
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
|
-
|
|
57
|
-
|
|
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
|
|
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(
|
|
78
|
-
const env: NodeJS.ProcessEnv = { GIT_OPTIONAL_LOCKS: "0"
|
|
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
|
|
86
|
-
* replacement reached through alternates) can never stand in for a
|
|
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
|
|
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-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
/**
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
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
|
|
223
|
-
const
|
|
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 =
|
|
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(
|
|
196
|
+
const present = fs.existsSync(carrierDir) ? fs.readdirSync(carrierDir) : [];
|
|
266
197
|
return present.filter((name) => !declared.has(name)).sort();
|
|
267
198
|
}
|
|
268
199
|
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
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 --
|
|
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 === "
|
|
313
|
+
if (mode === "--origin") {
|
|
339
314
|
const held =
|
|
340
|
-
|
|
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 --
|
|
348
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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 ${
|
|
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 =
|
|
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 ${
|
|
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
|
|
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
|
|
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": ["\
|
|
52
|
-
"replace": ["\
|
|
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",
|