@junghanacs/entwurf 0.38.1 → 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.
- package/BASELINE.md +9 -4
- package/CHANGELOG.md +87 -0
- package/LICENSES/node-gyp.txt +24 -0
- package/README.md +14 -2
- package/VERIFY.md +12 -3
- package/docs/setup-clean-host.md +3 -1
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +13 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/control-socket-path.js +56 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +25 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +14 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/socket-discovery.js +14 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/socket-probe.js +21 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/win32-native.js +182 -0
- package/mcp/entwurf-bridge/src/index.ts +13 -4
- package/native/win32-x64/entwurf_win.node +0 -0
- package/native/win32-x64/manifest.json +45 -0
- package/package.json +4 -2
- package/pi-extensions/entwurf-control.ts +6 -1
- package/pi-extensions/lib/control-socket-path.js +56 -1
- package/pi-extensions/lib/entwurf-control-rpc.ts +32 -0
- package/pi-extensions/lib/meta-session.ts +14 -0
- package/pi-extensions/lib/socket-discovery.ts +12 -1
- package/pi-extensions/lib/socket-probe.ts +20 -0
- package/pi-extensions/lib/win32-native.js +198 -0
- package/run.sh +105 -1
- package/scripts/check-entwurf-self-address.ts +15 -4
- package/scripts/check-gate-qualification.ts +2 -1
- package/scripts/mutants/pack-install.json +28 -2
- package/scripts/mutants/win32-preview.json +215 -0
|
@@ -0,0 +1,182 @@
|
|
|
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
|
+
import { createHash } from "node:crypto";
|
|
25
|
+
import * as fs from "node:fs";
|
|
26
|
+
import * as path from "node:path";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
28
|
+
export const WIN32_UNSUPPORTED_CODE = "ENTWURF_WIN32_UNSUPPORTED";
|
|
29
|
+
export const WIN32_DRIFT_CODE = "ENTWURF_WIN32_NATIVE_DRIFT";
|
|
30
|
+
const PACKAGE_NAME = "@junghanacs/entwurf";
|
|
31
|
+
const NATIVE_DIR = path.join("native", "win32-x64");
|
|
32
|
+
const ADDON_FILE = "entwurf_win.node";
|
|
33
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
34
|
+
// The addon is built against exactly this N-API version (native/win32/binding.gyp, build.mjs). A
|
|
35
|
+
// manifest that declares any other value describes some other build: drift, not a floor to honour.
|
|
36
|
+
const ADDON_NAPI_VERSION = 8;
|
|
37
|
+
// One addon object per process, even if two module instances of this file exist (a
|
|
38
|
+
// transpiling loader and Node's own loader can each evaluate it once).
|
|
39
|
+
const SLOT = Symbol.for("entwurf.win32-native.addon");
|
|
40
|
+
// DOS device names open a device, not a file, at any directory. The addon's GetFileType
|
|
41
|
+
// check refuses a non-disk handle anyway; this keeps the open from ever being attempted.
|
|
42
|
+
const DOS_DEVICE = /^(con|prn|aux|nul|com[0-9¹²³]|lpt[0-9¹²³]|conin\$|conout\$)$/i;
|
|
43
|
+
/**
|
|
44
|
+
* @param {string} code
|
|
45
|
+
* @param {string} message
|
|
46
|
+
* @param {unknown} [cause]
|
|
47
|
+
* @returns {Error & { code: string }}
|
|
48
|
+
*/
|
|
49
|
+
function refusal(code, message, cause) {
|
|
50
|
+
const err = /** @type {Error & { code: string }} */ (new Error(message, cause === undefined ? undefined : { cause }));
|
|
51
|
+
err.code = code;
|
|
52
|
+
return err;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The package root, found from where THIS module lives: the source depth
|
|
56
|
+
* (`<root>/pi-extensions/lib/`) first, then the compiled depth
|
|
57
|
+
* (`<root>/mcp/entwurf-bridge/dist/pi-extensions/lib/`). The root is the directory whose
|
|
58
|
+
* `package.json` names this package; `null` when neither depth does.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} [from]
|
|
61
|
+
* @returns {string | null}
|
|
62
|
+
*/
|
|
63
|
+
export function win32NativePackageRoot(from = path.dirname(fileURLToPath(import.meta.url))) {
|
|
64
|
+
for (const up of [
|
|
65
|
+
["..", ".."],
|
|
66
|
+
["..", "..", "..", "..", ".."],
|
|
67
|
+
]) {
|
|
68
|
+
const root = path.resolve(from, ...up);
|
|
69
|
+
let name;
|
|
70
|
+
try {
|
|
71
|
+
name = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"))?.name;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (name === PACKAGE_NAME)
|
|
77
|
+
return root;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Verify `<root>/native/win32-x64/` and return the addon path to load. Pure apart from
|
|
83
|
+
* reading the two files, so the refusals are provable on any host.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} root
|
|
86
|
+
* @param {{ napi?: string | undefined }} [versions]
|
|
87
|
+
* @returns {{ addonPath: string, sha256: string, napiVersion: number }}
|
|
88
|
+
*/
|
|
89
|
+
export function verifyWin32NativeAt(root, versions = process.versions) {
|
|
90
|
+
const dir = path.join(root, NATIVE_DIR);
|
|
91
|
+
const manifestPath = path.join(dir, "manifest.json");
|
|
92
|
+
let raw;
|
|
93
|
+
try {
|
|
94
|
+
raw = fs.readFileSync(manifestPath, "utf8");
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
if ( /** @type {{ code?: unknown }} */(err).code === "ENOENT") {
|
|
98
|
+
throw refusal(WIN32_UNSUPPORTED_CODE, `this entwurf install carries no Windows addon (${manifestPath} is absent): only the Windows preview ` +
|
|
99
|
+
"artifact runs on Windows; an ordinary pack does not.");
|
|
100
|
+
}
|
|
101
|
+
throw refusal(WIN32_DRIFT_CODE, `cannot read ${manifestPath}`, err);
|
|
102
|
+
}
|
|
103
|
+
let manifest;
|
|
104
|
+
try {
|
|
105
|
+
manifest = JSON.parse(raw);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
throw refusal(WIN32_DRIFT_CODE, `${manifestPath} is not JSON`, err);
|
|
109
|
+
}
|
|
110
|
+
if (manifest?.schema !== 1 ||
|
|
111
|
+
manifest.platform !== "win32" ||
|
|
112
|
+
manifest.arch !== "x64" ||
|
|
113
|
+
manifest.file !== ADDON_FILE ||
|
|
114
|
+
typeof manifest.sha256 !== "string" ||
|
|
115
|
+
!SHA256.test(manifest.sha256) ||
|
|
116
|
+
manifest.napiVersion !== ADDON_NAPI_VERSION) {
|
|
117
|
+
throw refusal(WIN32_DRIFT_CODE, `${manifestPath} does not describe one win32-x64 ${ADDON_FILE}`);
|
|
118
|
+
}
|
|
119
|
+
const napi = Number(versions.napi);
|
|
120
|
+
if (!(napi >= ADDON_NAPI_VERSION)) {
|
|
121
|
+
throw refusal(WIN32_UNSUPPORTED_CODE, `the Windows addon needs N-API ${ADDON_NAPI_VERSION}; this Node offers ${versions.napi ?? "none"}`);
|
|
122
|
+
}
|
|
123
|
+
const addonPath = path.join(dir, ADDON_FILE);
|
|
124
|
+
let bytes;
|
|
125
|
+
try {
|
|
126
|
+
bytes = fs.readFileSync(addonPath);
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
throw refusal(WIN32_DRIFT_CODE, `the manifest names ${addonPath}, which cannot be read`, err);
|
|
130
|
+
}
|
|
131
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
132
|
+
if (sha256 !== manifest.sha256) {
|
|
133
|
+
throw refusal(WIN32_DRIFT_CODE, `${addonPath} is not the addon its manifest names (sha256 ${sha256} != ${manifest.sha256})`);
|
|
134
|
+
}
|
|
135
|
+
return { addonPath, sha256, napiVersion: ADDON_NAPI_VERSION };
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* @typedef {object} Win32NativeAddon
|
|
139
|
+
* @property {(file: string) => Buffer} readNoFollow
|
|
140
|
+
* @property {(name: string, line: string, timeoutMs: number) => Promise<{ responseLine: string, serverPid: number }>} pipeRpc
|
|
141
|
+
* @property {(name: string, timeoutMs: number) => Promise<{ serverPid: number }>} pipeProbe
|
|
142
|
+
* @property {() => { live: number, cancelledIncomplete: number }} ioStats
|
|
143
|
+
*/
|
|
144
|
+
/** @returns {Win32NativeAddon} */
|
|
145
|
+
export function win32Native() {
|
|
146
|
+
const held = /** @type {Record<symbol, Win32NativeAddon | undefined>} */ ( /** @type {unknown} */(process))[SLOT];
|
|
147
|
+
if (held)
|
|
148
|
+
return held;
|
|
149
|
+
if (process.platform !== "win32" || process.arch !== "x64") {
|
|
150
|
+
throw refusal(WIN32_UNSUPPORTED_CODE, `the Windows native surface exists only on win32-x64, not ${process.platform}-${process.arch}`);
|
|
151
|
+
}
|
|
152
|
+
const root = win32NativePackageRoot();
|
|
153
|
+
if (root === null) {
|
|
154
|
+
throw refusal(WIN32_UNSUPPORTED_CODE, `cannot find the ${PACKAGE_NAME} package root beside this module`);
|
|
155
|
+
}
|
|
156
|
+
const { addonPath } = verifyWin32NativeAt(root);
|
|
157
|
+
const module = { exports: {} };
|
|
158
|
+
try {
|
|
159
|
+
process.dlopen(module, addonPath);
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
throw refusal(WIN32_UNSUPPORTED_CODE, `this host cannot load the verified Windows addon ${addonPath}`, err);
|
|
163
|
+
}
|
|
164
|
+
const addon = /** @type {Win32NativeAddon} */ (module.exports);
|
|
165
|
+
/** @type {Record<symbol, Win32NativeAddon | undefined>} */ ( /** @type {unknown} */(process))[SLOT] = addon;
|
|
166
|
+
return addon;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Read a record's bytes on Windows, refusing a final-component reparse point (`ELOOP`) and
|
|
170
|
+
* a non-regular entry (`ENTWURF_ENOTREG`) on the very handle the read uses. Over 16 MiB is
|
|
171
|
+
* `EFBIG`; other Win32 failures are `EWIN32_<n>`.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} file
|
|
174
|
+
* @returns {Buffer}
|
|
175
|
+
*/
|
|
176
|
+
export function win32ReadNoFollow(file) {
|
|
177
|
+
const stem = path.win32.basename(file).split(".")[0].replace(/[ ]+$/, "");
|
|
178
|
+
if (DOS_DEVICE.test(stem)) {
|
|
179
|
+
throw refusal("ENTWURF_ENOTREG", `${path.win32.basename(file)} names a DOS device, never a record`);
|
|
180
|
+
}
|
|
181
|
+
return win32Native().readNoFollow(file);
|
|
182
|
+
}
|
|
@@ -108,6 +108,7 @@ import {
|
|
|
108
108
|
} from "../../../pi-extensions/lib/meta-session.ts";
|
|
109
109
|
import { RESUME_CALL_REJECT_HINT, resumeCall } from "../../../pi-extensions/lib/mux-resume-call.ts";
|
|
110
110
|
import { registerNativeConversation } from "../../../pi-extensions/lib/native-push/register.ts";
|
|
111
|
+
import { probeSocketLiveness } from "../../../pi-extensions/lib/socket-probe.ts";
|
|
111
112
|
|
|
112
113
|
const HOME = os.homedir();
|
|
113
114
|
|
|
@@ -137,6 +138,14 @@ try {
|
|
|
137
138
|
// ENTWURF_DIR override the pi side does not. The path GRAMMAR is the shared leaf.
|
|
138
139
|
const ENTWURF_DIR = process.env.ENTWURF_DIR ?? defaultControlSocketDir(HOME);
|
|
139
140
|
|
|
141
|
+
/** Is this session's own control address up? A socket file's presence on Linux/macOS. On
|
|
142
|
+
* Windows (#78) the address is a named pipe, which has no file to look at and must not be
|
|
143
|
+
* opened by a stat (that connects): the owner-validated probe answers instead. */
|
|
144
|
+
async function controlAddressPresent(socketPath: string): Promise<boolean> {
|
|
145
|
+
if (process.platform === "win32") return (await probeSocketLiveness(socketPath)) === "alive";
|
|
146
|
+
return existsSync(socketPath);
|
|
147
|
+
}
|
|
148
|
+
|
|
140
149
|
// ============================================================================
|
|
141
150
|
// Live control-socket discovery for entwurf_peers lives in the TS fact-provider
|
|
142
151
|
// (pi-extensions/lib/entwurf-fact-provider.ts → listEntwurfFacts), which the
|
|
@@ -219,7 +228,7 @@ class EntwurfSenderIdentityError extends Error {
|
|
|
219
228
|
}
|
|
220
229
|
}
|
|
221
230
|
|
|
222
|
-
function buildStrictPiSenderEnvelope(): SenderEnvelope {
|
|
231
|
+
async function buildStrictPiSenderEnvelope(): Promise<SenderEnvelope> {
|
|
223
232
|
const sessionId = process.env.PI_SESSION_ID?.trim();
|
|
224
233
|
const agentId = process.env.PI_AGENT_ID?.trim();
|
|
225
234
|
const cwd = process.cwd();
|
|
@@ -239,7 +248,7 @@ function buildStrictPiSenderEnvelope(): SenderEnvelope {
|
|
|
239
248
|
const socketPath = controlSocketPathIn(ENTWURF_DIR, sessionId as string);
|
|
240
249
|
const self = computeSelfAddressability({
|
|
241
250
|
origin: "pi-session",
|
|
242
|
-
socketAlive:
|
|
251
|
+
socketAlive: await controlAddressPresent(socketPath),
|
|
243
252
|
socketPathComputable: true,
|
|
244
253
|
});
|
|
245
254
|
return {
|
|
@@ -338,7 +347,7 @@ async function resolveAuthoritativeSender(
|
|
|
338
347
|
): Promise<AuthoritativeSelf | null> {
|
|
339
348
|
const sessionId = process.env.PI_SESSION_ID?.trim();
|
|
340
349
|
const agentId = process.env.PI_AGENT_ID?.trim();
|
|
341
|
-
const pi = sessionId && agentId && cwd ? buildStrictPiSenderEnvelope() : null;
|
|
350
|
+
const pi = sessionId && agentId && cwd ? await buildStrictPiSenderEnvelope() : null;
|
|
342
351
|
const marker = resolveTrustedMetaSenderIdentity({
|
|
343
352
|
markerPath: process.env.ENTWURF_META_SENDER_MARKER?.trim() || undefined,
|
|
344
353
|
});
|
|
@@ -559,7 +568,7 @@ server.tool(
|
|
|
559
568
|
// live socket). The old code synthesized the path and printed it as if
|
|
560
569
|
// it existed — a lie when the session has no --entwurf-control (SE-1).
|
|
561
570
|
const socketPath = controlSocketPathIn(ENTWURF_DIR, sender.sessionId);
|
|
562
|
-
const socketState =
|
|
571
|
+
const socketState = (await controlAddressPresent(socketPath)) ? "alive" : "expected";
|
|
563
572
|
extra.socketPath = socketPath;
|
|
564
573
|
extra.socketState = socketState;
|
|
565
574
|
lines.push(
|
|
Binary file
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": 1,
|
|
3
|
+
"platform": "win32",
|
|
4
|
+
"arch": "x64",
|
|
5
|
+
"file": "entwurf_win.node",
|
|
6
|
+
"sha256": "9837893521ec1d033913ca6cd1b705cb6e942e8d5c288948a1ed0ca0e28655e5",
|
|
7
|
+
"napiVersion": 8,
|
|
8
|
+
"builtWith": {
|
|
9
|
+
"node": "24.18.1",
|
|
10
|
+
"nodeGyp": "13.1.0",
|
|
11
|
+
"platformToolset": "v145",
|
|
12
|
+
"windowsTargetPlatformVersion": "10.0.26100.0",
|
|
13
|
+
"visualStudio": {
|
|
14
|
+
"displayName": "Visual Studio Enterprise 2026",
|
|
15
|
+
"installationVersion": "18.10.12224.181"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"source": {
|
|
19
|
+
"entwurf_win.c": "aa8a2994833d6dbbbf56c78687d42df52f645f3f35073943d897f58169219c31",
|
|
20
|
+
"binding.gyp": "be95dcfb152a91377a011aedbadd7943965ede251612394eacb405b20689f18e",
|
|
21
|
+
"package.json": "6ba65c909857f18a5d8e16b63fa55201b1d17261cc4afb1f2eea10514b2cac6a",
|
|
22
|
+
"package-lock.json": "d7dc3c48d003f2356d961972ad5515f192f2c846af8d0b551fa08d20ce155ede",
|
|
23
|
+
"build.mjs": "68120f85289e9598f440b1e0be506feb50a37897414d234ffc6698b754b2988e"
|
|
24
|
+
},
|
|
25
|
+
"provenance": {
|
|
26
|
+
"statement": "tracked C + declared upstream build glue",
|
|
27
|
+
"compiled": {
|
|
28
|
+
"entwurf_win.c": "tracked source",
|
|
29
|
+
"node-gyp/src/win_delay_load_hook.cc": {
|
|
30
|
+
"version": "13.1.0",
|
|
31
|
+
"sha256": "ec2357ffdf512151c21a52326ad3396aaa650b83e5c4a31153d216a155f68ecc",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"notice": "LICENSES/node-gyp.txt",
|
|
34
|
+
"licenseSha256": "662a1b0115251cfb29c6aed0f221f8847bc49c6365d1c53a62c9f4bccc2489c3"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"declaredLinkInputs": [
|
|
38
|
+
"advapi32.lib",
|
|
39
|
+
"kernel32.lib",
|
|
40
|
+
"node.lib (import library, Node v24.18.1 win-x64)",
|
|
41
|
+
"MSVC static CRT (/MT)"
|
|
42
|
+
],
|
|
43
|
+
"headers": "Node v24.18.1 headers tarball (MIT), declarations only"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@junghanacs/entwurf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
4
4
|
"description": "Visible sibling sessions across independent agent harnesses: launch in Herdr or tmux, reopen pi in tmux, and deliver messages by garden id; native bridges and Claude/Cortex ACP adapters.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
"files": [
|
|
42
42
|
"protocol.js",
|
|
43
43
|
"pi-extensions/",
|
|
44
|
+
"native/win32-x64/",
|
|
45
|
+
"LICENSES/node-gyp.txt",
|
|
44
46
|
"mcp/",
|
|
45
47
|
"scripts/",
|
|
46
48
|
"prompts/",
|
|
@@ -194,7 +196,7 @@
|
|
|
194
196
|
"check:core": "pnpm run check:toolchain && pnpm run check:vitest && pnpm run check:contracts",
|
|
195
197
|
"check:toolchain": "pnpm lint && pnpm typecheck",
|
|
196
198
|
"check:vitest": "./run.sh check-mux-fresh-call && ./run.sh check-acp-sdk-surface && ./run.sh check-pi-queue-oracle && ./run.sh check-tests-beside-behavior",
|
|
197
|
-
"check:contracts": "./run.sh check-demo-reproducibility && ./run.sh check-shell-quote && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-v3-record && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-harness-admission-parity && ./run.sh check-capability-bundle-reach && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-capability-source && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-node-floor-coherence && ./run.sh check-claude-floor-coherence && ./run.sh check-copilot-statusline && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-send && ./run.sh check-entwurf-v2-runner && ./run.sh check-control-send-receipt && ./run.sh check-compaction-send-guard && ./run.sh check-entwurf-resume-args && ./run.sh check-entwurf-v2-native-push && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-resume-launch-identity && ./run.sh check-mux-placement && ./run.sh check-mux-launch && ./run.sh check-mux-resume-call && ./run.sh check-mux-parent-artifact && ./run.sh check-mux-launcher-fence && ./run.sh check-entwurf-v2-visible-resume && ./run.sh check-control-socket-path && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-herdr-placement && ./run.sh check-herdr-fresh-call && ./run.sh check-herdr-supply && ./run.sh check-fresh-call-dispatch && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-native-push-adapter && ./run.sh check-native-push-register && ./run.sh check-auth-boundary && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-carrier-augment && ./run.sh check-acp-foreground-only && ./run.sh check-acp-set-model-readback && ./run.sh check-acp-vendor-rejection && ./run.sh check-codex-sender-identity && ./run.sh check-codex-native-push && ./run.sh check-pi-durable-beside && ./run.sh check-pi-durable-carrier && ./run.sh check-pi-durable-native-module",
|
|
199
|
+
"check:contracts": "./run.sh check-demo-reproducibility && ./run.sh check-shell-quote && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-v3-record && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-harness-admission-parity && ./run.sh check-capability-bundle-reach && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-capability-source && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-node-floor-coherence && ./run.sh check-claude-floor-coherence && ./run.sh check-copilot-statusline && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-send && ./run.sh check-entwurf-v2-runner && ./run.sh check-control-send-receipt && ./run.sh check-compaction-send-guard && ./run.sh check-entwurf-resume-args && ./run.sh check-entwurf-v2-native-push && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-resume-launch-identity && ./run.sh check-mux-placement && ./run.sh check-mux-launch && ./run.sh check-mux-resume-call && ./run.sh check-mux-parent-artifact && ./run.sh check-mux-launcher-fence && ./run.sh check-entwurf-v2-visible-resume && ./run.sh check-control-socket-path && ./run.sh check-win32-preview && ./run.sh check-pack-win32-carrier && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-herdr-placement && ./run.sh check-herdr-fresh-call && ./run.sh check-herdr-supply && ./run.sh check-fresh-call-dispatch && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-native-push-adapter && ./run.sh check-native-push-register && ./run.sh check-auth-boundary && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-carrier-augment && ./run.sh check-acp-foreground-only && ./run.sh check-acp-set-model-readback && ./run.sh check-acp-vendor-rejection && ./run.sh check-codex-sender-identity && ./run.sh check-codex-native-push && ./run.sh check-pi-durable-beside && ./run.sh check-pi-durable-carrier && ./run.sh check-pi-durable-native-module",
|
|
198
200
|
"check:hermetic": "./run.sh check-bridge-delivery && ./run.sh smoke-pi-attach && ./run.sh check-fresh-cut-gate && ./run.sh check-hook-launch-topology && ./run.sh check-meta-hook-session-switch && ./run.sh check-copilot-birth-hook && ./run.sh check-copilot-receive-arm && ./run.sh check-copilot-launch && ./run.sh check-pi-launch && ./run.sh check-codex-app-server-launch && ./run.sh check-meta-identity-consumers && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh check-meta-doctor-oracle && ./run.sh check-agy-permission-matrix && ./run.sh smoke-agy-statusline-state && ./run.sh smoke-copilot-statusline-state && ./run.sh smoke-copilot-mcp-state && ./run.sh smoke-omp-bridge-state && ./run.sh smoke-omp-mcp-state && ./run.sh smoke-omp-receive-state && ./run.sh check-omp-birth-hook && ./run.sh check-omp-receive-arm && ./run.sh check-omp-fresh-preflight && ./run.sh smoke-agy-hooks-state && ./run.sh smoke-pi-provider-state && ./run.sh smoke-user-scope-citizen && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-bridge-boot && ./run.sh check-probe-bridge-command && ./run.sh check-meta-facts && ./run.sh check-peer-facts && ./run.sh check-herdr-plugin && ./run.sh check-herdr-plugin-profile && ./run.sh check-herdr-plugin-build && ./run.sh check-herdr-runtime-bootstrap && ./run.sh check-herdr-activation && ./run.sh check-agy-sender-identity && ./run.sh check-acp-provider-surface && ./run.sh check-acp-stop-reason && ./run.sh check-acp-prompt-lifecycle && ./run.sh check-acp-usage-accounting && ./run.sh check-acp-launch-namespace && ./run.sh check-acp-stream-hooks && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-typing-call-fence && ./run.sh check-gate-manifests && ./run.sh check-release-gate-outcomes && ./run.sh check-probe-ordering && ./run.sh check-probe-cli-shim && ./run.sh check-acp-cortex && ./run.sh check-codex-bridge-identity && ./run.sh check-pi-bridge-sender && ./run.sh check-pi-mcp-register && ./run.sh check-pi-mcp-bridge && ./run.sh check-codex-birth-hook && ./run.sh smoke-codex-config-state && ./run.sh smoke-codex-birth && ./run.sh check-herdr-sandbox",
|
|
199
201
|
"check:package": "./run.sh check-install-surface && ./run.sh smoke-meta-install-state && ./run.sh smoke-agy-install-state && ./run.sh smoke-setup-verdict && ./run.sh check-pack-pin-matcher && ./run.sh check-pack-consumer-roots && ./run.sh check-package-source-routing && ./run.sh check-install-preflight && ./run.sh check-pack"
|
|
200
202
|
},
|
|
@@ -147,8 +147,12 @@ async function ensureControlDir(): Promise<void> {
|
|
|
147
147
|
await fs.mkdir(ENTWURF_DIR, { recursive: true });
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
// On Windows the control address is a named pipe (`\\.\pipe\…`, #78): the kernel owns the
|
|
151
|
+
// name and drops it with the last handle, so there is no file to unlink and none to sweep.
|
|
152
|
+
const PIPE_ADDRESSED = process.platform === "win32";
|
|
153
|
+
|
|
150
154
|
async function removeSocket(socketPath: string | null): Promise<void> {
|
|
151
|
-
if (!socketPath) return;
|
|
155
|
+
if (!socketPath || PIPE_ADDRESSED) return;
|
|
152
156
|
try {
|
|
153
157
|
await fs.unlink(socketPath);
|
|
154
158
|
} catch (error) {
|
|
@@ -162,6 +166,7 @@ async function removeSocket(socketPath: string | null): Promise<void> {
|
|
|
162
166
|
// alias-era artifacts. Runs once per startControlServer call. We only touch
|
|
163
167
|
// entries that are demonstrably dead — a live peer's socket survives.
|
|
164
168
|
async function gcStaleSockets(): Promise<void> {
|
|
169
|
+
if (PIPE_ADDRESSED) return;
|
|
165
170
|
let entries: import("node:fs").Dirent[];
|
|
166
171
|
try {
|
|
167
172
|
entries = await fs.readdir(ENTWURF_DIR, { withFileTypes: true });
|
|
@@ -24,9 +24,18 @@
|
|
|
24
24
|
* adapter keeps its own root policy: the pi side derives from HOME, the MCP
|
|
25
25
|
* bridge honours `ENTWURF_DIR`. What is unified is the grammar, not the policy.
|
|
26
26
|
*
|
|
27
|
-
*
|
|
27
|
+
* Windows (#78) has no socket files: the address is a named pipe,
|
|
28
|
+
* `\\.\pipe\entwurf-<sha256(canonRoot(dir))[:16]>-<gardenId>`. The digest keeps two
|
|
29
|
+
* store roots on one host apart without putting a path in the pipe namespace; it is not
|
|
30
|
+
* a new address axis (still the garden id plus the store root). The inverse grammar has
|
|
31
|
+
* no Windows half because nothing scans the pipe namespace: candidates come from records.
|
|
32
|
+
*
|
|
33
|
+
* Keep dependency-free except node builtins (`node:path`, and `node:crypto`/`node:fs`
|
|
34
|
+
* for the Windows root canonicalisation only).
|
|
28
35
|
*/
|
|
29
36
|
|
|
37
|
+
import { createHash } from "node:crypto";
|
|
38
|
+
import * as fs from "node:fs";
|
|
30
39
|
import * as path from "node:path";
|
|
31
40
|
|
|
32
41
|
/** Control-socket filename suffix. The stem is a bare garden id. */
|
|
@@ -51,9 +60,55 @@ export function defaultControlSocketDir(home) {
|
|
|
51
60
|
* @returns {string}
|
|
52
61
|
*/
|
|
53
62
|
export function controlSocketPathIn(dir, gardenId) {
|
|
63
|
+
if (process.platform === "win32") return win32PipeNameFor(dir, gardenId);
|
|
54
64
|
return path.join(dir, `${gardenId}${CONTROL_SOCKET_SUFFIX}`);
|
|
55
65
|
}
|
|
56
66
|
|
|
67
|
+
/** Every Windows control pipe this package names starts here. */
|
|
68
|
+
export const WIN32_PIPE_PREFIX = "\\\\.\\pipe\\entwurf-";
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The one spelling of a Windows store root, so Pi A, Pi B, the bridge and peer-facts
|
|
72
|
+
* derive the same pipe name from differently written paths. The nearest EXISTING ancestor
|
|
73
|
+
* of `path.win32.resolve(dir)` goes through `realpath` (8.3 names expanded, case from the
|
|
74
|
+
* filesystem); the components below it are appended as written. Separators become `\`,
|
|
75
|
+
* there is no trailing separator, and the whole string is lower-cased. Nothing is created
|
|
76
|
+
* or touched, so the control dir need not exist yet. A failure other than ENOENT throws.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} dir
|
|
79
|
+
* @param {(p: string) => string} [realpath]
|
|
80
|
+
* @returns {string}
|
|
81
|
+
*/
|
|
82
|
+
export function canonRootWin32(dir, realpath = fs.realpathSync.native) {
|
|
83
|
+
let head = path.win32.resolve(dir);
|
|
84
|
+
/** @type {string[]} */
|
|
85
|
+
const tail = [];
|
|
86
|
+
for (;;) {
|
|
87
|
+
try {
|
|
88
|
+
const joined = path.win32.join(realpath(head), ...tail);
|
|
89
|
+
return joined.replace(/[\\/]+$/, "").toLowerCase();
|
|
90
|
+
} catch (err) {
|
|
91
|
+
const parent = path.win32.dirname(head);
|
|
92
|
+
if (/** @type {{ code?: unknown }} */ (err).code !== "ENOENT" || parent === head) throw err;
|
|
93
|
+
tail.unshift(path.win32.basename(head));
|
|
94
|
+
head = parent;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Forward grammar on Windows: `\\.\pipe\entwurf-<16 hex>-<gardenId>`.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} dir
|
|
103
|
+
* @param {string} gardenId
|
|
104
|
+
* @param {(p: string) => string} [realpath]
|
|
105
|
+
* @returns {string}
|
|
106
|
+
*/
|
|
107
|
+
export function win32PipeNameFor(dir, gardenId, realpath) {
|
|
108
|
+
const digest = createHash("sha256").update(canonRootWin32(dir, realpath), "utf8").digest("hex").slice(0, 16);
|
|
109
|
+
return `${WIN32_PIPE_PREFIX}${digest}-${gardenId}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
57
112
|
/**
|
|
58
113
|
* Inverse grammar: a socket filename back to its garden id, or `null` when the
|
|
59
114
|
* name does not carry the suffix.
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import * as net from "node:net";
|
|
18
|
+
import { win32Native } from "./win32-native.js";
|
|
18
19
|
|
|
19
20
|
// `sender` carries the transparency envelope (agentId / sessionId / cwd /
|
|
20
21
|
// timestamp). All four fields are mandatory whenever `sender` is present —
|
|
@@ -199,6 +200,7 @@ export async function sendRpcCommand(
|
|
|
199
200
|
options: RpcClientOptions = {},
|
|
200
201
|
): Promise<{ response: RpcResponse }> {
|
|
201
202
|
const { timeout = 5000 } = options;
|
|
203
|
+
if (process.platform === "win32") return await sendPipeRpcCommand(socketPath, command, timeout);
|
|
202
204
|
|
|
203
205
|
return new Promise((resolve, reject) => {
|
|
204
206
|
const socket = net.createConnection(socketPath);
|
|
@@ -277,3 +279,33 @@ export async function sendRpcCommand(
|
|
|
277
279
|
});
|
|
278
280
|
});
|
|
279
281
|
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* The Windows client (#78). The addon makes one owner-validated connect, writes the request
|
|
285
|
+
* line and returns the first response line, all inside one deadline; a timeout is
|
|
286
|
+
* `ETIMEDOUT`, a pipe owned by anyone but this token's expected owner is `EPERM` before any
|
|
287
|
+
* byte. The protocol is request-bound on its own connection, so that first line IS the
|
|
288
|
+
* answer: anything other than this command's response is refused, never waited past.
|
|
289
|
+
*/
|
|
290
|
+
async function sendPipeRpcCommand(
|
|
291
|
+
pipeName: string,
|
|
292
|
+
command: RpcCommand,
|
|
293
|
+
timeout: number,
|
|
294
|
+
): Promise<{ response: RpcResponse }> {
|
|
295
|
+
const { responseLine } = await win32Native().pipeRpc(
|
|
296
|
+
pipeName,
|
|
297
|
+
JSON.stringify(command),
|
|
298
|
+
Math.max(0, Math.floor(timeout)),
|
|
299
|
+
);
|
|
300
|
+
let msg: unknown;
|
|
301
|
+
try {
|
|
302
|
+
msg = JSON.parse(responseLine.trim());
|
|
303
|
+
} catch {
|
|
304
|
+
throw new Error(`control pipe answered ${command.type} with a line that is not JSON`);
|
|
305
|
+
}
|
|
306
|
+
const response = msg as RpcResponse & { type?: unknown };
|
|
307
|
+
if (response?.type !== "response" || response.command !== command.type) {
|
|
308
|
+
throw new Error(`control pipe answered something other than the ${command.type} response`);
|
|
309
|
+
}
|
|
310
|
+
return { response };
|
|
311
|
+
}
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
import { execFileSync } from "node:child_process";
|
|
54
54
|
import * as crypto from "node:crypto";
|
|
55
55
|
import * as fs from "node:fs";
|
|
56
|
+
import { createRequire } from "node:module";
|
|
56
57
|
import * as os from "node:os";
|
|
57
58
|
import * as path from "node:path";
|
|
58
59
|
import { generateSessionId, SESSION_ID_RE } from "./session-id.js";
|
|
@@ -1017,6 +1018,7 @@ const O_NONBLOCK_FLAG = typeof fs.constants.O_NONBLOCK === "number" ? fs.constan
|
|
|
1017
1018
|
* Shaping a message is the job of the caller that knows what the answer is FOR.
|
|
1018
1019
|
*/
|
|
1019
1020
|
export function readStoreRecordFile(file: string): string {
|
|
1021
|
+
if (process.platform === "win32") return win32RecordReader()(file).toString("utf8");
|
|
1020
1022
|
if (O_NOFOLLOW_FLAG === 0) {
|
|
1021
1023
|
throw new MetaRecordError(
|
|
1022
1024
|
`cannot read meta-record ${path.basename(file)} safely: this platform exposes no O_NOFOLLOW, so a record ` +
|
|
@@ -1039,6 +1041,18 @@ export function readStoreRecordFile(file: string): string {
|
|
|
1039
1041
|
}
|
|
1040
1042
|
}
|
|
1041
1043
|
|
|
1044
|
+
/**
|
|
1045
|
+
* Windows has no `O_NOFOLLOW` (libuv defines it as 0), so the same three decisions — a
|
|
1046
|
+
* final-component reparse point refused as `ELOOP`, a non-regular entry as
|
|
1047
|
+
* `ENTWURF_ENOTREG`, both on the handle the read uses — are made by the preview addon
|
|
1048
|
+
* (#78). It is reached only here and only on win32, through `createRequire`, so the
|
|
1049
|
+
* installers that copy this file without its siblings never load it on Linux or macOS.
|
|
1050
|
+
*/
|
|
1051
|
+
function win32RecordReader(): (file: string) => Buffer {
|
|
1052
|
+
const native = createRequire(import.meta.url)("./win32-native.js") as typeof import("./win32-native.js");
|
|
1053
|
+
return native.win32ReadNoFollow;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1042
1056
|
/** The reader every store-WIDE scan pairs with {@link readActiveStoreEntries}. The
|
|
1043
1057
|
* caller's entry kind decides the POLICY — whether a non-regular entry is a defect
|
|
1044
1058
|
* (certification), a diagnostic (listing) or a non-candidate (the rival scan) — because
|
|
@@ -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
|
-
|
|
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
|
+
}
|