@junghanacs/entwurf 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +85 -190
- package/BASELINE.md +6 -5
- package/CHANGELOG.md +220 -14
- package/CONTRIBUTING.md +1 -1
- package/DELIVERY.md +202 -60
- package/README.md +54 -26
- package/VERIFY.md +37 -7
- package/docs/acp-backend-rail.md +31 -15
- package/docs/external-mcp-host.md +58 -35
- package/docs/fresh-cut-policy.md +3 -2
- package/docs/setup-clean-host.md +110 -17
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +101 -109
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +6 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +326 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +22 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +30 -17
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +7 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +20 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +227 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +50 -15
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +10 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +403 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook-codex.js +325 -0
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +6 -1
- package/mcp/entwurf-bridge/src/index.ts +115 -111
- package/mcp/entwurf-bridge/tsconfig.build.json +1 -0
- package/package.json +14 -5
- package/pi-extensions/entwurf-control.ts +71 -19
- package/pi-extensions/lib/acp/acp-client.ts +3 -3
- package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
- package/pi-extensions/lib/acp/backend.ts +3 -3
- package/pi-extensions/lib/acp/event-mapper.ts +4 -4
- package/pi-extensions/lib/acp/tool-surface.ts +6 -0
- package/pi-extensions/lib/codex-fresh-preflight.ts +363 -0
- package/pi-extensions/lib/compaction-send-guard.ts +80 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +29 -3
- package/pi-extensions/lib/entwurf-peers-render.ts +4 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +6 -7
- package/pi-extensions/lib/entwurf-v2-native-push.ts +35 -18
- package/pi-extensions/lib/entwurf-v2-production.ts +10 -3
- package/pi-extensions/lib/entwurf-v2-surface.ts +7 -4
- package/pi-extensions/lib/meta-mailbox-body.ts +22 -13
- package/pi-extensions/lib/meta-sender-identity.ts +305 -0
- package/pi-extensions/lib/mux-fresh-call.ts +64 -19
- package/pi-extensions/lib/native-push/adapter.ts +21 -24
- package/pi-extensions/lib/native-push/codex-ws-client.ts +506 -0
- package/pi-extensions/lib/native-push/register.ts +7 -9
- package/pi-extensions/meta-bridge-hook-codex.ts +371 -0
- package/run.sh +251 -35
- package/scripts/check-acp-usage-accounting.ts +9 -9
- package/scripts/check-agy-sender-identity.ts +1 -1
- package/scripts/check-codex-birth-hook.ts +264 -0
- package/scripts/check-codex-bridge-identity.ts +179 -0
- package/scripts/check-codex-native-push.ts +386 -0
- package/scripts/check-codex-sender-identity.ts +495 -0
- package/scripts/check-compaction-send-guard.ts +130 -0
- package/scripts/check-copilot-receive-arm.ts +4 -1
- package/scripts/check-entwurf-fact-provider.ts +38 -0
- package/scripts/check-entwurf-peers-surface.ts +13 -1
- package/scripts/check-entwurf-self-address.ts +15 -16
- package/scripts/check-entwurf-v2-contract.ts +4 -3
- package/scripts/check-entwurf-v2-decider.ts +7 -5
- package/scripts/check-entwurf-v2-native-push.ts +35 -7
- package/scripts/check-entwurf-v2-production.ts +203 -11
- package/scripts/check-entwurf-v2-runner.ts +1 -1
- package/scripts/check-entwurf-v2-surface.ts +1 -1
- package/scripts/check-gate-qualification.ts +7 -4
- package/scripts/check-harness-admission-parity.ts +0 -1
- package/scripts/check-install-surface.ts +23 -7
- package/scripts/check-mux-launch-tmux.ts +47 -2
- package/scripts/check-native-push-adapter.ts +20 -16
- package/scripts/check-native-push-register.ts +5 -1
- package/scripts/check-release-gate-outcomes.ts +47 -1
- package/scripts/check-setup-qualification.sh +3 -1
- package/scripts/codex-birth-doctor.sh +276 -0
- package/scripts/codex-birth-install.sh +414 -0
- package/scripts/codex-birth-uninstall.sh +170 -0
- package/scripts/codex-mcp-config.py +435 -0
- package/scripts/codex-statusline-config.py +434 -0
- package/scripts/codex_toml_io.py +532 -0
- package/scripts/lib/codex-fresh-live-protocol.ts +113 -0
- package/scripts/lib/codex-fresh-source-receipts.ts +399 -0
- package/scripts/lib/launch-receipt-windows.ts +46 -0
- package/scripts/lib/tmux-coordinate-row.ts +58 -0
- package/scripts/meta-bridge-fresh-cut.ts +6 -1
- package/scripts/mutants/codex-native.json +838 -0
- package/scripts/mutants/compaction-send-guard.json +103 -0
- package/scripts/mutants/entwurf-peers.json +19 -0
- package/scripts/mutants/mux-fresh-call.json +19 -8
- package/scripts/mutants/omp-fresh.json +6 -4
- package/scripts/mutants/pi-package-ownership.json +26 -0
- package/scripts/mutants/release-gate.json +13 -0
- package/scripts/mutants/v2-surface.json +53 -1
- package/scripts/pi_settings_io.py +3 -1
- package/scripts/raw-async-delivery/README.md +2 -1
- package/scripts/raw-codex-measure/README.md +114 -46
- package/scripts/register-pi-package.py +38 -30
- package/scripts/register-pi-provider.py +3 -2
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-agy-native-push-live.ts +3 -1
- package/scripts/smoke-codex-birth.sh +347 -0
- package/scripts/smoke-codex-config-state.sh +511 -0
- package/scripts/smoke-codex-fresh-live.ts +1186 -0
- package/scripts/smoke-codex-native-push-live.ts +75 -0
- package/scripts/smoke-setup-verdict.sh +123 -10
- package/scripts/smoke-user-scope-citizen.sh +199 -71
- package/scripts/tsconfig.json +1 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { makeProductionEntwurfV2Deps } from "../pi-extensions/lib/entwurf-v2-production.ts";
|
|
6
|
+
import { runEntwurfV2 } from "../pi-extensions/lib/entwurf-v2-runner.ts";
|
|
7
|
+
import { upsertMetaSession } from "../pi-extensions/lib/meta-session.ts";
|
|
8
|
+
import { probeCodexThread } from "../pi-extensions/lib/native-push/codex-ws-client.ts";
|
|
9
|
+
import { skipLive } from "./lib/live-skip.ts";
|
|
10
|
+
|
|
11
|
+
if (process.env.LIVE !== "1") {
|
|
12
|
+
skipLive(
|
|
13
|
+
"smoke-codex-native-push-live",
|
|
14
|
+
"set LIVE=1 + CODEX_LIVE_THREAD_ID to run (sends a real turn to an already-loaded Codex thread).",
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const threadId = process.env.CODEX_LIVE_THREAD_ID?.trim();
|
|
19
|
+
if (!threadId) {
|
|
20
|
+
skipLive(
|
|
21
|
+
"smoke-codex-native-push-live",
|
|
22
|
+
"set CODEX_LIVE_THREAD_ID to a thread loaded by a TUI attached to the default Codex app-server.",
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-codex-live-"));
|
|
27
|
+
process.on("exit", () => fs.rmSync(root, { recursive: true, force: true }));
|
|
28
|
+
const sessionsDir = path.join(root, "meta-sessions");
|
|
29
|
+
const born = upsertMetaSession({
|
|
30
|
+
dir: sessionsDir,
|
|
31
|
+
input: {
|
|
32
|
+
backend: "codex",
|
|
33
|
+
nativeSessionId: threadId,
|
|
34
|
+
cwd: process.cwd(),
|
|
35
|
+
model: process.env.CODEX_LIVE_MODEL?.trim() || null,
|
|
36
|
+
transcriptPath: path.join(root, "codex-live-transcript.jsonl"),
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const before = await probeCodexThread(threadId);
|
|
41
|
+
assert.equal(before.status, "alive", `[QK:CODEX-LIVE-PROBE] ${before.status === "alive" ? "" : before.reason}`);
|
|
42
|
+
|
|
43
|
+
const token = `CODEX-NATIVE-LIVE-${Date.now()}`;
|
|
44
|
+
const result = await runEntwurfV2(
|
|
45
|
+
{
|
|
46
|
+
target: born.record.gardenId,
|
|
47
|
+
intent: "fire-and-forget",
|
|
48
|
+
wantsReply: true,
|
|
49
|
+
message: `Entwurf native-push live acceptance. Reply with exactly ${token}`,
|
|
50
|
+
},
|
|
51
|
+
makeProductionEntwurfV2Deps({
|
|
52
|
+
senderProvider: () => ({
|
|
53
|
+
sessionId: born.record.gardenId,
|
|
54
|
+
agentId: "meta-session/codex",
|
|
55
|
+
cwd: process.cwd(),
|
|
56
|
+
timestamp: new Date().toISOString(),
|
|
57
|
+
origin: "meta-session",
|
|
58
|
+
replyable: true,
|
|
59
|
+
}),
|
|
60
|
+
sessionsDir,
|
|
61
|
+
mailboxDir: path.join(root, "mailbox"),
|
|
62
|
+
controlSocketDir: path.join(root, "control"),
|
|
63
|
+
lockDir: path.join(root, "locks"),
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
assert.equal(result.kind, "executed", `[QK:CODEX-LIVE-PUBLIC-DISPATCH] ${JSON.stringify(result)}`);
|
|
67
|
+
assert.equal(result.kind === "executed" ? result.transport : null, "native-push");
|
|
68
|
+
const after = await probeCodexThread(threadId);
|
|
69
|
+
assert.equal(after.status, "alive", "Codex thread remains loaded after the one-shot queue acceptance");
|
|
70
|
+
|
|
71
|
+
console.log(`smoke-codex-native-push-live: delivered ${token}`);
|
|
72
|
+
console.log(` gardenId: ${born.record.gardenId}`);
|
|
73
|
+
console.log(` threadId: ${threadId}`);
|
|
74
|
+
console.log(` socket: ${before.status === "alive" ? before.route.socketPath : "unreachable"}`);
|
|
75
|
+
console.log(" verify the exact token appears in that visible Codex TUI; the delivery receipt is not the model result");
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
# core and later components still attempted
|
|
17
17
|
# S-8 omp present → the four omp units compose (birth/MCP/tools.xdev
|
|
18
18
|
# setting/receiver), states exist, second run idempotent
|
|
19
|
+
# S-8c codex present → birth publishes but stays a named FAIL until the vendor trust
|
|
20
|
+
# receipt exists; all three atoms compose twice, preserve foreign
|
|
21
|
+
# config, stay idempotent, and go green on the trusted rerun
|
|
19
22
|
# S-5 installed mode → the installed-vs-source branch is a NAMED verdict
|
|
20
23
|
# printed before anything else, and never reaches
|
|
21
24
|
# the source pnpm bootstrap
|
|
@@ -27,12 +30,15 @@
|
|
|
27
30
|
# below-floor and false-green shapes as living end-to-end evidence.
|
|
28
31
|
#
|
|
29
32
|
# Deterministic: no model, no network, no cost. Presence probes are pinned via
|
|
30
|
-
# PI_BIN / CLAUDE_BIN / AGY_BIN / COPILOT_BIN / OMP_BIN (the same hermetic seam
|
|
33
|
+
# PI_BIN / CLAUDE_BIN / AGY_BIN / COPILOT_BIN / OMP_BIN / CODEX_BIN (the same hermetic seam
|
|
31
34
|
# smoke-agy-install-state
|
|
32
35
|
# uses), and every write root is sandboxed: HOME, XDG roots, the pi agent dir,
|
|
33
|
-
# and the dev-bin dir.
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
+
# and the dev-bin dir. Source-mode cells put a strict fixture `pnpm` first on PATH:
|
|
37
|
+
# it accepts the no-op `install --frozen-lockfile` and the source-owned `run build-bridge`
|
|
38
|
+
# emit only. Package-manager behavior belongs to the package gates. This is load-bearing
|
|
39
|
+
# under mutation qualification, whose snapshot shares node_modules read-only — a real pnpm
|
|
40
|
+
# follows that symlink and attempts to rewrite the origin checkout. Any dist this gate had
|
|
41
|
+
# to build is removed on exit, restoring the snapshot's pre-gate surface.
|
|
36
42
|
set -euo pipefail
|
|
37
43
|
|
|
38
44
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
@@ -51,7 +57,13 @@ esac
|
|
|
51
57
|
export PYTHONDONTWRITEBYTECODE=1
|
|
52
58
|
|
|
53
59
|
SB="$(mktemp -d -t entwurf-setup-verdict.XXXXXX)"
|
|
54
|
-
|
|
60
|
+
DIST_ROOT="$REPO_DIR/mcp/entwurf-bridge/dist"
|
|
61
|
+
DIST_PREEXISTED=0
|
|
62
|
+
[ -d "$DIST_ROOT" ] && DIST_PREEXISTED=1
|
|
63
|
+
cleanup() {
|
|
64
|
+
[ "$DIST_PREEXISTED" -eq 1 ] || rm -rf "$DIST_ROOT"
|
|
65
|
+
rm -rf "$SB"
|
|
66
|
+
}
|
|
55
67
|
trap cleanup EXIT
|
|
56
68
|
|
|
57
69
|
export HOME="$SB/home"
|
|
@@ -62,12 +74,27 @@ export XDG_CONFIG_HOME="$SB/home/.config"
|
|
|
62
74
|
export PI_CODING_AGENT_DIR="$SB/home/.pi/agent"
|
|
63
75
|
export ENTWURF_DEV_BIN_DIR="$SB/bin"
|
|
64
76
|
export PATH="$SB/bin:$PATH"
|
|
65
|
-
mkdir -p "$HOME" "$PI_CODING_AGENT_DIR" "$SB/bin" "$SB/harness"
|
|
77
|
+
mkdir -p "$HOME" "$PI_CODING_AGENT_DIR" "$SB/bin" "$SB/harness" "$SB/bootstrap-bin"
|
|
78
|
+
cat > "$SB/bootstrap-bin/pnpm" <<'SH'
|
|
79
|
+
#!/bin/sh
|
|
80
|
+
if [ "$#" -eq 2 ] && [ "$1" = install ] && [ "$2" = --frozen-lockfile ]; then
|
|
81
|
+
exit 0
|
|
82
|
+
fi
|
|
83
|
+
if [ "$#" -eq 2 ] && [ "$1" = run ] && [ "$2" = build-bridge ]; then
|
|
84
|
+
# The qualification snapshot is process-exclusive. Invoke the source-owned emit
|
|
85
|
+
# directly so the production lock wrapper cannot leave its ignored parent dir.
|
|
86
|
+
exec bash scripts/build-bridge.sh
|
|
87
|
+
fi
|
|
88
|
+
echo "fixture pnpm accepts only: install --frozen-lockfile | run build-bridge" >&2
|
|
89
|
+
exit 64
|
|
90
|
+
SH
|
|
91
|
+
chmod +x "$SB/bootstrap-bin/pnpm"
|
|
92
|
+
export PATH="$SB/bootstrap-bin:$PATH"
|
|
66
93
|
|
|
67
94
|
# Presence pins: default every harness to a definitely-absent path; each cell
|
|
68
95
|
# re-pins what it needs. Production leaves these unset.
|
|
69
96
|
ABSENT="$SB/harness/definitely-absent"
|
|
70
|
-
export PI_BIN="$ABSENT" CLAUDE_BIN="$ABSENT" AGY_BIN="$ABSENT" COPILOT_BIN="$ABSENT" OMP_BIN="$ABSENT"
|
|
97
|
+
export PI_BIN="$ABSENT" CLAUDE_BIN="$ABSENT" AGY_BIN="$ABSENT" COPILOT_BIN="$ABSENT" OMP_BIN="$ABSENT" CODEX_BIN="$ABSENT"
|
|
71
98
|
|
|
72
99
|
PASS=0
|
|
73
100
|
ok() { PASS=$((PASS + 1)); printf ' ok %s\n' "$*"; }
|
|
@@ -94,7 +121,8 @@ echo "[smoke-setup-verdict] S-1 all-harness-absent setup"
|
|
|
94
121
|
PROJ1="$SB/proj1"; mkdir -p "$PROJ1"
|
|
95
122
|
seed_auth
|
|
96
123
|
set +e; OUT="$(bash "$REPO_DIR/run.sh" setup "$PROJ1" 2>&1)"; RC=$?; set -e
|
|
97
|
-
want "S-1: all-absent setup exits 0
|
|
124
|
+
want "S-1: all-absent setup exits 0 with a Codex SKIP" \
|
|
125
|
+
"[ '$RC' -eq 0 ] && printf '%s' \"\$OUT\" | grep -q 'codex: SKIP'"
|
|
98
126
|
want "S-1: mode is a named source-checkout branch, printed first" \
|
|
99
127
|
"printf '%s' \"\$OUT\" | head -n 1 | grep -q 'mode: source checkout'"
|
|
100
128
|
want "S-1: pi absent is an explicit zero-state SKIP" "printf '%s' \"\$OUT\" | grep -q 'pi: SKIP'"
|
|
@@ -112,6 +140,7 @@ want "S-1: no user-scope pi settings were created" "[ ! -e '$PI_CODING_AGENT_DIR
|
|
|
112
140
|
want "S-1: no agy config was created" "[ ! -e '$HOME/.gemini' ]"
|
|
113
141
|
want "S-1: no Copilot config/units were created" "[ ! -e '$HOME/.copilot' ]"
|
|
114
142
|
want "S-1: no OMP config/units were created" "[ ! -e '$HOME/.omp' ]"
|
|
143
|
+
want "S-1: no Codex config/units were created" "[ ! -e '$HOME/.codex' ]"
|
|
115
144
|
want_auth_untouched "S-1"
|
|
116
145
|
|
|
117
146
|
# ── S-2: pi resolvable but BELOW floor → detected FAIL, never SKIP, no writes ──
|
|
@@ -287,6 +316,90 @@ rm -rf "$HOME/.omp" "$XDG_DATA_HOME/entwurf/omp-bridge" "$XDG_DATA_HOME/entwurf/
|
|
|
287
316
|
"$XDG_DATA_HOME/entwurf/omp-config" "$XDG_DATA_HOME/entwurf/omp-receive" \
|
|
288
317
|
"$XDG_DATA_HOME/entwurf/meta-bridge-omp" "$XDG_DATA_HOME/entwurf/omp-receive"
|
|
289
318
|
|
|
319
|
+
# ── S-8c: codex PRESENT → setup publishes birth, and stays NON-GREEN until the vendor agrees ──
|
|
320
|
+
# The birth unit's paths are the operator's own, so setup installs it here for real, inside the
|
|
321
|
+
# fixture's HOME/CODEX_HOME/XDG. What setup CANNOT do is answer the vendor's one-time trust
|
|
322
|
+
# prompt: that is the operator's, in their own visible Codex. So the first run writes every
|
|
323
|
+
# byte and is honestly non-green, the two config atoms compose anyway, and the run after a
|
|
324
|
+
# vendor receipt appears is green and byte-idempotent. Nothing here ever writes [hooks.state] —
|
|
325
|
+
# the receipt is forged by the FIXTURE to stand in for the vendor, never by setup.
|
|
326
|
+
echo "[smoke-setup-verdict] S-8c codex present — incomplete three-unit composition"
|
|
327
|
+
PROJ8C="$SB/proj8c"; mkdir -p "$PROJ8C"
|
|
328
|
+
FAKE_CODEX="$SB/harness/codex-fake"; mkdir -p "$FAKE_CODEX"
|
|
329
|
+
printf '#!/usr/bin/env bash\necho "codex-cli 0.153.4"\n' > "$FAKE_CODEX/codex"
|
|
330
|
+
chmod +x "$FAKE_CODEX/codex"
|
|
331
|
+
CODEX_HOME_SANDBOX="$HOME/.codex"
|
|
332
|
+
mkdir -p "$CODEX_HOME_SANDBOX"
|
|
333
|
+
cat > "$CODEX_HOME_SANDBOX/config.toml" <<'TOML'
|
|
334
|
+
model = "operator-model"
|
|
335
|
+
|
|
336
|
+
[projects."/operator/keep"]
|
|
337
|
+
trust_level = "trusted"
|
|
338
|
+
|
|
339
|
+
[mcp_servers.operator-owned]
|
|
340
|
+
command = "/operator/bin/server"
|
|
341
|
+
args = ["--keep"]
|
|
342
|
+
|
|
343
|
+
[tui]
|
|
344
|
+
status_line = ["model-with-reasoning"]
|
|
345
|
+
TOML
|
|
346
|
+
REAL_BASH="$(command -v bash)"
|
|
347
|
+
CODEX_BIRTH_UNIT_ROOT="$XDG_DATA_HOME/entwurf/codex-birth"
|
|
348
|
+
seed_auth
|
|
349
|
+
set +e
|
|
350
|
+
OUT="$(CODEX_BIN="$FAKE_CODEX/codex" CODEX_HOME="$CODEX_HOME_SANDBOX" PATH="$FAKE_CODEX:$PATH" "$REAL_BASH" "$REPO_DIR/run.sh" setup "$PROJ8C" 2>&1)"
|
|
351
|
+
RC=$?
|
|
352
|
+
set -e
|
|
353
|
+
want "[QK:CODEX-SETUP-NEVER-PRESEEDS-TRUST] S-8c: setup wrote NO vendor trust receipt of its own" \
|
|
354
|
+
"! grep -q 'hooks.state' '$CODEX_HOME_SANDBOX/config.toml' && ! grep -q 'trusted_hash' '$CODEX_HOME_SANDBOX/config.toml'"
|
|
355
|
+
want "S-8c: detected-incomplete Codex is a named nonzero result, never an absent SKIP [QK:CODEX-SETUP-BIRTH-COSMETIC-PASS]" \
|
|
356
|
+
"[ '$RC' -ne 0 ] && printf '%s' \"\$OUT\" | grep -q 'codex-birth: FAIL' && ! printf '%s' \"\$OUT\" | grep -q 'codex: SKIP'"
|
|
357
|
+
want "S-8c: both user-owned Codex atoms still compose independently as PASS [QK:CODEX-SETUP-BIRTH-INDEPENDENT]" \
|
|
358
|
+
"printf '%s' \"\$OUT\" | grep -q 'codex-mcp: PASS' && printf '%s' \"\$OUT\" | grep -q 'codex-statusline: PASS'"
|
|
359
|
+
want "S-8c: summary is NON-GREEN and attributes it to the untrusted birth unit" \
|
|
360
|
+
"printf '%s' \"\$OUT\" | grep -q 'NON-GREEN (FAIL: codex-birth)' && ! printf '%s' \"\$OUT\" | grep -q 'result: green'"
|
|
361
|
+
want "S-8c: the reason names the operator's one-time answer, not a repair setup could have made" \
|
|
362
|
+
"printf '%s' \"\$OUT\" | grep -q 'no trust receipt' && printf '%s' \"\$OUT\" | grep -q 'Trust all and continue'"
|
|
363
|
+
want "S-8c: setup PUBLISHED the birth unit — declaration, launcher closure and ownership state" \
|
|
364
|
+
"[ -f '$CODEX_HOME_SANDBOX/hooks.json' ] && [ -x '$CODEX_BIRTH_UNIT_ROOT/helper/codex-birth-launch.sh' ] && [ -f '$CODEX_BIRTH_UNIT_ROOT/install-state.json' ]"
|
|
365
|
+
CODEX_MCP_STATE="$XDG_DATA_HOME/entwurf/codex-mcp/install-state.json"
|
|
366
|
+
CODEX_STATUSLINE_STATE="$XDG_DATA_HOME/entwurf/codex-statusline/install-state.json"
|
|
367
|
+
want "S-8c: both user atoms wrote their package-owned install states inside the sandbox" \
|
|
368
|
+
"[ -f '$CODEX_MCP_STATE' ] && [ -f '$CODEX_STATUSLINE_STATE' ] && grep -q '\"atom\": \"codex-mcp\"' '$CODEX_MCP_STATE' && grep -q '\"atom\": \"codex-statusline\"' '$CODEX_STATUSLINE_STATE'"
|
|
369
|
+
want "S-8c: MCP and visible-identity atoms landed in the sandbox Codex config" \
|
|
370
|
+
"grep -q '\\[mcp_servers\\.entwurf-bridge\\]' '$CODEX_HOME_SANDBOX/config.toml' && grep -q 'status_line = \\[\"thread-title\", \"model-with-reasoning\"\\]' '$CODEX_HOME_SANDBOX/config.toml'"
|
|
371
|
+
want "S-8c: unrelated operator config survives both atom writers" \
|
|
372
|
+
"grep -q 'model = \"operator-model\"' '$CODEX_HOME_SANDBOX/config.toml' && grep -q '\\[mcp_servers\\.operator-owned\\]' '$CODEX_HOME_SANDBOX/config.toml' && grep -q 'command = \"/operator/bin/server\"' '$CODEX_HOME_SANDBOX/config.toml'"
|
|
373
|
+
CODEX_CONFIG_AFTER="$(sha256sum "$CODEX_HOME_SANDBOX/config.toml" | cut -d' ' -f1)"
|
|
374
|
+
CODEX_MCP_STATE_AFTER="$(sha256sum "$CODEX_MCP_STATE" | cut -d' ' -f1)"
|
|
375
|
+
CODEX_STATUSLINE_STATE_AFTER="$(sha256sum "$CODEX_STATUSLINE_STATE" | cut -d' ' -f1)"
|
|
376
|
+
set +e
|
|
377
|
+
OUT2="$(CODEX_BIN="$FAKE_CODEX/codex" CODEX_HOME="$CODEX_HOME_SANDBOX" PATH="$FAKE_CODEX:$PATH" "$REAL_BASH" "$REPO_DIR/run.sh" setup "$PROJ8C" 2>&1)"
|
|
378
|
+
RC2=$?
|
|
379
|
+
set -e
|
|
380
|
+
want "S-8c: second setup still reports the untrusted birth FAIL and both user atoms PASS" \
|
|
381
|
+
"[ '$RC2' -ne 0 ] && printf '%s' \"\$OUT2\" | grep -q 'codex-birth: FAIL' && printf '%s' \"\$OUT2\" | grep -q 'codex-mcp: PASS' && printf '%s' \"\$OUT2\" | grep -q 'codex-statusline: PASS'"
|
|
382
|
+
want "S-8c: second setup is byte-idempotent for config and both ownership receipts" \
|
|
383
|
+
"[ \"\$(sha256sum '$CODEX_HOME_SANDBOX/config.toml' | cut -d' ' -f1)\" = '$CODEX_CONFIG_AFTER' ] && [ \"\$(sha256sum '$CODEX_MCP_STATE' | cut -d' ' -f1)\" = '$CODEX_MCP_STATE_AFTER' ] && [ \"\$(sha256sum '$CODEX_STATUSLINE_STATE' | cut -d' ' -f1)\" = '$CODEX_STATUSLINE_STATE_AFTER' ]"
|
|
384
|
+
want "S-8c: second setup rechecks rather than faking the missing receipt" \
|
|
385
|
+
"! printf '%s' \"\$OUT2\" | grep -q 'codex-birth: PASS' && ! grep -q 'trusted_hash' '$CODEX_HOME_SANDBOX/config.toml'"
|
|
386
|
+
|
|
387
|
+
# The vendor answers. Written by the FIXTURE, exactly as the vendor writes it after one
|
|
388
|
+
# 'Trust all': the receipt keyed to this declaration, carrying its own hash.
|
|
389
|
+
printf '\n[hooks.state."%s/hooks.json:session_start:0:0"]\ntrusted_hash = "sha256:%s"\n' \
|
|
390
|
+
"$CODEX_HOME_SANDBOX" "$(printf '4%.0s' $(seq 64))" >> "$CODEX_HOME_SANDBOX/config.toml"
|
|
391
|
+
CODEX_CONFIG_TRUSTED="$(sha256sum "$CODEX_HOME_SANDBOX/config.toml" | cut -d' ' -f1)"
|
|
392
|
+
set +e
|
|
393
|
+
OUT3="$(CODEX_BIN="$FAKE_CODEX/codex" CODEX_HOME="$CODEX_HOME_SANDBOX" PATH="$FAKE_CODEX:$PATH" "$REAL_BASH" "$REPO_DIR/run.sh" setup "$PROJ8C" 2>&1)"
|
|
394
|
+
RC3=$?
|
|
395
|
+
set -e
|
|
396
|
+
want "[QK:CODEX-SETUP-TRUSTED-RERUN-GREEN] S-8c: once the vendor receipt exists the same setup is GREEN" \
|
|
397
|
+
"[ '$RC3' -eq 0 ] && printf '%s' \"\$OUT3\" | grep -q 'codex-birth: PASS' && printf '%s' \"\$OUT3\" | grep -q 'result: green'"
|
|
398
|
+
want "S-8c: the trusted rerun is byte-idempotent — it neither rewrote the config nor touched the receipt" \
|
|
399
|
+
"[ \"\$(sha256sum '$CODEX_HOME_SANDBOX/config.toml' | cut -d' ' -f1)\" = '$CODEX_CONFIG_TRUSTED' ]"
|
|
400
|
+
want_auth_untouched "S-8c"
|
|
401
|
+
rm -rf "$HOME/.codex" "$XDG_DATA_HOME/entwurf/codex-mcp" "$XDG_DATA_HOME/entwurf/codex-statusline" "$CODEX_BIRTH_UNIT_ROOT"
|
|
402
|
+
|
|
290
403
|
# ── S-9: rail certification is a SEPARATE axis from install success (#78 D1) ──
|
|
291
404
|
# The 0.20.0 macOS lane opened the installers to Darwin (meta-bridge-install.sh's
|
|
292
405
|
# platform gate became its own python3/node dependency instead of the platform
|
|
@@ -379,8 +492,8 @@ PROJ9C="$SB/proj9c"; mkdir -p "$PROJ9C"
|
|
|
379
492
|
seed_auth
|
|
380
493
|
set +e; OUT="$(PATH="$FAKE_UNAME:$PATH" bash "$REPO_DIR/run.sh" setup "$PROJ9C" 2>&1)"; RC=$?; set -e
|
|
381
494
|
want "S-9c: a harness-free uncertified host still exits 0" "[ '$RC' -eq 0 ]"
|
|
382
|
-
want "S-9c: all
|
|
383
|
-
"printf '%s' \"\$OUT\" | grep -q 'pi: SKIP' && printf '%s' \"\$OUT\" | grep -q 'claude: SKIP' && printf '%s' \"\$OUT\" | grep -q 'agy: SKIP' && printf '%s' \"\$OUT\" | grep -q 'copilot: SKIP' && printf '%s' \"\$OUT\" | grep -q 'omp: SKIP'"
|
|
495
|
+
want "S-9c: all six harness probes are still zero-state SKIPs" \
|
|
496
|
+
"printf '%s' \"\$OUT\" | grep -q 'pi: SKIP' && printf '%s' \"\$OUT\" | grep -q 'claude: SKIP' && printf '%s' \"\$OUT\" | grep -q 'agy: SKIP' && printf '%s' \"\$OUT\" | grep -q 'copilot: SKIP' && printf '%s' \"\$OUT\" | grep -q 'omp: SKIP' && printf '%s' \"\$OUT\" | grep -q 'codex: SKIP'"
|
|
384
497
|
want "S-9c: core still PASSes and the computed summary is still green" \
|
|
385
498
|
"printf '%s' \"\$OUT\" | grep -q 'core: PASS' && printf '%s' \"\$OUT\" | grep -q 'result: green (computed from the component outcomes above)'"
|
|
386
499
|
want "S-9c: an absent harness is never given a rail verdict it did not earn" \
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# smoke-user-scope-citizen — deterministic gate for pi packages[] citizen
|
|
3
|
-
# registration/removal (register-pi-package.py). Offline/hermetic:
|
|
4
|
-
#
|
|
3
|
+
# registration/removal (register-pi-package.py). Offline/hermetic: fake settings
|
|
4
|
+
# files and fake REPO_DIRs under $TMP, sandboxed HOME/PI_CODING_AGENT_DIR/XDG,
|
|
5
|
+
# no network, nothing of the operator's ~/.pi read or written.
|
|
5
6
|
#
|
|
6
7
|
# Guards the wiring that dropped when `pi install` was removed from setup
|
|
7
8
|
# (2026-07-03: `--entwurf-control` / `--emacs-agent-socket` unknown in a foreign
|
|
@@ -10,10 +11,14 @@
|
|
|
10
11
|
# in another repo. This gate is that missing tripwire: if the registration ever
|
|
11
12
|
# regresses, `pnpm run check:full` goes red.
|
|
12
13
|
#
|
|
13
|
-
# Cells 11–13 close the second omission (#53 B): every case
|
|
14
|
-
# settings file with ABSOLUTE entries, so the portable, settings-relative form
|
|
15
|
-
#
|
|
16
|
-
#
|
|
14
|
+
# Cells 11–13 close the second omission (#53 B): every case before them drove a fake
|
|
15
|
+
# settings file with ABSOLUTE entries, so the portable, settings-relative form was
|
|
16
|
+
# never registered against — and `setup` duplicated and restyled a tracked file for
|
|
17
|
+
# four cuts without a single gate seeing it.
|
|
18
|
+
#
|
|
19
|
+
# Cell 15 closes the third (#110), and it is the one cell here that SPAWNS pi: the
|
|
20
|
+
# defect is not a byte, it is which of two checkouts wins `--entwurf-control`, and
|
|
21
|
+
# only the real loader can say. It uses the pinned devDep pi, never PATH.
|
|
17
22
|
set -euo pipefail
|
|
18
23
|
export PYTHONDONTWRITEBYTECODE=1 # snapshot purity: no ignored scripts/__pycache__ writes under qualification
|
|
19
24
|
|
|
@@ -193,69 +198,64 @@ assert d['defaultProvider']=='openai-codex', 'run.sh remove-user-scope dropped a
|
|
|
193
198
|
# idempotent: a second remove-user-scope is a clean no-op (no crash on absent entry)
|
|
194
199
|
if XDG_DATA_HOME="$TMP/xdg" PI_CODING_AGENT_DIR="$AGENT_DIR" bash "$RUN" remove-user-scope >/dev/null 2>&1; then ok "run.sh remove-user-scope is idempotent (no-op second run)"; else bad "run.sh remove-user-scope second run crashed"; fi
|
|
195
200
|
|
|
196
|
-
# ── 11–13. #53 B:
|
|
201
|
+
# ── 11–13. #53 B: a settings-relative self entry is source, never install state
|
|
197
202
|
# The cell whose ABSENCE let this ship. Every case above drove a fake settings file
|
|
198
|
-
# with
|
|
199
|
-
#
|
|
200
|
-
# against. `setup`
|
|
201
|
-
#
|
|
202
|
-
#
|
|
203
|
+
# with ABSOLUTE entries, so the portable, settings-relative form — the one shape
|
|
204
|
+
# register never WRITES, and must therefore never duplicate, absolutize or restyle —
|
|
205
|
+
# was never registered against. `setup` appended the absolute path BESIDE it and
|
|
206
|
+
# rewrote the file at indent=2, and `pnpm check` died at step 1 reading "format
|
|
207
|
+
# error" instead of "install wrote this".
|
|
203
208
|
#
|
|
204
|
-
# The seed is
|
|
205
|
-
# `".."
|
|
206
|
-
#
|
|
207
|
-
#
|
|
209
|
+
# The seed is a FIXTURE. It used to be this repo's OWN committed `.pi/settings.json`,
|
|
210
|
+
# because the repo shipped `packages: [".."]`; #110 retired that entry (entwurf's own
|
|
211
|
+
# checkout self-registers through user scope alone, so a second checkout can no
|
|
212
|
+
# longer register a rival copy of pi-extensions). A witness that depends on us still
|
|
213
|
+
# SHIPPING the shape it tests dies with the shape — and the shape it tests did not
|
|
214
|
+
# die. An operator may hand-write a settings-relative entry, and
|
|
215
|
+
# `is_settings_relative_self` is also what stops an unrelated relative package
|
|
216
|
+
# (`../../repos/gh/andenken`) from being read as ours. Dropping the fixture to a
|
|
217
|
+
# literal here makes the reason honest: the predicate is the subject, not our bytes.
|
|
208
218
|
CLONE="$TMP/selfclone"; mkdir -p "$CLONE/.pi"
|
|
209
219
|
CS="$CLONE/.pi/settings.json"
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
220
|
+
# Tab-indented with a compact array on purpose: cell 13 pins that a genuine rewrite
|
|
221
|
+
# preserves the file's own indent unit, and 11b below pins that this file is not
|
|
222
|
+
# rewritten at all. A seed written in the writer's default style could not tell the
|
|
223
|
+
# two apart.
|
|
224
|
+
printf '{\n\t"skills": ["../.claude/skills"],\n\t"packages": [".."]\n}\n' > "$CS"
|
|
225
|
+
BEFORE="$(sha256sum "$CS" | cut -d' ' -f1)"; MT_B="$(stat -c %Y "$CS")"; sleep 1
|
|
226
|
+
OUT_SELF="$(python3 "$REG" "$CS" "$CLONE")"
|
|
227
|
+
AFTER="$(sha256sum "$CS" | cut -d' ' -f1)"; MT_A="$(stat -c %Y "$CS")"
|
|
228
|
+
if printf '%s' "$OUT_SELF" | grep -q 'no-op'; then
|
|
229
|
+
ok "11 register against a settings-relative self entry is a no-op"
|
|
230
|
+
else
|
|
231
|
+
bad "11 register duplicated/absolutized the portable '..' entry" "$OUT_SELF"
|
|
215
232
|
fi
|
|
216
|
-
if [
|
|
217
|
-
|
|
218
|
-
elif ! python3 -c "
|
|
219
|
-
import json,sys
|
|
220
|
-
p=json.load(open(sys.argv[1])).get('packages')
|
|
221
|
-
sys.exit(0 if p==['..'] else 1)" "$CS"; then
|
|
222
|
-
bad "11 SETUP MISS: the seed ($seed_src) is not the committed portable form (packages != ['..']) — product verdict withheld"
|
|
233
|
+
if [ "$BEFORE" = "$AFTER" ] && [ "$MT_B" = "$MT_A" ]; then
|
|
234
|
+
ok "11b the formatter-governed bytes are UNCHANGED (sha256 + mtime)"
|
|
223
235
|
else
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
bad "11c --remove edited the repo's committed settings source" "$OUT_RM"
|
|
248
|
-
fi
|
|
249
|
-
if printf '%s' "$OUT_RM" | grep -q 'kept 1 settings-relative'; then
|
|
250
|
-
ok "11d the inverse REPORTS what it deliberately left behind (never a silent partial uninstall)"
|
|
251
|
-
else
|
|
252
|
-
bad "11d --remove left the entry without saying so" "$OUT_RM"
|
|
253
|
-
fi
|
|
254
|
-
if printf '%s' "$OUT_DRY" | grep -q 'no entwurf packages\[\] entry to remove'; then
|
|
255
|
-
ok "11e --dry-run agrees with remove (one predicate, no over-report)"
|
|
256
|
-
else
|
|
257
|
-
bad "11e --dry-run disagreed with what remove actually does" "$OUT_DRY"
|
|
258
|
-
fi
|
|
236
|
+
bad "11b register rewrote a settings file it only recognizes (sha $BEFORE -> $AFTER, mtime $MT_B -> $MT_A)"
|
|
237
|
+
fi
|
|
238
|
+
# The inverse direction of the same asymmetry: the shared matcher RECOGNIZES `".."`,
|
|
239
|
+
# so an uninstall that deleted it would edit source it never wrote — the same defect
|
|
240
|
+
# pointed the other way. remove leaves it and says so; dry-run agrees, because both
|
|
241
|
+
# ask one predicate.
|
|
242
|
+
BEFORE_R="$(sha256sum "$CS" | cut -d' ' -f1)"
|
|
243
|
+
OUT_RM="$(python3 "$REG" "$CS" "$CLONE" --remove)"
|
|
244
|
+
OUT_DRY="$(python3 "$REG" "$CS" "$CLONE" --remove --dry-run)"
|
|
245
|
+
if [ "$BEFORE_R" = "$(sha256sum "$CS" | cut -d' ' -f1)" ]; then
|
|
246
|
+
ok "11c --remove does NOT delete a settings-relative self entry (bytes unchanged)"
|
|
247
|
+
else
|
|
248
|
+
bad "11c --remove edited settings source it never authored" "$OUT_RM"
|
|
249
|
+
fi
|
|
250
|
+
if printf '%s' "$OUT_RM" | grep -q 'kept 1 settings-relative'; then
|
|
251
|
+
ok "11d the inverse REPORTS what it deliberately left behind (never a silent partial uninstall)"
|
|
252
|
+
else
|
|
253
|
+
bad "11d --remove left the entry without saying so" "$OUT_RM"
|
|
254
|
+
fi
|
|
255
|
+
if printf '%s' "$OUT_DRY" | grep -q 'no entwurf packages\[\] entry to remove'; then
|
|
256
|
+
ok "11e --dry-run agrees with remove (one predicate, no over-report)"
|
|
257
|
+
else
|
|
258
|
+
bad "11e --dry-run disagreed with what remove actually does" "$OUT_DRY"
|
|
259
259
|
fi
|
|
260
260
|
|
|
261
261
|
# 12. the state a pre-fix `setup` already left on real hosts: the portable entry AND
|
|
@@ -291,12 +291,20 @@ if grep -q $'^\t"packages"' "$S"; then ok "13 a rewrite preserves the file's tab
|
|
|
291
291
|
# indent=2 — semantically a no-op, byte-wise a RED `pnpm check` diagnosed as "formatting".
|
|
292
292
|
# A per-writer cell can never see that; only the real drive can. So this one runs
|
|
293
293
|
# `run.sh install <checkout>` for real and demands sha256 + mtime invariance.
|
|
294
|
-
#
|
|
295
294
|
# The checkout is a stand-in: run.sh resolves its OWN symlinks to find REPO_DIR, so
|
|
296
295
|
# run.sh is COPIED (a link would point the drive back at the operator's real repo) while
|
|
297
296
|
# the trees it only reads are linked. That makes REPO_DIR == the stand-in, which is the
|
|
298
|
-
# shape that matters —
|
|
299
|
-
#
|
|
297
|
+
# shape that matters — the drive installs entwurf INTO entwurf's own checkout, exactly
|
|
298
|
+
# as `./run.sh setup` does from a dev clone (PROJECT_DIR_DEFAULT=$(pwd)).
|
|
299
|
+
#
|
|
300
|
+
# #110 raised what this cell demands. "install is a no-op here" used to hold because
|
|
301
|
+
# the committed `packages: [".."]` already satisfied the writer. That entry is gone,
|
|
302
|
+
# so the writer must instead RECOGNIZE that the project it is installing into IS this
|
|
303
|
+
# root and skip project scope entirely — user scope already registers it, and a second
|
|
304
|
+
# entry naming a different checkout is what made pi load two rival copies of
|
|
305
|
+
# pi-extensions/entwurf-control.ts. The invariant, tested end to end below: entwurf's
|
|
306
|
+
# own checkout carries no project-scope self-registration, in tracked bytes OR in
|
|
307
|
+
# bytes a writer produced.
|
|
300
308
|
CK="$TMP/checkout"
|
|
301
309
|
mkdir -p "$CK/.pi"
|
|
302
310
|
cp "$REPO/run.sh" "$CK/run.sh"
|
|
@@ -317,8 +325,8 @@ if [ ! -s "$CKS" ]; then
|
|
|
317
325
|
elif ! python3 -c "
|
|
318
326
|
import json,sys
|
|
319
327
|
d=json.load(open(sys.argv[1]))
|
|
320
|
-
sys.exit(0 if d.get('packages')
|
|
321
|
-
bad "14 SETUP MISS: the seed ($ck_seed) is not the committed
|
|
328
|
+
sys.exit(0 if d.get('packages') is None and isinstance(d.get('entwurfProvider'),dict) else 1)" "$CKS"; then
|
|
329
|
+
bad "14 SETUP MISS: the seed ($ck_seed) is not the committed form (no packages key + entwurfProvider)"
|
|
322
330
|
else
|
|
323
331
|
CK_BEFORE="$(sha256sum "$CKS" | cut -d' ' -f1)"; CK_MT="$(stat -c %Y "$CKS")"
|
|
324
332
|
sleep 1
|
|
@@ -359,6 +367,124 @@ else
|
|
|
359
367
|
else
|
|
360
368
|
bad "14d the user-scope citizen was not written under the sandboxed PI_CODING_AGENT_DIR" "$ck_out"
|
|
361
369
|
fi
|
|
370
|
+
# The invariant, read off the file the drive just wrote through: no writer put a
|
|
371
|
+
# project-scope self-registration back. 14b already pins the bytes, but a writer
|
|
372
|
+
# that ADDED `packages` to a file with no `packages` key would fail 14b for a
|
|
373
|
+
# reason a reader could mistake for formatting; this says the thing #110 is about,
|
|
374
|
+
# in its own words.
|
|
375
|
+
L14E="14e install into entwurf's OWN checkout wrote no project-scope packages[] self-registration [QK:SELF-CHECKOUT-PROJECT-REGISTRATION]"
|
|
376
|
+
if python3 -c "
|
|
377
|
+
import json,sys
|
|
378
|
+
sys.exit(0 if json.load(open(sys.argv[1])).get('packages') is None else 1)" "$CKS"; then
|
|
379
|
+
ok "$L14E"
|
|
380
|
+
else
|
|
381
|
+
bad "$L14E — violated: install self-registered this checkout, so a second checkout will load a rival copy of pi-extensions" "$(cat "$CKS")"
|
|
382
|
+
fi
|
|
383
|
+
case "$ck_out" in
|
|
384
|
+
*"project-scope packages[] skipped"*) ok "14f the drive REPORTS the skip (a silent skip and a silent write read alike)" ;;
|
|
385
|
+
*) bad "14f install skipped project scope without saying so — 14e cannot tell that apart from a writer that never ran" "$ck_out" ;;
|
|
386
|
+
esac
|
|
387
|
+
fi
|
|
388
|
+
fi
|
|
389
|
+
|
|
390
|
+
# ── 15. #110: the operator-observable this whole contract exists for ───────────
|
|
391
|
+
# Cells 11–14 read settings bytes. Bytes are the mechanism; the SYMPTOM is what pi
|
|
392
|
+
# does with them, and that is what nobody could see: a second checkout booted pi,
|
|
393
|
+
# printed all seven `entwurf` model rows, and looked fine, while `--entwurf-control`
|
|
394
|
+
# — the flag that makes a pi session a garden citizen — was being served by the
|
|
395
|
+
# branch copy, and the INSTALLED extension failed to load with an error naming the
|
|
396
|
+
# installed path as the loser. Not silent. Designed to be misread.
|
|
397
|
+
#
|
|
398
|
+
# pi's package identity is `local:<resolved path>` and dedupePackages collapses only
|
|
399
|
+
# EQUAL identities (package-manager.js getPackageIdentity/dedupePackages), so two
|
|
400
|
+
# real checkouts can never merge — no realpath rule, ours or pi's, would fix this.
|
|
401
|
+
# The only lever is not registering the second one.
|
|
402
|
+
#
|
|
403
|
+
# The cell is control-then-assert on purpose. A sandbox where pi loads no project
|
|
404
|
+
# settings at all (untrusted cwd, missing extension deps, a probe that dies early)
|
|
405
|
+
# would report "no conflicts" for reasons that have nothing to do with the contract,
|
|
406
|
+
# and that green would be a lie. So it first PLANTS the retired entry and demands the
|
|
407
|
+
# conflict APPEAR; only a cell that has just watched the defect happen is allowed to
|
|
408
|
+
# assert its absence.
|
|
409
|
+
# Two different absences, and only one of them is allowed to be quiet. A tree with no
|
|
410
|
+
# `node_modules` at all is a genuinely reduced checkout and the property is untestable
|
|
411
|
+
# there — a named skip. A tree that HAS dependencies but no pinned pi is a broken dev
|
|
412
|
+
# checkout, and skipping it would hide the breakage behind an exit-0 gate, so that one
|
|
413
|
+
# fails loud. (The qualification snapshot is the first case only in principle: it
|
|
414
|
+
# symlinks the origin's node_modules in after its baseline commit — mutation-qualify.ts
|
|
415
|
+
# createRepoSnapshot — so cell 15 really runs there, which is what lets the two mutants
|
|
416
|
+
# below cover this skip. A skip nobody can reach with a planted defect would be exactly
|
|
417
|
+
# the quiet green this cell is built to refuse.)
|
|
418
|
+
PI_BIN_PINNED="$REPO/node_modules/.bin/pi"
|
|
419
|
+
if [ ! -d "$REPO/node_modules" ]; then
|
|
420
|
+
echo " skip 15 second-checkout probe skipped: this tree has no node_modules (reduced checkout); the property is untestable here, not violated"
|
|
421
|
+
elif [ ! -x "$PI_BIN_PINNED" ]; then
|
|
422
|
+
bad "15 the pinned pi is missing from a checkout that HAS dependencies ($PI_BIN_PINNED) — a broken dev checkout, not a reduced one; re-run pnpm install"
|
|
423
|
+
else
|
|
424
|
+
A15="$TMP/dual/installed"; B15="$TMP/dual/second"
|
|
425
|
+
AG15="$TMP/agent15"
|
|
426
|
+
mkdir -p "$A15/.pi" "$B15/.pi" "$AG15"
|
|
427
|
+
# pi-extensions is COPIED into each root, not linked: the defect is two DISTINCT
|
|
428
|
+
# real paths declaring the same flag, and a shared symlink target would be one path
|
|
429
|
+
# wearing two names — a fixture that cannot reproduce what two checkouts do.
|
|
430
|
+
for root in "$A15" "$B15"; do
|
|
431
|
+
cp -R "$REPO/pi-extensions" "$root/pi-extensions"
|
|
432
|
+
for entry in node_modules package.json protocol.js; do
|
|
433
|
+
[ -e "$REPO/$entry" ] && ln -s "$REPO/$entry" "$root/$entry"
|
|
434
|
+
done
|
|
435
|
+
done
|
|
436
|
+
# user scope registers the INSTALLED root, absolutely — what `register_user_scope_citizen`
|
|
437
|
+
# writes on a real host. Trust is seeded for the second checkout because `.pi/` is a
|
|
438
|
+
# trust-gated input: an untrusted cwd loads no project settings, which would silence
|
|
439
|
+
# the control below and make the assertion vacuous.
|
|
440
|
+
printf '{"packages": ["%s"]}\n' "$A15" > "$AG15/settings.json"
|
|
441
|
+
printf '{"%s": true}\n' "$B15" > "$AG15/trust.json"
|
|
442
|
+
# Two fences on the probe child, and BOTH are load-bearing.
|
|
443
|
+
#
|
|
444
|
+
# `< /dev/null`: `pi --print` reads a non-tty stdin as piped prompt input, so it
|
|
445
|
+
# inherits whatever the CALLER handed this gate. Under an interactive shell or a
|
|
446
|
+
# /dev/null stdin that ends immediately and the probe returns in ~2s; under a
|
|
447
|
+
# supervised runner — which hands the process an open socket — it blocks forever.
|
|
448
|
+
# Measured 2026-09-10: a release-gate run sat 1h56m on this line with `pi` alive
|
|
449
|
+
# and no output, and the gate has no timeout of its own to catch it. A gate must
|
|
450
|
+
# read the loader, never the stdin of whoever started it.
|
|
451
|
+
#
|
|
452
|
+
# `timeout 60`: even so, a probe that cannot finish must go RED rather than hang.
|
|
453
|
+
# 124 lands in the captured output, the conflict grep misses, and the cell fails
|
|
454
|
+
# with its own name — the honest outcome for "the loader never answered".
|
|
455
|
+
#
|
|
456
|
+
# Ambient identity carriers are stripped for the same reason: a live pi/ACP session
|
|
457
|
+
# exports PI_SESSION_ID/PI_AGENT_ID and CLAUDE_CONFIG_DIR into children.
|
|
458
|
+
probe15() {
|
|
459
|
+
(cd "$B15" && env -u PI_SESSION_ID -u PI_AGENT_ID -u CLAUDE_CONFIG_DIR \
|
|
460
|
+
HOME="$TMP/home" PI_CODING_AGENT_DIR="$AG15" \
|
|
461
|
+
XDG_DATA_HOME="$TMP/xdg" XDG_STATE_HOME="$TMP/state" XDG_CACHE_HOME="$TMP/cache" \
|
|
462
|
+
timeout 60 "$PI_BIN_PINNED" --model zzz-no-such-provider/zzz-no-such-model --print x \
|
|
463
|
+
</dev/null 2>&1 || true)
|
|
464
|
+
}
|
|
465
|
+
# CONTROL: the retired shape, re-planted. `".."` resolves against <B>/.pi, so the
|
|
466
|
+
# project entry names B while user scope names A — two identities, both loaded.
|
|
467
|
+
printf '{"packages": [".."]}\n' > "$B15/.pi/settings.json"
|
|
468
|
+
OUT15_CTL="$(probe15)"
|
|
469
|
+
if printf '%s' "$OUT15_CTL" | grep -q 'conflicts with'; then
|
|
470
|
+
ok "15 control: a second checkout that registers itself DOES collide on the extension flags"
|
|
471
|
+
else
|
|
472
|
+
bad "15 SETUP MISS: the planted second registration produced no conflict — this sandbox cannot observe the defect, so 15b would be vacuous" "$OUT15_CTL"
|
|
473
|
+
fi
|
|
474
|
+
# ASSERT: this repo's real `.pi/settings.json`, read from the WORKING TREE — not
|
|
475
|
+
# from the candidate index the way cells 11/14 read theirs. The subject here is
|
|
476
|
+
# what pi's loader does with the file that exists ON DISK when a session starts;
|
|
477
|
+
# the index is a different question and check-install-surface S7c already owns it.
|
|
478
|
+
# Reading the index here also made this cell blind to its own replant: a mutation
|
|
479
|
+
# writes the file, git show reads the staged bytes, and the gate stayed green over
|
|
480
|
+
# a planted defect (measured — qualification reported SURVIVED, 2026-09-10).
|
|
481
|
+
cp "$REPO/.pi/settings.json" "$B15/.pi/settings.json"
|
|
482
|
+
OUT15="$(probe15)"
|
|
483
|
+
L15B="15b a second checkout of this repo boots pi with zero extension conflicts [QK:SECOND-CHECKOUT-EXTENSION-COLLISION]"
|
|
484
|
+
if printf '%s' "$OUT15" | grep -q 'conflicts with'; then
|
|
485
|
+
bad "$L15B — violated: the branch copy wins --entwurf-control and the INSTALLED extension fails, printing the installed path as the loser" "$OUT15"
|
|
486
|
+
else
|
|
487
|
+
ok "$L15B"
|
|
362
488
|
fi
|
|
363
489
|
fi
|
|
364
490
|
|
|
@@ -740,14 +866,16 @@ if [ "$RC34B" -ne 0 ] && printf '%s' "$OUT34B" | grep -q 'CORRUPT'; then
|
|
|
740
866
|
ok "34b doctor coupling FAILs on a wrong-type provider installerRoot (named CORRUPT) $QK34"
|
|
741
867
|
else bad "34b doctor stayed green/silent over a corrupt installerRoot (rc=$RC34B): $OUT34B $QK34"; fi
|
|
742
868
|
|
|
743
|
-
#
|
|
869
|
+
# 35. WIRING: both writers of this one file must share the serializer, not copy it.
|
|
744
870
|
# A duplicated indent-detector is how the provider writer stayed open after the
|
|
745
871
|
# package writer was closed; a parity check is cheaper than a third round.
|
|
872
|
+
# (Numbered 35, after the ownership block: it used to be a second "15", which
|
|
873
|
+
# stopped being merely untidy once #110 added the real cell 15 above.)
|
|
746
874
|
for w in register-pi-package.py register-pi-provider.py; do
|
|
747
875
|
if grep -q "from pi_settings_io import" "$REPO/scripts/$w"; then
|
|
748
|
-
ok "
|
|
876
|
+
ok "35 $w routes through the shared pi_settings_io serializer"
|
|
749
877
|
else
|
|
750
|
-
bad "
|
|
878
|
+
bad "35 $w serializes settings on its own again (copied rule = the #53 B shape)"
|
|
751
879
|
fi
|
|
752
880
|
done
|
|
753
881
|
|
package/scripts/tsconfig.json
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"../pi-extensions/model-lock.ts",
|
|
32
32
|
"../pi-extensions/meta-bridge-hook.ts",
|
|
33
33
|
"../pi-extensions/meta-bridge-hook-copilot.ts",
|
|
34
|
+
"../pi-extensions/meta-bridge-hook-codex.ts",
|
|
34
35
|
"../pi-extensions/meta-bridge-omp.ts",
|
|
35
36
|
"../pi-extensions/meta-bridge-receive-omp.ts",
|
|
36
37
|
"../mcp/entwurf-bridge/src/resume-mode.ts"
|