@junghanacs/entwurf 0.39.0 → 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 (31) hide show
  1. package/BASELINE.md +5 -3
  2. package/CHANGELOG.md +236 -0
  3. package/DELIVERY.md +34 -0
  4. package/README.md +11 -9
  5. package/VERIFY.md +11 -1
  6. package/docs/setup-clean-host.md +42 -3
  7. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +19 -5
  8. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +10 -4
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-receive.js +123 -0
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-send.js +259 -0
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-transport.js +80 -0
  12. package/mcp/entwurf-bridge/dist/scripts/remote-pair-receive.js +65 -0
  13. package/mcp/entwurf-bridge/src/index.ts +25 -5
  14. package/mcp/entwurf-bridge/tsconfig.build.json +7 -0
  15. package/package.json +2 -2
  16. package/pi-extensions/lib/meta-mailbox-body.ts +13 -4
  17. package/pi-extensions/lib/remote-pair-receive.ts +184 -0
  18. package/pi-extensions/lib/remote-pair-send.ts +325 -0
  19. package/pi-extensions/lib/remote-pair-transport.ts +81 -0
  20. package/run.sh +44 -0
  21. package/scripts/check-gate-qualification.ts +6 -0
  22. package/scripts/check-remote-pair-installed.ts +242 -0
  23. package/scripts/lib/remote-pair-installed-audit.ts +25 -0
  24. package/scripts/mutants/control-send-receipt.json +2 -2
  25. package/scripts/mutants/remote-pair-display.json +53 -0
  26. package/scripts/mutants/remote-pair-installed-audit.json +28 -0
  27. package/scripts/mutants/remote-pair-receive.json +126 -0
  28. package/scripts/mutants/remote-pair-roundtrip.json +83 -0
  29. package/scripts/mutants/remote-pair-send.json +194 -0
  30. package/scripts/mutants/remote-pair-transport.json +66 -0
  31. package/scripts/remote-pair-receive.ts +73 -0
@@ -0,0 +1,194 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "lane": "remote-pair-send",
4
+ "mutants": [
5
+ {
6
+ "claim": "REMOTE-ROUTE-UNDECLARED-LOCAL-UNCHANGED",
7
+ "title": "a declared route captures every target, so a gid the declaration never named is sent to the other host instead of the local decider",
8
+ "subject": "pi-extensions/lib/remote-pair-send.ts",
9
+ "find": ["\tif (route === null || params.target !== route.remoteGardenId) {"],
10
+ "replace": ["\tif (route === null) {"],
11
+ "gate": ["bash", "run.sh", "check-tests-beside-behavior"],
12
+ "timeoutSeconds": 600,
13
+ "signature": "[QK:REMOTE-ROUTE-UNDECLARED-LOCAL-UNCHANGED]",
14
+ "signatureSource": "pi-extensions/lib/remote-pair-send.test.ts",
15
+ "build": {
16
+ "argv": ["bash", "scripts/build-bridge.sh"],
17
+ "output": "mcp/entwurf-bridge/dist"
18
+ }
19
+ },
20
+ {
21
+ "claim": "REMOTE-ROUTE-LOCAL-CONFLICT-FAILS-CLOSED",
22
+ "title": "a declared remote gid that this host also answers to is sent away anyway, so two hosts' citizens share one address and the sender silently picks the remote",
23
+ "subject": "pi-extensions/lib/remote-pair-send.ts",
24
+ "find": ["\tif (await opts.localTrace(route.remoteGardenId)) {"],
25
+ "replace": ["\tif (Boolean(0)) {"],
26
+ "gate": ["bash", "run.sh", "check-tests-beside-behavior"],
27
+ "timeoutSeconds": 600,
28
+ "signature": "[QK:REMOTE-ROUTE-LOCAL-CONFLICT-FAILS-CLOSED]",
29
+ "signatureSource": "pi-extensions/lib/remote-pair-send.test.ts",
30
+ "build": {
31
+ "argv": ["bash", "scripts/build-bridge.sh"],
32
+ "output": "mcp/entwurf-bridge/dist"
33
+ }
34
+ },
35
+ {
36
+ "claim": "REMOTE-ROUTE-SENDER-MUST-BE-DECLARED",
37
+ "title": "any present envelope may use the route, so an anonymous caller or another citizen on this host speaks to the other host as the declared sender",
38
+ "subject": "pi-extensions/lib/remote-pair-send.ts",
39
+ "find": ["\tif (!sender || sender.origin === \"external-mcp\" || sender.sessionId !== route.localGardenId) {"],
40
+ "replace": ["\tif (!sender || Boolean(0)) {"],
41
+ "gate": ["bash", "run.sh", "check-tests-beside-behavior"],
42
+ "timeoutSeconds": 600,
43
+ "signature": "[QK:REMOTE-ROUTE-SENDER-MUST-BE-DECLARED]",
44
+ "signatureSource": "pi-extensions/lib/remote-pair-send.test.ts",
45
+ "build": {
46
+ "argv": ["bash", "scripts/build-bridge.sh"],
47
+ "output": "mcp/entwurf-bridge/dist"
48
+ }
49
+ },
50
+ {
51
+ "claim": "REMOTE-ROUTE-EXACT-ARGV-ONCE",
52
+ "title": "the transport runs ssh without BatchMode, so a missing key turns into an interactive prompt nobody can answer instead of a bounded failure",
53
+ "subject": "pi-extensions/lib/remote-pair-send.ts",
54
+ "find": ["\t\t\"BatchMode=yes\","],
55
+ "replace": ["\t\t\"BatchMode=no\","],
56
+ "gate": ["bash", "run.sh", "check-tests-beside-behavior"],
57
+ "timeoutSeconds": 600,
58
+ "signature": "[QK:REMOTE-ROUTE-EXACT-ARGV-ONCE]",
59
+ "signatureSource": "pi-extensions/lib/remote-pair-send.test.ts",
60
+ "build": {
61
+ "argv": ["bash", "scripts/build-bridge.sh"],
62
+ "output": "mcp/entwurf-bridge/dist"
63
+ }
64
+ },
65
+ {
66
+ "claim": "REMOTE-ROUTE-NO-RETRY",
67
+ "title": "the send runs the transport a second time, so an uncertain first attempt can enqueue the same message twice on the other host",
68
+ "subject": "pi-extensions/lib/remote-pair-send.ts",
69
+ "find": ["\tconst result = await opts.transport.run("],
70
+ "replace": [
71
+ "\tawait opts.transport.run(remoteReceiveArgv(route), JSON.stringify(claim), 1);\n\tconst result = await opts.transport.run("
72
+ ],
73
+ "gate": ["bash", "run.sh", "check-tests-beside-behavior"],
74
+ "timeoutSeconds": 600,
75
+ "signature": "[QK:REMOTE-ROUTE-NO-RETRY]",
76
+ "signatureSource": "pi-extensions/lib/remote-pair-send.test.ts",
77
+ "build": {
78
+ "argv": ["bash", "scripts/build-bridge.sh"],
79
+ "output": "mcp/entwurf-bridge/dist"
80
+ }
81
+ },
82
+ {
83
+ "claim": "REMOTE-RECEIPT-EXIT-AND-KIND-AGREE",
84
+ "title": "an accepted-looking JSON line counts as accepted whatever the exit code says, so a refusal or a crash after printing reads as delivery",
85
+ "subject": "pi-extensions/lib/remote-pair-send.ts",
86
+ "find": [
87
+ "\tif (r.exitCode === 0 && keys === \"isError,kind,text\" && o.kind === \"dispatched\" && o.isError === false) {"
88
+ ],
89
+ "replace": ["\tif (keys === \"isError,kind,text\" && o.kind === \"dispatched\" && o.isError === false) {"],
90
+ "gate": ["bash", "run.sh", "check-tests-beside-behavior"],
91
+ "timeoutSeconds": 600,
92
+ "signature": "[QK:REMOTE-RECEIPT-EXIT-AND-KIND-AGREE]",
93
+ "signatureSource": "pi-extensions/lib/remote-pair-send.test.ts",
94
+ "build": {
95
+ "argv": ["bash", "scripts/build-bridge.sh"],
96
+ "output": "mcp/entwurf-bridge/dist"
97
+ }
98
+ },
99
+ {
100
+ "claim": "REMOTE-ROUTE-DECL-NO-QUOTING-NEEDED",
101
+ "title": "the remote declaration path may carry spaces or shell metacharacters, which ssh joins into the remote shell string and the other host reinterprets",
102
+ "subject": "pi-extensions/lib/remote-pair-send.ts",
103
+ "find": ["const REMOTE_PATH = /^\\/[A-Za-z0-9._/-]+$/;"],
104
+ "replace": ["const REMOTE_PATH = /^\\/.+$/;"],
105
+ "gate": ["bash", "run.sh", "check-tests-beside-behavior"],
106
+ "timeoutSeconds": 600,
107
+ "signature": "[QK:REMOTE-ROUTE-DECL-NO-QUOTING-NEEDED]",
108
+ "signatureSource": "pi-extensions/lib/remote-pair-send.test.ts",
109
+ "build": {
110
+ "argv": ["bash", "scripts/build-bridge.sh"],
111
+ "output": "mcp/entwurf-bridge/dist"
112
+ }
113
+ },
114
+ {
115
+ "claim": "REMOTE-ROUTE-LOCAL-TRACE-ANY-STATE",
116
+ "title": "a socket at the gid's control path in a named socket dir is not counted as a local trace, so a live local citizen without a record is bypassed for the remote",
117
+ "subject": "pi-extensions/lib/remote-pair-send.ts",
118
+ "find": ["\t\t\tif (inspection.kind !== \"absent\") return true;"],
119
+ "replace": ["\t\t\tif (false) return true;"],
120
+ "gate": ["bash", "run.sh", "check-tests-beside-behavior"],
121
+ "timeoutSeconds": 600,
122
+ "signature": "[QK:REMOTE-ROUTE-LOCAL-TRACE-ANY-STATE]",
123
+ "signatureSource": "pi-extensions/lib/remote-pair-send.test.ts",
124
+ "build": {
125
+ "argv": ["bash", "scripts/build-bridge.sh"],
126
+ "output": "mcp/entwurf-bridge/dist"
127
+ }
128
+ },
129
+ {
130
+ "claim": "REMOTE-ROUTE-RECEIPT-PATHS-OTHER-HOST",
131
+ "title": "the accepted receipt drops the note that its paths are on the other host, so the other host's messagePath reads as a file the sender could open",
132
+ "subject": "pi-extensions/lib/remote-pair-send.ts",
133
+ "find": ["read/answer unknown until a reply arrives; paths below are on the other host)"],
134
+ "replace": ["read/answer unknown until a reply arrives)"],
135
+ "gate": ["bash", "run.sh", "check-tests-beside-behavior"],
136
+ "timeoutSeconds": 600,
137
+ "signature": "[QK:REMOTE-ROUTE-RECEIPT-PATHS-OTHER-HOST]",
138
+ "signatureSource": "pi-extensions/lib/remote-pair-send.test.ts",
139
+ "build": {
140
+ "argv": ["bash", "scripts/build-bridge.sh"],
141
+ "output": "mcp/entwurf-bridge/dist"
142
+ }
143
+ },
144
+ {
145
+ "claim": "REMOTE-PAIR-OPT-IN-ABSENT-IS-OFF",
146
+ "title": "an absent opt-in file throws instead of meaning off, so every local send on a host that never opted in fails",
147
+ "subject": "pi-extensions/lib/remote-pair-send.ts",
148
+ "find": ["\t\tif ((err as NodeJS.ErrnoException).code === \"ENOENT\") return null;"],
149
+ "replace": ["\t\tif (false) return null;"],
150
+ "gate": ["bash", "run.sh", "check-tests-beside-behavior"],
151
+ "timeoutSeconds": 600,
152
+ "signature": "[QK:REMOTE-PAIR-OPT-IN-ABSENT-IS-OFF]",
153
+ "signatureSource": "pi-extensions/lib/remote-pair-send.test.ts",
154
+ "build": {
155
+ "argv": ["bash", "scripts/build-bridge.sh"],
156
+ "output": "mcp/entwurf-bridge/dist"
157
+ }
158
+ },
159
+ {
160
+ "claim": "REMOTE-PAIR-OPT-IN-BROKEN-REFUSED",
161
+ "title": "a symlinked or non-regular opt-in is followed, so the route the bridge walks is whatever the link points at today",
162
+ "subject": "pi-extensions/lib/remote-pair-send.ts",
163
+ "find": ["\tif (!st.isFile()) {\n\t\tthrow new RemoteRouteDeclarationError(`remote pair opt-in"],
164
+ "replace": ["\tif (false) {\n\t\tthrow new RemoteRouteDeclarationError(`remote pair opt-in"],
165
+ "gate": ["bash", "run.sh", "check-tests-beside-behavior"],
166
+ "timeoutSeconds": 600,
167
+ "signature": "[QK:REMOTE-PAIR-OPT-IN-BROKEN-REFUSED]",
168
+ "signatureSource": "pi-extensions/lib/remote-pair-send.test.ts",
169
+ "build": {
170
+ "argv": ["bash", "scripts/build-bridge.sh"],
171
+ "output": "mcp/entwurf-bridge/dist"
172
+ }
173
+ },
174
+ {
175
+ "claim": "REMOTE-REPORTED-ERROR-NO-DELIVERY-INFERENCE",
176
+ "title": "an error the other host reports after a possible write is headed as a non-delivery, inviting a resend that would deliver twice",
177
+ "subject": "pi-extensions/lib/remote-pair-send.ts",
178
+ "find": [
179
+ "\t\t\t\ttext: `entwurf_v2 remote ${via} → the other host reported an error while processing; read its receipt below, delivery is not inferred from this line; not retried (paths below are on the other host)\\n${o.text}`,"
180
+ ],
181
+ "replace": [
182
+ "\t\t\t\ttext: `entwurf_v2 remote ${via} → the other host did not deliver (paths below are on the other host)\\n${o.text}`,"
183
+ ],
184
+ "gate": ["bash", "run.sh", "check-tests-beside-behavior"],
185
+ "timeoutSeconds": 600,
186
+ "signature": "[QK:REMOTE-REPORTED-ERROR-NO-DELIVERY-INFERENCE]",
187
+ "signatureSource": "pi-extensions/lib/remote-pair-send.test.ts",
188
+ "build": {
189
+ "argv": ["bash", "scripts/build-bridge.sh"],
190
+ "output": "mcp/entwurf-bridge/dist"
191
+ }
192
+ }
193
+ ]
194
+ }
@@ -0,0 +1,66 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "lane": "remote-pair-transport",
4
+ "mutants": [
5
+ {
6
+ "claim": "REMOTE-TRANSPORT-NO-SHELL-SEPARATE-STREAMS",
7
+ "title": "the transport runs its argv through a shell, so a remote declaration word is expanded on the sending host before ssh ever sees it",
8
+ "subject": "pi-extensions/lib/remote-pair-transport.ts",
9
+ "find": ["{ shell: false, stdio: [\"pipe\", \"pipe\", \"pipe\"] }"],
10
+ "replace": ["{ shell: true, stdio: [\"pipe\", \"pipe\", \"pipe\"] }"],
11
+ "gate": ["bash", "run.sh", "check-tests-beside-behavior"],
12
+ "timeoutSeconds": 600,
13
+ "signature": "[QK:REMOTE-TRANSPORT-NO-SHELL-SEPARATE-STREAMS]",
14
+ "signatureSource": "pi-extensions/lib/remote-pair-transport.test.ts",
15
+ "build": {
16
+ "argv": ["bash", "scripts/build-bridge.sh"],
17
+ "output": "mcp/entwurf-bridge/dist"
18
+ }
19
+ },
20
+ {
21
+ "claim": "REMOTE-TRANSPORT-TIMEOUT-REAPS-OWN-CHILD",
22
+ "title": "the bound only sends TERM, so an ssh child that ignores it outlives the send and the call never returns",
23
+ "subject": "pi-extensions/lib/remote-pair-transport.ts",
24
+ "find": ["\t\t\t\t\tchild.kill(\"SIGKILL\");"],
25
+ "replace": ["\t\t\t\t\tvoid 0;"],
26
+ "gate": ["bash", "run.sh", "check-tests-beside-behavior"],
27
+ "timeoutSeconds": 600,
28
+ "signature": "[QK:REMOTE-TRANSPORT-TIMEOUT-REAPS-OWN-CHILD]",
29
+ "signatureSource": "pi-extensions/lib/remote-pair-transport.test.ts",
30
+ "build": {
31
+ "argv": ["bash", "scripts/build-bridge.sh"],
32
+ "output": "mcp/entwurf-bridge/dist"
33
+ }
34
+ },
35
+ {
36
+ "claim": "REMOTE-TRANSPORT-SPAWN-FAILURE-INDETERMINATE",
37
+ "title": "a child that never started reports exit 0, so a missing ssh binary reads as a clean exit instead of an unknown outcome",
38
+ "subject": "pi-extensions/lib/remote-pair-transport.ts",
39
+ "find": ["\t\t\t\t\texitCode: spawnError ? null : code,"],
40
+ "replace": ["\t\t\t\t\texitCode: spawnError ? 0 : code,"],
41
+ "gate": ["bash", "run.sh", "check-tests-beside-behavior"],
42
+ "timeoutSeconds": 600,
43
+ "signature": "[QK:REMOTE-TRANSPORT-SPAWN-FAILURE-INDETERMINATE]",
44
+ "signatureSource": "pi-extensions/lib/remote-pair-transport.test.ts",
45
+ "build": {
46
+ "argv": ["bash", "scripts/build-bridge.sh"],
47
+ "output": "mcp/entwurf-bridge/dist"
48
+ }
49
+ },
50
+ {
51
+ "claim": "REMOTE-TRANSPORT-HELD-PIPES-STILL-SETTLE",
52
+ "title": "the send waits for a close that a pipe-holding descendant never lets happen, so one stuck remote leaves entwurf_v2 hanging forever",
53
+ "subject": "pi-extensions/lib/remote-pair-transport.ts",
54
+ "find": ["\t\t\t\t\tfinalTimer = setTimeout(() => {"],
55
+ "replace": ["\t\t\t\t\tfinalTimer = setTimeout(() => {\n\t\t\t\t\t\treturn;"],
56
+ "gate": ["bash", "run.sh", "check-tests-beside-behavior"],
57
+ "timeoutSeconds": 600,
58
+ "signature": "[QK:REMOTE-TRANSPORT-HELD-PIPES-STILL-SETTLE]",
59
+ "signatureSource": "pi-extensions/lib/remote-pair-transport.test.ts",
60
+ "build": {
61
+ "argv": ["bash", "scripts/build-bridge.sh"],
62
+ "output": "mcp/entwurf-bridge/dist"
63
+ }
64
+ }
65
+ ]
66
+ }
@@ -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);