@junghanacs/entwurf 0.38.1 → 0.40.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 (50) hide show
  1. package/BASELINE.md +11 -4
  2. package/CHANGELOG.md +323 -0
  3. package/DELIVERY.md +34 -0
  4. package/LICENSES/node-gyp.txt +24 -0
  5. package/README.md +20 -6
  6. package/VERIFY.md +23 -4
  7. package/docs/setup-clean-host.md +43 -2
  8. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +32 -9
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/control-socket-path.js +56 -1
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +25 -0
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +10 -4
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +14 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-receive.js +123 -0
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-send.js +259 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-transport.js +80 -0
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/socket-discovery.js +14 -2
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/socket-probe.js +21 -0
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/win32-native.js +182 -0
  19. package/mcp/entwurf-bridge/dist/scripts/remote-pair-receive.js +65 -0
  20. package/mcp/entwurf-bridge/src/index.ts +38 -9
  21. package/mcp/entwurf-bridge/tsconfig.build.json +7 -0
  22. package/native/win32-x64/entwurf_win.node +0 -0
  23. package/native/win32-x64/manifest.json +45 -0
  24. package/package.json +5 -3
  25. package/pi-extensions/entwurf-control.ts +6 -1
  26. package/pi-extensions/lib/control-socket-path.js +56 -1
  27. package/pi-extensions/lib/entwurf-control-rpc.ts +32 -0
  28. package/pi-extensions/lib/meta-mailbox-body.ts +13 -4
  29. package/pi-extensions/lib/meta-session.ts +14 -0
  30. package/pi-extensions/lib/remote-pair-receive.ts +184 -0
  31. package/pi-extensions/lib/remote-pair-send.ts +325 -0
  32. package/pi-extensions/lib/remote-pair-transport.ts +81 -0
  33. package/pi-extensions/lib/socket-discovery.ts +12 -1
  34. package/pi-extensions/lib/socket-probe.ts +20 -0
  35. package/pi-extensions/lib/win32-native.js +198 -0
  36. package/run.sh +149 -1
  37. package/scripts/check-entwurf-self-address.ts +15 -4
  38. package/scripts/check-gate-qualification.ts +8 -1
  39. package/scripts/check-remote-pair-installed.ts +242 -0
  40. package/scripts/lib/remote-pair-installed-audit.ts +25 -0
  41. package/scripts/mutants/control-send-receipt.json +2 -2
  42. package/scripts/mutants/pack-install.json +28 -2
  43. package/scripts/mutants/remote-pair-display.json +53 -0
  44. package/scripts/mutants/remote-pair-installed-audit.json +28 -0
  45. package/scripts/mutants/remote-pair-receive.json +126 -0
  46. package/scripts/mutants/remote-pair-roundtrip.json +83 -0
  47. package/scripts/mutants/remote-pair-send.json +194 -0
  48. package/scripts/mutants/remote-pair-transport.json +66 -0
  49. package/scripts/mutants/win32-preview.json +215 -0
  50. package/scripts/remote-pair-receive.ts +73 -0
@@ -0,0 +1,215 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "lane": "win32-preview",
4
+ "mutants": [
5
+ {
6
+ "claim": "WIN32-NATIVE-ABSENT-UNSUPPORTED",
7
+ "title": "an install without the addon reads as drift instead of the honest 'no Windows addon here', so an ordinary Linux pack on Windows is reported as tampering",
8
+ "subject": "pi-extensions/lib/win32-native.js",
9
+ "find": ["\t\tif (/** @type {{ code?: unknown }} */ (err).code === \"ENOENT\") {"],
10
+ "replace": ["\t\tif (false) {"],
11
+ "gate": ["bash", "run.sh", "check-win32-preview"],
12
+ "timeoutSeconds": 600,
13
+ "signature": "[QK:WIN32-NATIVE-ABSENT-UNSUPPORTED]",
14
+ "signatureSource": "pi-extensions/lib/win32-native.test.ts"
15
+ },
16
+ {
17
+ "claim": "WIN32-NATIVE-SHA-DRIFT",
18
+ "title": "the addon digest is no longer compared, so bytes the manifest never named reach process.dlopen",
19
+ "subject": "pi-extensions/lib/win32-native.js",
20
+ "find": ["\tif (sha256 !== manifest.sha256) {"],
21
+ "replace": ["\tif (false) {"],
22
+ "gate": ["bash", "run.sh", "check-win32-preview"],
23
+ "timeoutSeconds": 600,
24
+ "signature": "[QK:WIN32-NATIVE-SHA-DRIFT]",
25
+ "signatureSource": "pi-extensions/lib/win32-native.test.ts"
26
+ },
27
+ {
28
+ "claim": "WIN32-NATIVE-MANIFEST-DRIFT",
29
+ "title": "a manifest for another architecture is accepted, so a foreign binary is loaded as the win32-x64 addon",
30
+ "subject": "pi-extensions/lib/win32-native.js",
31
+ "find": ["\t\tmanifest.arch !== \"x64\" ||"],
32
+ "replace": ["\t\tfalse ||"],
33
+ "gate": ["bash", "run.sh", "check-win32-preview"],
34
+ "timeoutSeconds": 600,
35
+ "signature": "[QK:WIN32-NATIVE-MANIFEST-DRIFT]",
36
+ "signatureSource": "pi-extensions/lib/win32-native.test.ts"
37
+ },
38
+ {
39
+ "claim": "WIN32-NATIVE-NAPI-FLOOR",
40
+ "title": "the N-API floor is dropped, so an older Node loads an addon whose symbols it may not provide",
41
+ "subject": "pi-extensions/lib/win32-native.js",
42
+ "find": ["\tif (!(napi >= ADDON_NAPI_VERSION)) {"],
43
+ "replace": ["\tif (false) {"],
44
+ "gate": ["bash", "run.sh", "check-win32-preview"],
45
+ "timeoutSeconds": 600,
46
+ "signature": "[QK:WIN32-NATIVE-NAPI-FLOOR]",
47
+ "signatureSource": "pi-extensions/lib/win32-native.test.ts"
48
+ },
49
+ {
50
+ "claim": "WIN32-NATIVE-OFF-WINDOWS",
51
+ "title": "the platform guard is dropped, so a non-Windows host goes looking for the Windows addon and fails for the wrong reason",
52
+ "subject": "pi-extensions/lib/win32-native.js",
53
+ "find": ["\tif (process.platform !== \"win32\" || process.arch !== \"x64\") {"],
54
+ "replace": ["\tif (false) {"],
55
+ "gate": ["bash", "run.sh", "check-win32-preview"],
56
+ "timeoutSeconds": 600,
57
+ "signature": "[QK:WIN32-NATIVE-OFF-WINDOWS]",
58
+ "signatureSource": "pi-extensions/lib/win32-native.test.ts"
59
+ },
60
+ {
61
+ "claim": "WIN32-NATIVE-DOS-DEVICE",
62
+ "title": "a DOS device name reaches CreateFileW, which opens the device rather than a store file",
63
+ "subject": "pi-extensions/lib/win32-native.js",
64
+ "find": ["\tif (DOS_DEVICE.test(stem)) {"],
65
+ "replace": ["\tif (false) {"],
66
+ "gate": ["bash", "run.sh", "check-win32-preview"],
67
+ "timeoutSeconds": 600,
68
+ "signature": "[QK:WIN32-NATIVE-DOS-DEVICE]",
69
+ "signatureSource": "pi-extensions/lib/win32-native.test.ts"
70
+ },
71
+ {
72
+ "claim": "WIN32-PIPE-CANON-STABLE",
73
+ "title": "a control dir that does not exist yet makes the root canonicalisation throw, so a passive peer-facts run before the first birth cannot name any pipe",
74
+ "subject": "pi-extensions/lib/control-socket-path.js",
75
+ "find": ["\t\t\tif (/** @type {{ code?: unknown }} */ (err).code !== \"ENOENT\" || parent === head) throw err;"],
76
+ "replace": ["\t\t\tthrow err;"],
77
+ "gate": ["bash", "run.sh", "check-win32-preview"],
78
+ "timeoutSeconds": 600,
79
+ "signature": "[QK:WIN32-PIPE-CANON-STABLE]",
80
+ "signatureSource": "pi-extensions/lib/win32-wiring.test.ts"
81
+ },
82
+ {
83
+ "claim": "WIN32-PIPE-CANON-ONLY-ENOENT",
84
+ "title": "an access-denied ancestor is walked past like an absent one, so a root this process cannot resolve is silently spelled from a shorter path",
85
+ "subject": "pi-extensions/lib/control-socket-path.js",
86
+ "find": ["\t\t\tif (/** @type {{ code?: unknown }} */ (err).code !== \"ENOENT\" || parent === head) throw err;"],
87
+ "replace": ["\t\t\tif (parent === head) throw err;"],
88
+ "gate": ["bash", "run.sh", "check-win32-preview"],
89
+ "timeoutSeconds": 600,
90
+ "signature": "[QK:WIN32-PIPE-CANON-ONLY-ENOENT]",
91
+ "signatureSource": "pi-extensions/lib/win32-wiring.test.ts"
92
+ },
93
+ {
94
+ "claim": "WIN32-PIPE-CANON-FOLD",
95
+ "title": "the root is no longer case-folded, so two spellings of one store root derive two pipe names and the citizens never find each other",
96
+ "subject": "pi-extensions/lib/control-socket-path.js",
97
+ "find": ["\t\t\treturn joined.replace(/[\\\\/]+$/, \"\").toLowerCase();"],
98
+ "replace": ["\t\t\treturn joined.replace(/[\\\\/]+$/, \"\");"],
99
+ "gate": ["bash", "run.sh", "check-win32-preview"],
100
+ "timeoutSeconds": 600,
101
+ "signature": "[QK:WIN32-PIPE-CANON-FOLD]",
102
+ "signatureSource": "pi-extensions/lib/win32-wiring.test.ts"
103
+ },
104
+ {
105
+ "claim": "WIN32-PIPE-POSIX-UNCHANGED",
106
+ "title": "the Windows grammar leaks onto Linux/macOS, replacing the `<dir>/<gid>.sock` address every existing citizen listens on",
107
+ "subject": "pi-extensions/lib/control-socket-path.js",
108
+ "find": ["\tif (process.platform === \"win32\") return win32PipeNameFor(dir, gardenId);"],
109
+ "replace": ["\tif (true) return win32PipeNameFor(dir, gardenId);"],
110
+ "gate": ["bash", "run.sh", "check-win32-preview"],
111
+ "timeoutSeconds": 600,
112
+ "signature": "[QK:WIN32-PIPE-POSIX-UNCHANGED]",
113
+ "signatureSource": "pi-extensions/lib/win32-wiring.test.ts"
114
+ },
115
+ {
116
+ "claim": "WIN32-PIPE-NEVER-STAT",
117
+ "title": "a pipe name is stat-ed like a socket file, and on Windows that open connects to whoever holds the name",
118
+ "subject": "pi-extensions/lib/socket-discovery.ts",
119
+ "find": ["\tif (socketPath.startsWith(WIN32_PIPE_PREFIX)) return { kind: \"socket-file\", socketPath };"],
120
+ "replace": ["\tif (false) return { kind: \"socket-file\", socketPath };"],
121
+ "gate": ["bash", "run.sh", "check-win32-preview"],
122
+ "timeoutSeconds": 600,
123
+ "signature": "[QK:WIN32-PIPE-NEVER-STAT]",
124
+ "signatureSource": "pi-extensions/lib/win32-wiring.test.ts"
125
+ },
126
+ {
127
+ "claim": "WIN32-PIPE-NO-RECORDLESS-CLAIM",
128
+ "title": "an unstat-ed pipe name is reported as a confirmed record-less socket, a presence claim nothing measured",
129
+ "subject": "pi-extensions/lib/socket-discovery.ts",
130
+ "find": ["\treturn inspection.kind === \"socket-file\" && !inspection.socketPath.startsWith(WIN32_PIPE_PREFIX);"],
131
+ "replace": ["\treturn inspection.kind === \"socket-file\";"],
132
+ "gate": ["bash", "run.sh", "check-win32-preview"],
133
+ "timeoutSeconds": 600,
134
+ "signature": "[QK:WIN32-PIPE-NO-RECORDLESS-CLAIM]",
135
+ "signatureSource": "pi-extensions/lib/win32-wiring.test.ts"
136
+ },
137
+ {
138
+ "claim": "WIN32-DISCOVERY-FROM-RECORDS",
139
+ "title": "the Windows scan lists the control dir again, so a stray `.sock` file adds a citizen no record names",
140
+ "subject": "pi-extensions/lib/socket-discovery.ts",
141
+ "find": ["\t\t\tif (process.platform === \"win32\") return [];"],
142
+ "replace": ["\t\t\tif (false) return [];"],
143
+ "gate": ["bash", "run.sh", "check-win32-preview"],
144
+ "timeoutSeconds": 600,
145
+ "signature": "[QK:WIN32-DISCOVERY-FROM-RECORDS]",
146
+ "signatureSource": "pi-extensions/lib/win32-wiring.test.ts"
147
+ },
148
+ {
149
+ "claim": "WIN32-PROBE-CLASSIFY",
150
+ "title": "the Windows probe falls back to a Unix-socket connect, which cannot reach a named pipe and reads every live citizen as dead",
151
+ "subject": "pi-extensions/lib/socket-probe.ts",
152
+ "find": ["\tif (process.platform === \"win32\") return await probePipeLiveness(socketPath, timeoutMs);"],
153
+ "replace": ["\tif (false) return await probePipeLiveness(socketPath, timeoutMs);"],
154
+ "gate": ["bash", "run.sh", "check-win32-preview"],
155
+ "timeoutSeconds": 600,
156
+ "signature": "[QK:WIN32-PROBE-CLASSIFY]",
157
+ "signatureSource": "pi-extensions/lib/win32-wiring.test.ts"
158
+ },
159
+ {
160
+ "claim": "WIN32-PROBE-REFUSAL-THROWS",
161
+ "title": "a missing or drifted addon is swallowed into an indeterminate liveness instead of refusing loudly",
162
+ "subject": "pi-extensions/lib/socket-probe.ts",
163
+ "find": ["\tconst native = win32Native();", "\ttry {"],
164
+ "replace": ["\ttry {", "\t\tconst native = win32Native();"],
165
+ "gate": ["bash", "run.sh", "check-win32-preview"],
166
+ "timeoutSeconds": 600,
167
+ "signature": "[QK:WIN32-PROBE-REFUSAL-THROWS]",
168
+ "signatureSource": "pi-extensions/lib/win32-wiring.test.ts"
169
+ },
170
+ {
171
+ "claim": "WIN32-RPC-FIRST-LINE",
172
+ "title": "the Windows rpc accepts another command's response as this one's answer",
173
+ "subject": "pi-extensions/lib/entwurf-control-rpc.ts",
174
+ "find": ["\tif (response?.type !== \"response\" || response.command !== command.type) {"],
175
+ "replace": ["\tif (response?.type !== \"response\") {"],
176
+ "gate": ["bash", "run.sh", "check-win32-preview"],
177
+ "timeoutSeconds": 600,
178
+ "signature": "[QK:WIN32-RPC-FIRST-LINE]",
179
+ "signatureSource": "pi-extensions/lib/win32-wiring.test.ts"
180
+ },
181
+ {
182
+ "claim": "WIN32-RECORD-READ-NATIVE",
183
+ "title": "a Windows record read goes back to the Node open, which follows a final-component reparse point",
184
+ "subject": "pi-extensions/lib/meta-session.ts",
185
+ "find": ["\tif (process.platform === \"win32\") return win32RecordReader()(file).toString(\"utf8\");"],
186
+ "replace": ["\tif (false) return win32RecordReader()(file).toString(\"utf8\");"],
187
+ "gate": ["bash", "run.sh", "check-win32-preview"],
188
+ "timeoutSeconds": 600,
189
+ "signature": "[QK:WIN32-RECORD-READ-NATIVE]",
190
+ "signatureSource": "pi-extensions/lib/win32-wiring.test.ts"
191
+ },
192
+ {
193
+ "claim": "WIN32-NATIVE-ABI-DRIFT",
194
+ "title": "a manifest declaring another N-API version is accepted, so a build for some other ABI is verified as this addon",
195
+ "subject": "pi-extensions/lib/win32-native.js",
196
+ "find": ["\t\tmanifest.napiVersion !== ADDON_NAPI_VERSION"],
197
+ "replace": ["\t\tfalse"],
198
+ "gate": ["bash", "run.sh", "check-win32-preview"],
199
+ "timeoutSeconds": 600,
200
+ "signature": "[QK:WIN32-NATIVE-ABI-DRIFT]",
201
+ "signatureSource": "pi-extensions/lib/win32-native.test.ts"
202
+ },
203
+ {
204
+ "claim": "WIN32-NATIVE-COMPILED-DEPTH",
205
+ "title": "the compiled depth is dropped from the root search, so the bridge's dist copy of the loader cannot find the package it ships in",
206
+ "subject": "pi-extensions/lib/win32-native.js",
207
+ "find": ["\t\t[\"..\", \"..\", \"..\", \"..\", \"..\"],"],
208
+ "replace": [""],
209
+ "gate": ["bash", "run.sh", "check-win32-preview"],
210
+ "timeoutSeconds": 600,
211
+ "signature": "[QK:WIN32-NATIVE-COMPILED-DEPTH]",
212
+ "signatureSource": "pi-extensions/lib/win32-native.test.ts"
213
+ }
214
+ ]
215
+ }
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * remote-pair-receive — `entwurf remote-pair-receive --pair <absolute file>` (#107 rail 1).
4
+ *
5
+ * The receiving host's entry for one declared remote pair. The sending host runs it over one
6
+ * SSH call and writes one JSON claim to stdin. This entry reads the operator-named pair
7
+ * declaration once, applies the pair scope check, and hands a matching claim to the existing
8
+ * v2 dispatch. The scope and trust model are in pi-extensions/lib/remote-pair-receive.ts.
9
+ *
10
+ * Scope: `--pair` must name THIS host's own opt-in file (`remotePairOptInPath()`), absolute and
11
+ * normalized-equal; any other path — even a valid declaration — is exit 2. That one file is the pair
12
+ * this host accepts and, because it names the sender's host as its route, the declared route back.
13
+ * The sender's declaration carries the path only so the argv shape is explicit; it selects nothing.
14
+ *
15
+ * EXIT CONTRACT (stdout carries a JSON receipt line only where named):
16
+ * 0 dispatched, the dispatch did not report an error — receipt on stdout
17
+ * 4 dispatched, the dispatch reported an error (its text says what is known, e.g. a
18
+ * rejection or `retry-safe: false` after a possible write) — receipt
19
+ * 3 refused by the pair scope check, no dispatch ran — receipt
20
+ * 2 --pair is not this host's opt-in, the opt-in is absent/broken, or the claim is invalid; no
21
+ * dispatch ran — `{kind:"invalid"}` receipt;
22
+ * wrong argv is ALSO 2 but prints usage on stderr only, with no receipt
23
+ * 1 an uncaught dispatch/store throw (crash, don't warn) — no receipt; stderr carries the
24
+ * error. The sending host reads that as indeterminate.
25
+ */
26
+
27
+ import { readFileSync } from "node:fs";
28
+ import * as path from "node:path";
29
+ import { runAndRenderEntwurfV2FromSurface } from "../pi-extensions/lib/entwurf-v2-surface.ts";
30
+ import {
31
+ parseRemoteClaim,
32
+ type RemotePairDeclaration,
33
+ type RemoteReceiveReceipt,
34
+ receiveRemoteClaim,
35
+ } from "../pi-extensions/lib/remote-pair-receive.ts";
36
+ import { readRemotePairOptIn, remotePairOptInPath } from "../pi-extensions/lib/remote-pair-send.ts";
37
+
38
+ function emit(receipt: RemoteReceiveReceipt | { kind: "invalid"; detail: string }, code: number): never {
39
+ process.stdout.write(`${JSON.stringify(receipt)}\n`);
40
+ process.exit(code);
41
+ }
42
+
43
+ const args = process.argv.slice(2);
44
+ if (args.length !== 2 || args[0] !== "--pair") {
45
+ console.error("usage: entwurf remote-pair-receive --pair <this host's opt-in file> (claim JSON on stdin)");
46
+ process.exit(2);
47
+ }
48
+ const pairFile = args[1];
49
+
50
+ let decl: RemotePairDeclaration;
51
+ let claim: ReturnType<typeof parseRemoteClaim>;
52
+ try {
53
+ // The scope is THIS host's own opt-in, never a file the sender picks: `--pair` must name it
54
+ // exactly (absolute, normalized), and that one file is read for this receive.
55
+ const own = remotePairOptInPath();
56
+ if (!path.isAbsolute(pairFile) || path.normalize(pairFile) !== own) {
57
+ throw new Error(`--pair must be this host's opt-in file ${own} (got ${pairFile})`);
58
+ }
59
+ const route = readRemotePairOptIn(own);
60
+ if (route === null) throw new Error(`this host has no remote pair opt-in (${own} is absent)`);
61
+ decl = { remoteGardenId: route.remoteGardenId, ssh: route.ssh, localGardenId: route.localGardenId };
62
+ claim = parseRemoteClaim(readFileSync(0, "utf8"));
63
+ } catch (err) {
64
+ emit({ kind: "invalid", detail: err instanceof Error ? err.message : String(err) }, 2);
65
+ }
66
+
67
+ const receipt = await receiveRemoteClaim(claim, decl, {
68
+ dispatch: (params, sender) => runAndRenderEntwurfV2FromSurface(params, { senderProvider: () => sender }),
69
+ // The scope file IS this host's own opt-in, and it names the sender's host as its route: a
70
+ // matching claim always has a declared route back. Declared, not probed.
71
+ replyRoute: "declared",
72
+ });
73
+ emit(receipt, receipt.kind === "rejected" ? 3 : receipt.isError ? 4 : 0);