@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/run.sh
CHANGED
|
@@ -181,7 +181,7 @@ Usage:
|
|
|
181
181
|
./run.sh check-pi-durable-contact # #129 L-identity gate, CHECKOUT-ONLY (an installed package refuses it in run_ts). Opens the REAL durable app from THIS checkout's verified carrier (pi/pi-durable/carrier/ under its resolver, against the checkout's pinned pi SDK set — refused by name first) with the pi-durable contact, births the citizen and runs its tools adapter-direct through a privately emitted branch bridge (ENTWURF_PI_DURABLE_BRIDGE_ENTRY; its registry must match this checkout). Matrix roots {default, explicit} x {fresh, --continue}: one record keyed by the durable session dir, sender marker on the spawned host pid, exactly one bridge child whose /proc PPID is that host, poisoned identity carriers kept from it, its four root carriers bound to the roots written, the receiver ARMED (raw marker owned by the host pid + start key as pi-durable-host, joined to the sender marker; removed on close), self = the record and replyable, peers from an outside bridge, same garden id on reopen; the bundle's compiled owner join must admit pi-durable-host. Reaps only the host/bridges it owns on any failure. Harness-bypassed: no model turn, no ToolTask. Missing inputs → SKIP (97), never a pass
|
|
182
182
|
./run.sh check-pi-durable-send # #129 L-send FIRST CELL, CHECKOUT-ONLY. Same input as check-pi-durable-contact. One native durable turn: the app's own controller input → GenerationTask → ToolTask → the contact's entwurf_v2 → the private bridge → a seeded self-fetch target (C2 recipe, owned by a separate idle child). S = a 127.0.0.1 scripted OpenAI-compatible endpoint configured through the app's documented models.json (dummy key, PI_OFFLINE); H = the native ToolTask path; V (a vendor turn) is NOT run. Oracles: the wire (tools offered, v2 schema = the bridge's tools/list, exactly two requests, the tool result + receipt in the second), the raw mailbox (.msg 1, signal, sender = the host record), the native view (one call, its settled result, root current). Not exactly-once, not recovery, not a recipient turn. Missing inputs → SKIP (97)
|
|
183
183
|
./run.sh check-demo-reproducibility # source-checkout-only: discover offline demo Node tests; no LIVE, model, render or publication
|
|
184
|
-
./run.sh check-pi-durable-beside # #129 mutant execution coordinate (VERIFY rule 3): run_vitest over the six pi-durable beside tests (adapter, carrier/SDK-set verifier, preflight, owner join, carrier relocation/resolver
|
|
184
|
+
./run.sh check-pi-durable-beside # #129 mutant execution coordinate (VERIFY rule 3): run_vitest over the six pi-durable beside tests (adapter, carrier/SDK-set verifier, preflight, owner join, carrier relocation/resolver, owned-source emission and typecheck fence, task footer, native-module file and directory ingress) — the pi-durable qualification lane's gate argv. Same files check-tests-beside-behavior already runs.
|
|
185
185
|
./run.sh check-pi-durable-receive # #129 L-receive FIRST CELL, CHECKOUT-ONLY. Same input (the bundle must carry the pi-durable-host owner join). The host driver gives the app NO input; a seeded claude-code sender (marker owned by a separate idle child, handed to a gate-owned sending bridge through ENTWURF_META_SENDER_MARKER) enqueues one message, the host's doorbell admits its notice to the ROOT conversation, and the scripted model drains it with entwurf_inbox_read. S withholds response #1 until the ENQUEUE receipt, the .delivered bytes, the signal, the host-owned receiver marker and the notice are captured. Oracles: receiver() admission, native view, raw archive (.delivered.read + lastReadAt), wire (exactly two requests, drain result = view), and after a green reap a forensic read-only SQLite copy (submission row by request id = reported id, root, input, done, placed entry = the notice). V NOT run. Missing inputs → SKIP (97)
|
|
186
186
|
./run.sh check-copilot-receive-arm # #82 RAIL 5 gate: the REAL receiver installer + the REAL extension.mjs forked with a stubbed SDK. Arms only after birth, marker owned by the WATCHER pid, self-fetch dispatch answer, doorbell carries the garden id and NOT the body, id-drift/foreign-parent refusals. Hermetic; no Copilot, no model turn
|
|
187
187
|
./run.sh check-copilot-launch # #82 RAIL 7 gate: the MANAGED launch `entwurf copilot`, driven through its public address against a FAKE VENDOR on a sandbox PATH. Receiver precondition refusals, EXTENSIONS token + operator token preservation, injected defaults before the `--` terminator, byte-identical argv, the 11 explicit permission/surface policy overrides that suppress `--yolo`, exec (not fork) pid identity, exit passthrough, recursion refusal. Hermetic; no Copilot, no model turn
|
|
@@ -273,7 +273,7 @@ Usage:
|
|
|
273
273
|
./run.sh uninstall-meta-bridge # 1.0.0 meta-bridge Phase 2: stateful GLOBAL uninstall (restore only keys/items captured in install-state)
|
|
274
274
|
./run.sh doctor-meta-bridge # THE RELEASE ORACLE (#51, Linux-certified repair axis). exit 0 = every required layer was MEASURED on this Linux host: toolchain + state + plugin/MCP + resolved-artifact launch-form classification (all 3 owner hooks + doorbell static contract) + synthetic owner join + store scan + hook errors + SessionStart evidence + REQUIRED live MCP↔marker join + writer-version parity. Missing live evidence is NOT CERTIFIED (open a Claude session and re-run), never a pass; Darwin is NOT CERTIFIED and stays nonzero, on ONE narrowed reason — start-key and ppid are portable there (`ps` fallback, same string the TS core mints), so what holds the lane closed is per-process environment DISCOVERY alone, pending physical-host evidence. Detection power is held by check-meta-doctor-oracle
|
|
275
275
|
./run.sh copilot [args...] # #82 RAIL 7: the MANAGED Copilot launch. exec()s the vendor CLI in THIS terminal (cwd/pid/exit preserved) with COPILOT_CLI_ENABLED_FEATURE_FLAGS=EXTENSIONS set for that one invocation — entwurf owns no part of your shell and writes nothing to it, but it owns the process it is about to become. Without that flag Copilot skips the extension scan SILENTLY. Refuses to launch unless the receiver unit is actually installed. Injects `--model auto` when no --model was given and `--yolo` when no explicit permission/surface policy flag was given, both BEFORE any `--`; every argument you pass is forwarded byte-identical. RUNNING THIS IS YOUR CONSENT to that profile — use plain `copilot` for stock vendor behaviour. Not tmux, not fresh-call, mints no citizen (birth is still the first prompt)
|
|
276
|
-
./run.sh pi-durable [args...] # #129/#130: the MANAGED durable launch. Verifies this package's pi-durable CARRIER (pi/pi-durable/carrier/,
|
|
276
|
+
./run.sh pi-durable [args...] # #129/#130: the MANAGED durable launch. Verifies this package's pi-durable CARRIER (pi/pi-durable/carrier/, emitted deterministically from Entwurf's owned source pi/pi-durable/src) and the pi SDK set it resolves — every declared member/direct edge at exactly the pinned version — refusing by name (package-incomplete / carrier-drift / sdk-absent / sdk-mismatch, exit 3), then exec()s node under the carrier resolver with the packaged bootstrap (pi/pi-durable/bootstrap.mjs) in this terminal. A new session needs --provider <p> --model <exact catalog id> --width task-wide [--entwurf-bootstrap <{v:2,task}>]; --continue takes none of them. Nothing outside the package is located, installed or repaired
|
|
277
277
|
./run.sh install-copilot-bridge # #82: GLOBAL install of the Copilot BIRTH plugin (own marketplace root; node+entry baked into the no-argv exec string). MCP wiring and the RECEIVER extension are separate install surfaces (install-copilot-mcp, install-copilot-receive). Also retires the stale Claude unit (--keep-stale-claude-unit opts out)
|
|
278
278
|
./run.sh uninstall-copilot-bridge # #86 C3a: honest inverse of install-copilot-bridge from the package-owned install-state (exact qualified id + exact marketplace name/path + recorded assembly only; state deleted LAST; a failing vendor list is UNKNOWN and refuses; never --force, never the stale Claude unit)
|
|
279
279
|
./run.sh doctor-copilot-bridge # #82/#86: fail-loud surface for that unit — runtime axis (red = a hook that RAN and failed, or a broken/unbaked artifact; "installed with zero records" is NOT red and is reported as NOT-YET: a Copilot session is born on its FIRST PROMPT, not when the window opens, measured) + ownership axis (install-state shape/binding, marketplace path drift, UNKNOWN vendor lists; legacy no-state install = named non-green, repair = install-copilot-bridge adoption). Either axis red = red
|
|
@@ -1596,7 +1596,10 @@ check_mux_fresh_call() {
|
|
|
1596
1596
|
# pi-durable-fresh-live-receipts rides here for tmux-coordinate-row's reason: it is the
|
|
1597
1597
|
# deterministic half of a LIVE sibling (smoke-pi-durable-fresh-live, #129) — the oracles that judge
|
|
1598
1598
|
# that gate's receipts, proven on inline rows, so no model turn and no durable host is needed.
|
|
1599
|
-
|
|
1599
|
+
# codex-socket-listener rides here for the same reason (#128): it is the ownership precondition
|
|
1600
|
+
# of smoke-codex-fresh-live, judged on synthetic wrapper/self shapes and observed on a throwaway
|
|
1601
|
+
# wrapper → listener pair of its own, so no Codex, no app-server and no model turn is needed.
|
|
1602
|
+
run_vitest test/mux-fresh-call.test.ts test/fresh-call-composition.contract.test.ts test/copilot-fresh-preflight.test.ts test/codex-fresh-preflight.test.ts test/fresh-call-surfaces.contract.test.ts test/fresh-call-provider.contract.test.ts test/omp-fresh-bootstrap.contract.test.ts test/tmux-coordinate-row.test.ts test/codex-fresh-live-protocol.test.ts test/codex-caller-seat.test.ts test/pi-durable-fresh-live-receipts.test.ts test/codex-socket-listener.test.ts
|
|
1600
1603
|
}
|
|
1601
1604
|
|
|
1602
1605
|
check_pi_durable_beside() {
|
|
@@ -1606,7 +1609,7 @@ check_pi_durable_beside() {
|
|
|
1606
1609
|
# from re-running every beside test. The six files are one contact: the adapter and its packaged
|
|
1607
1610
|
# bootstrap, the installed carrier/SDK-set verifier (which also reads the three checkout gates as
|
|
1608
1611
|
# SOURCE), the fresh preflight, the owner join that admits the durable host, the carrier's
|
|
1609
|
-
# relocation grammar, resolver
|
|
1612
|
+
# relocation grammar, resolver, owned-source emitter (#130, #141) and source fence with the carrier's task footer (#134),
|
|
1610
1613
|
# and the bootstrap's explicit --native-module ingress (#130 P2) and its --native-module-dir (#134).
|
|
1611
1614
|
run_vitest pi-extensions/meta-bridge-pi-durable.test.ts pi-extensions/lib/pi-durable-runtime.test.ts pi-extensions/lib/pi-durable-fresh-preflight.test.ts pi-extensions/lib/entwurf-deliverability.owner-join.test.ts pi-extensions/lib/pi-durable-carrier.test.ts pi-extensions/lib/pi-durable-native-module.test.ts
|
|
1612
1615
|
}
|
|
@@ -2435,8 +2438,8 @@ const nextFloorRange = `>=${piAi} <${piMaj}.${piMin + 1}`;
|
|
|
2435
2438
|
const floorGaps = [];
|
|
2436
2439
|
const PI_CONSTELLATION = [
|
|
2437
2440
|
'@earendil-works/pi-ai', '@earendil-works/pi-coding-agent', '@earendil-works/pi-tui',
|
|
2438
|
-
'@earendil-works/pi-agent-core', '@earendil-works/pi-
|
|
2439
|
-
|
|
2441
|
+
'@earendil-works/pi-agent-core', '@earendil-works/pi-telemetry', '@earendil-works/chord',
|
|
2442
|
+
// `[측정 2026-10-13]` 1.2.0: pi-client and pi-protocol left — upstream removed both, E404 at 1.2.0.
|
|
2440
2443
|
// `[측정 2026-09-30]` 0.99.1: pi-coding-agent's runtime deps gained the built-in extensions' packages.
|
|
2441
2444
|
'@earendil-works/pi-codemode', '@earendil-works/pi-mcp',
|
|
2442
2445
|
// #130: the durable library the pi-durable carrier imports — a production dependency since 0.32.
|
|
@@ -2577,6 +2580,65 @@ for (const [file, re, min, label] of SITES) {
|
|
|
2577
2580
|
bound += hits.length;
|
|
2578
2581
|
}
|
|
2579
2582
|
|
|
2583
|
+
// #141: the pi-durable carrier's emitter identity, the one place CI runs an EXACT Node. The check job
|
|
2584
|
+
// runs the Node the pin records as the carrier's emitter (CI-BOUND), and its check:full floor step runs
|
|
2585
|
+
// with ENTWURF_REQUIRE_EMITTER_NODE=1 (CI-REQUIRED), so that floor proves the emission under that
|
|
2586
|
+
// identity; every other job floats the major so the newest release of the lane stays covered. Both are
|
|
2587
|
+
// read from ACTIVE workflow lines only — the step's own `with:` / `env:` mapping, comments stripped —
|
|
2588
|
+
// so commented-out text or the same key on another step can never stand in for them.
|
|
2589
|
+
const EMITTER_BOUND_QK = '[QK:PI-DURABLE-EMITTER-NODE-CI-BOUND]';
|
|
2590
|
+
const EMITTER_REQUIRED_QK = '[QK:PI-DURABLE-EMITTER-NODE-CI-REQUIRED]';
|
|
2591
|
+
const emitterNode = JSON.parse(readFileSync('pi/pi-durable/overlay/upstream-pin.json', 'utf8')).carrier?.emitter?.node;
|
|
2592
|
+
assert.match(String(emitterNode), /^v\d+\.\d+\.\d+$/,
|
|
2593
|
+
`${EMITTER_BOUND_QK} pi/pi-durable/overlay/upstream-pin.json carrier.emitter.node must be an exact vX.Y.Z (got ${emitterNode})`);
|
|
2594
|
+
// jobs → steps → active lines. A full-line comment is dropped and a trailing ` # …` is cut; job heads are
|
|
2595
|
+
// the two-space keys under `jobs:`, a step starts at its six-space `- ` item.
|
|
2596
|
+
const ciJobs = new Map();
|
|
2597
|
+
{
|
|
2598
|
+
let inJobs = false;
|
|
2599
|
+
let steps;
|
|
2600
|
+
for (const raw of readFileSync('.github/workflows/ci.yml', 'utf8').split('\n')) {
|
|
2601
|
+
const line = raw.replace(/\s+#.*$/, '');
|
|
2602
|
+
if (/^\s*#/.test(raw) || line.trim() === '') continue;
|
|
2603
|
+
if (/^\S/.test(line)) { inJobs = line === 'jobs:'; steps = undefined; continue; }
|
|
2604
|
+
if (!inJobs) continue;
|
|
2605
|
+
const head = /^ ([a-z][a-z0-9-]*):$/.exec(line);
|
|
2606
|
+
if (head) { steps = []; ciJobs.set(head[1], steps); continue; }
|
|
2607
|
+
if (steps === undefined) continue;
|
|
2608
|
+
if (/^ - /.test(line)) steps.push([` ${line.slice(8)}`]);
|
|
2609
|
+
else if (steps.length > 0 && /^ /.test(line)) steps.at(-1).push(line);
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
// A step's own value for `key` (`with:` / `env:` child at ten spaces), or a top-level step key at eight.
|
|
2613
|
+
const stepKey = (step, key) => step.find((l) => l.startsWith(` ${key}: `))?.slice(10 + key.length);
|
|
2614
|
+
const stepChild = (step, block, key) => {
|
|
2615
|
+
const at = step.indexOf(` ${block}:`);
|
|
2616
|
+
if (at === -1) return undefined;
|
|
2617
|
+
const children = [];
|
|
2618
|
+
for (const l of step.slice(at + 1)) { if (!l.startsWith(' ')) break; children.push(l); }
|
|
2619
|
+
return children.find((l) => l.startsWith(` ${key}: `))?.slice(12 + key.length);
|
|
2620
|
+
};
|
|
2621
|
+
const setupNodes = (job) => (ciJobs.get(job) ?? []).filter((s) => /^actions\/setup-node@/.test(stepKey(s, 'uses') ?? ''));
|
|
2622
|
+
const checkSetup = setupNodes('check');
|
|
2623
|
+
assert.equal(checkSetup.length, 1,
|
|
2624
|
+
`${EMITTER_BOUND_QK} .github/workflows/ci.yml: the check job must have exactly one actions/setup-node step (found ${checkSetup.length})`);
|
|
2625
|
+
const checkNode = stepChild(checkSetup[0], 'with', 'node-version');
|
|
2626
|
+
assert.equal(`v${checkNode}`, emitterNode,
|
|
2627
|
+
`${EMITTER_BOUND_QK} .github/workflows/ci.yml: the check job's setup-node runs Node ${checkNode}, but the carrier's pinned emitter is ${emitterNode} — its floor would prove the emission under another identity`);
|
|
2628
|
+
const floors = (ciJobs.get('check') ?? []).filter((s) => stepKey(s, 'run') === 'pnpm run check:full');
|
|
2629
|
+
assert.equal(floors.length, 1,
|
|
2630
|
+
`${EMITTER_REQUIRED_QK} .github/workflows/ci.yml: the check job must run \`pnpm run check:full\` in exactly one step (found ${floors.length})`);
|
|
2631
|
+
assert.equal(stepChild(floors[0], 'env', 'ENTWURF_REQUIRE_EMITTER_NODE'), '"1"',
|
|
2632
|
+
`${EMITTER_REQUIRED_QK} .github/workflows/ci.yml: the check job's check:full step does not set ENTWURF_REQUIRE_EMITTER_NODE: "1" in its own env, so an emitter-identity mismatch on that floor would be a skip, not red`);
|
|
2633
|
+
for (const [job] of ciJobs) {
|
|
2634
|
+
if (job === 'check') continue;
|
|
2635
|
+
for (const s of setupNodes(job)) {
|
|
2636
|
+
const value = stepChild(s, 'with', 'node-version');
|
|
2637
|
+
assert.match(String(value), /^\d+$/,
|
|
2638
|
+
`${EMITTER_BOUND_QK} .github/workflows/ci.yml: job ${job} pins node-version ${value}; only the check job is exact, the others float the major`);
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2580
2642
|
// Sweep the tracked first-party contract TEXT surfaces, NOT just the files
|
|
2581
2643
|
// already registered above. The first cut of this gate swept only SITES' own
|
|
2582
2644
|
// files and therefore could not see a declaration in an UNREGISTERED file —
|
|
@@ -2627,7 +2689,7 @@ assert.ok(swept >= 6,
|
|
|
2627
2689
|
// `bound` is the real declaration count; SITES.length is the number of RULES
|
|
2628
2690
|
// (the CI rule alone matches two node-version lines). Naming the rule count a
|
|
2629
2691
|
// declaration count would be this gate telling a small lie about its own reach.
|
|
2630
|
-
console.log(`[check-node-floor-coherence] ok — Node >=${MAJOR} is coherent across engines.node, ${bound} bound declarations from ${SITES.length} site rules, and ${swept} declarations swept over ${tracked.length} tracked first-party contract text surfaces (single supported axis, no legacy lane). Non-text carriers (e.g. a Dockerfile \`FROM node:*\`) are OUTSIDE this pattern and must be bound by their own gate.`);
|
|
2692
|
+
console.log(`[check-node-floor-coherence] ok — Node >=${MAJOR} is coherent across engines.node, ${bound} bound declarations from ${SITES.length} site rules, and ${swept} declarations swept over ${tracked.length} tracked first-party contract text surfaces (single supported axis, no legacy lane); the pi-durable emitter ${emitterNode} is the CI check job's exact Node. Non-text carriers (e.g. a Dockerfile \`FROM node:*\`) are OUTSIDE this pattern and must be bound by their own gate.`);
|
|
2631
2693
|
EOF
|
|
2632
2694
|
)
|
|
2633
2695
|
}
|
|
@@ -2916,7 +2978,7 @@ check_pi_import_surface() {
|
|
|
2916
2978
|
# resolve under the extension loader.
|
|
2917
2979
|
#
|
|
2918
2980
|
# #130 CARRIER FENCE (replaces the #129 bootstrap exception, retired with the source runtime):
|
|
2919
|
-
# the pi-durable carrier (pi/pi-durable/carrier/*.js,
|
|
2981
|
+
# the pi-durable carrier (pi/pi-durable/carrier/*.js, emitted from Entwurf's owned source by
|
|
2920
2982
|
# scripts/emit-pi-durable-carrier.ts) is the ONE place pi-coding-agent internals are reached,
|
|
2921
2983
|
# and only through the reserved `entwurf-pi-dist:` scheme its resolver answers. The fence is
|
|
2922
2984
|
# declared and asserted here, not inferred from the root-only scan staying quiet:
|
|
@@ -3758,7 +3820,6 @@ check_pack() {
|
|
|
3758
3820
|
"mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js"
|
|
3759
3821
|
"pi/pi-durable/bootstrap.mjs"
|
|
3760
3822
|
"pi/pi-durable/overlay/upstream-pin.json"
|
|
3761
|
-
"pi/pi-durable/overlay/runtime-contacts.patch"
|
|
3762
3823
|
"pi/pi-durable/carrier-resolver.mjs"
|
|
3763
3824
|
"pi/pi-durable/carrier-relocation.mjs"
|
|
3764
3825
|
"pi/pi-durable/carrier/runtime.js"
|
|
@@ -3845,6 +3906,11 @@ check_pack() {
|
|
|
3845
3906
|
# Maintainer take/renderer/helper TS is source-only, including non-test implementations.
|
|
3846
3907
|
'^demo/.*\.ts$'
|
|
3847
3908
|
)
|
|
3909
|
+
# #141: the owned pi-durable source, its typecheck fence and the retired overlay patch are maintainer
|
|
3910
|
+
# input. An installed consumer runs the emitted carrier JS (and its LICENSE) only — Rule 11: compiled
|
|
3911
|
+
# JS for installed entrypoints. The files array lists pi/pi-durable per FILE, so nothing admits these
|
|
3912
|
+
# today; this tripwire is what fails loud the day an allowlist entry would.
|
|
3913
|
+
local durable_source_pattern='^pi/pi-durable/(src/|tsconfig\.json$|overlay/runtime-contacts\.patch$)'
|
|
3848
3914
|
|
|
3849
3915
|
local pass=1 f pat hit
|
|
3850
3916
|
|
|
@@ -3864,6 +3930,13 @@ check_pack() {
|
|
|
3864
3930
|
fi
|
|
3865
3931
|
done
|
|
3866
3932
|
|
|
3933
|
+
hit=$(grep -E "$durable_source_pattern" <<<"$file_list" || true)
|
|
3934
|
+
if [ -n "$hit" ]; then
|
|
3935
|
+
fail "[check-pack] [QK:PI-DURABLE-SOURCE-NOT-SHIPPED] the owned pi-durable source rides the tarball — an installed consumer runs the carrier JS only:"
|
|
3936
|
+
echo "$hit" | sed 's/^/ /' >&2
|
|
3937
|
+
pass=0
|
|
3938
|
+
fi
|
|
3939
|
+
|
|
3867
3940
|
local total
|
|
3868
3941
|
total=$(printf '%s\n' "$file_list" | wc -l | tr -d ' ')
|
|
3869
3942
|
echo "[check-pack] $total files in tarball"
|
|
@@ -3898,7 +3971,7 @@ check_pack() {
|
|
|
3898
3971
|
# which is exactly what property (1)'s `(_|$)` absorbs; all three shapes are fixtures
|
|
3899
3972
|
# below so a future suffix change cannot pass vacuously.)
|
|
3900
3973
|
pack_install_leaked_pi() {
|
|
3901
|
-
grep '^@earendil-works+' | grep -Ev '@1\.
|
|
3974
|
+
grep '^@earendil-works+' | grep -Ev '@1\.2\.0(_|$)' || true
|
|
3902
3975
|
}
|
|
3903
3976
|
|
|
3904
3977
|
# Matcher self-test on SYNTHETIC lookalikes: a healthy install tree cannot exercise either
|
|
@@ -3918,15 +3991,15 @@ check_pack_pin_matcher() {
|
|
|
3918
3991
|
# None may leak; the two lookalikes must — a PREFIX-EXTENDED version (`0.86.0-beta.1`,
|
|
3919
3992
|
# the prerelease shape that an unanchored match would bless) and an off-pin version.
|
|
3920
3993
|
matcher_probe=$(printf '%s\n' \
|
|
3921
|
-
'@earendil-works+pi-ai@1.
|
|
3922
|
-
'@earendil-works+pi-ai@1.
|
|
3923
|
-
'@earendil-works+pi-ai@1.
|
|
3924
|
-
'@earendil-works+pi-ai@1.
|
|
3925
|
-
'@earendil-works+pi-ai@1.
|
|
3994
|
+
'@earendil-works+pi-ai@1.2.0' \
|
|
3995
|
+
'@earendil-works+pi-ai@1.2.0_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6' \
|
|
3996
|
+
'@earendil-works+pi-ai@1.2.0_ws@8.21.3' \
|
|
3997
|
+
'@earendil-works+pi-ai@1.2.0_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6__ws@8.21.3_zod@4.3.6' \
|
|
3998
|
+
'@earendil-works+pi-ai@1.2.0-beta.1' \
|
|
3926
3999
|
'@earendil-works+pi-agent-core@0.87.1' | pack_install_leaked_pi)
|
|
3927
|
-
if [ "$matcher_probe" != '@earendil-works+pi-ai@1.
|
|
4000
|
+
if [ "$matcher_probe" != '@earendil-works+pi-ai@1.2.0-beta.1
|
|
3928
4001
|
@earendil-works+pi-agent-core@0.87.1' ]; then
|
|
3929
|
-
fail "[QK:PACK-INSTALL-PIN-MATCHER-BOUNDED] the pin-leak matcher must flag the prefix-extended 1.
|
|
4002
|
+
fail "[QK:PACK-INSTALL-PIN-MATCHER-BOUNDED] the pin-leak matcher must flag the prefix-extended 1.2.0-beta.1 and the off-pin 0.87.1 lookalikes, and pass 1.2.0 bare or with any measured peer-hash — got: ${matcher_probe:-<nothing leaked>}"
|
|
3930
4003
|
return 1
|
|
3931
4004
|
fi
|
|
3932
4005
|
|
|
@@ -3934,11 +4007,11 @@ check_pack_pin_matcher() {
|
|
|
3934
4007
|
# (0.85.0 onward, still true at 0.99.1). An off-pin chord MUST leak; the pinned one must not. A matcher narrowed
|
|
3935
4008
|
# back to `^@earendil-works+pi-` sees nothing here and dies at this signature.
|
|
3936
4009
|
matcher_probe=$(printf '%s\n' \
|
|
3937
|
-
'@earendil-works+chord@1.
|
|
4010
|
+
'@earendil-works+chord@1.2.0' \
|
|
3938
4011
|
'@earendil-works+chord@0.87.1' \
|
|
3939
|
-
'@earendil-works+pi-ai@1.
|
|
4012
|
+
'@earendil-works+pi-ai@1.2.0' | pack_install_leaked_pi)
|
|
3940
4013
|
if [ "$matcher_probe" != '@earendil-works+chord@0.87.1' ]; then
|
|
3941
|
-
fail "[QK:PACK-INSTALL-PIN-MATCHER-COVERS-CLOSURE] the pin-leak matcher must cover every @earendil-works closure member, not just the pi-* families — an off-pin @earendil-works/chord has to leak (it
|
|
4014
|
+
fail "[QK:PACK-INSTALL-PIN-MATCHER-COVERS-CLOSURE] the pin-leak matcher must cover every @earendil-works closure member, not just the pi-* families — an off-pin @earendil-works/chord has to leak (it was a runtime dependency of pi-coding-agent, pi-agent-core, pi-client and pi-protocol — `[측정 2026-09-20]` at 0.86.0, re-measured `[측정 2026-09-22]` at 0.87.0, `[측정 2026-09-23]` at 0.87.1 and `[측정 2026-09-30]` at 0.99.1, where pi-coding-agent adds pi-codemode and pi-mcp beside them; `[측정 2026-10-13]` at 1.2.0 pi-durable alone declares it) — got: ${matcher_probe:-<nothing leaked>}"
|
|
3942
4015
|
return 1
|
|
3943
4016
|
fi
|
|
3944
4017
|
|
|
@@ -4113,8 +4186,8 @@ ORACLE
|
|
|
4113
4186
|
# copied from this checkout into the sandbox — it never ships (test/ is outside the package files).
|
|
4114
4187
|
# Its initialization appends `init` to the receipt this cell names; the same public launch must then
|
|
4115
4188
|
# reach the same carrier model refusal, so the installed bootstrap loaded the module before the
|
|
4116
|
-
# runtime and the native registry installed its extension (carrier/runtime.js
|
|
4117
|
-
# refusal (runtime.js
|
|
4189
|
+
# runtime and the native registry installed its extension (carrier/runtime.js 159) before that
|
|
4190
|
+
# refusal (runtime.js 175). A module claiming an extension name the app installs itself is refused by
|
|
4118
4191
|
# name before anything opens: no durable session directory, no citizen, in a fresh sandbox home.
|
|
4119
4192
|
# Native SDK file opens here are ordinary (the module runs in the native process); none is judged.
|
|
4120
4193
|
local mod_dir="$box/pd-module" mod_home="$box/pd-module-home" mod_env=()
|
|
@@ -4169,7 +4242,7 @@ ORACLE
|
|
|
4169
4242
|
# aligned host binary (the pinned version, installed above for L1) must be inside the range.
|
|
4170
4243
|
# The previous host version is a variable, not a quoted `@pkg@x.y.z` spec: check-dep-versions reads
|
|
4171
4244
|
# those specs as this repo's install pins, and this one is deliberately off the floor.
|
|
4172
|
-
local host_home host_proj host_old="$box/pd-host-old" host_env=() host_old_ver="1.0
|
|
4245
|
+
local host_home host_proj host_old="$box/pd-host-old" host_env=() host_old_ver="1.1.0"
|
|
4173
4246
|
mkdir -p "$host_old"
|
|
4174
4247
|
for spec in "${host_specs[@]}"; do host_old_specs+=("${spec%@*}@$host_old_ver"); done
|
|
4175
4248
|
out=$(cd "$host_old" && npm install "${host_old_specs[@]}" --no-audit --no-fund 2>&1) || {
|
|
@@ -4187,7 +4260,7 @@ ORACLE
|
|
|
4187
4260
|
fail "[check-pack-install] host $cell: the native pi-durable row must stay PASS whatever the PATH host is:"; echo "$out" | tail -20 | sed 's/^/ /' >&2; return 1
|
|
4188
4261
|
fi
|
|
4189
4262
|
if [ "$cell" = old ]; then
|
|
4190
|
-
if [ "$rc" -eq 0 ] || ! grep -q "pi: FAIL — detected pi $host_old_ver is outside the supported range >=$pin_ver <1.
|
|
4263
|
+
if [ "$rc" -eq 0 ] || ! grep -q "pi: FAIL — detected pi $host_old_ver is outside the supported range >=$pin_ver <1.3" <<<"$out" || [ -e "$host_proj/.pi" ] || [ -e "$host_home/.pi/agent/settings.json" ]; then
|
|
4191
4264
|
fail "[QK:PACK-INSTALL-HOST-RANGE-SEPARATE] [check-pack-install] a real pi $host_old_ver host beside the $pin_ver native set must be a named pi FAIL with zero Pi wiring and a non-green setup (rc=$rc):"
|
|
4192
4265
|
echo "$out" | tail -20 | sed 's/^/ /' >&2
|
|
4193
4266
|
return 1
|
|
@@ -4395,7 +4468,6 @@ _check_pack_install_impl() {
|
|
|
4395
4468
|
"mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js"
|
|
4396
4469
|
"pi/pi-durable/bootstrap.mjs"
|
|
4397
4470
|
"pi/pi-durable/overlay/upstream-pin.json"
|
|
4398
|
-
"pi/pi-durable/overlay/runtime-contacts.patch"
|
|
4399
4471
|
"pi/pi-durable/carrier-resolver.mjs"
|
|
4400
4472
|
"pi/pi-durable/carrier-relocation.mjs"
|
|
4401
4473
|
"pi/pi-durable/carrier/runtime.js"
|
|
@@ -4558,21 +4630,26 @@ _check_pack_install_impl() {
|
|
|
4558
4630
|
# pi-ai, pi-codemode, pi-mcp, pi-tui}, pi-ai {pi-telemetry}, pi-agent-core {pi-ai}, pi-durable
|
|
4559
4631
|
# {chord, pi-ai}, and `npm view` gives pi-client {chord, pi-protocol}, pi-protocol {chord}. All
|
|
4560
4632
|
# eleven are published at 1.1.0.
|
|
4561
|
-
|
|
4633
|
+
# `[측정 2026-10-13]` at 1.2.0 the constellation SHRINKS to the nine-member SDK set: upstream
|
|
4634
|
+
# removed pi-client and pi-protocol (7472cda0d), and `npm view <pkg>@1.2.0` answers E404 for both,
|
|
4635
|
+
# so their pins cannot move with the floor and leave instead of staying redundant. The 1.2.0
|
|
4636
|
+
# registry manifests give pi-coding-agent {pi-agent-core, pi-ai, pi-codemode, pi-mcp, pi-tui}
|
|
4637
|
+
# (chord dropped), pi-ai {pi-telemetry}, pi-agent-core {pi-ai}, pi-durable {chord, pi-ai,
|
|
4638
|
+
# pi-codemode}; all nine are published at 1.2.0
|
|
4639
|
+
# (.tmp-verify/141-kickoff/s1a/map-receipts/01-tarballs.txt).
|
|
4640
|
+
echo "[check-pack-install] pnpm add into $tmp (with the aligned 1.2.x SDK + chord + codemode + mcp + durable + typebox)"
|
|
4562
4641
|
local install_log
|
|
4563
4642
|
install_log=$(cd "$tmp" && pnpm add \
|
|
4564
4643
|
"$tgz_path" \
|
|
4565
|
-
"@earendil-works/pi-ai@1.
|
|
4566
|
-
"@earendil-works/pi-coding-agent@1.
|
|
4567
|
-
"@earendil-works/pi-tui@1.
|
|
4568
|
-
"@earendil-works/pi-agent-core@1.
|
|
4569
|
-
"@earendil-works/pi-
|
|
4570
|
-
"@earendil-works/
|
|
4571
|
-
"@earendil-works/pi-
|
|
4572
|
-
"@earendil-works/
|
|
4573
|
-
"@earendil-works/pi-
|
|
4574
|
-
"@earendil-works/pi-mcp@1.1.0" \
|
|
4575
|
-
"@earendil-works/pi-durable@1.1.0" \
|
|
4644
|
+
"@earendil-works/pi-ai@1.2.0" \
|
|
4645
|
+
"@earendil-works/pi-coding-agent@1.2.0" \
|
|
4646
|
+
"@earendil-works/pi-tui@1.2.0" \
|
|
4647
|
+
"@earendil-works/pi-agent-core@1.2.0" \
|
|
4648
|
+
"@earendil-works/pi-telemetry@1.2.0" \
|
|
4649
|
+
"@earendil-works/chord@1.2.0" \
|
|
4650
|
+
"@earendil-works/pi-codemode@1.2.0" \
|
|
4651
|
+
"@earendil-works/pi-mcp@1.2.0" \
|
|
4652
|
+
"@earendil-works/pi-durable@1.2.0" \
|
|
4576
4653
|
"typebox@latest" \
|
|
4577
4654
|
--ignore-workspace --ignore-scripts 2>&1) || {
|
|
4578
4655
|
fail "[check-pack-install] pnpm add failed:"
|
|
@@ -4588,11 +4665,11 @@ _check_pack_install_impl() {
|
|
|
4588
4665
|
local leaked_pi
|
|
4589
4666
|
leaked_pi=$(ls "$tmp/node_modules/.pnpm" 2>/dev/null | pack_install_leaked_pi)
|
|
4590
4667
|
if [ -n "$leaked_pi" ]; then
|
|
4591
|
-
fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 1.
|
|
4668
|
+
fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 1.2.0):"
|
|
4592
4669
|
printf '%s\n' "$leaked_pi" | sed 's/^/ /' >&2
|
|
4593
4670
|
return 1
|
|
4594
4671
|
fi
|
|
4595
|
-
echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works package is 1.
|
|
4672
|
+
echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works package is 1.2.0 (chord and pi-durable included)"
|
|
4596
4673
|
|
|
4597
4674
|
# Resolve the installed package.json and confirm pi.extensions
|
|
4598
4675
|
# arrived intact. If pi.extensions is empty or missing, the
|
|
@@ -39,6 +39,7 @@ import { pathToFileURL } from "node:url";
|
|
|
39
39
|
import type { Api, AssistantMessageEvent, Message, Model, TranscriptContext } from "@earendil-works/pi-ai";
|
|
40
40
|
import { normalizeContext } from "@earendil-works/pi-ai";
|
|
41
41
|
import { isRetryableAssistantError } from "@earendil-works/pi-ai/compat";
|
|
42
|
+
import { assertOverlayInSandbox, sandboxAcpGateHome } from "./lib/acp-gate-home.ts";
|
|
42
43
|
|
|
43
44
|
const sonnet = { id: "claude-sonnet-5-5" } as unknown as Model<Api>;
|
|
44
45
|
|
|
@@ -196,8 +197,6 @@ function makeHarness(recordDir: string) {
|
|
|
196
197
|
await notifier?.({ sessionUpdate: "agent_message_chunk", content: { type: "text", text } });
|
|
197
198
|
},
|
|
198
199
|
deps: {
|
|
199
|
-
resolveLaunch: () => ({ command: "node", args: ["fake"] }),
|
|
200
|
-
ensureOverlay: () => {},
|
|
201
200
|
spawnChild: () => {
|
|
202
201
|
const c = makeFakeChild();
|
|
203
202
|
children.push(c);
|
|
@@ -279,6 +278,7 @@ function reuseCtx(prior: string, latest: string): TranscriptContext {
|
|
|
279
278
|
const TMP_EMIT = ".tmp-verify/acp-prompt-lifecycle";
|
|
280
279
|
rmSync(TMP_EMIT, { recursive: true, force: true });
|
|
281
280
|
const recordDir = mkdtempSync(resolve(tmpdir(), "acp-prompt-life-"));
|
|
281
|
+
const gateHome = sandboxAcpGateHome("acp-prompt-lifecycle-home-");
|
|
282
282
|
|
|
283
283
|
try {
|
|
284
284
|
execFileSync("node_modules/.bin/tsc", ["--outDir", TMP_EMIT, "--rootDir", ".", "--noEmit", "false"], {
|
|
@@ -288,6 +288,7 @@ try {
|
|
|
288
288
|
mkdirSync(promptsOut, { recursive: true });
|
|
289
289
|
copyFileSync("pi-extensions/lib/acp/prompts/engraving.md", resolve(promptsOut, "engraving.md"));
|
|
290
290
|
const backend = (await import(pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/backend.js")).href)) as any;
|
|
291
|
+
await assertOverlayInSandbox(TMP_EMIT, gateHome);
|
|
291
292
|
|
|
292
293
|
// ----------------------------------------------------------------------
|
|
293
294
|
// CELL 1 — a long / quiet turn is NOT killed.
|
|
@@ -918,6 +919,7 @@ try {
|
|
|
918
919
|
// non-empty or already gone — fine either way
|
|
919
920
|
}
|
|
920
921
|
rmSync(recordDir, { recursive: true, force: true });
|
|
922
|
+
gateHome.restore();
|
|
921
923
|
}
|
|
922
924
|
|
|
923
925
|
console.log(
|
|
@@ -32,6 +32,7 @@ import { resolve } from "node:path";
|
|
|
32
32
|
import { pathToFileURL } from "node:url";
|
|
33
33
|
import type { Api, AssistantMessageEvent, Context, Message, Model, TranscriptContext } from "@earendil-works/pi-ai";
|
|
34
34
|
import { normalizeContext } from "@earendil-works/pi-ai";
|
|
35
|
+
import { assertOverlayInSandbox, sandboxAcpGateHome } from "./lib/acp-gate-home.ts";
|
|
35
36
|
|
|
36
37
|
// The pi built-ins the Claude child also exposes natively. pi declares its tool
|
|
37
38
|
// surface on every turn; since 0.86 that declaration reaches a provider as the
|
|
@@ -175,8 +176,6 @@ function makeHarness(recordDir: string, config: any = DEFAULT_RESOLVED_CONFIG, e
|
|
|
175
176
|
});
|
|
176
177
|
|
|
177
178
|
const deps = {
|
|
178
|
-
resolveLaunch: () => ({ command: "node", args: ["fake"] }),
|
|
179
|
-
ensureOverlay: () => {},
|
|
180
179
|
spawnChild: () => {
|
|
181
180
|
const c = makeFakeChild();
|
|
182
181
|
children.push(c);
|
|
@@ -268,6 +267,7 @@ function reuseCtx(prior: string, latest: string): TranscriptContext {
|
|
|
268
267
|
const TMP_EMIT = ".tmp-verify/acp-session-reuse";
|
|
269
268
|
rmSync(TMP_EMIT, { recursive: true, force: true });
|
|
270
269
|
const recordDir = mkdtempSync(resolve(tmpdir(), "acp-reuse-rec-"));
|
|
270
|
+
const gateHome = sandboxAcpGateHome("acp-session-reuse-home-");
|
|
271
271
|
|
|
272
272
|
try {
|
|
273
273
|
execFileSync("node_modules/.bin/tsc", ["--outDir", TMP_EMIT, "--rootDir", ".", "--noEmit", "false"], {
|
|
@@ -283,6 +283,7 @@ try {
|
|
|
283
283
|
const backendUrl = pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/backend.js")).href;
|
|
284
284
|
const storeUrl = pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/session-store.js")).href;
|
|
285
285
|
const backend = (await import(backendUrl)) as any;
|
|
286
|
+
await assertOverlayInSandbox(TMP_EMIT, gateHome);
|
|
286
287
|
const store = (await import(storeUrl)) as any;
|
|
287
288
|
const configUrl = pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/config.js")).href;
|
|
288
289
|
const configMod = (await import(configUrl)) as any;
|
|
@@ -814,6 +815,7 @@ try {
|
|
|
814
815
|
// non-empty or already gone — fine either way
|
|
815
816
|
}
|
|
816
817
|
rmSync(recordDir, { recursive: true, force: true });
|
|
818
|
+
gateHome.restore();
|
|
817
819
|
}
|
|
818
820
|
|
|
819
821
|
console.log(
|
|
@@ -54,6 +54,7 @@ import { resolve } from "node:path";
|
|
|
54
54
|
import { pathToFileURL } from "node:url";
|
|
55
55
|
import type { Api, AssistantMessageEvent, Model } from "@earendil-works/pi-ai";
|
|
56
56
|
import { normalizeContext } from "@earendil-works/pi-ai";
|
|
57
|
+
import { assertOverlayInSandbox, sandboxAcpGateHome } from "./lib/acp-gate-home.ts";
|
|
57
58
|
|
|
58
59
|
const sonnet = { id: "claude-sonnet-5-5" } as unknown as Model<Api>;
|
|
59
60
|
|
|
@@ -118,8 +119,6 @@ const DEFAULT_RESOLVED_CONFIG: any = {
|
|
|
118
119
|
*/
|
|
119
120
|
function makeHarness(recordDir: string, wireReason: string | null) {
|
|
120
121
|
return {
|
|
121
|
-
resolveLaunch: () => ({ command: "node", args: ["fake"] }),
|
|
122
|
-
ensureOverlay: () => {},
|
|
123
122
|
spawnChild: () => makeFakeChild(),
|
|
124
123
|
createConnection: (_child: any, handlers: any) => ({
|
|
125
124
|
initialize: async () => ({ agentCapabilities: {} }),
|
|
@@ -143,6 +142,7 @@ function makeHarness(recordDir: string, wireReason: string | null) {
|
|
|
143
142
|
const TMP_EMIT = ".tmp-verify/acp-stop-reason";
|
|
144
143
|
rmSync(TMP_EMIT, { recursive: true, force: true });
|
|
145
144
|
const recordDir = mkdtempSync(resolve(tmpdir(), "acp-stop-rec-"));
|
|
145
|
+
const gateHome = sandboxAcpGateHome("acp-stop-reason-home-");
|
|
146
146
|
|
|
147
147
|
/**
|
|
148
148
|
* One driven turn, described by everything an operator could observe.
|
|
@@ -250,6 +250,7 @@ try {
|
|
|
250
250
|
const backendUrl = pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/backend.js")).href;
|
|
251
251
|
const eventUrl = pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/event-mapper.js")).href;
|
|
252
252
|
const backend = (await import(backendUrl)) as any;
|
|
253
|
+
await assertOverlayInSandbox(TMP_EMIT, gateHome);
|
|
253
254
|
const eventMod = (await import(eventUrl)) as any;
|
|
254
255
|
|
|
255
256
|
// ----------------------------------------------------------------------
|
|
@@ -354,6 +355,7 @@ try {
|
|
|
354
355
|
// non-empty or already gone — fine either way
|
|
355
356
|
}
|
|
356
357
|
rmSync(recordDir, { recursive: true, force: true });
|
|
358
|
+
gateHome.restore();
|
|
357
359
|
}
|
|
358
360
|
|
|
359
361
|
console.log(
|
|
@@ -34,6 +34,7 @@ import { resolve } from "node:path";
|
|
|
34
34
|
import { pathToFileURL } from "node:url";
|
|
35
35
|
import type { Api, AssistantMessageEvent, Message, Model, TranscriptContext } from "@earendil-works/pi-ai";
|
|
36
36
|
import { normalizeContext } from "@earendil-works/pi-ai";
|
|
37
|
+
import { assertOverlayInSandbox, sandboxAcpGateHome } from "./lib/acp-gate-home.ts";
|
|
37
38
|
|
|
38
39
|
const sonnet = { id: "claude-sonnet-5-5" } as unknown as Model<Api>;
|
|
39
40
|
|
|
@@ -108,8 +109,6 @@ function makeHarness(recordDir: string) {
|
|
|
108
109
|
children,
|
|
109
110
|
promptCalls,
|
|
110
111
|
deps: {
|
|
111
|
-
resolveLaunch: () => ({ command: "node", args: ["fake"] }),
|
|
112
|
-
ensureOverlay: () => {},
|
|
113
112
|
spawnChild: () => {
|
|
114
113
|
const c = makeFakeChild();
|
|
115
114
|
children.push(c);
|
|
@@ -187,6 +186,7 @@ const onResponse = () => {
|
|
|
187
186
|
const TMP_EMIT = ".tmp-verify/acp-stream-hooks";
|
|
188
187
|
rmSync(TMP_EMIT, { recursive: true, force: true });
|
|
189
188
|
const recordDir = mkdtempSync(resolve(tmpdir(), "acp-stream-hooks-"));
|
|
189
|
+
const gateHome = sandboxAcpGateHome("acp-stream-hooks-home-");
|
|
190
190
|
|
|
191
191
|
try {
|
|
192
192
|
execFileSync("node_modules/.bin/tsc", ["--outDir", TMP_EMIT, "--rootDir", ".", "--noEmit", "false"], {
|
|
@@ -196,6 +196,7 @@ try {
|
|
|
196
196
|
mkdirSync(promptsOut, { recursive: true });
|
|
197
197
|
copyFileSync("pi-extensions/lib/acp/prompts/engraving.md", resolve(promptsOut, "engraving.md"));
|
|
198
198
|
const backend = (await import(pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/backend.js")).href)) as any;
|
|
199
|
+
await assertOverlayInSandbox(TMP_EMIT, gateHome);
|
|
199
200
|
|
|
200
201
|
// ----------------------------------------------------------------------
|
|
201
202
|
// CELL 1 — NEW turn: the hook sees the EXACT wire params; undefined keeps them.
|
|
@@ -510,6 +511,7 @@ try {
|
|
|
510
511
|
// non-empty or already gone — fine either way
|
|
511
512
|
}
|
|
512
513
|
rmSync(recordDir, { recursive: true, force: true });
|
|
514
|
+
gateHome.restore();
|
|
513
515
|
}
|
|
514
516
|
|
|
515
517
|
console.log(
|
|
@@ -60,6 +60,7 @@ import type {
|
|
|
60
60
|
} from "@earendil-works/pi-ai";
|
|
61
61
|
import { isContextOverflow, normalizeContext } from "@earendil-works/pi-ai";
|
|
62
62
|
import { calculateContextTokens } from "@earendil-works/pi-coding-agent";
|
|
63
|
+
import { assertOverlayInSandbox, sandboxAcpGateHome } from "./lib/acp-gate-home.ts";
|
|
63
64
|
|
|
64
65
|
const claude = { id: "claude-sonnet-5-5" } as unknown as Model<Api>;
|
|
65
66
|
const cortex = { id: "cortex-auto" } as unknown as Model<Api>;
|
|
@@ -193,8 +194,6 @@ function makeHarness(recordDir: string, scripts: TurnScript[]) {
|
|
|
193
194
|
return newSessionCalls;
|
|
194
195
|
},
|
|
195
196
|
deps: {
|
|
196
|
-
resolveLaunch: () => ({ command: "node", args: ["fake"] }),
|
|
197
|
-
ensureOverlay: () => {},
|
|
198
197
|
spawnChild: () => {
|
|
199
198
|
const c = makeFakeChild();
|
|
200
199
|
children.push(c);
|
|
@@ -297,6 +296,7 @@ function reuseCtx3(a: string, b: string, c: string): TranscriptContext {
|
|
|
297
296
|
const TMP_EMIT = ".tmp-verify/acp-usage-accounting";
|
|
298
297
|
rmSync(TMP_EMIT, { recursive: true, force: true });
|
|
299
298
|
const recordDir = mkdtempSync(resolve(tmpdir(), "acp-usage-acct-"));
|
|
299
|
+
const gateHome = sandboxAcpGateHome("acp-usage-accounting-home-");
|
|
300
300
|
|
|
301
301
|
try {
|
|
302
302
|
execFileSync("node_modules/.bin/tsc", ["--outDir", TMP_EMIT, "--rootDir", ".", "--noEmit", "false"], {
|
|
@@ -306,6 +306,7 @@ try {
|
|
|
306
306
|
mkdirSync(promptsOut, { recursive: true });
|
|
307
307
|
copyFileSync("pi-extensions/lib/acp/prompts/engraving.md", resolve(promptsOut, "engraving.md"));
|
|
308
308
|
const backend = (await import(pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/backend.js")).href)) as any;
|
|
309
|
+
await assertOverlayInSandbox(TMP_EMIT, gateHome);
|
|
309
310
|
|
|
310
311
|
// ----------------------------------------------------------------------
|
|
311
312
|
// CELL 1 — the turn aggregate never reaches pi's per-request fields.
|
|
@@ -1062,6 +1063,7 @@ try {
|
|
|
1062
1063
|
// non-empty or already gone — fine either way
|
|
1063
1064
|
}
|
|
1064
1065
|
rmSync(recordDir, { recursive: true, force: true });
|
|
1066
|
+
gateHome.restore();
|
|
1065
1067
|
}
|
|
1066
1068
|
|
|
1067
1069
|
/** Distinct-but-uninteresting token partition for the cost-focused cells. */
|
|
@@ -322,25 +322,35 @@ exit "\${FAKE_CODEX_EXIT:-0}"
|
|
|
322
322
|
);
|
|
323
323
|
}
|
|
324
324
|
{
|
|
325
|
+
// The classifier's INDETERMINATE domain, as one claim: two unidentifiable paths, both
|
|
326
|
+
// reaching the launcher through the one `indeterminate)` arm. A path that exists and is
|
|
327
|
+
// not a socket must read indeterminate, not stale, so the launcher refuses instead of
|
|
328
|
+
// clobbering it. A symlink that is NOT Codex's exact rendezvous alias must be refused with
|
|
329
|
+
// the product classifier's own reason, which is what pins the WIRING: a launcher that grew
|
|
330
|
+
// its own symlink rule again would refuse there too, but not in those words. Both runs are
|
|
331
|
+
// made before the one assertion, so a regression on that shared arm fails HERE, at this
|
|
332
|
+
// claim, and not at an earlier unnamed cell. The exact alias's accept path is certified on
|
|
333
|
+
// `checkCodexSocketFile` (check-codex-native-push), not here: this gate has no seam to put
|
|
334
|
+
// Codex's socket directory in a sandbox.
|
|
325
335
|
const fileHome = path.join(root, "file-home");
|
|
326
336
|
const fileSock = resolveCodexDefaultSocketPath({ CODEX_HOME: fileHome });
|
|
327
337
|
mkdirSync(path.dirname(fileSock), { recursive: true });
|
|
328
338
|
writeFileSync(fileSock, "");
|
|
329
|
-
const
|
|
330
|
-
ok(
|
|
331
|
-
"a path that exists and is not a socket is indeterminate, not stale — the launcher refuses instead of clobbering something it cannot identify",
|
|
332
|
-
r.status !== 0 && r.out.includes("codex-app-server-socket-indeterminate") && r.args.length === 0,
|
|
333
|
-
);
|
|
334
|
-
}
|
|
335
|
-
{
|
|
339
|
+
const regular = launch([], { CODEX_HOME: fileHome });
|
|
336
340
|
const linkHome = path.join(root, "link-home");
|
|
337
341
|
const linkSock = resolveCodexDefaultSocketPath({ CODEX_HOME: linkHome });
|
|
338
342
|
mkdirSync(path.dirname(linkSock), { recursive: true });
|
|
339
343
|
symlinkSync(path.join(root, "nowhere.sock"), linkSock);
|
|
340
|
-
const
|
|
344
|
+
const link = launch([], { CODEX_HOME: linkHome });
|
|
341
345
|
ok(
|
|
342
|
-
"[QK:CODEX-APP-SERVER-REFUSES-INDETERMINATE-SOCKET] a
|
|
343
|
-
|
|
346
|
+
"[QK:CODEX-APP-SERVER-REFUSES-INDETERMINATE-SOCKET] an unidentifiable path is refused, never launched over: a path that exists and is not a socket, and a symlink that is not Codex's rendezvous alias (refused with the classifier's own reason) — the same classification the delivery rail's socket check uses",
|
|
347
|
+
regular.status !== 0 &&
|
|
348
|
+
regular.out.includes("codex-app-server-socket-indeterminate") &&
|
|
349
|
+
regular.args.length === 0 &&
|
|
350
|
+
link.status !== 0 &&
|
|
351
|
+
link.out.includes("codex-app-server-socket-indeterminate") &&
|
|
352
|
+
link.out.includes("is not Codex's rendezvous alias") &&
|
|
353
|
+
link.args.length === 0,
|
|
344
354
|
);
|
|
345
355
|
}
|
|
346
356
|
{
|
|
@@ -358,17 +368,50 @@ exit "\${FAKE_CODEX_EXIT:-0}"
|
|
|
358
368
|
// Hard Rule 15: an unrecognised reading is the one case where proceeding is unsafe,
|
|
359
369
|
// because every branch above is a decision about whether this launch would clobber a
|
|
360
370
|
// running server. The stimulus is a sandbox `python3` that exits 0 while printing
|
|
361
|
-
// something nobody wrote — the exact shape a silent fall-through needs.
|
|
371
|
+
// something nobody wrote — the exact shape a silent fall-through needs. The launcher runs
|
|
372
|
+
// that connect probe only on an endpoint the classifier verified as a socket, so the cell
|
|
373
|
+
// puts a (dead) socket file there; without one the probe would never be asked.
|
|
374
|
+
const oddHome = path.join(root, "odd-home");
|
|
375
|
+
const oddSock = resolveCodexDefaultSocketPath({ CODEX_HOME: oddHome });
|
|
376
|
+
mkdirSync(path.dirname(oddSock), { recursive: true });
|
|
377
|
+
spawnSync("python3", ["-c", "import socket,sys;s=socket.socket(socket.AF_UNIX);s.bind(sys.argv[1])", oddSock]);
|
|
362
378
|
const oddBin = path.join(root, "odd-probe-bin");
|
|
363
379
|
mkdirSync(oddBin, { recursive: true });
|
|
364
380
|
const oddPython = path.join(oddBin, "python3");
|
|
365
381
|
writeFileSync(oddPython, "#!/usr/bin/env bash\ncat >/dev/null\necho 'unexpected-probe-status'\nexit 0\n");
|
|
366
382
|
chmodSync(oddPython, 0o755);
|
|
367
|
-
const r = launch([], { PATH: `${oddBin}:${bin}:${process.env.PATH ?? ""}` });
|
|
383
|
+
const r = launch([], { CODEX_HOME: oddHome, PATH: `${oddBin}:${bin}:${process.env.PATH ?? ""}` });
|
|
384
|
+
// The second stimulus is the CLASSIFIER answering a word it never gives. `stale` is the
|
|
385
|
+
// discriminating one: it is a valid verdict of the connect probe, so a launcher that passed
|
|
386
|
+
// unknown classifier words through would read it as a dead socket and LAUNCH. A sandbox
|
|
387
|
+
// `node` answers for the classifier's basename only and hands every other invocation to
|
|
388
|
+
// the real Node — a test fixture on this gate's PATH, not a seam the product reads.
|
|
389
|
+
const oddClassifierHome = path.join(root, "odd-classifier-home");
|
|
390
|
+
const oddClassifierBin = path.join(root, "odd-classifier-bin");
|
|
391
|
+
mkdirSync(oddClassifierBin, { recursive: true });
|
|
392
|
+
const oddNode = path.join(oddClassifierBin, "node");
|
|
393
|
+
writeFileSync(
|
|
394
|
+
oddNode,
|
|
395
|
+
"#!/usr/bin/env bash\n" +
|
|
396
|
+
'for a in "$@"; do case "$a" in */codex-socket-classify.js) printf \'stale\'; exit 0 ;; esac; done\n' +
|
|
397
|
+
`exec ${JSON.stringify(process.execPath)} "$@"\n`,
|
|
398
|
+
);
|
|
399
|
+
chmodSync(oddNode, 0o755);
|
|
400
|
+
const c = launch([], {
|
|
401
|
+
CODEX_HOME: oddClassifierHome,
|
|
402
|
+
PATH: `${oddClassifierBin}:${bin}:${process.env.PATH ?? ""}`,
|
|
403
|
+
});
|
|
368
404
|
ok(
|
|
369
405
|
"[QK:CODEX-APP-SERVER-REFUSES-UNRECOGNISED-PROBE] a socket classifier that exits 0 with a reading nobody wrote REFUSES instead of falling through to the exec",
|
|
370
406
|
r.status !== 0 && r.out.includes("codex-app-server-socket-probe-unrecognised") && r.args.length === 0,
|
|
371
407
|
);
|
|
408
|
+
ok(
|
|
409
|
+
`[QK:CODEX-APP-SERVER-REFUSES-UNRECOGNISED-CLASSIFIER] a classifier word the classifier never gives is refused before it can be read as one of the probe's own verdicts — 'stale' would otherwise launch (status ${c.status}, ${c.args.length} vendor args)`,
|
|
410
|
+
c.status !== 0 &&
|
|
411
|
+
c.out.includes("codex-app-server-socket-probe-unrecognised") &&
|
|
412
|
+
c.out.includes("the socket classifier answered\n 'stale'") &&
|
|
413
|
+
c.args.length === 0,
|
|
414
|
+
);
|
|
372
415
|
}
|
|
373
416
|
|
|
374
417
|
// ── 5. refusals that keep this from becoming something it is not ────────────
|