@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
package/BASELINE.md
CHANGED
|
@@ -23,7 +23,8 @@ or **unverified**. Since 0.20.0 the table also carries two qualifiers:
|
|
|
23
23
|
runner is not a physical host, so bare **certified** still means a physical-host
|
|
24
24
|
doctor green; and **NOT CERTIFIED — pending physical host** marks a rail whose
|
|
25
25
|
missing evidence only a physical host can supply. That is an evidence boundary,
|
|
26
|
-
not **UNSUPPORTED** — the deliberate never, which today
|
|
26
|
+
not **UNSUPPORTED** — the deliberate never, which today names the native-Windows surfaces
|
|
27
|
+
outside the experimental preview (rows below).
|
|
27
28
|
|
|
28
29
|
| Surface | Verdict | Evidence |
|
|
29
30
|
|---|---|---|
|
|
@@ -43,8 +44,11 @@ not **UNSUPPORTED** — the deliberate never, which today is native Windows only
|
|
|
43
44
|
| Doctor fail-closed on unreadable environ | Linux measured; Darwin unmeasured | Copilot launch-flag and omp identity-carrier: missing `/proc/<pid>/environ` as predicate INPUT is `UNVERIFIABLE` (non-green), not a benign note. omp distinguishes `pgrep` exit 1 (absent) from exit 2 (enumeration failed); copilot distinguishes ENOENT/ESRCH (gone) from other errno (read denied). |
|
|
44
45
|
| setup on uncertified platform with harness present | named non-green | All 13 harness units ask `harness_rail_certified_platform()`; uncertified → FAIL whose wording is not "install failed". Harness-absent Darwin setup (the CI cell) can still be computed green. |
|
|
45
46
|
| macOS physical-host probe | shipped, unrun on a Mac | `scripts/raw-macos-measure/` — `/bin/sh`, cells M1–M9, 3.5 s; needs neither entwurf install nor login. Still unmeasured: Darwin foreign-process environ (`ps -Eww`) and whether macOS `python3` is a CommandLineTools stub. |
|
|
46
|
-
| WSL2 |
|
|
47
|
-
|
|
|
47
|
+
| WSL2 Entwurf-only install + zero-token Pi citizen (GitHub Actions `windows-2025` runner, not a physical machine) | shape-only | Experimental, non-voting CI (not read by the release oracle): 2026-10-18, `.github/workflows/wsl2-install-surface.yml` run 39098773010 @ `b03d646` ([#78 receipt](https://github.com/junghan0611/entwurf/issues/78#issuecomment-6245947301)). Guest kernel `6.18.40.1-microsoft-standard-WSL2`, host `wsl --list --verbose` VERSION 2, guest fstype quoted raw as `ext2/ext3`. The run's packed tgz digest is equal across producer, host and guest; guest Node 24.18.1; harness-absent `setup` green with protected auth/harness state unchanged. Pi 1.3.0 (sha512 pinned to the registry integrity) wired by `setup`: one V3 record whose filename is its gid, a gid-keyed control socket with a native connect, `peer-facts` alive; only `get_state` was sent and no `agent_start` was observed. Pi creating an empty `auth.json` is not a login. It certifies no harness rail. Linux-guest runtime evidence only: not native-Windows evidence, and not a substitute for the required Linux CI jobs. |
|
|
48
|
+
| WSL2 delivery, non-pi harnesses, vendor turn, physical user machine | unverified | No receipt. Authentication and model turns were deliberately excluded from the CI cells' scope (not a CI impossibility); non-pi harnesses, delivery, fresh/resume and physical machines are unmeasured. |
|
|
49
|
+
| native Windows experimental preview: Pi citizen + `entwurf_v2` delivery (GitHub Actions `windows-2025` x64 runner, Node 24.18.1, Pi 1.3.0; 0.39.0 candidate, not yet released; release and npm publication pending; the evidence below is the feature-branch CI artifact at `3b65bdc`) | shape-only | Experimental, non-voting CI (not read by the release oracle): 2026-10-18, `.github/workflows/windows-native-preview.yml` run 39123555360 @ `3b65bdc`, with the required `ci.yml` four jobs green at the same SHA (run 39123555224). Build: MSVC (VS 2026, toolset v145, SDK 10.0.26100.0), two builds byte-identical (`entwurf_win.node` sha256 `9837893521ec…`), imports advapi32 + kernel32 with `node.exe` the only delay-load; node-gyp 13.1.0 and the Node headers/`node.lib` pinned. The pack carries exactly the addon and its manifest; npm installed it into a sandboxed consumer prefix; Pi came from a tarball checked against its pinned SRI. Six addon cells PASS (final-component reparse and non-regular refusals on the read handle, no connect through a symlink to a pipe, one-deadline timeout with reclaim, pipe-name stability and loader refusals). B1: two `pi --entwurf-control` citizens, one V3 record each, record-derived pipes alive through the product probe, `peer-facts` alive, graceful exit with both addresses dead afterwards. D1: A's model called `entwurf_v2` toward B's garden id with a nonce; A's receipt `control-socket → sent`, B's model context and B's transcript `entwurf-message` entry from A carry it. The model is an offline pi-ai `fauxProvider`: no vendor turn and no comprehension claim. The transcript fact is the harness's assertion; the raw transcript is not archived. The run used the elevated Administrators owner branch, so user-SID-only privacy is not measured. Pi's `peerDependencies` warning was observed, not waived. Not evidence for any other harness on Windows. |
|
|
50
|
+
| native Windows: own pre-existing pipe-name birth refusal, record link/rename syscalls, descendant cleanup after a forced kill, cross-user, mixed elevation, remote callers, user-SID-only privacy, physical user machine | unverified | No receipt; the preview cells name these as unmeasured. |
|
|
51
|
+
| native Windows: other harnesses, `setup` and the `.sh` bins, fresh/resume, mux/Herdr/ACP, ARM64, records over 16 MiB, reparse-backed store folders | UNSUPPORTED | Outside the preview's scope by decision; the preview loader refuses an install without the Windows addon by name. |
|
|
48
52
|
|
|
49
53
|
Notes the table cannot carry without becoming prose again:
|
|
50
54
|
|
|
@@ -65,7 +69,8 @@ Notes the table cannot carry without becoming prose again:
|
|
|
65
69
|
- **macOS evidence is split, not a support claim.** The Entwurf-only install surface is
|
|
66
70
|
CERTIFIED (CI) (row above); the harness rails are NOT CERTIFIED — pending
|
|
67
71
|
physical host (rental remains unscheduled). The package-level `os` field stays
|
|
68
|
-
unrestricted, and native Windows is
|
|
72
|
+
unrestricted, and native Windows is an experimental preview (0.39.0 candidate, not
|
|
73
|
+
yet released; release and npm publication pending) with its own named gaps (rows above), not a supported platform.
|
|
69
74
|
|
|
70
75
|
**Operator acceptance rule:** on a claimed Claude host, reinstall from the released
|
|
71
76
|
artifact, restart every old Claude process, open a new session, and run the doctor
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,93 @@ All notable changes to this project will be documented here. Format follows [Kee
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.39.0 - 2026-10-18
|
|
8
|
+
|
|
9
|
+
### Native Windows (experimental preview, #78)
|
|
10
|
+
|
|
11
|
+
- **A native-Windows Pi citizen, shape-only.** On Windows x64 the package carries a small
|
|
12
|
+
N-API addon (`native/win32-x64/entwurf_win.node` plus its `manifest.json`), loaded by
|
|
13
|
+
`pi-extensions/lib/win32-native.js` only after the manifest's schema, platform, arch,
|
|
14
|
+
file, sha256 and `napiVersion` 8 agree with the bytes (`ENTWURF_WIN32_UNSUPPORTED` /
|
|
15
|
+
`ENTWURF_WIN32_NATIVE_DRIFT` otherwise). It supplies the two primitives POSIX gets from
|
|
16
|
+
the OS: the record read refuses a final-component reparse point (`ELOOP`) and a
|
|
17
|
+
non-regular file (`ENTWURF_ENOTREG`) on the read handle itself, and the control
|
|
18
|
+
address is a named pipe `\\.\pipe\entwurf-<sha256(root)[:16]>-<gid>` whose
|
|
19
|
+
probe and RPC check the server's owner SID and connect at identification level. The
|
|
20
|
+
POSIX `O_NOFOLLOW` and ancestor policies are unchanged, and non-Windows hosts take the
|
|
21
|
+
existing POSIX branches of the shared modules; each axis's own CI and LIVE gates are
|
|
22
|
+
that evidence, not a claim of zero impact.
|
|
23
|
+
- **Wiring.** `meta-session`, `control-socket-path`, `socket-probe`, `socket-discovery`,
|
|
24
|
+
`entwurf-control-rpc`, `entwurf-control` and the bridge take the win32 branch through
|
|
25
|
+
that loader; pipe names come from records and are never stat-ed; nothing unlinks or
|
|
26
|
+
GCs a pipe on win32.
|
|
27
|
+
- **Carrier and provenance.** The release carrier is produced in CI (MSVC, `/Brepro`, two builds
|
|
28
|
+
compared byte-for-byte, imports exactly advapi32 + kernel32 with `node.exe` the only
|
|
29
|
+
delay-load, node-gyp 13.1.0 and the Node headers/`node.lib` pinned) and ships with
|
|
30
|
+
node-gyp's MIT notice (`LICENSES/node-gyp.txt`). `check-pack` reports the carrier as
|
|
31
|
+
`absent`, `pair-broken`, `invalid` or `present <sha>` through the loader's own verifier.
|
|
32
|
+
A Linux checkout pack is addon-less, so this release's candidate is the preview
|
|
33
|
+
workflow's pack at the prepared SHA (release skill M3w), not a local `npm pack`.
|
|
34
|
+
- **Measured scope.** windows-native-preview run 39123555360 @ `3b65bdc` on a GitHub
|
|
35
|
+
Actions `windows-2025` x64 runner, Node 24.18.1, Pi 1.3.0: six addon cells, then two
|
|
36
|
+
`pi --entwurf-control` citizens (one V3 record each, pipes alive through the product
|
|
37
|
+
probe, `peer-facts`, graceful exit with both addresses dead) and one garden-id
|
|
38
|
+
`entwurf_v2` delivery between them (`control-socket → sent`, B's model context and
|
|
39
|
+
transcript entry from A), with an offline pi-ai `fauxProvider`: no vendor turn.
|
|
40
|
+
- **Limits.** Experimental and shape-only; not a supported platform. UNSUPPORTED: other
|
|
41
|
+
harnesses, `setup` and the `.sh` bins, fresh/resume, mux/Herdr/ACP, ARM64, records over
|
|
42
|
+
16 MiB, reparse-backed store folders. Unmeasured: the own pre-existing pipe-name birth
|
|
43
|
+
refusal, record link/rename syscalls, descendant cleanup after a forced kill,
|
|
44
|
+
cross-user, mixed elevation and remote callers, user-SID-only privacy (the runs took
|
|
45
|
+
the elevated Administrators owner branch), worker/environment teardown with a pipe
|
|
46
|
+
call in flight, and any physical user machine. Rows in BASELINE.
|
|
47
|
+
|
|
48
|
+
### Verification
|
|
49
|
+
|
|
50
|
+
- **Windows workflows (#78).** `windows-native-probe.yml`, `-e1a.yml`, `-e1b.yml` and
|
|
51
|
+
`-preview.yml` are experimental and non-voting; their push triggers name the feature
|
|
52
|
+
branch, so on main they run only by dispatch. The release oracle still reads only the
|
|
53
|
+
four required `ci.yml` jobs. New gates `check-win32-preview` and
|
|
54
|
+
`check-pack-win32-carrier` (in `check:contracts`); one new mutant lane, `win32-preview`
|
|
55
|
+
(19 claims), and the carrier's 2 claims appended to the existing `pack-install` lane.
|
|
56
|
+
The committed set is now 1000 mutants across 73 lanes.
|
|
57
|
+
- **Experimental WSL2 workflow (#78).** `.github/workflows/wsl2-install-surface.yml`
|
|
58
|
+
installs this run's packed tgz inside a real WSL2 guest on a GitHub-hosted Windows runner,
|
|
59
|
+
runs a harness-absent `setup`, then births a zero-token Pi citizen (record, socket,
|
|
60
|
+
`peer-facts`; `get_state` only). It is not a release axis: the release oracle reads only
|
|
61
|
+
`ci.yml`, whose four required jobs are unchanged. It runs when the workflow file changes
|
|
62
|
+
on main, or by manual dispatch. No WSL2 support, harness rail or physical-machine claim.
|
|
63
|
+
|
|
64
|
+
### Release boundary and verification
|
|
65
|
+
|
|
66
|
+
- **Implementation commits.** WSL2 `afcda79`..`9b492a4` (landed `86f0eb5`); native
|
|
67
|
+
Windows `95933c7`..`5da8f12`, landed with the carrier supply contract `32fcc1c`
|
|
68
|
+
(exact push CI 39128022109, four jobs green). The runtime proof stays on `3b65bdc`'s
|
|
69
|
+
own SHA. Coordinator-routed independent reviews (primary and supplemental) closed
|
|
70
|
+
every shipped implementation leaf.
|
|
71
|
+
- Kill-power re-measured at this shipped SHA: none (all 1000 committed claims not
|
|
72
|
+
re-proved, #144).
|
|
73
|
+
- Earlier-candidate PARTIAL (operator-attested, not this SHA): head 864e026877ca surface
|
|
74
|
+
200536056020 -- `["bash","run.sh","check-win32-preview"]` 19/19 receipt b4de9e681dfb
|
|
75
|
+
- Earlier-candidate PARTIAL (operator-attested, not this SHA): head 864e026877ca surface
|
|
76
|
+
200536056020 -- `["bash","run.sh","check-pack-win32-carrier"]` 2/2 receipt
|
|
77
|
+
c51ef11b84a6
|
|
78
|
+
- Earlier-candidate PARTIAL (operator-attested, not this SHA): head 3e527cdbea81 surface
|
|
79
|
+
8f5cc4c34473 -- `["bash","run.sh","check-node-floor-coherence"]` 2/2 receipt
|
|
80
|
+
6c0ec40a291f
|
|
81
|
+
- **Static/LIVE split.** Local P4/M0 `check:full` is not run on this operator host (its
|
|
82
|
+
FULL includes Herdr checks prohibited here) and is not counted as passed: the whole
|
|
83
|
+
static floor is owed to this prepared SHA's exact push CI.
|
|
84
|
+
- **Cut blockers.** VERIFY §3.4 (strict `doctor-meta-bridge` on an installed 0.39.0
|
|
85
|
+
Claude host in a new post-install session) and §3.7 (three agy doctors and a same-gid
|
|
86
|
+
native-push reply in a new operator conversation) need the operator and are not met by
|
|
87
|
+
this record; no tag or GitHub release until they are or GLG decides otherwise. npm
|
|
88
|
+
publication is outside the current authorization and was not performed; it needs a
|
|
89
|
+
separate GLG GO.
|
|
90
|
+
- **Herdr follows core.** `plugins/herdr/runtime-lock.json` stays on the closed
|
|
91
|
+
`herdr-checkout` source (comment re-dated only); no plugin manifest version, registry
|
|
92
|
+
pin or snapshot changes in this release.
|
|
93
|
+
|
|
7
94
|
## 0.38.1 - 2026-10-17
|
|
8
95
|
|
|
9
96
|
### pi-durable
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
(The MIT License)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2012 Nathan Rajlich <nathan@tootallnate.net>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person
|
|
6
|
+
obtaining a copy of this software and associated documentation
|
|
7
|
+
files (the "Software"), to deal in the Software without
|
|
8
|
+
restriction, including without limitation the rights to use,
|
|
9
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the
|
|
11
|
+
Software is furnished to do so, subject to the following
|
|
12
|
+
conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be
|
|
15
|
+
included in all copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
18
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
19
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
20
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
21
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
22
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
23
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
24
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -78,8 +78,20 @@ Plain MCP registration alone does not make a garden citizen.
|
|
|
78
78
|
installation is **CERTIFIED (CI)**, weaker than a physical-host doctor; its native
|
|
79
79
|
harness rails, ACP and mux remain **NOT CERTIFIED — pending physical host**.
|
|
80
80
|
Detected harness wiring on Darwin can therefore be written while setup stays
|
|
81
|
-
non-green.
|
|
82
|
-
|
|
81
|
+
non-green. WSL2: a CI runner (experimental) measured the Entwurf-only install and a
|
|
82
|
+
zero-token Pi citizen; general, physical-machine or harness-parity WSL2 support is **not
|
|
83
|
+
declared**, and delivery, other harnesses and physical machines are unmeasured. Native
|
|
84
|
+
Windows has an **experimental first preview**, a CI-built x64 addon on `main` for the
|
|
85
|
+
0.39.0 candidate (not yet released; release and npm publication pending); the registry's
|
|
86
|
+
0.38.1 has no Windows addon. On a GitHub Actions `windows-2025` x64 runner with Node
|
|
87
|
+
24.18.1 and Pi 1.3.0, a CI-built artifact was installed, wired with `pi install`, birthed
|
|
88
|
+
two Pi citizens and
|
|
89
|
+
delivered one garden-id `entwurf_v2` message between them, with an offline scripted model
|
|
90
|
+
and no vendor turn. Every other native-Windows surface (other harnesses, `setup` and the
|
|
91
|
+
`.sh` bins, fresh/resume, mux/Herdr/ACP, physical machines) is **not declared**; the
|
|
92
|
+
measured scope and its named gaps are in [BASELINE](./BASELINE.md#release-host-baseline).
|
|
93
|
+
The package has no npm `os` restriction; that is not certification of every platform's
|
|
94
|
+
harness rails.
|
|
83
95
|
|
|
84
96
|
### Native durable (pi-durable)
|
|
85
97
|
|
package/VERIFY.md
CHANGED
|
@@ -276,14 +276,16 @@ caller/target/cross-harness LIVE are owed by `smoke-pi-durable-fresh-live`, a re
|
|
|
276
276
|
| Linux artifact consumer | required `check-install-container` CI job against one preserved candidate | Fixtures prove package/oracle shape, not a real Claude lifecycle. |
|
|
277
277
|
| Exact release commit | the exact SHA's push run: all four required CI jobs green (the qualification body is not read, #144) | A different green SHA is not transferable evidence. |
|
|
278
278
|
| macOS Entwurf-only install | required `macos-install-surface` CI job at the exact release SHA | Shape-only, CERTIFIED (CI): certifies the Entwurf-only install path on macOS only — never Claude/Copilot/OMP/agy wiring, the pi rail, ACP or mux. Promoted from non-voting (`continue-on-error`) to required in 0.20.0 after its first CERTIFIED (CI) green on a GitHub Actions `macos-latest` runner (not a physical host): run 34303884286 @ `70eda03` (2026-09-09), 65s, all 10 steps success — the packed candidate installed clean, `entwurf --help` and `entwurf check-bridge` ran (the latter listing the seven `entwurf-bridge` MCP tools of that date; eight today), an all-harness-absent `entwurf setup` reached `result: green` across the five documented SKIP probes (`pi`/`claude`/`agy`/`copilot`/`omp`), and five checked paths (`.pi`, `settings.json`, `.gemini`, `.copilot`, `auth.json.bak`) plus a byte-identical `auth.json` proved zero harness/credential writes. A macOS runner failure now blocks every release cut, the same as any other required job. |
|
|
279
|
+
| Native-Windows carrier (carrier-bearing release, from 0.39.0) | one `windows-native-preview.yml` dispatch at the exact prepared SHA after its four `ci.yml` jobs are green: accepted build receipt (two byte-identical MSVC builds, exact import set), `pack` printing `present`, eight Windows addon/citizen cells on the installed tarball; that run's `win32-preview-tgz` is the release candidate, retrieved (never packed locally) with its `SHA256SUMS`, the Windows-printed install hash and the downloaded hash equal, then accepted by the Linux `check-install-container` unchanged (release skill M3w) | Experimental, shape-only, non-voting for `ci.yml`: no vendor turn, x64 runner only, named gaps in BASELINE. The four `ci.yml` jobs (incl. macOS) saw an addon-less pack, so the carrier-bearing tarball's macOS install is unmeasured. Artifacts expire after 14 days; the preserved copy is the durable one. |
|
|
279
280
|
| LIVE runtime | `LIVE=1 ./run.sh release-gate <scratch> --cut` plus any shipped on-demand backend axis | `--cut` enforces `SKIP=0`; a red wired gate blocks the cut. |
|
|
280
281
|
| Native Claude host | installed strict doctor against a new real session | Missing live join is `NOT CERTIFIED`, not a fixture PASS. |
|
|
281
282
|
| Native agy host | three doctors plus conversation-id-gated native-push round trip | Aggregate release-gate does not own an agy conversation id. |
|
|
282
283
|
| Native Codex host | caller-seat topology accepted 2026-09-16 (56 assertions, exit 0); caller-DIRECTORY axis accepted the same day (65 assertions, exit 0) | Operator-owned birth/MCP/statusline/terminal-title atoms and the on-demand four-coordinate chain are measured on Linux at Codex 0.153.4: the app-server alone in session A (`$30/@41/%45`, pid 737636), and initial Pi / omitted-placement fresh Codex / Codex-opened outbound Pi all in a different session S (`$2`, `@46`/`@47`/`@48`; `@58`/`@59`/`@61` in the lane C run); exact callbacks and addressed delivery both ways. A ≠ S is the load-bearing half — the app-server's inherited `TMUX` names A, so the outbound Pi landing in S can only have come from the caller's own pane title, and its receipt records `seat-source=codex-title-anchor`. **#95 lane C added the second axis to the same card: WHERE the sibling starts.** Hop 1 joins four authorities that cannot borrow from each other — the pane's `#{pane_current_path}`, the vendor rollout's `session_meta.cwd`, the Entwurf record and the requested scratch — and hop 1's leg requests a cwd while hop 2 requests NONE, so hop 2's directory can only come from the Codex caller's own record; both are asserted different from the app-server's live `/proc/<pid>/cwd`. 65 and 56 are different contracts, not a regression. The 2026-09-12 fixed-home chain is PAST CONTRACT (#95 D1); `DELIVERY.md` owns every receipt and digest. Placement beside a TUI whose thread nobody named is unsupported and unclaimed. |
|
|
283
284
|
|
|
284
285
|
The repo-local `entwurf-release` skill owns the `land → prepare → make → publish`
|
|
285
|
-
state machine. Each mode is a separate GLG authorization. Preserve one candidate
|
|
286
|
-
|
|
286
|
+
state machine. Each mode is a separate GLG authorization. Preserve one candidate
|
|
287
|
+
(for a carrier-bearing release, the one retrieved from the preview run, never a local
|
|
288
|
+
pack), accept and publish those exact bytes without repacking, and record its digest and
|
|
287
289
|
consumer image outside this standing protocol.
|
|
288
290
|
|
|
289
291
|
### Verifying the two capabilities a gate cannot fully judge
|
|
@@ -385,6 +387,13 @@ not certified, a detected harness is named non-green (FAIL) whose wording is
|
|
|
385
387
|
not an install failure: the wiring WAS written, but the rail is NOT CERTIFIED —
|
|
386
388
|
pending physical host. That FAIL is the honest setup verdict, not a skip.
|
|
387
389
|
|
|
390
|
+
WSL2: a separate experimental workflow (`.github/workflows/wsl2-install-surface.yml`)
|
|
391
|
+
measures the Entwurf-only install and a zero-token Pi citizen inside a real WSL2 guest on
|
|
392
|
+
a GitHub-hosted Windows runner. The release oracle does not read it
|
|
393
|
+
(`verify-exact-ci.sh` selects only `ci.yml` runs), so it is not a release axis and a red
|
|
394
|
+
there blocks no cut. It runs when that workflow file changes on main, or by manual
|
|
395
|
+
dispatch. Recorded in [BASELINE.md](./BASELINE.md).
|
|
396
|
+
|
|
388
397
|
### 1.1 Variables (optional)
|
|
389
398
|
|
|
390
399
|
```bash
|
|
@@ -518,7 +527,7 @@ Pass: user/assistant turns accumulate normally; the transcript is not broken/emp
|
|
|
518
527
|
The minimum passing bar:
|
|
519
528
|
|
|
520
529
|
1. **Deterministic floor green:** `pnpm run check:full` passes (the core tier plus the hermetic-integration and package/install tiers, incl. `check-pack`), in the exact-SHA CI push run of the same prepared candidate (criterion 2's run). The qualification body is not a criterion (#144).
|
|
521
|
-
2. **All four required CI jobs green on the exact release commit:** `check`, `install-surface`, the required Linux `artifact-consumer`, and `macos-install-surface` — promoted from non-voting to required in 0.20.0 (see the release acceptance axes table above for what its green run does and does not certify); preserve the Linux artifact-consumer's tarball digest and image identity. A macOS runner failure now blocks the cut like any other required job.
|
|
530
|
+
2. **All four required CI jobs green on the exact release commit:** `check`, `install-surface`, the required Linux `artifact-consumer`, and `macos-install-surface` — promoted from non-voting to required in 0.20.0 (see the release acceptance axes table above for what its green run does and does not certify); preserve the Linux artifact-consumer's tarball digest and image identity. For a carrier-bearing release those CI tarballs are addon-less and are NOT the release candidate: the exact release bytes are the M3w-retrieved preview tarball, accepted separately by the eight Windows cells and the Linux container (axes row "Native-Windows carrier"). The four-job floor, macOS included, is unchanged. A macOS runner failure now blocks the cut like any other required job.
|
|
522
531
|
3. **Live floor MUST green:** `LIVE=1 ./run.sh release-gate <dir> --cut` exits 0 reporting `MUST PASS=N FAIL=0 SKIP=0`; with `--cut` a single SKIP is red, so the exit code itself now carries this condition. A BEHAVIOR FAIL is advisory, not blocking.
|
|
523
532
|
4. **Native-host doctor green where the Claude meta-bridge is claimed:** a new post-install Claude session exists, live evidence is present, and the installed `doctor-meta-bridge` exits 0. `NOT CERTIFIED` is a release failure for that host, not a skip.
|
|
524
533
|
5. **Honest self-recognition:** the bridged model identifies its actual harness/backend, lists `entwurf-bridge` as the single MCP server with its eight current tools, and presents a backend-native (not normalized) tool surface.
|
package/docs/setup-clean-host.md
CHANGED
|
@@ -18,7 +18,9 @@ evidence split, and its three states must not collapse into "macOS is supported"
|
|
|
18
18
|
ACP turn, mux. Wiring may be written; that is not a rail receipt. The remaining
|
|
19
19
|
unmeasured Darwin doctor reason is per-process environment DISCOVERY
|
|
20
20
|
(start-key is portable).
|
|
21
|
-
- **UNSUPPORTED** — native Windows
|
|
21
|
+
- **UNSUPPORTED** — native Windows `setup` and its harness rails. macOS is never this.
|
|
22
|
+
An experimental native-Windows Pi preview (0.39.0 candidate, not yet released; release
|
|
23
|
+
and npm publication pending) exists outside `setup`; see [BASELINE](../BASELINE.md#release-host-baseline).
|
|
22
24
|
|
|
23
25
|
## Requirements
|
|
24
26
|
|
|
@@ -84,6 +84,7 @@ import { probeNativeSenderAlive, reconcileSenderIdentityClaims, resolveCodexRequ
|
|
|
84
84
|
import { applyOmpBridgeChildRootPolicy, defaultMetaMailboxDir, defaultMetaSessionsDir, makeStoreRecordReader, readActiveStoreEntries, readMetaIdentityByGardenId, readMetaInbox, readMetaReceiverMarker, readMetaSenderMarker, requireBackend, } from "../../../pi-extensions/lib/meta-session.js";
|
|
85
85
|
import { RESUME_CALL_REJECT_HINT, resumeCall } from "../../../pi-extensions/lib/mux-resume-call.js";
|
|
86
86
|
import { registerNativeConversation } from "../../../pi-extensions/lib/native-push/register.js";
|
|
87
|
+
import { probeSocketLiveness } from "../../../pi-extensions/lib/socket-probe.js";
|
|
87
88
|
const HOME = os.homedir();
|
|
88
89
|
// ============================================================================
|
|
89
90
|
// OMP root policy — FIRST, before any lazy default-root consumer (#87 B1).
|
|
@@ -110,6 +111,14 @@ catch (err) {
|
|
|
110
111
|
// Directory SOURCE is this adapter's own policy — the bridge honours an explicit
|
|
111
112
|
// ENTWURF_DIR override the pi side does not. The path GRAMMAR is the shared leaf.
|
|
112
113
|
const ENTWURF_DIR = process.env.ENTWURF_DIR ?? defaultControlSocketDir(HOME);
|
|
114
|
+
/** Is this session's own control address up? A socket file's presence on Linux/macOS. On
|
|
115
|
+
* Windows (#78) the address is a named pipe, which has no file to look at and must not be
|
|
116
|
+
* opened by a stat (that connects): the owner-validated probe answers instead. */
|
|
117
|
+
async function controlAddressPresent(socketPath) {
|
|
118
|
+
if (process.platform === "win32")
|
|
119
|
+
return (await probeSocketLiveness(socketPath)) === "alive";
|
|
120
|
+
return existsSync(socketPath);
|
|
121
|
+
}
|
|
113
122
|
// ============================================================================
|
|
114
123
|
// Live control-socket discovery for entwurf_peers lives in the TS fact-provider
|
|
115
124
|
// (pi-extensions/lib/entwurf-fact-provider.ts → listEntwurfFacts), which the
|
|
@@ -171,7 +180,7 @@ class EntwurfSenderIdentityError extends Error {
|
|
|
171
180
|
"the send is then marked external/non-replyable).");
|
|
172
181
|
}
|
|
173
182
|
}
|
|
174
|
-
function buildStrictPiSenderEnvelope() {
|
|
183
|
+
async function buildStrictPiSenderEnvelope() {
|
|
175
184
|
const sessionId = process.env.PI_SESSION_ID?.trim();
|
|
176
185
|
const agentId = process.env.PI_AGENT_ID?.trim();
|
|
177
186
|
const cwd = process.cwd();
|
|
@@ -195,7 +204,7 @@ function buildStrictPiSenderEnvelope() {
|
|
|
195
204
|
const socketPath = controlSocketPathIn(ENTWURF_DIR, sessionId);
|
|
196
205
|
const self = computeSelfAddressability({
|
|
197
206
|
origin: "pi-session",
|
|
198
|
-
socketAlive:
|
|
207
|
+
socketAlive: await controlAddressPresent(socketPath),
|
|
199
208
|
socketPathComputable: true,
|
|
200
209
|
});
|
|
201
210
|
return {
|
|
@@ -258,7 +267,7 @@ async function buildMetaSenderEnvelope(identity, cwd, trustedMarker) {
|
|
|
258
267
|
async function resolveAuthoritativeSender(context, cwd = process.cwd()) {
|
|
259
268
|
const sessionId = process.env.PI_SESSION_ID?.trim();
|
|
260
269
|
const agentId = process.env.PI_AGENT_ID?.trim();
|
|
261
|
-
const pi = sessionId && agentId && cwd ? buildStrictPiSenderEnvelope() : null;
|
|
270
|
+
const pi = sessionId && agentId && cwd ? await buildStrictPiSenderEnvelope() : null;
|
|
262
271
|
const marker = resolveTrustedMetaSenderIdentity({
|
|
263
272
|
markerPath: process.env.ENTWURF_META_SENDER_MARKER?.trim() || undefined,
|
|
264
273
|
});
|
|
@@ -463,7 +472,7 @@ server.tool("entwurf_self", "Return this caller's authoritative identity envelop
|
|
|
463
472
|
// live socket). The old code synthesized the path and printed it as if
|
|
464
473
|
// it existed — a lie when the session has no --entwurf-control (SE-1).
|
|
465
474
|
const socketPath = controlSocketPathIn(ENTWURF_DIR, sender.sessionId);
|
|
466
|
-
const socketState =
|
|
475
|
+
const socketState = (await controlAddressPresent(socketPath)) ? "alive" : "expected";
|
|
467
476
|
extra.socketPath = socketPath;
|
|
468
477
|
extra.socketState = socketState;
|
|
469
478
|
lines.push(socketState === "alive"
|
|
@@ -24,8 +24,17 @@
|
|
|
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
|
*/
|
|
36
|
+
import { createHash } from "node:crypto";
|
|
37
|
+
import * as fs from "node:fs";
|
|
29
38
|
import * as path from "node:path";
|
|
30
39
|
/** Control-socket filename suffix. The stem is a bare garden id. */
|
|
31
40
|
export const CONTROL_SOCKET_SUFFIX = ".sock";
|
|
@@ -47,8 +56,54 @@ export function defaultControlSocketDir(home) {
|
|
|
47
56
|
* @returns {string}
|
|
48
57
|
*/
|
|
49
58
|
export function controlSocketPathIn(dir, gardenId) {
|
|
59
|
+
if (process.platform === "win32")
|
|
60
|
+
return win32PipeNameFor(dir, gardenId);
|
|
50
61
|
return path.join(dir, `${gardenId}${CONTROL_SOCKET_SUFFIX}`);
|
|
51
62
|
}
|
|
63
|
+
/** Every Windows control pipe this package names starts here. */
|
|
64
|
+
export const WIN32_PIPE_PREFIX = "\\\\.\\pipe\\entwurf-";
|
|
65
|
+
/**
|
|
66
|
+
* The one spelling of a Windows store root, so Pi A, Pi B, the bridge and peer-facts
|
|
67
|
+
* derive the same pipe name from differently written paths. The nearest EXISTING ancestor
|
|
68
|
+
* of `path.win32.resolve(dir)` goes through `realpath` (8.3 names expanded, case from the
|
|
69
|
+
* filesystem); the components below it are appended as written. Separators become `\`,
|
|
70
|
+
* there is no trailing separator, and the whole string is lower-cased. Nothing is created
|
|
71
|
+
* or touched, so the control dir need not exist yet. A failure other than ENOENT throws.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} dir
|
|
74
|
+
* @param {(p: string) => string} [realpath]
|
|
75
|
+
* @returns {string}
|
|
76
|
+
*/
|
|
77
|
+
export function canonRootWin32(dir, realpath = fs.realpathSync.native) {
|
|
78
|
+
let head = path.win32.resolve(dir);
|
|
79
|
+
/** @type {string[]} */
|
|
80
|
+
const tail = [];
|
|
81
|
+
for (;;) {
|
|
82
|
+
try {
|
|
83
|
+
const joined = path.win32.join(realpath(head), ...tail);
|
|
84
|
+
return joined.replace(/[\\/]+$/, "").toLowerCase();
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
const parent = path.win32.dirname(head);
|
|
88
|
+
if ( /** @type {{ code?: unknown }} */(err).code !== "ENOENT" || parent === head)
|
|
89
|
+
throw err;
|
|
90
|
+
tail.unshift(path.win32.basename(head));
|
|
91
|
+
head = parent;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Forward grammar on Windows: `\\.\pipe\entwurf-<16 hex>-<gardenId>`.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} dir
|
|
99
|
+
* @param {string} gardenId
|
|
100
|
+
* @param {(p: string) => string} [realpath]
|
|
101
|
+
* @returns {string}
|
|
102
|
+
*/
|
|
103
|
+
export function win32PipeNameFor(dir, gardenId, realpath) {
|
|
104
|
+
const digest = createHash("sha256").update(canonRootWin32(dir, realpath), "utf8").digest("hex").slice(0, 16);
|
|
105
|
+
return `${WIN32_PIPE_PREFIX}${digest}-${gardenId}`;
|
|
106
|
+
}
|
|
52
107
|
/**
|
|
53
108
|
* Inverse grammar: a socket filename back to its garden id, or `null` when the
|
|
54
109
|
* name does not carry the suffix.
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* asked to open, and touches nothing pi-specific — it is a pure transport over a path.
|
|
15
15
|
*/
|
|
16
16
|
import * as net from "node:net";
|
|
17
|
+
import { win32Native } from "./win32-native.js";
|
|
17
18
|
/**
|
|
18
19
|
* THE `<sender_info>` synthesis — the one place the sender envelope becomes
|
|
19
20
|
* message text. ONE consumer since the visible-first cut (#50 C3):
|
|
@@ -101,6 +102,8 @@ export async function fetchControlSocketRuntimeInfo(socketPath, options = {}) {
|
|
|
101
102
|
}
|
|
102
103
|
export async function sendRpcCommand(socketPath, command, options = {}) {
|
|
103
104
|
const { timeout = 5000 } = options;
|
|
105
|
+
if (process.platform === "win32")
|
|
106
|
+
return await sendPipeRpcCommand(socketPath, command, timeout);
|
|
104
107
|
return new Promise((resolve, reject) => {
|
|
105
108
|
const socket = net.createConnection(socketPath);
|
|
106
109
|
socket.setEncoding("utf8");
|
|
@@ -173,3 +176,25 @@ export async function sendRpcCommand(socketPath, command, options = {}) {
|
|
|
173
176
|
});
|
|
174
177
|
});
|
|
175
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* The Windows client (#78). The addon makes one owner-validated connect, writes the request
|
|
181
|
+
* line and returns the first response line, all inside one deadline; a timeout is
|
|
182
|
+
* `ETIMEDOUT`, a pipe owned by anyone but this token's expected owner is `EPERM` before any
|
|
183
|
+
* byte. The protocol is request-bound on its own connection, so that first line IS the
|
|
184
|
+
* answer: anything other than this command's response is refused, never waited past.
|
|
185
|
+
*/
|
|
186
|
+
async function sendPipeRpcCommand(pipeName, command, timeout) {
|
|
187
|
+
const { responseLine } = await win32Native().pipeRpc(pipeName, JSON.stringify(command), Math.max(0, Math.floor(timeout)));
|
|
188
|
+
let msg;
|
|
189
|
+
try {
|
|
190
|
+
msg = JSON.parse(responseLine.trim());
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
throw new Error(`control pipe answered ${command.type} with a line that is not JSON`);
|
|
194
|
+
}
|
|
195
|
+
const response = msg;
|
|
196
|
+
if (response?.type !== "response" || response.command !== command.type) {
|
|
197
|
+
throw new Error(`control pipe answered something other than the ${command.type} response`);
|
|
198
|
+
}
|
|
199
|
+
return { response };
|
|
200
|
+
}
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
import { execFileSync } from "node:child_process";
|
|
53
53
|
import * as crypto from "node:crypto";
|
|
54
54
|
import * as fs from "node:fs";
|
|
55
|
+
import { createRequire } from "node:module";
|
|
55
56
|
import * as os from "node:os";
|
|
56
57
|
import * as path from "node:path";
|
|
57
58
|
import { generateSessionId, SESSION_ID_RE } from "./session-id.js";
|
|
@@ -845,6 +846,8 @@ const O_NONBLOCK_FLAG = typeof fs.constants.O_NONBLOCK === "number" ? fs.constan
|
|
|
845
846
|
* Shaping a message is the job of the caller that knows what the answer is FOR.
|
|
846
847
|
*/
|
|
847
848
|
export function readStoreRecordFile(file) {
|
|
849
|
+
if (process.platform === "win32")
|
|
850
|
+
return win32RecordReader()(file).toString("utf8");
|
|
848
851
|
if (O_NOFOLLOW_FLAG === 0) {
|
|
849
852
|
throw new MetaRecordError(`cannot read meta-record ${path.basename(file)} safely: this platform exposes no O_NOFOLLOW, so a record ` +
|
|
850
853
|
"read cannot refuse a symlink swapped in at the final component. entwurf certifies Linux only.");
|
|
@@ -863,6 +866,17 @@ export function readStoreRecordFile(file) {
|
|
|
863
866
|
fs.closeSync(fd);
|
|
864
867
|
}
|
|
865
868
|
}
|
|
869
|
+
/**
|
|
870
|
+
* Windows has no `O_NOFOLLOW` (libuv defines it as 0), so the same three decisions — a
|
|
871
|
+
* final-component reparse point refused as `ELOOP`, a non-regular entry as
|
|
872
|
+
* `ENTWURF_ENOTREG`, both on the handle the read uses — are made by the preview addon
|
|
873
|
+
* (#78). It is reached only here and only on win32, through `createRequire`, so the
|
|
874
|
+
* installers that copy this file without its siblings never load it on Linux or macOS.
|
|
875
|
+
*/
|
|
876
|
+
function win32RecordReader() {
|
|
877
|
+
const native = createRequire(import.meta.url)("./win32-native.js");
|
|
878
|
+
return native.win32ReadNoFollow;
|
|
879
|
+
}
|
|
866
880
|
/** The reader every store-WIDE scan pairs with {@link readActiveStoreEntries}. The
|
|
867
881
|
* caller's entry kind decides the POLICY — whether a non-regular entry is a defect
|
|
868
882
|
* (certification), a diagnostic (listing) or a non-candidate (the rival scan) — because
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
*/
|
|
52
52
|
import * as fs from "node:fs/promises";
|
|
53
53
|
import * as os from "node:os";
|
|
54
|
-
import { CONTROL_SOCKET_SUFFIX, controlSocketPathIn, defaultControlSocketDir, gardenIdFromSocketFilename, } from "./control-socket-path.js";
|
|
54
|
+
import { CONTROL_SOCKET_SUFFIX, controlSocketPathIn, defaultControlSocketDir, gardenIdFromSocketFilename, WIN32_PIPE_PREFIX, } from "./control-socket-path.js";
|
|
55
55
|
import { SESSION_ID_RE } from "./session-id.js";
|
|
56
56
|
import { probeSocketLiveness } from "./socket-probe.js";
|
|
57
57
|
/** Canonical control-socket directory; the socket filename IS the gardenId
|
|
@@ -81,7 +81,9 @@ export function controlSocketPath(gardenId, dir = CONTROL_SOCKET_DIR) {
|
|
|
81
81
|
* `record-less-socket` reject (a diagnostic state) — no probe, no acceptance.
|
|
82
82
|
*/
|
|
83
83
|
export function isRecordLessSocketCandidate(inspection) {
|
|
84
|
-
|
|
84
|
+
// A Windows pipe name (#78) is never lstat-ed (see inspectControlSocketPath), so its
|
|
85
|
+
// `socket-file` carries no presence evidence and cannot confirm a record-less socket.
|
|
86
|
+
return inspection.kind === "socket-file" && !inspection.socketPath.startsWith(WIN32_PIPE_PREFIX);
|
|
85
87
|
}
|
|
86
88
|
/**
|
|
87
89
|
* Inspect the EXACT control-socket path given (no gid re-derivation) and classify it by
|
|
@@ -94,6 +96,12 @@ export function isRecordLessSocketCandidate(inspection) {
|
|
|
94
96
|
* symlink is caught as an address-conflict and never connected, P1).
|
|
95
97
|
*/
|
|
96
98
|
export async function inspectControlSocketPath(socketPath, lstatFn = (p) => fs.lstat(p)) {
|
|
99
|
+
// Windows (#78): the address is a named pipe. The pipe filesystem has no symlinks to
|
|
100
|
+
// catch, and opening the name to stat it would CONNECT to it and consume an instance.
|
|
101
|
+
// So the name is never stat-ed: it is safe to probe because the probe itself refuses a
|
|
102
|
+
// pipe whose owner is not this token's expected owner before any byte.
|
|
103
|
+
if (socketPath.startsWith(WIN32_PIPE_PREFIX))
|
|
104
|
+
return { kind: "socket-file", socketPath };
|
|
97
105
|
let st;
|
|
98
106
|
try {
|
|
99
107
|
st = await lstatFn(socketPath);
|
|
@@ -160,8 +168,12 @@ export async function mapInspectionToLiveness(inspection, probeSocket) {
|
|
|
160
168
|
*/
|
|
161
169
|
export async function scanSocketProbes(piCitizenGardenIds, deps = {}) {
|
|
162
170
|
const dir = deps.dir ?? CONTROL_SOCKET_DIR;
|
|
171
|
+
// Windows (#78) has no socket files to list and nothing scans the pipe namespace: the
|
|
172
|
+
// candidates are the record-backed citizens alone.
|
|
163
173
|
const readdir = deps.readdir ??
|
|
164
174
|
(async (d) => {
|
|
175
|
+
if (process.platform === "win32")
|
|
176
|
+
return [];
|
|
165
177
|
const dirents = await fs.readdir(d, { withFileTypes: true });
|
|
166
178
|
return dirents.map((e) => ({ name: e.name, isSymbolicLink: e.isSymbolicLink() }));
|
|
167
179
|
});
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* hidden — `entwurf_peers` shows it and the v2 decider rejects on it by name.
|
|
29
29
|
*/
|
|
30
30
|
import * as net from "node:net";
|
|
31
|
+
import { win32Native } from "./win32-native.js";
|
|
31
32
|
export const DEFAULT_PROBE_TIMEOUT_MS = 300;
|
|
32
33
|
/**
|
|
33
34
|
* Pure classification of a connect-time error code into liveness. Only the two
|
|
@@ -56,6 +57,8 @@ export function shouldUnlinkOnGc(liveness) {
|
|
|
56
57
|
*/
|
|
57
58
|
export async function probeSocketLiveness(socketPath, opts = {}) {
|
|
58
59
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
60
|
+
if (process.platform === "win32")
|
|
61
|
+
return await probePipeLiveness(socketPath, timeoutMs);
|
|
59
62
|
return await new Promise((resolve) => {
|
|
60
63
|
const socket = net.createConnection(socketPath);
|
|
61
64
|
const timer = setTimeout(() => {
|
|
@@ -76,3 +79,21 @@ export async function probeSocketLiveness(socketPath, opts = {}) {
|
|
|
76
79
|
});
|
|
77
80
|
});
|
|
78
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* The Windows probe (#78): the control address is a named pipe, and a connect alone would
|
|
84
|
+
* talk to whoever holds the name. The addon connects within one deadline and refuses a pipe
|
|
85
|
+
* whose owner is not this token's expected owner (`EPERM`) before any byte. Only `ENOENT`
|
|
86
|
+
* (no instance) is dead; a refused owner, a busy pipe past the deadline and every Win32
|
|
87
|
+
* failure stay indeterminate. A missing or drifted addon is not a liveness fact: that
|
|
88
|
+
* refusal throws.
|
|
89
|
+
*/
|
|
90
|
+
async function probePipeLiveness(pipeName, timeoutMs) {
|
|
91
|
+
const native = win32Native();
|
|
92
|
+
try {
|
|
93
|
+
await native.pipeProbe(pipeName, Math.max(0, Math.floor(timeoutMs)));
|
|
94
|
+
return "alive";
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
return classifyConnectError(err.code);
|
|
98
|
+
}
|
|
99
|
+
}
|