@junghanacs/entwurf 0.38.0 → 0.39.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 (32) hide show
  1. package/BASELINE.md +9 -4
  2. package/CHANGELOG.md +221 -0
  3. package/LICENSES/node-gyp.txt +24 -0
  4. package/README.md +14 -2
  5. package/VERIFY.md +14 -5
  6. package/docs/setup-clean-host.md +3 -1
  7. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +13 -4
  8. package/mcp/entwurf-bridge/dist/pi-extensions/lib/control-socket-path.js +56 -1
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +25 -0
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +14 -0
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/socket-discovery.js +14 -2
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/socket-probe.js +21 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/win32-native.js +182 -0
  14. package/mcp/entwurf-bridge/src/index.ts +13 -4
  15. package/native/win32-x64/entwurf_win.node +0 -0
  16. package/native/win32-x64/manifest.json +45 -0
  17. package/package.json +4 -2
  18. package/pi/pi-durable/carrier/harness-setup.js +3 -3
  19. package/pi/pi-durable/overlay/upstream-pin.json +2 -2
  20. package/pi-extensions/entwurf-control.ts +6 -1
  21. package/pi-extensions/lib/control-socket-path.js +56 -1
  22. package/pi-extensions/lib/entwurf-control-rpc.ts +32 -0
  23. package/pi-extensions/lib/meta-session.ts +14 -0
  24. package/pi-extensions/lib/socket-discovery.ts +12 -1
  25. package/pi-extensions/lib/socket-probe.ts +20 -0
  26. package/pi-extensions/lib/win32-native.js +198 -0
  27. package/run.sh +105 -1
  28. package/scripts/check-entwurf-self-address.ts +15 -4
  29. package/scripts/check-gate-qualification.ts +2 -1
  30. package/scripts/mutants/pack-install.json +28 -2
  31. package/scripts/mutants/pi-durable.json +5 -5
  32. package/scripts/mutants/win32-preview.json +215 -0
@@ -57,6 +57,7 @@ import {
57
57
  controlSocketPathIn,
58
58
  defaultControlSocketDir,
59
59
  gardenIdFromSocketFilename,
60
+ WIN32_PIPE_PREFIX,
60
61
  } from "./control-socket-path.js";
61
62
  import type { SocketProbe } from "./entwurf-facts.ts";
62
63
  import { SESSION_ID_RE } from "./session-id.js";
@@ -117,7 +118,9 @@ export type TargetSocketInspection =
117
118
  * `record-less-socket` reject (a diagnostic state) — no probe, no acceptance.
118
119
  */
119
120
  export function isRecordLessSocketCandidate(inspection: TargetSocketInspection): boolean {
120
- return inspection.kind === "socket-file";
121
+ // A Windows pipe name (#78) is never lstat-ed (see inspectControlSocketPath), so its
122
+ // `socket-file` carries no presence evidence and cannot confirm a record-less socket.
123
+ return inspection.kind === "socket-file" && !inspection.socketPath.startsWith(WIN32_PIPE_PREFIX);
121
124
  }
122
125
 
123
126
  export interface LstatLike {
@@ -139,6 +142,11 @@ export async function inspectControlSocketPath(
139
142
  socketPath: string,
140
143
  lstatFn: (p: string) => Promise<LstatLike> = (p) => fs.lstat(p),
141
144
  ): Promise<TargetSocketInspection> {
145
+ // Windows (#78): the address is a named pipe. The pipe filesystem has no symlinks to
146
+ // catch, and opening the name to stat it would CONNECT to it and consume an instance.
147
+ // So the name is never stat-ed: it is safe to probe because the probe itself refuses a
148
+ // pipe whose owner is not this token's expected owner before any byte.
149
+ if (socketPath.startsWith(WIN32_PIPE_PREFIX)) return { kind: "socket-file", socketPath };
142
150
  let st: LstatLike;
143
151
  try {
144
152
  st = await lstatFn(socketPath);
@@ -243,9 +251,12 @@ export async function scanSocketProbes(
243
251
  deps: Partial<SocketScanDeps> = {},
244
252
  ): Promise<SocketScanResult> {
245
253
  const dir = deps.dir ?? CONTROL_SOCKET_DIR;
254
+ // Windows (#78) has no socket files to list and nothing scans the pipe namespace: the
255
+ // candidates are the record-backed citizens alone.
246
256
  const readdir =
247
257
  deps.readdir ??
248
258
  (async (d: string): Promise<SocketDirEntry[]> => {
259
+ if (process.platform === "win32") return [];
249
260
  const dirents = await fs.readdir(d, { withFileTypes: true });
250
261
  return dirents.map((e) => ({ name: e.name, isSymbolicLink: e.isSymbolicLink() }));
251
262
  });
@@ -29,6 +29,7 @@
29
29
  */
30
30
 
31
31
  import * as net from "node:net";
32
+ import { win32Native } from "./win32-native.js";
32
33
 
33
34
  export type SocketLiveness = "alive" | "dead" | "indeterminate";
34
35
 
@@ -66,6 +67,7 @@ export async function probeSocketLiveness(
66
67
  opts: { timeoutMs?: number } = {},
67
68
  ): Promise<SocketLiveness> {
68
69
  const timeoutMs = opts.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
70
+ if (process.platform === "win32") return await probePipeLiveness(socketPath, timeoutMs);
69
71
  return await new Promise<SocketLiveness>((resolve) => {
70
72
  const socket = net.createConnection(socketPath);
71
73
  const timer = setTimeout(() => {
@@ -86,3 +88,21 @@ export async function probeSocketLiveness(
86
88
  });
87
89
  });
88
90
  }
91
+
92
+ /**
93
+ * The Windows probe (#78): the control address is a named pipe, and a connect alone would
94
+ * talk to whoever holds the name. The addon connects within one deadline and refuses a pipe
95
+ * whose owner is not this token's expected owner (`EPERM`) before any byte. Only `ENOENT`
96
+ * (no instance) is dead; a refused owner, a busy pipe past the deadline and every Win32
97
+ * failure stay indeterminate. A missing or drifted addon is not a liveness fact: that
98
+ * refusal throws.
99
+ */
100
+ async function probePipeLiveness(pipeName: string, timeoutMs: number): Promise<SocketLiveness> {
101
+ const native = win32Native();
102
+ try {
103
+ await native.pipeProbe(pipeName, Math.max(0, Math.floor(timeoutMs)));
104
+ return "alive";
105
+ } catch (err) {
106
+ return classifyConnectError((err as { code?: string }).code);
107
+ }
108
+ }
@@ -0,0 +1,198 @@
1
+ /**
2
+ * win32-native — the Windows preview's native surface (#78), and the only way to reach it.
3
+ *
4
+ * Node cannot open a file without following a final-component reparse point on Windows
5
+ * (libuv `UV_FS_O_NOFOLLOW` is 0), and it cannot check who owns a named pipe before
6
+ * talking to it. Those two capabilities come from one Entwurf-owned N-API addon,
7
+ * `native/win32-x64/entwurf_win.node`, built in CI from `native/win32/entwurf_win.c` and
8
+ * shipped beside `native/win32-x64/manifest.json`. This module verifies the manifest and
9
+ * the addon bytes BEFORE `process.dlopen`, and refuses by name otherwise:
10
+ *
11
+ * ENTWURF_WIN32_UNSUPPORTED not win32-x64, no addon in this install (an ordinary
12
+ * Linux pack), a Node without the N-API floor, or a
13
+ * verified addon this host cannot load
14
+ * ENTWURF_WIN32_NATIVE_DRIFT the manifest is malformed, or the addon bytes are not the
15
+ * bytes the manifest names
16
+ *
17
+ * There is no repair, no download and no fallback: a refusal is the whole answer.
18
+ *
19
+ * Authored as `.js` for the reason `control-socket-path.js` is: it is imported from the
20
+ * tsc-emit root program and from the strip-types lanes alike. `meta-session.ts` reaches it
21
+ * only through a win32-only `createRequire`, so the installers that copy `meta-session.ts`
22
+ * on their own are unchanged on Linux and macOS. Keep dependency-free except node builtins.
23
+ */
24
+
25
+ import { createHash } from "node:crypto";
26
+ import * as fs from "node:fs";
27
+ import * as path from "node:path";
28
+ import { fileURLToPath } from "node:url";
29
+
30
+ export const WIN32_UNSUPPORTED_CODE = "ENTWURF_WIN32_UNSUPPORTED";
31
+ export const WIN32_DRIFT_CODE = "ENTWURF_WIN32_NATIVE_DRIFT";
32
+
33
+ const PACKAGE_NAME = "@junghanacs/entwurf";
34
+ const NATIVE_DIR = path.join("native", "win32-x64");
35
+ const ADDON_FILE = "entwurf_win.node";
36
+ const SHA256 = /^[0-9a-f]{64}$/;
37
+ // The addon is built against exactly this N-API version (native/win32/binding.gyp, build.mjs). A
38
+ // manifest that declares any other value describes some other build: drift, not a floor to honour.
39
+ const ADDON_NAPI_VERSION = 8;
40
+ // One addon object per process, even if two module instances of this file exist (a
41
+ // transpiling loader and Node's own loader can each evaluate it once).
42
+ const SLOT = Symbol.for("entwurf.win32-native.addon");
43
+ // DOS device names open a device, not a file, at any directory. The addon's GetFileType
44
+ // check refuses a non-disk handle anyway; this keeps the open from ever being attempted.
45
+ const DOS_DEVICE = /^(con|prn|aux|nul|com[0-9¹²³]|lpt[0-9¹²³]|conin\$|conout\$)$/i;
46
+
47
+ /**
48
+ * @param {string} code
49
+ * @param {string} message
50
+ * @param {unknown} [cause]
51
+ * @returns {Error & { code: string }}
52
+ */
53
+ function refusal(code, message, cause) {
54
+ const err = /** @type {Error & { code: string }} */ (new Error(message, cause === undefined ? undefined : { cause }));
55
+ err.code = code;
56
+ return err;
57
+ }
58
+
59
+ /**
60
+ * The package root, found from where THIS module lives: the source depth
61
+ * (`<root>/pi-extensions/lib/`) first, then the compiled depth
62
+ * (`<root>/mcp/entwurf-bridge/dist/pi-extensions/lib/`). The root is the directory whose
63
+ * `package.json` names this package; `null` when neither depth does.
64
+ *
65
+ * @param {string} [from]
66
+ * @returns {string | null}
67
+ */
68
+ export function win32NativePackageRoot(from = path.dirname(fileURLToPath(import.meta.url))) {
69
+ for (const up of [
70
+ ["..", ".."],
71
+ ["..", "..", "..", "..", ".."],
72
+ ]) {
73
+ const root = path.resolve(from, ...up);
74
+ let name;
75
+ try {
76
+ name = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"))?.name;
77
+ } catch {
78
+ continue;
79
+ }
80
+ if (name === PACKAGE_NAME) return root;
81
+ }
82
+ return null;
83
+ }
84
+
85
+ /**
86
+ * Verify `<root>/native/win32-x64/` and return the addon path to load. Pure apart from
87
+ * reading the two files, so the refusals are provable on any host.
88
+ *
89
+ * @param {string} root
90
+ * @param {{ napi?: string | undefined }} [versions]
91
+ * @returns {{ addonPath: string, sha256: string, napiVersion: number }}
92
+ */
93
+ export function verifyWin32NativeAt(root, versions = process.versions) {
94
+ const dir = path.join(root, NATIVE_DIR);
95
+ const manifestPath = path.join(dir, "manifest.json");
96
+ let raw;
97
+ try {
98
+ raw = fs.readFileSync(manifestPath, "utf8");
99
+ } catch (err) {
100
+ if (/** @type {{ code?: unknown }} */ (err).code === "ENOENT") {
101
+ throw refusal(
102
+ WIN32_UNSUPPORTED_CODE,
103
+ `this entwurf install carries no Windows addon (${manifestPath} is absent): only the Windows preview ` +
104
+ "artifact runs on Windows; an ordinary pack does not.",
105
+ );
106
+ }
107
+ throw refusal(WIN32_DRIFT_CODE, `cannot read ${manifestPath}`, err);
108
+ }
109
+ let manifest;
110
+ try {
111
+ manifest = JSON.parse(raw);
112
+ } catch (err) {
113
+ throw refusal(WIN32_DRIFT_CODE, `${manifestPath} is not JSON`, err);
114
+ }
115
+ if (
116
+ manifest?.schema !== 1 ||
117
+ manifest.platform !== "win32" ||
118
+ manifest.arch !== "x64" ||
119
+ manifest.file !== ADDON_FILE ||
120
+ typeof manifest.sha256 !== "string" ||
121
+ !SHA256.test(manifest.sha256) ||
122
+ manifest.napiVersion !== ADDON_NAPI_VERSION
123
+ ) {
124
+ throw refusal(WIN32_DRIFT_CODE, `${manifestPath} does not describe one win32-x64 ${ADDON_FILE}`);
125
+ }
126
+ const napi = Number(versions.napi);
127
+ if (!(napi >= ADDON_NAPI_VERSION)) {
128
+ throw refusal(
129
+ WIN32_UNSUPPORTED_CODE,
130
+ `the Windows addon needs N-API ${ADDON_NAPI_VERSION}; this Node offers ${versions.napi ?? "none"}`,
131
+ );
132
+ }
133
+ const addonPath = path.join(dir, ADDON_FILE);
134
+ let bytes;
135
+ try {
136
+ bytes = fs.readFileSync(addonPath);
137
+ } catch (err) {
138
+ throw refusal(WIN32_DRIFT_CODE, `the manifest names ${addonPath}, which cannot be read`, err);
139
+ }
140
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
141
+ if (sha256 !== manifest.sha256) {
142
+ throw refusal(
143
+ WIN32_DRIFT_CODE,
144
+ `${addonPath} is not the addon its manifest names (sha256 ${sha256} != ${manifest.sha256})`,
145
+ );
146
+ }
147
+ return { addonPath, sha256, napiVersion: ADDON_NAPI_VERSION };
148
+ }
149
+
150
+ /**
151
+ * @typedef {object} Win32NativeAddon
152
+ * @property {(file: string) => Buffer} readNoFollow
153
+ * @property {(name: string, line: string, timeoutMs: number) => Promise<{ responseLine: string, serverPid: number }>} pipeRpc
154
+ * @property {(name: string, timeoutMs: number) => Promise<{ serverPid: number }>} pipeProbe
155
+ * @property {() => { live: number, cancelledIncomplete: number }} ioStats
156
+ */
157
+
158
+ /** @returns {Win32NativeAddon} */
159
+ export function win32Native() {
160
+ const held = /** @type {Record<symbol, Win32NativeAddon | undefined>} */ (/** @type {unknown} */ (process))[SLOT];
161
+ if (held) return held;
162
+ if (process.platform !== "win32" || process.arch !== "x64") {
163
+ throw refusal(
164
+ WIN32_UNSUPPORTED_CODE,
165
+ `the Windows native surface exists only on win32-x64, not ${process.platform}-${process.arch}`,
166
+ );
167
+ }
168
+ const root = win32NativePackageRoot();
169
+ if (root === null) {
170
+ throw refusal(WIN32_UNSUPPORTED_CODE, `cannot find the ${PACKAGE_NAME} package root beside this module`);
171
+ }
172
+ const { addonPath } = verifyWin32NativeAt(root);
173
+ const module = { exports: {} };
174
+ try {
175
+ process.dlopen(module, addonPath);
176
+ } catch (err) {
177
+ throw refusal(WIN32_UNSUPPORTED_CODE, `this host cannot load the verified Windows addon ${addonPath}`, err);
178
+ }
179
+ const addon = /** @type {Win32NativeAddon} */ (module.exports);
180
+ /** @type {Record<symbol, Win32NativeAddon | undefined>} */ (/** @type {unknown} */ (process))[SLOT] = addon;
181
+ return addon;
182
+ }
183
+
184
+ /**
185
+ * Read a record's bytes on Windows, refusing a final-component reparse point (`ELOOP`) and
186
+ * a non-regular entry (`ENTWURF_ENOTREG`) on the very handle the read uses. Over 16 MiB is
187
+ * `EFBIG`; other Win32 failures are `EWIN32_<n>`.
188
+ *
189
+ * @param {string} file
190
+ * @returns {Buffer}
191
+ */
192
+ export function win32ReadNoFollow(file) {
193
+ const stem = path.win32.basename(file).split(".")[0].replace(/[ ]+$/, "");
194
+ if (DOS_DEVICE.test(stem)) {
195
+ throw refusal("ENTWURF_ENOTREG", `${path.win32.basename(file)} names a DOS device, never a record`);
196
+ }
197
+ return win32Native().readNoFollow(file);
198
+ }
package/run.sh CHANGED
@@ -1340,6 +1340,15 @@ check_control_send_receipt() {
1340
1340
  run_vitest pi-extensions/lib/control-send-receipt.test.ts
1341
1341
  }
1342
1342
 
1343
+ check_win32_preview() {
1344
+ # MUTANT EXECUTION COORDINATE, not a discovery path — the Windows preview's (#78) loader and
1345
+ # platform wiring tests are found by check-tests-beside-behavior like every other beside-behaviour
1346
+ # lane. This case exists because the WIN32-* mutants name it as gate argv. The addon's own Win32
1347
+ # behaviour is measured only on Windows, by windows-native-preview.yml.
1348
+ section "Windows preview loader + platform wiring (mutant execution coordinate)"
1349
+ run_vitest pi-extensions/lib/win32-native.test.ts pi-extensions/lib/win32-wiring.test.ts
1350
+ }
1351
+
1343
1352
  check_entwurf_v2_native_push() {
1344
1353
  # MUTANT EXECUTION COORDINATE, not a discovery path — the test is found by
1345
1354
  # check-tests-beside-behavior like every other beside-behaviour lane. This case exists
@@ -2667,6 +2676,8 @@ for (const [job] of ciJobs) {
2667
2676
  // to what is already bound is not a safety net; it is a restatement.
2668
2677
  // pnpm-lock.yaml is excluded on purpose: those engine ranges belong to third-
2669
2678
  // party dependencies and are not this repo's supported axis.
2679
+ // native/win32/package-lock.json (#78) is excluded for the same reason: a generated
2680
+ // lock of the Windows addon's build-only dependency tree (node-gyp), never shipped.
2670
2681
  // `--cached` ONLY — deliberately not `--others`. The corpus must be the
2671
2682
  // candidate contract (what is tracked/staged), never the operator's working
2672
2683
  // directory: an un-ignored scratch file would otherwise turn this gate RED on
@@ -2676,7 +2687,7 @@ for (const [job] of ciJobs) {
2676
2687
  // before the commit that would ship it. (Review lane, 2026-07-21.)
2677
2688
  const tracked = execFileSync('git', ['ls-files', '--cached', '--', '*.sh', '*.json', '*.ts', '*.md', '*.yml', '*.yaml'], { encoding: 'utf8' })
2678
2689
  .split('\n')
2679
- .filter((f) => f && f !== 'pnpm-lock.yaml' && f !== 'CHANGELOG.md' && f !== 'NEXT.md');
2690
+ .filter((f) => f && f !== 'pnpm-lock.yaml' && f !== 'native/win32/package-lock.json' && f !== 'CHANGELOG.md' && f !== 'NEXT.md');
2680
2691
  assert.ok(tracked.length >= 20,
2681
2692
  `git ls-files returned ${tracked.length} first-party files — the sweep lost its corpus and would pass vacuously`);
2682
2693
 
@@ -3851,6 +3862,14 @@ check_pack() {
3851
3862
  "pi/pi-durable/carrier/LICENSE"
3852
3863
  "pi/meta-bridge-omp/entwurf-meta-omp/package.json"
3853
3864
  "mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js"
3865
+ # #78 — the Windows preview loader. The compiled meta-session reaches it only through a
3866
+ # win32-only createRequire, not the import graph, so its compiled copy is pinned by name.
3867
+ "pi-extensions/lib/win32-native.js"
3868
+ "mcp/entwurf-bridge/dist/pi-extensions/lib/win32-native.js"
3869
+ # #78 — the MIT notice for node-gyp's win_delay_load_hook.cc, which build.mjs compiles into the
3870
+ # Windows addon. It rides every pack (one per-FILE entry), so the preview carrier never ships
3871
+ # without it; build.mjs refuses unless it equals the pinned node-gyp LICENSE.
3872
+ "LICENSES/node-gyp.txt"
3854
3873
  "scripts/postinstall-chmod.cjs"
3855
3874
  "pi/entwurf-capabilities.json"
3856
3875
  "pi/meta-bridge/.claude-plugin/marketplace.json"
@@ -3925,6 +3944,9 @@ check_pack() {
3925
3944
  '\.test\.[cm]?[jt]s$'
3926
3945
  # Maintainer take/renderer/helper TS is source-only, including non-test implementations.
3927
3946
  '^demo/.*\.ts$'
3947
+ # #78 — the Windows addon's C source, gyp file and build pin are CI build inputs, never a
3948
+ # consumer repair surface: only the built native/win32-x64/ carrier may ship.
3949
+ '^native/win32/'
3928
3950
  )
3929
3951
  # #141: the owned pi-durable source, its typecheck fence and the retired overlay patch are maintainer
3930
3952
  # input. An installed consumer runs the emitted carrier JS (and its LICENSE) only — Rule 11: compiled
@@ -3957,6 +3979,20 @@ check_pack() {
3957
3979
  pass=0
3958
3980
  fi
3959
3981
 
3982
+ # #78 — a pack carries the Windows addon not at all, or as exactly a verified addon + manifest
3983
+ # pair (pack_win32_carrier_verdict, proven on owned fixtures by check-pack-win32-carrier).
3984
+ # "present" means the carrier verified, never that the build was approved for publication.
3985
+ local carrier_verdict
3986
+ carrier_verdict=$(pack_win32_carrier_verdict "$file_list" "$REPO_DIR")
3987
+ case "$carrier_verdict" in
3988
+ absent) echo "[check-pack] win32 addon: absent (not a Windows preview artifact)" ;;
3989
+ present\ *) echo "[check-pack] win32 addon: present sha256=${carrier_verdict#present } (carrier verified; not a publication approval)" ;;
3990
+ *)
3991
+ fail "[check-pack] native/win32-x64/ is not one verified addon + manifest pair: $carrier_verdict"
3992
+ pass=0
3993
+ ;;
3994
+ esac
3995
+
3960
3996
  local total
3961
3997
  total=$(printf '%s\n' "$file_list" | wc -l | tr -d ' ')
3962
3998
  echo "[check-pack] $total files in tarball"
@@ -3969,6 +4005,68 @@ check_pack() {
3969
4005
  return 1
3970
4006
  }
3971
4007
 
4008
+ # #78 — the Windows addon carrier decision, shared by check_pack (the real tarball list) and
4009
+ # check_pack_win32_carrier (owned fake-byte fixtures). $1 = tarball file list, $2 = the package root
4010
+ # holding native/win32-x64/. Prints ONE verdict: `absent`, `pair-broken`, `invalid <code>` or
4011
+ # `present <sha256>`. The manifest + bytes check IS the loader's own verifyWin32NativeAt (manifest
4012
+ # shape, N-API 8, digest), so the pack and the consumer cannot disagree about what a carrier is.
4013
+ pack_win32_carrier_verdict() {
4014
+ local list="$1" root="$2" carrier
4015
+ carrier=$(grep -E '^native/win32-x64/' <<<"$list" | sort || true)
4016
+ if [ -z "$carrier" ]; then echo "absent"; return 0; fi
4017
+ if [ "$carrier" != $'native/win32-x64/entwurf_win.node\nnative/win32-x64/manifest.json' ]; then echo "pair-broken"; return 0; fi
4018
+ node -e '
4019
+ import(require("node:url").pathToFileURL(process.argv[1]).href).then((m) => {
4020
+ try { process.stdout.write(`present ${m.verifyWin32NativeAt(process.argv[2], { napi: "8" }).sha256}`); }
4021
+ catch (err) { process.stdout.write(`invalid ${err.code ?? "uncoded"}`); }
4022
+ });' "$REPO_DIR/pi-extensions/lib/win32-native.js" "$root"
4023
+ }
4024
+
4025
+ check_pack_win32_carrier() {
4026
+ section "pack: Windows addon carrier verdict (owned fake-byte fixtures)"
4027
+ # Fake bytes and node-written manifests, never a PE: this proves the pair/digest/ABI decision,
4028
+ # not a loadable addon (windows-native-preview.yml owns that, on installed bytes).
4029
+ local fx pair=$'native/win32-x64/entwurf_win.node\nnative/win32-x64/manifest.json' got
4030
+ fx=$(mktemp -d "${TMPDIR:-/tmp}/entwurf-pack-carrier.XXXXXX")
4031
+ trap 'rm -rf -- "$fx"' RETURN
4032
+ win32_carrier_cell() { # name napiVersion -> a root with a matching addon + manifest
4033
+ mkdir -p "$fx/$1/native/win32-x64"
4034
+ printf 'fake addon %s\n' "$1" >"$fx/$1/native/win32-x64/entwurf_win.node"
4035
+ node -e '
4036
+ const fs = require("node:fs"), dir = process.argv[1];
4037
+ const sha = require("node:crypto").createHash("sha256").update(fs.readFileSync(dir + "/entwurf_win.node")).digest("hex");
4038
+ fs.writeFileSync(dir + "/manifest.json", JSON.stringify({ schema: 1, platform: "win32", arch: "x64", file: "entwurf_win.node", sha256: sha, napiVersion: Number(process.argv[2]) }));
4039
+ ' "$fx/$1/native/win32-x64" "$2"
4040
+ }
4041
+ win32_carrier_cell match 8
4042
+ win32_carrier_cell drift 8 && printf 'changed\n' >"$fx/drift/native/win32-x64/entwurf_win.node"
4043
+ win32_carrier_cell abi 7
4044
+ win32_carrier_cell garbled 8 && printf 'not json' >"$fx/garbled/native/win32-x64/manifest.json"
4045
+ local want_sha
4046
+ want_sha=$(sha256sum "$fx/match/native/win32-x64/entwurf_win.node" | cut -d' ' -f1)
4047
+ got=$(pack_win32_carrier_verdict "package.json" "$fx/match")
4048
+ [ "$got" = "absent" ] || { fail "[check-pack-win32-carrier] a list without native/win32-x64/ must be absent — got: $got"; return 1; }
4049
+ for cell in drift abi garbled; do
4050
+ got=$(pack_win32_carrier_verdict "$pair" "$fx/$cell")
4051
+ [ "$got" = "invalid ENTWURF_WIN32_NATIVE_DRIFT" ] || {
4052
+ fail "[QK:WIN32-PACK-DIGEST] the $cell carrier (changed byte / N-API 7 / unparsable manifest) must be invalid as drift, never present — got: $got"
4053
+ return 1
4054
+ }
4055
+ done
4056
+ # The positive cell comes after the refusals, so a weakened check dies on its own QK first.
4057
+ got=$(pack_win32_carrier_verdict "$pair" "$fx/match")
4058
+ [ "$got" = "present $want_sha" ] || { fail "[check-pack-win32-carrier] a matching pair must be present $want_sha — got: $got"; return 1; }
4059
+ local list
4060
+ for list in 'native/win32-x64/entwurf_win.node' 'native/win32-x64/manifest.json' "$pair"$'\nnative/win32-x64/x.dll'; do
4061
+ got=$(pack_win32_carrier_verdict "$list" "$fx/match")
4062
+ [ "$got" = "pair-broken" ] || {
4063
+ fail "[QK:WIN32-PACK-PAIR] native/win32-x64/ must carry exactly the addon + manifest pair — $(tr '\n' ' ' <<<"$list") gave: $got"
4064
+ return 1
4065
+ }
4066
+ done
4067
+ echo "[check-pack-win32-carrier] ok — absent, present, three drift shapes and three broken pairs decided by the shared verdict"
4068
+ }
4069
+
3972
4070
  # The pin-leak filter for the install tree's .pnpm listing, shared by the real scan and its
3973
4071
  # self-test below. TWO properties are load-bearing and each has its own cell below.
3974
4072
  #
@@ -7378,6 +7476,9 @@ case "$cmd" in
7378
7476
  check-control-send-receipt)
7379
7477
  check_control_send_receipt
7380
7478
  ;;
7479
+ check-win32-preview)
7480
+ check_win32_preview
7481
+ ;;
7381
7482
  check-entwurf-v2-native-push)
7382
7483
  check_entwurf_v2_native_push
7383
7484
  ;;
@@ -8489,6 +8590,9 @@ case "$cmd" in
8489
8590
  check-pack-pin-matcher)
8490
8591
  check_pack_pin_matcher
8491
8592
  ;;
8593
+ check-pack-win32-carrier)
8594
+ check_pack_win32_carrier
8595
+ ;;
8492
8596
  check-pack-consumer-roots)
8493
8597
  check_pack_consumer_roots
8494
8598
  ;;
@@ -263,8 +263,19 @@ ok(
263
263
  // call: the dir is still this adapter's own policy (ENTWURF_DIR honours the env
264
264
  // override the pi side does not), but the join is not re-authored here.
265
265
  ok(
266
- "buildStrictPiSenderEnvelope existsSync-probes the canonical socket via the shared grammar (controlSocketPathIn(ENTWURF_DIR, …))",
267
- /existsSync\s*\(/.test(piBody) && /controlSocketPathIn\s*\(\s*ENTWURF_DIR\s*,/.test(piBody),
266
+ "buildStrictPiSenderEnvelope probes the canonical address via the shared grammar (controlSocketPathIn(ENTWURF_DIR, …))",
267
+ /controlAddressPresent\s*\(\s*socketPath\s*\)/.test(piBody) &&
268
+ /controlSocketPathIn\s*\(\s*ENTWURF_DIR\s*,/.test(piBody),
269
+ );
270
+ // The presence probe keeps the socket-file existsSync off Windows; on Windows (#78) the address
271
+ // is a named pipe, which only the owner-validated probe may touch (a stat would connect).
272
+ const presentBody = functionBody("controlAddressPresent");
273
+ ok(
274
+ "controlAddressPresent: existsSync(socketPath) off Windows, the owner-validated pipe probe on it",
275
+ /existsSync\s*\(\s*socketPath\s*\)/.test(presentBody) &&
276
+ /process\.platform === "win32"\)\s*return \(await probeSocketLiveness\(socketPath\)\) === "alive"/.test(
277
+ presentBody,
278
+ ),
268
279
  );
269
280
 
270
281
  const metaBuilder = functionBody("buildMetaSenderEnvelope");
@@ -280,8 +291,8 @@ ok(
280
291
 
281
292
  const selfRegion = toolRegion("entwurf_self");
282
293
  ok(
283
- "entwurf_self existsSync-probes the pi socket (alive vs expected, no synthesized path lie)",
284
- /existsSync\s*\(/.test(selfRegion),
294
+ "entwurf_self probes the pi control address (alive vs expected, no synthesized path lie)",
295
+ /controlAddressPresent\s*\(\s*socketPath\s*\)/.test(selfRegion),
285
296
  );
286
297
 
287
298
  // ── SLICE INTEGRITY (before any claim is read off the slice) ─────────────────
@@ -3488,7 +3488,7 @@ let manifestCount: number;
3488
3488
  "mux-fresh-call": 65,
3489
3489
  "mux-launcher-fence": 8,
3490
3490
  "mux-parent-artifact": 3,
3491
- "pack-install": 3,
3491
+ "pack-install": 5,
3492
3492
  "pi-package-ownership": 8,
3493
3493
  "qualification-ledger": 42,
3494
3494
  "mux-resume-call": 10,
@@ -3506,6 +3506,7 @@ let manifestCount: number;
3506
3506
  "typing-call-fence": 1,
3507
3507
  "v2-surface": 13,
3508
3508
  "v2-visible-resume": 18,
3509
+ "win32-preview": 19,
3509
3510
  };
3510
3511
  const laneTally: Record<string, number> = {};
3511
3512
  for (const man of manifests) laneTally[man.lane] = (laneTally[man.lane] ?? 0) + man.mutants.length;
@@ -4,7 +4,7 @@
4
4
  "mutants": [
5
5
  {
6
6
  "claim": "PACK-INSTALL-PIN-MATCHER-BOUNDED",
7
- "title": "the pin-leak matcher loses its version boundary and blesses substring lookalikes — @0.85.10 reads as the pinned 0.85.1 and the gate announces a verified tree it never verified (independent review, 2026-08-25)",
7
+ "title": "the pin-leak matcher loses its version boundary and blesses substring lookalikes \u2014 @0.85.10 reads as the pinned 0.85.1 and the gate announces a verified tree it never verified (independent review, 2026-08-25)",
8
8
  "subject": "run.sh",
9
9
  "find": [" grep '^@earendil-works+' | grep -Ev '@1\\.3\\.0(_|$)' || true"],
10
10
  "replace": [" grep '^@earendil-works+' | grep -v '@0\\.87\\.1' || true"],
@@ -15,7 +15,7 @@
15
15
  },
16
16
  {
17
17
  "claim": "PACK-INSTALL-PIN-MATCHER-COVERS-CLOSURE",
18
- "title": "the pin-leak matcher narrows back to the pi-* families and goes blind to a non-pi member of the same runtime closure — @earendil-works/chord (new in pi 0.85.0, a runtime dependency of pi-coding-agent/pi-agent-core/pi-client/pi-protocol) floats unpinned while the gate prints a verified pin (measured 2026-09-06 on a real 0.85.1 install tree)",
18
+ "title": "the pin-leak matcher narrows back to the pi-* families and goes blind to a non-pi member of the same runtime closure \u2014 @earendil-works/chord (new in pi 0.85.0, a runtime dependency of pi-coding-agent/pi-agent-core/pi-client/pi-protocol) floats unpinned while the gate prints a verified pin (measured 2026-09-06 on a real 0.85.1 install tree)",
19
19
  "subject": "run.sh",
20
20
  "find": [" grep '^@earendil-works+' | grep -Ev '@1\\.3\\.0(_|$)' || true"],
21
21
  "replace": [" grep '^@earendil-works+pi-' | grep -Ev '@1\\.3\\.0(_|$)' || true"],
@@ -34,6 +34,32 @@
34
34
  "timeoutSeconds": 120,
35
35
  "signature": "[QK:PACK-INSTALL-CONSUMER-ROOTS-CONTAINED]",
36
36
  "signatureSource": "run.sh"
37
+ },
38
+ {
39
+ "claim": "WIN32-PACK-PAIR",
40
+ "title": "the Windows carrier pair check becomes a presence test, so an addon without its manifest, or with an extra DLL beside it, is packed as a carrier",
41
+ "subject": "run.sh",
42
+ "find": [
43
+ " if [ \"$carrier\" != $'native/win32-x64/entwurf_win.node\\nnative/win32-x64/manifest.json' ]; then echo \"pair-broken\"; return 0; fi"
44
+ ],
45
+ "replace": [" if ! grep -q 'entwurf_win.node' <<<\"$carrier\"; then echo \"pair-broken\"; return 0; fi"],
46
+ "gate": ["bash", "run.sh", "check-pack-win32-carrier"],
47
+ "timeoutSeconds": 120,
48
+ "signature": "[QK:WIN32-PACK-PAIR]",
49
+ "signatureSource": "run.sh"
50
+ },
51
+ {
52
+ "claim": "WIN32-PACK-DIGEST",
53
+ "title": "the pack stops verifying the carrier with the loader's own check, so changed bytes or a foreign N-API manifest are packed as `present`",
54
+ "subject": "run.sh",
55
+ "find": [
56
+ " try { process.stdout.write(`present ${m.verifyWin32NativeAt(process.argv[2], { napi: \"8\" }).sha256}`); }"
57
+ ],
58
+ "replace": [" try { process.stdout.write(`present unverified`); }"],
59
+ "gate": ["bash", "run.sh", "check-pack-win32-carrier"],
60
+ "timeoutSeconds": 120,
61
+ "signature": "[QK:WIN32-PACK-DIGEST]",
62
+ "signatureSource": "run.sh"
37
63
  }
38
64
  ]
39
65
  }
@@ -1177,14 +1177,14 @@
1177
1177
  "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
1178
1178
  },
1179
1179
  {
1180
- "claim": "PI-DURABLE-COMPACTION-WAITS",
1181
- "title": "the Harness settings hand the SDK pi's compaction settings alone: its default background policy summarizes beside a running turn instead of the turn waiting",
1180
+ "claim": "PI-DURABLE-COMPACTION-SDK-BACKGROUND",
1181
+ "title": "the Harness settings force background compaction off again: past the SDK's background threshold the turn asks for no summary beside it",
1182
1182
  "subject": "pi/pi-durable/carrier/harness-setup.js",
1183
- "find": ["\t\t\treturn { ...settingsManager.getCompactionSettings(), backgroundTokens: 0 };"],
1184
- "replace": ["\t\t\treturn settingsManager.getCompactionSettings();"],
1183
+ "find": ["\t\t\treturn settingsManager.getCompactionSettings();"],
1184
+ "replace": ["\t\t\treturn { ...settingsManager.getCompactionSettings(), backgroundTokens: 0 };"],
1185
1185
  "gate": ["bash", "run.sh", "check-pi-durable-carrier"],
1186
1186
  "timeoutSeconds": 300,
1187
- "signature": "[QK:PI-DURABLE-COMPACTION-WAITS]",
1187
+ "signature": "[QK:PI-DURABLE-COMPACTION-SDK-BACKGROUND]",
1188
1188
  "signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
1189
1189
  "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
1190
1190
  },