@junghanacs/entwurf 0.35.1 → 0.36.1
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/AGENTS.md +2 -2
- package/CHANGELOG.md +403 -0
- package/CONTRIBUTING.md +1 -1
- package/DELIVERY.md +17 -0
- package/FAQ.md +3 -3
- package/README.md +30 -16
- package/VERIFY.md +12 -10
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +69 -5
- package/docs/setup-clean-host.md +30 -22
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +4 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +99 -18
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +3 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +59 -14
- package/mcp/entwurf-bridge/dist/scripts/codex-socket-classify.js +44 -0
- package/mcp/entwurf-bridge/tsconfig.build.json +5 -0
- package/package.json +21 -16
- package/pi/pi-durable/bootstrap.mjs +12 -8
- package/pi/pi-durable/carrier/runtime.js +52 -2
- package/pi/pi-durable/carrier/subagent.js +6 -1
- package/pi/pi-durable/carrier/tui.js +31 -4
- package/pi/pi-durable/carrier-relocation.mjs +2 -2
- package/pi/pi-durable/overlay/upstream-pin.json +79 -36
- package/pi-extensions/lib/codex-fresh-preflight.ts +4 -3
- package/pi-extensions/lib/native-push/codex-ws-client.ts +129 -18
- package/pi-extensions/lib/pi-durable-runtime.ts +4 -4
- package/pi-extensions/meta-bridge-pi-durable.ts +77 -21
- package/run.sh +116 -39
- package/scripts/check-acp-prompt-lifecycle.ts +4 -2
- package/scripts/check-acp-session-reuse.ts +4 -2
- package/scripts/check-acp-stop-reason.ts +4 -2
- package/scripts/check-acp-stream-hooks.ts +4 -2
- package/scripts/check-acp-usage-accounting.ts +4 -2
- package/scripts/check-codex-app-server-launch.ts +55 -12
- package/scripts/check-codex-native-push.ts +231 -12
- package/scripts/check-gate-qualification.ts +4 -4
- package/scripts/check-herdr-supply.ts +32 -7
- package/scripts/codex-app-server-launch.sh +54 -29
- package/scripts/codex-socket-classify.ts +46 -0
- package/scripts/emit-pi-durable-carrier.ts +159 -168
- package/scripts/fixtures/herdr-supply.json +10 -9
- package/scripts/lib/acp-gate-home.ts +75 -0
- package/scripts/lib/codex-socket-listener.ts +429 -0
- package/scripts/mutants/acp-stop-reason.json +11 -0
- package/scripts/mutants/codex-app-server-launch.json +39 -12
- package/scripts/mutants/codex-native.json +67 -0
- package/scripts/mutants/control-send-receipt.json +1 -1
- package/scripts/mutants/herdr-supply.json +6 -6
- package/scripts/mutants/pack-install.json +3 -3
- package/scripts/mutants/pi-durable.json +165 -16
- package/scripts/mutants/pi-floor.json +5 -5
- package/scripts/pi-durable-contact-host.mjs +1 -1
- package/scripts/smoke-codex-fresh-live.ts +31 -7
- package/scripts/smoke-pi-durable-fresh-live.ts +22 -3
- package/pi/pi-durable/overlay/runtime-contacts.patch +0 -283
package/VERIFY.md
CHANGED
|
@@ -106,7 +106,7 @@ that moved out of `scripts/` and into a file beside its subject would otherwise
|
|
|
106
106
|
|
|
107
107
|
> **The herdr rail is an on-demand axis until it has run.** `smoke-herdr-fresh-call-live` opens a real caller inside a real herdr pane, has it invoke the PUBLIC `entwurf_fresh_call`, and reads the child's nonce callback — the one axis no deterministic gate reaches, since the rail is selected from the caller's own process environment. **The release aggregate does not yet require the herdr rail**: herdr is optional (a cut on a host without it must not block), and first evidence precedes promotion — a rail whose first acceptance run is days old has not earned a place in the gate that blocks every cut. Run it directly with `LIVE=1 ./run.sh smoke-herdr-fresh-call-live` whenever a cut changes herdr rail code, and promote it to a MUST only after the axis has a run history to point at. Without `LIVE=1`, or without herdr/pi/claude on PATH, it reports protocol SKIP — never a pass.
|
|
108
108
|
|
|
109
|
-
> **The plugin's FIRST USER PATH is its own on-demand axis, and an npm acquisition receipt holds only for the ref it ran on (#118 홉 1).** Every other herdr gate proves the plugin against a source this repository controls: `check-herdr-plugin-build` drives a stub, and `smoke-herdr-plugin-build-live` redirects the product remote to a local bare clone with git `insteadOf` and asserts `identity.kind === "herdr-checkout"`. npm has been the production authority class since `dd84ac0`; the 0.30.0 follow-up re-pinned `@junghanacs/entwurf@0.30.0` with its published sha512 after the candidate windows rode `herdr-checkout` (`8b92e09`), and the 0.30.1 preparation rode that closed verification carrier again without inventing a registry integrity. The plugin snapshot `v2026.10.9-herdr.0.34.0` pinned the published `@junghanacs/entwurf@0.34.0` with its measured sha512 (#132); 0.31–0.33 were never pinned. The 0.34.1 preparation rode the closed `herdr-checkout` verification carrier again without inventing a registry integrity, and the snapshot `v2026.10.9-herdr.0.34.1` re-pins the published `@junghanacs/entwurf@0.34.1` with its measured sha512 (#135). The 0.34.2 preparation rode the same carrier, and the snapshot `v2026.10.11-herdr.0.34.2` re-pins the published `@junghanacs/entwurf@0.34.2` with its measured sha512 (#136). The source re-proof has separate dated npm rows: 2026-09-23 public `main=113ca53`, plugin 0.5.0, runtime 0.25.0 (CHANGELOG 0.25.1), and the 0.30.0 re-pin accepted at public `506c46f` in the [#126 thread](https://github.com/junghan0611/entwurf/issues/126). Neither certifies the 0.34.0 registry acquisition. That pin's real-npm swap / torn-swap recovery was measured locally on 2026-10-09 by driving the registry-shipped `scripts/herdr-runtime.mjs` (byte-equal to this checkout's) with its own npm acquisition: a 0.33.0 → 0.34.0 swap, and the same upgrade torn between its two renames then resumed, where the last-good 0.33.0 tree was restored before 0.34.0 was acquired again. Both ended `runtime-ready` at observed digest `sha256-e9d995ff…`, the accepted 0.34.0 artifact. That local receipt is recorded in the [#132 thread](https://github.com/junghan0611/entwurf/issues/132); it covers runtime recovery, not this smoke's cells. This smoke's public-ref acceptance of the 0.34.0 pin was measured locally at `--ref main`: PASS at resolved `b5d15d8` and again at the docs follow-up `8ed4d1a`, each acquiring `kind=npm @junghanacs/entwurf@0.34.0` — a main-ref receipt, not a tag-ref, native or full-Herdr claim. The 0.34.1 re-pin's npm-source rows — swap / torn-swap recovery, this smoke at a public ref, and the package consumer — are its own, recorded with that pin and never inherited from 0.34.0; checkout candidate evidence does not transfer. `LIVE=1 ./run.sh smoke-herdr-raw-install-live [--ref <REF>]` fills that row in a clean `node:24` container with no socket, config, cache or repo mounted, no `insteadOf`, the public remote spelled as a user would type it, and pi / herdr / Claude Code installed in the IMAGE BUILD as scaffolding (Hard Rule 17 — the product face begins at `herdr integration install pi`). herdr itself comes from `scripts/install-herdr-ci.sh` against `scripts/fixtures/herdr-supply.json`, so no version or digest lives here. `--ref` defaults to `main` rather than a pinned SHA because receipt (a) is defined as what a user gets today; the resolved commit is printed, so the receipt stays exact. It takes a REMOTE REF and not an arbitrary commit — measured on herdr 0.9.1, `--ref <sha>` fails as `couldn't find remote ref`, because the checkout fetches by name — so a candidate is addressed by its branch and pinned by the resolved commit the gate prints. **Its first question is a measurement, not an assumption:** herdr registers a plugin through a running server and falls back to `persist_plugin_offline` only on a connection error (`src/cli/plugin.rs:914-981,1016-1021` @ `c77af189`, 0.9.0 source), and a container has no server. The 2026-09-23 PASS installed with no server on the 0.9.1 binary `113ca53`'s `herdr-supply.json` pins, so that install path worked on 0.9.1 — inferred from the PASS, not a direct observation of the fallback branch; a later "no" on another herdr is #118 investigation output rather than a completion. `--yes` is always passed so a non-interactive exit 2 (`:189`) can never be misread as a missing server. **At a ref whose lock names `herdr-checkout` the acquisition axis reports a NAMED SKIP rather than a red, and the rest of the journey still runs.** That lock is the candidate carrier a cut legitimately sits on — 0.23.0 was tagged that way and `dd84ac0` pinned npm afterwards — so a red there would report a broken install when what is true is that the ref does not lock npm. What is asserted at BOTH lock kinds is that the runtime came from the source the ref locks, and the package name is read from the checkout manifest rather than the lock, because a `herdr-checkout` lock carries no `name` (`herdr-runtime.mjs:317-328`) and joining `undefined` throws instead of failing an assertion. **Cells [6]–[9] carry it past the install into the USE path (#118 H1-4/H1-5).** An install is not a use, so the same container then runs what those bytes became: the compiled bridge entry, the three `REQUIRED_BINS` as executables, and a real `entwurf check-bridge` printing its exact eight-verb set — the installed-runtime row below, measured rather than inferred from the bootstrap's refusal to install without them. Then the pi that this plugin WIRED is started with no `-e` and no `--no-extensions`, because the subject is the wiring and not this checkout: one `get_state` in `--mode rpc` costs zero tokens and needs no provider argument (a bogus `--provider` exits 1 before session_start; omitting the pair lets pi resolve the default the extension itself registers), and while that session is alive the cell reads its V3 record, its gardenId-keyed control socket, and the installed `entwurf peer-facts` reporting it `alive`. The plugin's own status fan is then driven with NEITHER side stubbed, in both states a user can be in: with no herdr server it must name `herdr-agent-list-failed` and go red rather than draw an empty table, and under a headless `herdr server` — scaffolding for that second read only, stopped again before the teardown — it exits 0 and COUNTS the citizen as `unobserved` instead of dropping it. Last, the shipped `entwurf herdr-plugin-deactivate` runs from the installed bin, deletes the runtime it is executing from, and the plugin reinstalls onto the host it fully left — the deactivation row, on the npm source. **What these cells still do not close:** swap / torn-swap recovery on the npm source, the package-consumer proof, and the fan drawing a citizen as a ROW — that needs `placement.kind === "herdr-pane"`, so the pi session must live in a herdr pane a session reference joins, and a headless container has no panes. **This smoke carries no test beside behavior and no mutant**, and that is a property of its class rather than an omission: it is a real-live container journey whose oracle is the container's own end state, so there is no pure seam to assert against and nothing a source mutation could kill deterministically. Like the other two herdr LIVE lanes it is **on-demand and not an aggregate MUST** — herdr is optional, a cut on a host without it must not block, and first evidence precedes promotion. Without `LIVE=1`, or without a usable Docker daemon, it reports a named SKIP; `ENTWURF_REQUIRE_DOCKER=1` turns the Docker absence red. Neither skip is a pass.
|
|
109
|
+
> **The plugin's FIRST USER PATH is its own on-demand axis, and an npm acquisition receipt holds only for the ref it ran on (#118 홉 1).** Every other herdr gate proves the plugin against a source this repository controls: `check-herdr-plugin-build` drives a stub, and `smoke-herdr-plugin-build-live` redirects the product remote to a local bare clone with git `insteadOf` and asserts `identity.kind === "herdr-checkout"`. npm has been the production authority class since `dd84ac0`; the 0.30.0 follow-up re-pinned `@junghanacs/entwurf@0.30.0` with its published sha512 after the candidate windows rode `herdr-checkout` (`8b92e09`), and the 0.30.1 preparation rode that closed verification carrier again without inventing a registry integrity. The plugin snapshot `v2026.10.9-herdr.0.34.0` pinned the published `@junghanacs/entwurf@0.34.0` with its measured sha512 (#132); 0.31–0.33 were never pinned. The 0.34.1 preparation rode the closed `herdr-checkout` verification carrier again without inventing a registry integrity, and the snapshot `v2026.10.9-herdr.0.34.1` re-pins the published `@junghanacs/entwurf@0.34.1` with its measured sha512 (#135). The 0.34.2 preparation rode the same carrier, and the snapshot `v2026.10.11-herdr.0.34.2` re-pins the published `@junghanacs/entwurf@0.34.2` with its measured sha512 (#136). The source re-proof has separate dated npm rows: 2026-09-23 public `main=113ca53`, plugin 0.5.0, runtime 0.25.0 (CHANGELOG 0.25.1), and the 0.30.0 re-pin accepted at public `506c46f` in the [#126 thread](https://github.com/junghan0611/entwurf/issues/126). Neither certifies the 0.34.0 registry acquisition. That pin's real-npm swap / torn-swap recovery was measured locally on 2026-10-09 by driving the registry-shipped `scripts/herdr-runtime.mjs` (byte-equal to this checkout's) with its own npm acquisition: a 0.33.0 → 0.34.0 swap, and the same upgrade torn between its two renames then resumed, where the last-good 0.33.0 tree was restored before 0.34.0 was acquired again. Both ended `runtime-ready` at observed digest `sha256-e9d995ff…`, the accepted 0.34.0 artifact. That local receipt is recorded in the [#132 thread](https://github.com/junghan0611/entwurf/issues/132); it covers runtime recovery, not this smoke's cells. This smoke's public-ref acceptance of the 0.34.0 pin was measured locally at `--ref main`: PASS at resolved `b5d15d8` and again at the docs follow-up `8ed4d1a`, each acquiring `kind=npm @junghanacs/entwurf@0.34.0` — a main-ref receipt, not a tag-ref, native or full-Herdr claim. The 0.34.1 re-pin's npm-source rows — swap / torn-swap recovery, this smoke at a public ref, and the package consumer — are its own, recorded with that pin and never inherited from 0.34.0; checkout candidate evidence does not transfer. The 0.36.0 re-pin's public-ref row was measured on Herdr 0.9.4 at `--ref main`: PASS at resolved `9f33dd1`, acquiring `kind=npm @junghanacs/entwurf@0.36.0` at the accepted digest — a main-ref receipt, not a tag-ref claim. Its version-changing cross-ref reinstall (`herdr plugin install … --ref main` over the `v2026.10.13-herdr.0.35.1` installation) across a pinned Pi upgrade was a driver-level journey recorded in that snapshot's release notes, not a row of this smoke; `herdr plugin update` there was measured only as a same-ref already-up-to-date no-op. `LIVE=1 ./run.sh smoke-herdr-raw-install-live [--ref <REF>]` fills that row in a clean `node:24` container with no socket, config, cache or repo mounted, no `insteadOf`, the public remote spelled as a user would type it, and pi / herdr / Claude Code installed in the IMAGE BUILD as scaffolding (Hard Rule 17 — the product face begins at `herdr integration install pi`). herdr itself comes from `scripts/install-herdr-ci.sh` against `scripts/fixtures/herdr-supply.json`, so no version or digest lives here. `--ref` defaults to `main` rather than a pinned SHA because receipt (a) is defined as what a user gets today; the resolved commit is printed, so the receipt stays exact. It takes a REMOTE REF and not an arbitrary commit — measured on herdr 0.9.1, `--ref <sha>` fails as `couldn't find remote ref`, because the checkout fetches by name — so a candidate is addressed by its branch and pinned by the resolved commit the gate prints. **Its first question is a measurement, not an assumption:** herdr registers a plugin through a running server and falls back to `persist_plugin_offline` only on a connection error (`src/cli/plugin.rs:914-981,1016-1021` @ `c77af189`, 0.9.0 source), and a container has no server. The 2026-09-23 PASS installed with no server on the 0.9.1 binary `113ca53`'s `herdr-supply.json` pins, so that install path worked on 0.9.1 — inferred from the PASS, not a direct observation of the fallback branch; a later "no" on another herdr is #118 investigation output rather than a completion. `--yes` is always passed so a non-interactive exit 2 (`:189`) can never be misread as a missing server. **At a ref whose lock names `herdr-checkout` the acquisition axis reports a NAMED SKIP rather than a red, and the rest of the journey still runs.** That lock is the candidate carrier a cut legitimately sits on — 0.23.0 was tagged that way and `dd84ac0` pinned npm afterwards — so a red there would report a broken install when what is true is that the ref does not lock npm. What is asserted at BOTH lock kinds is that the runtime came from the source the ref locks, and the package name is read from the checkout manifest rather than the lock, because a `herdr-checkout` lock carries no `name` (`herdr-runtime.mjs:317-328`) and joining `undefined` throws instead of failing an assertion. **Cells [6]–[9] carry it past the install into the USE path (#118 H1-4/H1-5).** An install is not a use, so the same container then runs what those bytes became: the compiled bridge entry, the three `REQUIRED_BINS` as executables, and a real `entwurf check-bridge` printing its exact eight-verb set — the installed-runtime row below, measured rather than inferred from the bootstrap's refusal to install without them. Then the pi that this plugin WIRED is started with no `-e` and no `--no-extensions`, because the subject is the wiring and not this checkout: one `get_state` in `--mode rpc` costs zero tokens and needs no provider argument (a bogus `--provider` exits 1 before session_start; omitting the pair lets pi resolve the default the extension itself registers), and while that session is alive the cell reads its V3 record, its gardenId-keyed control socket, and the installed `entwurf peer-facts` reporting it `alive`. The plugin's own status fan is then driven with NEITHER side stubbed, in both states a user can be in: with no herdr server it must name `herdr-agent-list-failed` and go red rather than draw an empty table, and under a headless `herdr server` — scaffolding for that second read only, stopped again before the teardown — it exits 0 and COUNTS the citizen as `unobserved` instead of dropping it. Last, the shipped `entwurf herdr-plugin-deactivate` runs from the installed bin, deletes the runtime it is executing from, and the plugin reinstalls onto the host it fully left — the deactivation row, on the npm source. **What these cells still do not close:** swap / torn-swap recovery on the npm source, the package-consumer proof, and the fan drawing a citizen as a ROW — that needs `placement.kind === "herdr-pane"`, so the pi session must live in a herdr pane a session reference joins, and a headless container has no panes. **This smoke carries no test beside behavior and no mutant**, and that is a property of its class rather than an omission: it is a real-live container journey whose oracle is the container's own end state, so there is no pure seam to assert against and nothing a source mutation could kill deterministically. Like the other two herdr LIVE lanes it is **on-demand and not an aggregate MUST** — herdr is optional, a cut on a host without it must not block, and first evidence precedes promotion. Without `LIVE=1`, or without a usable Docker daemon, it reports a named SKIP; `ENTWURF_REQUIRE_DOCKER=1` turns the Docker absence red. Neither skip is a pass.
|
|
110
110
|
|
|
111
111
|
> **The Herdr plugin's install-time build has two axes, and one of them needs the network (#116 M3-b3).** `check-herdr-plugin-build` is deterministic and rides `check:hermetic`: the listing reaches the runner through its own spawn seam (there is no fake `herdr` binary in this repo), and the one cell that drives the REAL binary reports a named SKIP on a host without it — `ENTWURF_REQUIRE_HERDR=1` makes that absence red. What it cannot have is the real acquisition: `npm pack git+https://github.com/junghan0611/entwurf.git#<full sha>` builds the bridge through `prepare`, which installs devDependencies from the registry. So `LIVE=1 ./run.sh smoke-herdr-plugin-build-live` owns that axis, and its cells are **two kinds of evidence, labelled as such**: three are the real Herdr journey — a real `herdr plugin install` driving the real `[[build]]`, with the UNCHANGED product remote redirected to a local bare clone by git `insteadOf` (the one substitution, which is what makes an unpushed candidate testable) — covering remote-commit available, the `{pi}` → `{pi, claude-code}` reinstall with OpenCode as a zero-write negative control and exactly ONE counted vendor MCP owner entry, and the post-build Herdr-commit gap (where the ledger has already rebound to the gap commit, so the honest assertion is that transition and not unchanged bytes). The fourth cell — remote-commit **unavailable** — calls the shipped runtime leaf directly with the fixed product argv and is therefore **acquisition-leaf evidence, not a journey**; routing it through Herdr would add a second explanation for one red. Its oracle is the honest one too: active runtime, ledger and harness bytes preserved, while the journal is left as the certified `installing` retry authority the transaction deliberately writes before acquiring, and a normal install afterwards is green. Its Claude cell is where the vendor question the deterministic gates refuse to claim gets answered — whether Claude Code accepts an ABSOLUTE executable as an MCP command — and it reports a named skip on a host without `claude`, never a pass. Like the herdr fresh-call rail, this smoke is **on-demand and not an aggregate MUST**. The release aggregate does not require the plugin build smoke: herdr is optional, a cut on a host without it must not block, and first evidence precedes promotion.
|
|
112
112
|
|
|
@@ -213,8 +213,8 @@ Do not collapse source, package, fixture, and native-host evidence into one “g
|
|
|
213
213
|
**Native durable contact (#129, #130 carrier).** `check-pi-durable-beside` is the deterministic
|
|
214
214
|
beside-test/mutant coordinate in `check:contracts`: the adapter, the installed carrier/SDK-set
|
|
215
215
|
verifier, the preflight, the owner join, the carrier's relocation grammar, resolver and
|
|
216
|
-
maintainer emitter (its
|
|
217
|
-
|
|
216
|
+
maintainer emitter (its `--check` reads only the owned in-repo source, #141; the overlay patch is
|
|
217
|
+
retired, and `--origin <clone>` checks provenance only), and the bootstrap's explicit `--native-module` ingress
|
|
218
218
|
(#130 P2) with its paired `--native-module-dir` (#134), driven through the packaged `main`'s own
|
|
219
219
|
seams: initialization before the TUI and the open, named refusals, the identity and directory
|
|
220
220
|
guards, the top-level `*.extension.mjs` filter and byte-wise order, cross-module name refusals, the
|
|
@@ -250,11 +250,13 @@ constructed depth-3 mismatch refused by the verifier and seen by the oracle, the
|
|
|
250
250
|
app's own model refusal before any birth — also with one installed `--native-module` initialized
|
|
251
251
|
first, a reserved extension name refused before anything opens, one installed `--native-module-dir`
|
|
252
252
|
initializing only its top-level `*.extension.mjs` (#134) and a directory without a module refused
|
|
253
|
-
before anything opens — and the HOST kept a separate subject — a real pi 1.0
|
|
253
|
+
before anything opens — and the HOST kept a separate subject — a real pi 1.1.0
|
|
254
254
|
binary (the previous generation) beside the pinned native set is a named pi FAIL with zero Pi wiring while pi-durable still
|
|
255
|
-
PASSes — not native admission. The
|
|
256
|
-
equality is
|
|
257
|
-
|
|
255
|
+
PASSes — not native admission. The carrier's rebuild
|
|
256
|
+
equality is the owned source's emission (#141): the beside claim `PI-DURABLE-CARRIER-EMIT-SOURCE-EQUAL`
|
|
257
|
+
on every floor, `emit-pi-durable-carrier --check` as its CLI. The emitter's exact Node is red in the CI
|
|
258
|
+
`check` job (`ENTWURF_REQUIRE_EMITTER_NODE=1`) and a named SKIP on a host running another Node, not
|
|
259
|
+
acceptance there. First-release visible fresh/callback and native
|
|
258
260
|
caller/target/cross-harness LIVE are owed by `smoke-pi-durable-fresh-live`, a release-gate MUST
|
|
259
261
|
(LIVE=1; vendor V on a real Pi and a real pi-durable host); SDK or package green does not waive it.
|
|
260
262
|
|
|
@@ -348,7 +350,7 @@ The goal is not merely "invoke Claude Code." We want:
|
|
|
348
350
|
**One install command to remember: `./run.sh setup <project>`.** It is idempotent — re-run the exact same command whenever anything looks wrong. There is no second install surface to juggle: from a clone `setup` runs the whole floor in order.
|
|
349
351
|
|
|
350
352
|
1. `pnpm install` — installs the pinned development dependencies and builds the bridge (source-checkout bootstrap only; an installed package never runs npm/pnpm inside `node_modules`)
|
|
351
|
-
2. pi wiring → `<project>/.pi/settings.json` + user-scope registration — only when a `pi` inside the supported range (`>=1.
|
|
353
|
+
2. pi wiring → `<project>/.pi/settings.json` + user-scope registration — only when a `pi` inside the supported range (`>=1.2.0 <1.3`) is on PATH; absent pi is an explicit zero-state SKIP, a below-floor pi is a detected FAIL. The user-scope entry is owner-recorded (#86 C2): another root's live-or-missing ownership makes this step a zero-write refusal (setup: pi FAIL) that names `takeover-user-scope`; the install-states bind the exact managed settings path (a drifted/symlinked/corrupt target is a zero-write refusal) and the inverse removes only the recorded owner's exact entry; `doctor-pi-package` reports the verdict
|
|
352
354
|
3. Claude meta-bridge global plugin — only when `claude` is on PATH; otherwise skipped cleanly
|
|
353
355
|
4. source stable-bin exposure — including certified `entwurf` → this checkout's `run.sh`, the managed runtime Copilot fresh resolves; helper units are attempted independently and a foreign helper is a named FAIL
|
|
354
356
|
5. agy bridge + exact permission + statusline + `PreInvocation` hook — only when `agy` is on PATH; each adapter is idempotent and independently doctorable
|
|
@@ -452,14 +454,14 @@ The single-turn / multi-turn / cross-process / persistence-boundary / shutdown i
|
|
|
452
454
|
|
|
453
455
|
| Invariant | Current gate (pointer) |
|
|
454
456
|
|---|---|
|
|
455
|
-
| Single-turn prompt extraction, SessionStart hook not mistaken for prompt | `smoke-acp-raw-turn-live
|
|
457
|
+
| Single-turn prompt extraction, SessionStart hook not mistaken for prompt | `smoke-acp-raw-turn-live` (a bare-vendor shape by design: no overlay and no session meta, so it is not the production session shape), `check-acp-prompt-builder` |
|
|
456
458
|
| Multi-turn continuity + recall (process-scoped reuse) | `smoke-acp-session-reuse-live`, `check-acp-session-reuse` |
|
|
457
459
|
| Cross-process continuity / cache before-after | `check-acp-session-store` (signature, decideBootstrap, persist/parse) |
|
|
458
460
|
| Lifecycle policy — turn-scoped is always new; process-scoped may reuse only the live in-memory session; persisted records are not a resume/load path today | `check-acp-session-store`, `check-acp-session-reuse` |
|
|
459
461
|
| Tool-call / event mapping | `check-acp-event-mapper`, `smoke-acp-provider-live` |
|
|
460
462
|
| Prompt lifecycle — no wall-clock cutoff on a running turn; abort ends it by ACP `session/cancel` with bounded cleanup; a child death is reported with exit status + stderr on new AND reuse turns; our prompt-phase error text is not classified transient by pi | `check-acp-prompt-lifecycle`, `check-probe-ordering` |
|
|
461
463
|
| Claude typed `incomplete_tool_call` prompt failure — real pi retry/overflow classifiers reject the fixed verdict, raw evidence stays in existing diagnostics, exactly one seal and drop on new/reuse; other failures retain the legacy stderr path, cancel stays separate. A deterministic receipt does not certify the target vendor runtime. No LIVE gate induces this rejection; the raw-turn and reuse smokes certify only their own rows above. | `check-acp-vendor-rejection` (four mutants) |
|
|
462
|
-
| Claude set-model answer read back before the prompt — an exact row, or the one alias row (value and name) measured for the requested id; a missing model option, another row or any other value/name pair refuses with non-transient text. Deterministic only: the measured 0.85.1
|
|
464
|
+
| Claude set-model answer read back before the prompt — an exact row, or the one alias row (value and name) measured for the requested id; a missing model option, another row or any other value/name pair refuses with non-transient text. Deterministic only: the answer shapes measured at 0.85.1, and unchanged at 0.90.0 with the production session meta (no prompt), are fixture literals, not a runtime certificate. | `check-acp-set-model-readback` (two mutants), `check-probe-ordering` |
|
|
463
465
|
| Operator mcpServers / skills reach the live session | `smoke-acp-mcp-live`, `smoke-acp-skill-live`, `check-acp-config` |
|
|
464
466
|
| Overlay isolation + memory containment | `check-acp-overlay`, `smoke-acp-memory-containment-live`, `check-acp-tool-surface` |
|
|
465
467
|
|
package/demo/README.md
CHANGED
|
@@ -24,7 +24,7 @@ takes/helpers/tests are maintainer tooling and are deliberately excluded from th
|
|
|
24
24
|
|
|
25
25
|
- Node at the `package.json#engines.node` floor; the checkout's pinned dependencies and
|
|
26
26
|
current compiled bridge. After a source/bootstrap change, build with `pnpm run build-bridge`.
|
|
27
|
-
- Pi at the checkout's supported floor (**current floor 1.
|
|
27
|
+
- Pi at the checkout's supported floor (**current floor 1.2.0**); `entwurf` on PATH must be
|
|
28
28
|
the intended source stable-bin. Source evidence is not registry-package acceptance.
|
|
29
29
|
- `herdr` on PATH at exactly `scripts/fixtures/herdr-supply.json#version`; `asciinema` v3.
|
|
30
30
|
- Herdr's existing Pi integration at
|
package/docs/acp-backend-rail.md
CHANGED
|
@@ -134,10 +134,10 @@ ledger, 2026-10-03).
|
|
|
134
134
|
| Surface | Declaration | Class | What a green actually says |
|
|
135
135
|
|---|---|---|---|
|
|
136
136
|
| Entwurf package | `package.json` `version` | checkout declaration, not a release receipt | names the package label these rows sit beside; whether that label shipped, and with which rows, is CHANGELOG's record |
|
|
137
|
-
| pi runtime | SDK exact `1.
|
|
138
|
-
| ACP wire SDK | `@agentclientprotocol/sdk 1.
|
|
139
|
-
| Claude ACP adapter | `@agentclientprotocol/claude-agent-acp 0.
|
|
140
|
-
| Claude Agent SDK | `0.3.
|
|
137
|
+
| pi runtime | SDK exact `1.2.0`, host `>=1.2.0 <1.3` | **exact** oracle + **closed range** | the SDK set this checkout builds, gates and (since #130) ships as exact production dependencies; an ordinary Pi host inside the range is accepted, and the ceiling moves only on measurement. The published 0.36.0 ships this 1.2.0 set (#141). Historical: the published 0.33.0 through 0.35.1 ship SDK exact 1.1.0, 0.32.0 ships SDK exact 1.0.4 with the next-minor ceiling (ROADMAP's 2026-10-07 bump entry), and 0.31.0 declared the earlier peer floor 1.0.2 |
|
|
138
|
+
| ACP wire SDK | `@agentclientprotocol/sdk 1.8.0` | **exact** | the shared wire oracle both adapters speak |
|
|
139
|
+
| Claude ACP adapter | `@agentclientprotocol/claude-agent-acp 0.90.0` | **exact**, bundled | checkout candidate; resolved before any PATH fallback; declaration is not compatibility evidence |
|
|
140
|
+
| Claude Agent SDK | `0.3.296` (transitive) | **exact** oracle | the runtime risk surface behind the adapter |
|
|
141
141
|
| Anthropic SDK | `0.100.1` | **exact**, peer-resolution only | satisfies the Agent SDK peer floor (0.93.0+); never an API client here (gate L4) |
|
|
142
142
|
| Claude Code runtime | `>=2.1.217` (`entwurf.claudeCodeFloor`) | **floor** | below it, hook args are silently dropped; entwurf enforces this itself |
|
|
143
143
|
| Node | `>=24` (`engines.node`) | **floor** | single axis, derived everywhere else |
|
|
@@ -333,6 +333,68 @@ different reasons, and collapsing them would hide a real risk**:
|
|
|
333
333
|
`turnQuotaMeta` and `quotaTokenCount` are byte-identical as extracted entry bodies. Turn
|
|
334
334
|
settlement moved inside the changed `runConsumer`.
|
|
335
335
|
|
|
336
|
+
- **0.85.1 → 0.90.0 (#128) is a compatibility bump; Entwurf writes no new contract for it.**
|
|
337
|
+
`[측정 2026-10-14; upstream compare 72 commits / 185 files read through the GitHub API; nothing
|
|
338
|
+
executed but the readback below]`. The closure and evidence classes are in the ROADMAP Dep bump
|
|
339
|
+
entry for 2026-10-14.
|
|
340
|
+
- **Set-model readback re-measured, not inherited.** One no-prompt run against the installed
|
|
341
|
+
0.90.0 through the production launch and overlay seam. On the wire: `initialize` ×1,
|
|
342
|
+
`session/new` ×4, `session/set_config_option` ×3, no `session/prompt`, and no message,
|
|
343
|
+
thought, tool, plan or usage update. Production `enforceModel` accepted all three curated
|
|
344
|
+
ids with the 0.85.1 answers, so `CLAUDE_SET_MODEL_ALIAS_ROWS` is unchanged. The offered list
|
|
345
|
+
grew from 12 to 13 rows: Fable 5.1 is now offered as the alias `fable` (the set answer still
|
|
346
|
+
names its own `claude-fable-5-1` row), `haiku` is named `Haiku 5.5`, and
|
|
347
|
+
`claude-haiku-4-5-20251001` is new. Offered rows are not Entwurf support: only the three
|
|
348
|
+
curated ids (`model-ids.js`) were set and read back, and no other row is measured or
|
|
349
|
+
admitted. The vendor's auth-status notifications and context probes are not model turns,
|
|
350
|
+
and nothing here proves zero API calls; the billing rail is unmeasured.
|
|
351
|
+
- **Evidence limits, stated before anything builds on this entry.** The readback followed the
|
|
352
|
+
production launch and overlay seam only. Its session meta carried the model alone and
|
|
353
|
+
omitted the production `settingSources: []` (`tool-surface.ts:152`), so it is not a full
|
|
354
|
+
production-shape measurement. The focused green of `check-acp-{stop-reason,usage-accounting,
|
|
355
|
+
prompt-lifecycle,stream-hooks,session-reuse}` at that point proved fake semantics only, not
|
|
356
|
+
Rule 12 isolation: their `ensureOverlay` stub sat on deps, which backend never reads, so the
|
|
357
|
+
real adapter refreshed the operator overlay. The earlier "no gate touches the operator
|
|
358
|
+
overlay" claim is retired.
|
|
359
|
+
- **Both limits repaired afterwards (2026-10-14).** The five gates now sandbox HOME, XDG and the
|
|
360
|
+
pi agent dir before importing the compiled backend, assert that the overlay constant resolves
|
|
361
|
+
inside that sandbox (`scripts/lib/acp-gate-home.ts`, paired mutant
|
|
362
|
+
`ACP-GATE-HOME-SANDBOXED`), and no longer carry the dead stubs. A second single no-prompt run
|
|
363
|
+
then built every session through the production path — `resolveProviderConfig` over explicit
|
|
364
|
+
empty operator input, `loadCarrier`, `buildSessionMeta`, `enrichMcpServersWithEnvelope` — so
|
|
365
|
+
each `session/new` carried `settingSources: []`, the default tools, the inline settings and
|
|
366
|
+
the shipped carrier. On the wire: `initialize` ×1, `session/new` ×3, `session/set_config_option`
|
|
367
|
+
×3, seven responses, no `session/prompt` and no turn update. The three curated ids answered
|
|
368
|
+
exactly as before. These are the shipped defaults; an operator's own settings were not
|
|
369
|
+
measured.
|
|
370
|
+
- **Two operating observations from that run, both with the cause unmeasured.** The overlay
|
|
371
|
+
`projects/` held 524 entries after `ensureOverlay` and before the spawn, and 469 after the
|
|
372
|
+
child group was reaped; the run itself deletes nothing there and left no entry for its
|
|
373
|
+
scratch cwd. Which process removed the 55 entries, and whether they were transcripts, is not
|
|
374
|
+
measured, and it is not claimed for earlier driver versions. Separately, the inbound wire tap
|
|
375
|
+
saw one stream error event in a phase that was not recorded, while every request was
|
|
376
|
+
answered and no line failed to parse. In the production-meta run the overlay `projects/`
|
|
377
|
+
count stayed 469 → 469, which is consistent with the vendor skipping its retention sweep when
|
|
378
|
+
`settingSources` is empty but does not establish that cause, and the one inbound-tap stream
|
|
379
|
+
error fell in its teardown phase; that does not date the first run's.
|
|
380
|
+
- **Permission options are unchanged.** The nine option builders under `permissions/` are
|
|
381
|
+
byte-identical between the tags. `presentation.ts` adds titles only for v2 clients, and
|
|
382
|
+
`exit-plan.ts` publishes the approved plan only behind the `plan` client capability; we
|
|
383
|
+
advertise neither.
|
|
384
|
+
- **Cancel and turn settlement are fixed upstream and compatible here.** The stop-reason set is
|
|
385
|
+
unchanged; only `cancelled` gains emission sites (5 → 7), across #1228, #1229, #1242 and
|
|
386
|
+
#1243 (0.86) and #1221, #1309, #1313 and #1327 (0.90). The typed `incomplete_tool_call`
|
|
387
|
+
rejection keeps its kind and text. Our abort is still `session/cancel` → grace → teardown;
|
|
388
|
+
the runtime effect of these fixes is UNRUN.
|
|
389
|
+
- **Ambient only:** 0.90 merges model settings as `{...CLAUDE_MODEL_CONFIG, ...provided}`, so
|
|
390
|
+
an ambient `CLAUDE_MODEL_CONFIG` can now shape the model rows. Entwurf neither sets nor
|
|
391
|
+
scrubs it (the spawn env spreads `process.env`), the same posture as
|
|
392
|
+
`CLAUDE_AGENT_ACP_EXPERIMENTAL_V2`.
|
|
393
|
+
- **Behind capabilities we do not advertise:** v2 router fixes, RFD subagents, the AIR session
|
|
394
|
+
index and background terminals. Observation: a prompt that is exactly `/mcp` is answered by
|
|
395
|
+
the agent (#1218); a new session's first prompt carries our augment ahead of the user text,
|
|
396
|
+
a reuse turn does not.
|
|
397
|
+
|
|
336
398
|
- **The one 0.73.0 → 0.75.1 change that DOES reach us:** context compaction is now surfaced as a
|
|
337
399
|
synthetic ACP tool lifecycle (0.75.0, #991) — a `tool_call` with `kind: "think"`, title
|
|
338
400
|
`Compact conversation`, and `_meta.contextCompaction` schema v1 — where it used to arrive as
|
|
@@ -405,7 +467,9 @@ typing `pi --model …` keeps the upstream fuzzy behaviour, so use the full 5.5
|
|
|
405
467
|
The adapter reads its set-model answer back before the prompt. The driver resolves the
|
|
406
468
|
requested id onto one of the rows it offers and answers with that row's `value` as the
|
|
407
469
|
model option's `currentValue` (0.85.1 `dist/acp-agent.js:5459-5499`). Measured on
|
|
408
|
-
2026-10-12 against 0.85.1
|
|
470
|
+
2026-10-12 against 0.85.1, and again unchanged on 2026-10-14 against 0.90.0 (the second
|
|
471
|
+
time also with the production session meta), with no prompt sent: `claude-fable-5-1`
|
|
472
|
+
answers its own row,
|
|
409
473
|
while `claude-sonnet-5-5` and `claude-opus-5-5` answer the alias rows `sonnet` and
|
|
410
474
|
`opus`, named `Sonnet 5.5` and `Opus 5.5`. The id an alias row resolves to is not on the
|
|
411
475
|
ACP wire, so `CLAUDE_SET_MODEL_ALIAS_ROWS` (`backend-adapter.ts`) declares, for each
|
package/docs/setup-clean-host.md
CHANGED
|
@@ -28,8 +28,8 @@ evidence split, and its three states must not collapse into "macOS is supported"
|
|
|
28
28
|
| npm/pnpm | npm is bundled with Node; pnpm is required for source setup | package or source installation |
|
|
29
29
|
| Python 3 | required by `setup`/`install` (project-path normalization + settings writers); `--help`/`check-bridge` stay Python-free | pi/Claude/agy/Copilot/Codex wiring writers |
|
|
30
30
|
| entwurf | global/project-local `@junghanacs/entwurf`, or a source checkout | operator command and garden capability |
|
|
31
|
-
| pi | optional-by-presence, `>=1.
|
|
32
|
-
| Native durable (`pi-durable`) | supplied by the Entwurf package (#130):
|
|
31
|
+
| pi | optional-by-presence, `>=1.2.0 <1.3` — absent is an explicit setup SKIP, below-floor is a named FAIL | ACP provider, control sockets |
|
|
32
|
+
| Native durable (`pi-durable`) | supplied by the Entwurf package (#130): Entwurf's owned app carrier plus the exact 1.2.0 pi SDK set as production dependencies — setup PASS when they verify, a named FAIL otherwise (never SKIP) | the `entwurf pi-durable` contact; native birth/LIVE on this supply surface is a separate release proof (§2b) |
|
|
33
33
|
| Claude Code | optional, **`>=2.1.217`** — the exec-form hook floor | Claude ACP auth/runtime and mailbox-backed native citizen |
|
|
34
34
|
| GitHub Copilot CLI | optional-by-presence, operator-installed and authenticated — absent is an explicit setup SKIP; detected composes all four units (birth/MCP/receiver/footer) | self-fetch citizen and visible fresh |
|
|
35
35
|
| OMP (`omp`) | optional-by-presence, operator-installed — absent is an explicit setup SKIP; detected composes all four units (birth/MCP/`tools.xdev` setting/receiver) | self-fetch citizen and visible fresh (accepted on one host — see §4b) |
|
|
@@ -209,7 +209,7 @@ on Darwin; that is wiring, not a rail receipt. If `pi` is on PATH,
|
|
|
209
209
|
Install the exact release floor, then wire the project:
|
|
210
210
|
|
|
211
211
|
```bash
|
|
212
|
-
npm install -g @earendil-works/pi-coding-agent@1.
|
|
212
|
+
npm install -g @earendil-works/pi-coding-agent@1.2.0
|
|
213
213
|
pi --version
|
|
214
214
|
|
|
215
215
|
cd ~/entwurf-smoke
|
|
@@ -217,7 +217,7 @@ entwurf install .
|
|
|
217
217
|
pi -e "$(npm root -g)/@junghanacs/entwurf" --list-models entwurf
|
|
218
218
|
```
|
|
219
219
|
|
|
220
|
-
The supported range is `>=1.
|
|
220
|
+
The supported range is `>=1.2.0 <1.3`. It is a hard minimum: installing this
|
|
221
221
|
release onto a 0.83.x pi host upgrades the runtime rather than keeping the older
|
|
222
222
|
minor. A host using only the external MCP bridge can skip pi until it needs a
|
|
223
223
|
control socket; no delivery rail launches a pi process.
|
|
@@ -313,23 +313,22 @@ by `entwurf pi` or by registering ordinary Pi's adapter.
|
|
|
313
313
|
What the Entwurf package supplies:
|
|
314
314
|
|
|
315
315
|
1. **The pi SDK set** — `@earendil-works/{pi-coding-agent, pi-ai, pi-tui, pi-agent-core,
|
|
316
|
-
pi-durable, chord, pi-mcp, pi-codemode, pi-telemetry}` at exactly `1.
|
|
316
|
+
pi-durable, chord, pi-mcp, pi-codemode, pi-telemetry}` at exactly `1.2.0`, plus
|
|
317
317
|
`proper-lockfile`, as Entwurf's own production dependencies (declared once in
|
|
318
318
|
`pi/pi-durable/overlay/upstream-pin.json` `sdk`). Model data rides the published
|
|
319
319
|
`pi-ai`; there is no catalog copy.
|
|
320
320
|
2. **The native app/TUI carrier** — upstream does not publish the experimental durable
|
|
321
321
|
app (`pi-coding-agent` excludes `dist/experimental`; `pi-durable` on npm is the
|
|
322
|
-
library). Entwurf
|
|
323
|
-
|
|
324
|
-
applied to `runtime.ts`, type-stripped and relocated, in `pi/pi-durable/carrier/`
|
|
322
|
+
library). Entwurf owns the six app files as canonical source (`pi/pi-durable/src`,
|
|
323
|
+
first taken from upstream tag `v1.1.0` `abe508e1b89912adde45528136c3221eb69acdd7`) and ships them type-stripped and relocated in `pi/pi-durable/carrier/`
|
|
325
324
|
with upstream's MIT license. Each file's source blob and emitted digest are in the pin.
|
|
326
325
|
The carrier reaches pi-coding-agent's internals only through its resolver
|
|
327
326
|
(`pi/pi-durable/carrier-resolver.mjs`), which maps them to the published package's
|
|
328
327
|
own `dist/` as installed — never through a source checkout.
|
|
329
328
|
|
|
330
|
-
The upstream [library guide](https://github.com/earendil-works/pi/blob/v1.
|
|
331
|
-
and [experimental app guide](https://github.com/earendil-works/pi/blob/v1.1.0/packages/coding-agent/src/experimental/durable/README.md)
|
|
332
|
-
describe what the carrier is. Running the bare upstream app does not attach Entwurf's
|
|
329
|
+
The upstream [library guide](https://github.com/earendil-works/pi/blob/v1.2.0/packages/durable/README.md)
|
|
330
|
+
and the [experimental app guide](https://github.com/earendil-works/pi/blob/v1.1.0/packages/coding-agent/src/experimental/durable/README.md)
|
|
331
|
+
(last published at v1.1.0) describe what the carrier is. Running the bare upstream app does not attach Entwurf's
|
|
333
332
|
garden contact. Entwurf does not rebuild the controller/model/auth/storage system or
|
|
334
333
|
proxy a subscription; native execution, models, auth, queues, SQLite and recovery stay
|
|
335
334
|
upstream-owned.
|
|
@@ -411,7 +410,7 @@ tree (for example an override that binds pi-durable's `pi-ai` to an older publis
|
|
|
411
410
|
manifest field; it is a known constraint of supplying both from one package, accepted for 0.32,
|
|
412
411
|
not a claim that every load path is harmless.
|
|
413
412
|
|
|
414
|
-
The ordinary Pi CLI on PATH is a separate subject (§2, range `>=1.
|
|
413
|
+
The ordinary Pi CLI on PATH is a separate subject (§2, range `>=1.2.0 <1.3`): a host Pi is
|
|
415
414
|
not needed to run pi-durable, and a host inside the range need not be the same copy as the
|
|
416
415
|
carrier's. Native Pi owns credentials, model settings and durable SQLite sessions; keep them
|
|
417
416
|
in place rather than copying credentials or migrating ordinary Pi transcripts. Native
|
|
@@ -424,19 +423,28 @@ proof ([#130](https://github.com/junghan0611/entwurf/issues/130)).
|
|
|
424
423
|
|
|
425
424
|
### Maintainer: re-emitting the carrier
|
|
426
425
|
|
|
427
|
-
Only a maintainer
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
426
|
+
Only a maintainer changing the pi-durable app runs `scripts/emit-pi-durable-carrier.ts`. Its
|
|
427
|
+
input is Entwurf's own source in this repository (`pi/pi-durable/src`, #141, typechecked by
|
|
428
|
+
`pi/pi-durable/tsconfig.json` against the installed SDK's declarations): it type-strips with
|
|
429
|
+
Node's own strip-only TypeScript, relocates, and checks that the six files import exactly the
|
|
430
|
+
pin's declared `distTargets`:
|
|
432
431
|
|
|
433
432
|
```bash
|
|
434
|
-
node scripts/emit-pi-durable-carrier.ts --
|
|
435
|
-
node scripts/emit-pi-durable-carrier.ts --
|
|
433
|
+
node scripts/emit-pi-durable-carrier.ts --write # names every generated field it changes
|
|
434
|
+
node scripts/emit-pi-durable-carrier.ts --check # rebuild equality
|
|
435
|
+
node scripts/emit-pi-durable-carrier.ts --origin <clone holding origin.commit> # provenance only
|
|
436
436
|
```
|
|
437
437
|
|
|
438
|
-
|
|
439
|
-
|
|
438
|
+
A missing source file is a named refusal, never a skip. The byte comparison also runs as a
|
|
439
|
+
beside claim on every floor. The emitter's Node is part of the pin: under another Node,
|
|
440
|
+
`--check` compares the bytes and then reports the identity as a named SKIP, never rebuild
|
|
441
|
+
acceptance; the CI `check` job runs the pinned Node with `ENTWURF_REQUIRE_EMITTER_NODE=1`, where
|
|
442
|
+
a mismatch is red. `--origin` checks the frozen `origin` record against the upstream objects
|
|
443
|
+
it names (`<commit>:<path>`, `--no-replace-objects`) and never feeds a rebuild.
|
|
444
|
+
|
|
445
|
+
The owned source is formatted at upstream width (`biome.json` override, indentWidth 3), with no
|
|
446
|
+
per-file exception. A formatting change to an owned file changes its emitted bytes. So it is
|
|
447
|
+
re-emitted with `--write`, and its generated digest is reviewed like any other carrier change.
|
|
440
448
|
|
|
441
449
|
## 3. Optional Claude Code native citizen
|
|
442
450
|
|
|
@@ -601,7 +609,7 @@ as a note rather than a fault.
|
|
|
601
609
|
|
|
602
610
|
**entwurf sets no OMP version floor in code, and will not grow one on schedule.** Detection is
|
|
603
611
|
presence-only (`command -v "${OMP_BIN:-omp}"`); there is no `entwurf.ompFloor`, no coherence
|
|
604
|
-
gate, and no exact pin — unlike Node (`engines.node`), pi (`>=1.
|
|
612
|
+
gate, and no exact pin — unlike Node (`engines.node`), pi (`>=1.2.0 <1.3`) and Claude Code
|
|
605
613
|
(`entwurf.claudeCodeFloor`), each of which has an enforcement point. That asymmetry is a
|
|
606
614
|
decision, not an omission. A floor is the answer to a vendor that fails SILENTLY — Claude Code
|
|
607
615
|
earned one because an older binary validates the exec manifest, drops `args` at runtime, and
|
|
@@ -549,10 +549,11 @@ export async function codexFreshPreflight(env, deps = {}) {
|
|
|
549
549
|
return "codex-mcp-hand-missing";
|
|
550
550
|
if (visibleIdentityMissing(config))
|
|
551
551
|
return "codex-visible-identity-missing";
|
|
552
|
-
const
|
|
553
|
-
const socket = (deps.checkSocket ?? checkCodexSocketFile)(socketPath);
|
|
552
|
+
const socket = (deps.checkSocket ?? checkCodexSocketFile)(resolveCodexDefaultSocketPath(env));
|
|
554
553
|
if (!socket.ok)
|
|
555
554
|
return "codex-app-server-unavailable";
|
|
556
|
-
|
|
555
|
+
// The protocol exchange goes to the endpoint the check verified, never the advertised path
|
|
556
|
+
// again (on Codex 0.162.1 that path is an alias; see `checkCodexSocketFile`).
|
|
557
|
+
const live = await appServerIsLive(socket.connectPath, deps.openProtocol ?? realCodexProtocolOpener, deps.appServerTimeoutMs ?? DEFAULT_APP_SERVER_TIMEOUT_MS);
|
|
557
558
|
return live ? null : "codex-app-server-unavailable";
|
|
558
559
|
}
|
|
@@ -15,30 +15,108 @@ export function resolveCodexHome(env) {
|
|
|
15
15
|
export function resolveCodexDefaultSocketPath(env) {
|
|
16
16
|
return path.join(resolveCodexHome(env), "app-server-control", "app-server-control.sock");
|
|
17
17
|
}
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* The advertised path is exactly one of two layouts, and anything else is refused.
|
|
20
|
+
*
|
|
21
|
+
* (A) A plain socket owned by the effective uid: Codex up to 0.153.4 binds the advertised
|
|
22
|
+
* path itself. `connectPath` is that path.
|
|
23
|
+
* (B) Codex's rendezvous alias. From 0.162.1 the advertised path "is only an alias": the
|
|
24
|
+
* server binds a 0600 socket under `realpath(/tmp)/codex-daemon-<euid>/` named by the
|
|
25
|
+
* sha256 of the canonical advertised path, then symlinks the advertised path to it
|
|
26
|
+
* (`[source]` rust-v0.162.1 `app-server-transport/src/transport/unix_socket.rs:43-131,
|
|
27
|
+
* 290-304`; `uds/src/daemon_directory.rs:1-38`). Upstream's reason is a privileged
|
|
28
|
+
* rendezvous root that sandboxes hide; the alias also keeps an over-long advertised path
|
|
29
|
+
* usable (`uds/src/lib.rs:167-174`). The alias is accepted only when every hop is the
|
|
30
|
+
* vendor's own: the link is owned by the effective uid; its target is BYTE-EQUAL to the
|
|
31
|
+
* path Codex derives (a relative, foreign, re-hashed or chained target is refused); the
|
|
32
|
+
* daemon directory is a real directory owned by the effective uid with `mode & 0o777`
|
|
33
|
+
* exactly 0700, the vendor's own rule; and the target is a socket owned by the effective
|
|
34
|
+
* uid that no group or other user can open. `connectPath` is the verified target, so the
|
|
35
|
+
* alias is never re-resolved between this check and the connect. A same-euid process can
|
|
36
|
+
* still replace the verified socket file itself; that residual is named, not locked.
|
|
37
|
+
*
|
|
38
|
+
* Only `lstat` and one `readlink` touch the alias; it is never followed or `realpath`ed.
|
|
39
|
+
* The effective uid is the vendor's (`libc::geteuid`), not the real uid.
|
|
40
|
+
*/
|
|
41
|
+
export function checkCodexSocketFile(socketPath, deps = {}) {
|
|
42
|
+
const euid = deps.euid ?? process.geteuid?.();
|
|
43
|
+
let stat;
|
|
19
44
|
try {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (
|
|
25
|
-
return { ok: false, status: "
|
|
45
|
+
stat = fs.lstatSync(socketPath);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
const code = error.code;
|
|
49
|
+
if (code === "ENOENT") {
|
|
50
|
+
return { ok: false, status: "dead", layout: "absent", reason: `app-server socket is absent: ${socketPath}` };
|
|
26
51
|
}
|
|
27
|
-
|
|
52
|
+
return { ok: false, status: "indeterminate", reason: error instanceof Error ? error.message : String(error) };
|
|
53
|
+
}
|
|
54
|
+
if (stat.isSymbolicLink())
|
|
55
|
+
return checkCodexRendezvousAlias(socketPath, stat, euid, deps);
|
|
56
|
+
if (!stat.isSocket()) {
|
|
57
|
+
return { ok: false, status: "indeterminate", reason: `app-server path is not a socket: ${socketPath}` };
|
|
58
|
+
}
|
|
59
|
+
if (euid !== undefined && stat.uid !== euid) {
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
status: "indeterminate",
|
|
63
|
+
reason: `app-server socket is owned by uid ${stat.uid}, not the effective uid ${euid}`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return { ok: true, connectPath: socketPath };
|
|
67
|
+
}
|
|
68
|
+
function checkCodexRendezvousAlias(socketPath, link, euid, deps) {
|
|
69
|
+
const refuse = (reason) => ({ ok: false, status: "indeterminate", reason });
|
|
70
|
+
if (euid === undefined) {
|
|
71
|
+
return refuse(`app-server socket is a symlink and this platform has no effective uid to verify it: ${socketPath}`);
|
|
72
|
+
}
|
|
73
|
+
if (link.uid !== euid) {
|
|
74
|
+
return refuse(`app-server socket symlink is owned by uid ${link.uid}, not the effective uid ${euid}`);
|
|
75
|
+
}
|
|
76
|
+
let daemonRoot;
|
|
77
|
+
let expected;
|
|
78
|
+
let target;
|
|
79
|
+
try {
|
|
80
|
+
daemonRoot = deps.daemonRoot ?? path.join(fs.realpathSync("/tmp"), `codex-daemon-${euid}`);
|
|
81
|
+
const canonical = path.join(fs.realpathSync(path.dirname(socketPath)), path.basename(socketPath));
|
|
82
|
+
expected = path.join(daemonRoot, crypto.createHash("sha256").update(canonical).digest("hex"));
|
|
83
|
+
target = fs.readlinkSync(socketPath);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
return refuse(`could not verify the app-server socket symlink ${socketPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
87
|
+
}
|
|
88
|
+
if (target !== expected) {
|
|
89
|
+
return refuse(`app-server socket is a symlink that is not Codex's rendezvous alias: ${socketPath} -> ${target}`);
|
|
90
|
+
}
|
|
91
|
+
let directory;
|
|
92
|
+
try {
|
|
93
|
+
directory = fs.lstatSync(daemonRoot);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
return refuse(`Codex's socket directory cannot be read: ${error instanceof Error ? error.message : String(error)}`);
|
|
97
|
+
}
|
|
98
|
+
if (!directory.isDirectory() || directory.uid !== euid || (directory.mode & 0o777) !== 0o700) {
|
|
99
|
+
return refuse(`Codex's socket directory ${daemonRoot} must be a real directory owned by uid ${euid} with mode 0700`);
|
|
100
|
+
}
|
|
101
|
+
let physical;
|
|
102
|
+
try {
|
|
103
|
+
physical = fs.lstatSync(expected);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (error.code === "ENOENT") {
|
|
28
107
|
return {
|
|
29
108
|
ok: false,
|
|
30
|
-
status: "
|
|
31
|
-
|
|
109
|
+
status: "dead",
|
|
110
|
+
layout: "dangling-alias",
|
|
111
|
+
reason: `app-server socket is a dangling rendezvous alias: ${socketPath} -> ${expected}`,
|
|
32
112
|
};
|
|
33
113
|
}
|
|
34
|
-
return
|
|
114
|
+
return refuse(error instanceof Error ? error.message : String(error));
|
|
35
115
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (code === "ENOENT")
|
|
39
|
-
return { ok: false, status: "dead", reason: `app-server socket is absent: ${socketPath}` };
|
|
40
|
-
return { ok: false, status: "indeterminate", reason: error instanceof Error ? error.message : String(error) };
|
|
116
|
+
if (!physical.isSocket() || physical.uid !== euid || (physical.mode & 0o077) !== 0) {
|
|
117
|
+
return refuse(`Codex's physical socket ${expected} must be a socket owned by uid ${euid} that no group or other user can open`);
|
|
41
118
|
}
|
|
119
|
+
return { ok: true, connectPath: expected };
|
|
42
120
|
}
|
|
43
121
|
function promiseResolvers() {
|
|
44
122
|
return Promise.withResolvers();
|
|
@@ -302,10 +380,13 @@ const realCodexExecRunner = {
|
|
|
302
380
|
},
|
|
303
381
|
};
|
|
304
382
|
export async function probeCodexThread(nativeSessionId, deps = {}) {
|
|
305
|
-
const
|
|
306
|
-
const socketCheck = (deps.checkSocket ?? checkCodexSocketFile)(socketPath);
|
|
383
|
+
const socketCheck = (deps.checkSocket ?? checkCodexSocketFile)(resolveCodexDefaultSocketPath(deps.env ?? process.env));
|
|
307
384
|
if (!socketCheck.ok)
|
|
308
385
|
return { status: socketCheck.status, reason: socketCheck.reason };
|
|
386
|
+
// The probe, and the queue send its route carries, both use the endpoint just verified. The
|
|
387
|
+
// advertised path is not resolved again: on 0.162.1 it is an alias that could be retargeted
|
|
388
|
+
// between the check and a second transport.
|
|
389
|
+
const socketPath = socketCheck.connectPath;
|
|
309
390
|
let protocol;
|
|
310
391
|
try {
|
|
311
392
|
protocol = await (deps.openProtocol ?? realCodexProtocolOpener).open(socketPath);
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* under (#129 W, #130).
|
|
4
4
|
*
|
|
5
5
|
* The experimental durable app is source-only upstream: published `@earendil-works/pi-coding-agent`
|
|
6
|
-
* ships no `dist/experimental`. Entwurf therefore ships the app itself as a CARRIER —
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* ships no `dist/experimental`. Entwurf therefore ships the app itself as a CARRIER — its owned
|
|
7
|
+
* source (pi/pi-durable/src, #141), type-stripped and relocated (pi/pi-durable/carrier/, emitted only
|
|
8
|
+
* by scripts/emit-pi-durable-carrier.ts) — and declares the published SDK set it runs
|
|
9
9
|
* against as exact production dependencies. Nothing is provisioned beside the package: there is no
|
|
10
10
|
* runtime directory, no source checkout and no model data to copy (it rides the published pi-ai).
|
|
11
11
|
*
|