@junghanacs/entwurf 0.35.0 → 0.36.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 (91) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +338 -0
  3. package/CONTRIBUTING.md +1 -1
  4. package/DELIVERY.md +15 -0
  5. package/FAQ.md +3 -3
  6. package/README.md +30 -16
  7. package/VERIFY.md +14 -11
  8. package/demo/README.md +1 -1
  9. package/docs/acp-backend-rail.md +29 -3
  10. package/docs/setup-clean-host.md +31 -23
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +46 -2
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/model-ids.js +67 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +9 -20
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +4 -3
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +11 -0
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +27 -1
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +3 -0
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +2 -0
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +99 -18
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +3 -3
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +13 -4
  22. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +178 -25
  23. package/mcp/entwurf-bridge/dist/scripts/codex-socket-classify.js +44 -0
  24. package/mcp/entwurf-bridge/tsconfig.build.json +5 -0
  25. package/package.json +20 -15
  26. package/pi/pi-durable/bootstrap.mjs +12 -8
  27. package/pi/pi-durable/carrier/runtime.js +52 -2
  28. package/pi/pi-durable/carrier/subagent.js +6 -1
  29. package/pi/pi-durable/carrier/tui.js +31 -4
  30. package/pi/pi-durable/carrier-relocation.mjs +2 -2
  31. package/pi/pi-durable/overlay/upstream-pin.json +79 -36
  32. package/pi-extensions/lib/acp/backend-adapter.ts +52 -2
  33. package/pi-extensions/lib/acp/model-ids.js +77 -0
  34. package/pi-extensions/lib/acp/models.ts +15 -22
  35. package/pi-extensions/lib/codex-fresh-preflight.ts +4 -3
  36. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +12 -0
  37. package/pi-extensions/lib/fresh-call-dispatch.ts +25 -1
  38. package/pi-extensions/lib/herdr-fresh-call.ts +5 -0
  39. package/pi-extensions/lib/mux-fresh-call.ts +3 -0
  40. package/pi-extensions/lib/native-push/codex-ws-client.ts +129 -18
  41. package/pi-extensions/lib/pi-durable-runtime.ts +4 -4
  42. package/pi-extensions/lib/resume-launch-identity.ts +12 -4
  43. package/pi-extensions/meta-bridge-pi-durable.ts +230 -34
  44. package/run.sh +193 -58
  45. package/scripts/check-acp-backend-preflight.ts +10 -2
  46. package/scripts/check-acp-cortex.ts +7 -6
  47. package/scripts/check-acp-prompt-lifecycle.ts +2 -2
  48. package/scripts/check-acp-provider-surface.ts +1 -1
  49. package/scripts/check-acp-session-reuse.ts +5 -5
  50. package/scripts/check-acp-stop-reason.ts +2 -2
  51. package/scripts/check-acp-stream-hooks.ts +2 -2
  52. package/scripts/check-acp-usage-accounting.ts +2 -2
  53. package/scripts/check-codex-app-server-launch.ts +55 -12
  54. package/scripts/check-codex-native-push.ts +231 -12
  55. package/scripts/check-fresh-call-dispatch.ts +47 -4
  56. package/scripts/check-gate-qualification.ts +8 -6
  57. package/scripts/check-probe-ordering.ts +13 -6
  58. package/scripts/check-resume-launch-identity.ts +38 -0
  59. package/scripts/codex-app-server-launch.sh +54 -29
  60. package/scripts/codex-socket-classify.ts +46 -0
  61. package/scripts/emit-pi-durable-carrier.ts +159 -168
  62. package/scripts/mutants/acp-cortex.json +17 -8
  63. package/scripts/mutants/acp-set-model-readback.json +28 -0
  64. package/scripts/mutants/codex-app-server-launch.json +39 -12
  65. package/scripts/mutants/codex-native.json +15 -0
  66. package/scripts/mutants/control-send-receipt.json +1 -1
  67. package/scripts/mutants/fresh-call-dispatch.json +11 -0
  68. package/scripts/mutants/herdr-dev-checkout.json +70 -0
  69. package/scripts/mutants/pack-install.json +3 -3
  70. package/scripts/mutants/pi-durable.json +282 -16
  71. package/scripts/mutants/pi-floor.json +5 -5
  72. package/scripts/mutants/resume-launch-identity.json +15 -0
  73. package/scripts/mutants/v2-visible-resume.json +11 -0
  74. package/scripts/pi-durable-contact-host.mjs +1 -1
  75. package/scripts/resolve-acp-bridge.ts +1 -1
  76. package/scripts/smoke-acp-bundled-mcp-live.ts +1 -1
  77. package/scripts/smoke-acp-carrier-augment-live.ts +1 -1
  78. package/scripts/smoke-acp-long-turn-live.ts +1 -1
  79. package/scripts/smoke-acp-mcp-live.ts +1 -1
  80. package/scripts/smoke-acp-memory-containment-live.ts +1 -1
  81. package/scripts/smoke-acp-ordering-probe-live.ts +1 -1
  82. package/scripts/smoke-acp-overlay-live.ts +1 -1
  83. package/scripts/smoke-acp-provider-live.ts +1 -1
  84. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  85. package/scripts/smoke-acp-session-reuse-live.ts +1 -1
  86. package/scripts/smoke-acp-skill-live.ts +1 -1
  87. package/scripts/smoke-acp-socket-citizen-live.ts +2 -2
  88. package/scripts/smoke-acp-v2-send-live.ts +1 -1
  89. package/scripts/smoke-entwurf-chain-live.ts +1 -1
  90. package/scripts/smoke-mux-lifecycle-live.ts +1 -1
  91. package/pi/pi-durable/overlay/runtime-contacts.patch +0 -283
@@ -3441,6 +3441,7 @@ let manifestCount: number;
3441
3441
  "acp-launch-namespace": 2,
3442
3442
  "acp-overlay": 1,
3443
3443
  "acp-prompt-lifecycle": 15,
3444
+ "acp-set-model-readback": 2,
3444
3445
  "acp-stop-reason": 6,
3445
3446
  "acp-stream-hooks": 10,
3446
3447
  "acp-usage-accounting": 12,
@@ -3449,10 +3450,10 @@ let manifestCount: number;
3449
3450
  "bridge-boot-resume": 5,
3450
3451
  "bridge-command-boot": 12,
3451
3452
  "capability-cache": 3,
3452
- "codex-app-server-launch": 9,
3453
+ "codex-app-server-launch": 10,
3453
3454
  "codex-caller-seat": 28,
3454
3455
  "control-send-receipt": 19,
3455
- "codex-native": 82,
3456
+ "codex-native": 83,
3456
3457
  "compaction-send-guard": 7,
3457
3458
  "control-socket-disconnect": 4,
3458
3459
  "demo-reproducibility": 12,
@@ -3463,10 +3464,10 @@ let manifestCount: number;
3463
3464
  "pi-bridge-sender": 4,
3464
3465
  "pi-mcp-bridge": 2,
3465
3466
  "pi-mcp-register": 3,
3466
- "pi-durable": 58,
3467
+ "pi-durable": 78,
3467
3468
  "copilot-receive": 20,
3468
3469
  "entwurf-peers": 1,
3469
- "fresh-call-dispatch": 11,
3470
+ "fresh-call-dispatch": 12,
3470
3471
  "fresh-cut": 3,
3471
3472
  "gate-qualification": 10,
3472
3473
  "herdr-placement": 13,
@@ -3474,6 +3475,7 @@ let manifestCount: number;
3474
3475
  "herdr-plugin-profile": 14,
3475
3476
  "herdr-activation": 34,
3476
3477
  "herdr-plugin-build": 20,
3478
+ "herdr-dev-checkout": 4,
3477
3479
  "herdr-runtime-bootstrap": 31,
3478
3480
  "herdr-fresh-call": 38,
3479
3481
  "herdr-supply": 11,
@@ -3497,13 +3499,13 @@ let manifestCount: number;
3497
3499
  "release-gate": 24,
3498
3500
  "retired-sent-surface": 2,
3499
3501
  "resume-args": 6,
3500
- "resume-launch-identity": 6,
3502
+ "resume-launch-identity": 7,
3501
3503
  "self-address": 8,
3502
3504
  "setup-verdict": 14,
3503
3505
  "source-install": 2,
3504
3506
  "typing-call-fence": 1,
3505
3507
  "v2-surface": 13,
3506
- "v2-visible-resume": 17,
3508
+ "v2-visible-resume": 18,
3507
3509
  };
3508
3510
  const laneTally: Record<string, number> = {};
3509
3511
  for (const man of manifests) laneTally[man.lane] = (laneTally[man.lane] ?? 0) + man.mutants.length;
@@ -174,7 +174,7 @@ function makeRecordingConnection(calls: RecordedCall[]): AcpConnectionLike {
174
174
  },
175
175
  setSessionConfigOption: async (params) => {
176
176
  calls.push({ method: "setSessionConfigOption", params });
177
- return {};
177
+ return { configOptions: [{ id: "model", currentValue: (params as { value?: unknown }).value }] };
178
178
  },
179
179
  prompt: async (params) => {
180
180
  calls.push({ method: "prompt", params });
@@ -381,12 +381,19 @@ function makeRecordingConnection(calls: RecordedCall[]): AcpConnectionLike {
381
381
  "backend.ts still sends the clientInfo the probe mirrors",
382
382
  );
383
383
  // The claude adapter's enforceModel wire shape — the LIVE probe executes THIS
384
- // method (emitted twin), so its shape is pinned where it lives.
384
+ // method (emitted twin), so its shape is pinned where it lives. Sliced to the claude
385
+ // adapter: the cortex adapter makes the same call, and must not satisfy this pin.
386
+ const claudeAdapterSrc = ADAPTER_SRC.slice(
387
+ ADAPTER_SRC.indexOf("export const claudeAdapter"),
388
+ ADAPTER_SRC.indexOf("export const cortexAdapter"),
389
+ );
385
390
  assert.ok(
386
- ADAPTER_SRC.includes(
387
- 'await setConfig.call(connection, { sessionId: acpSessionId, configId: "model", value: nativeModelId });',
388
- ),
389
- 'claudeAdapter.enforceModel still sends setSessionConfigOption({configId:"model"})',
391
+ claudeAdapterSrc.length > 0 &&
392
+ claudeAdapterSrc.includes(
393
+ 'await setConfig.call(connection, {\n\t\t\tsessionId: acpSessionId,\n\t\t\tconfigId: "model",\n\t\t\tvalue: nativeModelId,\n\t\t});',
394
+ ) &&
395
+ claudeAdapterSrc.includes("confirmClaudeSetModelAnswer(answer, nativeModelId);"),
396
+ 'claudeAdapter.enforceModel still sends setSessionConfigOption({configId:"model"}) and reads its answer back',
390
397
  );
391
398
 
392
399
  // Permission policy: the runner's copy must mirror backend.ts's private
@@ -42,6 +42,7 @@ import assert from "node:assert/strict";
42
42
  import * as fs from "node:fs";
43
43
  import * as os from "node:os";
44
44
  import * as path from "node:path";
45
+ import { AcpModelNotCuratedError } from "../pi-extensions/lib/acp/model-ids.js";
45
46
  import { serializeMetaIdentity, upsertMetaSession } from "../pi-extensions/lib/meta-session.ts";
46
47
  import {
47
48
  ResumeBackendUnsupportedError,
@@ -162,6 +163,43 @@ function main(): void {
162
163
  );
163
164
  }
164
165
 
166
+ // 4d. #128 — the second EXPECTED refusal. Under the `entwurf` provider Pi resolves `--model`
167
+ // by substring, so a retired Claude 5 id would reopen on a Cortex row. The pair judged is the
168
+ // one the launch would EMIT: a bare Claude id recorded under another provider is routed to
169
+ // `entwurf` too, so judging the recorded provider alone would let that one through.
170
+ {
171
+ const acpModelLine = (provider: string, modelId: string): string =>
172
+ `${JSON.stringify({ type: "model_change", provider, modelId })}\n`;
173
+ const settle = (gid: string): { identity?: ReturnType<typeof resolveResumeLaunchIdentity>; err?: unknown } => {
174
+ try {
175
+ return { identity: resolveResumeLaunchIdentity(gid) };
176
+ } catch (err) {
177
+ return { err };
178
+ }
179
+ };
180
+ const minted = (native: string, name: string, provider: string, modelId: string): string =>
181
+ mintRecord(native, writeTranscript(name, sessionLine(native) + acpModelLine(provider, modelId)));
182
+ const retired = settle(
183
+ minted("0199a5a5-1111-4222-8333-444455556666", "acp-retired.jsonl", "entwurf", "claude-sonnet-5"),
184
+ );
185
+ const routed = settle(
186
+ minted("0199b5b5-1111-4222-8333-444455556666", "acp-routed.jsonl", "anthropic", "claude-opus-5"),
187
+ );
188
+ const current = settle(
189
+ minted("0199c5c5-1111-4222-8333-444455556666", "acp-current.jsonl", "entwurf", "claude-sonnet-5-5"),
190
+ );
191
+ ok(
192
+ "4d: an entwurf-provider resume of a non-curated id is a TYPED refusal — recorded under entwurf, or a bare Claude id the launch would route to entwurf — while the exact 5.5 id resolves unchanged [QK:RESUME-ID-ACP-EXACT-CURATION]",
193
+ retired.err instanceof AcpModelNotCuratedError &&
194
+ retired.err.reason === "acp-model-not-curated" &&
195
+ retired.err.model === "claude-sonnet-5" &&
196
+ routed.err instanceof AcpModelNotCuratedError &&
197
+ routed.err.model === "claude-opus-5" &&
198
+ current.identity?.provider === "entwurf" &&
199
+ current.identity.model === "claude-sonnet-5-5",
200
+ );
201
+ }
202
+
165
203
  const gidNoFile = mintRecord("0199dddd-1111-4222-8333-444455556666", null);
166
204
  rejects(gidNoFile, "no recorded transcriptPath", "4: transcriptPath null → nothing to resume");
167
205
 
@@ -156,38 +156,53 @@ done
156
156
  # --- is somebody already listening there? ----------------------------------
157
157
  # Three outcomes, and they are NOT the same repair. A live socket means a second server
158
158
  # would either lose the bind race or silently replace the endpoint every citizen's record
159
- # already points at — refuse and name the holder. An indeterminate path (symlink, not a
160
- # socket, owned by another uid) is the classification `checkCodexSocketFile` already uses in
161
- # the product, and a launcher that guessed past it would be clobbering something it cannot
162
- # identify. A dead socket file is the ordinary leftover of a Ctrl-C, and the vendor replaces
163
- # it — that one is a fact line, not a refusal.
164
- probe="$(python3 - "$SOCK" <<'PY'
165
- import os, socket, stat, sys
166
-
167
- path = sys.argv[1]
168
- try:
169
- st = os.lstat(path)
170
- except FileNotFoundError:
171
- print("absent")
172
- raise SystemExit(0)
173
- except OSError as exc:
174
- print(f"indeterminate\t{exc.strerror or exc}")
175
- raise SystemExit(0)
176
-
177
- if stat.S_ISLNK(st.st_mode):
178
- print("indeterminate\tthe path is a symlink")
179
- raise SystemExit(0)
180
- if not stat.S_ISSOCK(st.st_mode):
181
- print("indeterminate\tthe path exists and is not a socket")
182
- raise SystemExit(0)
183
- if st.st_uid != os.getuid():
184
- print(f"indeterminate\tthe socket is owned by uid {st.st_uid}, not {os.getuid()}")
185
- raise SystemExit(0)
159
+ # already points at — refuse and name the holder. An indeterminate path (a symlink that is not
160
+ # Codex's own alias, not a socket, owned by another uid) is refused, because a launcher that
161
+ # guessed past it would be clobbering something it cannot identify. A dead socket file is the
162
+ # ordinary leftover of a Ctrl-C, and the vendor replaces it — that one is a fact line, not a
163
+ # refusal. So is a dangling rendezvous alias: from Codex 0.162.1 the advertised path is a
164
+ # symlink into the vendor's own socket directory, and the vendor removes exactly that alias
165
+ # when it starts.
166
+ #
167
+ # WHAT A LAUNCH IS ALLOWED TO TRUST is decided ONCE, by the classifier delivery and preflight
168
+ # use (`checkCodexSocketFile`). This launcher keeps no copy of it: telling Codex's exact alias
169
+ # from any other symlink takes a hash and an exact readlink comparison, and a second
170
+ # implementation here would be the transcription `codex-socket-path` already measured drifting.
171
+ # It runs that classifier's COMPILED twin directly with `node`, in a checkout and in an installed
172
+ # package alike: `build-bridge` emits it (`pnpm install` runs it through `prepare`, and the package
173
+ # ships it through `prepack`), so a missing file is an incomplete installation, refused by name —
174
+ # never a reason to fall back to the TypeScript source. The Python below only CONNECTS to the
175
+ # endpoint the classifier verified, to tell a live server from a stale socket file. An answer the
176
+ # classifier never gives is refused right here, before it can be read as one of the probe's own
177
+ # verdicts.
178
+ #
179
+ # The gate's limit, stated rather than hidden: it executes this launcher on non-alias layouts
180
+ # and on a non-exact symlink, so it proves the launcher accepts no more than the classifier.
181
+ # The exact-alias accept path is proven on `checkCodexSocketFile` itself, with a sandbox socket
182
+ # directory. This launcher's own alias branch is NOT exercised by an existing-server LIVE
183
+ # diagnostic; that launch/refusal path needs its own explicitly authorised LIVE observation.
184
+ CLASSIFIER="$REPO_DIR/mcp/entwurf-bridge/dist/scripts/codex-socket-classify.js"
185
+ [ -f "$CLASSIFIER" ] \
186
+ || fail "codex-app-server-classifier-missing: the compiled socket classifier is not at
187
+ $CLASSIFIER
188
+ This launcher runs it directly and keeps no other copy of it. Reinstall @junghanacs/entwurf (the
189
+ package ships it through prepack build-bridge), or run 'pnpm run build-bridge' in a source checkout."
190
+ if ! classified="$(node "$CLASSIFIER")"; then
191
+ fail "could not classify the control socket at $SOCK.
192
+ 'node $CLASSIFIER' failed; it is the only classification of this socket,
193
+ and this launcher will not guess past it. Its output above says why."
194
+ fi
195
+ classified_word="${classified%% *}"
196
+ classified_detail="${classified#* }"
197
+ case "$classified_word" in
198
+ socket)
199
+ probe="$(python3 - "$classified_detail" <<'PY'
200
+ import socket, sys
186
201
 
187
202
  probe = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
188
203
  probe.settimeout(2)
189
204
  try:
190
- probe.connect(path)
205
+ probe.connect(sys.argv[1])
191
206
  except (ConnectionRefusedError, FileNotFoundError):
192
207
  print("stale")
193
208
  except OSError as exc:
@@ -197,7 +212,17 @@ else:
197
212
  finally:
198
213
  probe.close()
199
214
  PY
200
- )" || fail "could not classify the control socket at $SOCK (python3 is required by this launcher)."
215
+ )" || fail "could not probe the control socket at $classified_detail (python3 is required by this launcher)."
216
+ ;;
217
+ absent) probe="absent" ;;
218
+ dangling) probe="stale $classified_detail" ;;
219
+ indeterminate) probe="indeterminate $classified_detail" ;;
220
+ *)
221
+ fail "codex-app-server-socket-probe-unrecognised: the socket classifier answered
222
+ '$classified_word', which is not one of socket/absent/dangling/indeterminate. Refusing to launch on a
223
+ reading nothing understands."
224
+ ;;
225
+ esac
201
226
 
202
227
  probe_status="${probe%% *}"
203
228
  probe_reason="${probe#* }"
@@ -0,0 +1,46 @@
1
+ /**
2
+ * codex-socket-classify — classify the Codex app-server default control socket for THIS
3
+ * environment, through the one product classifier, and print the answer for a shell.
4
+ *
5
+ * WHY A LEAF. `entwurf codex-app-server` is a bash launcher, and it has to decide whether a
6
+ * launch would clobber something. From Codex 0.162.1 the advertised socket path is a
7
+ * rendezvous alias into a vendor-owned directory, and telling the vendor's exact alias from any
8
+ * other symlink takes a hash, an exact readlink comparison and ownership/mode checks. Doing that
9
+ * in the launcher's Python would be a second implementation of a security classification, and
10
+ * `codex-socket-path.ts` records what a second spelling of this socket did once (it diverged).
11
+ * So the launcher asks `checkCodexSocketFile`, the classifier delivery and preflight use, and
12
+ * only connects to what it was told.
13
+ *
14
+ * HOW IT IS REACHED. The launcher runs this file's COMPILED twin,
15
+ * `mcp/entwurf-bridge/dist/scripts/codex-socket-classify.js`, directly with `node` — in a checkout
16
+ * and in an installed package alike. There is no `run.sh` verb and no `run_ts` crossing for it:
17
+ * `build-bridge` emits it (`pnpm install` runs that through `prepare`, packing through `prepack`),
18
+ * and a missing twin is an incomplete installation the launcher refuses by name.
19
+ *
20
+ * One line on stdout, a word then a tab-separated detail:
21
+ * socket <connectPath> a verified socket; the launcher connects there to tell live from stale
22
+ * absent nothing at the advertised path
23
+ * dangling <reason> Codex's own alias whose socket is gone; the vendor removes it on start
24
+ * indeterminate <reason> anything else; the launcher refuses
25
+ *
26
+ * Reads `process.env` and takes no argument, for the reason `codex-socket-path.ts` gives: an argv
27
+ * seam would be a way to redirect the address. It has no daemon-root input either; the gate's
28
+ * exact-alias cells drive `checkCodexSocketFile` directly with a sandbox root.
29
+ */
30
+
31
+ import {
32
+ checkCodexSocketFile,
33
+ resolveCodexDefaultSocketPath,
34
+ } from "../pi-extensions/lib/native-push/codex-ws-client.ts";
35
+
36
+ if (process.argv.length > 2) {
37
+ console.error("usage: codex-socket-classify (no arguments; the environment is the input)");
38
+ process.exit(2);
39
+ }
40
+
41
+ const oneLine = (text: string): string => text.replace(/[\t\r\n]+/g, " ");
42
+ const check = checkCodexSocketFile(resolveCodexDefaultSocketPath(process.env));
43
+ if (check.ok) process.stdout.write(`socket\t${check.connectPath}`);
44
+ else if (check.status === "dead" && check.layout === "absent") process.stdout.write("absent");
45
+ else if (check.status === "dead") process.stdout.write(`dangling\t${oneLine(check.reason)}`);
46
+ else process.stdout.write(`indeterminate\t${oneLine(check.reason)}`);