@junghanacs/entwurf 0.25.1 → 0.30.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 +1 -1
- package/BASELINE.md +1 -1
- package/CHANGELOG.md +190 -0
- package/DELIVERY.md +1 -1
- package/README.md +183 -674
- package/VERIFY.md +21 -17
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +115 -18
- package/docs/external-mcp-host.md +1 -1
- package/docs/setup-clean-host.md +346 -10
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +27 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +55 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-mailbox.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +3 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +3 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +13 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +3 -3
- package/mcp/entwurf-bridge/src/index.ts +30 -3
- package/package.json +13 -13
- package/pi-extensions/entwurf-control.ts +50 -637
- package/pi-extensions/lib/acp/backend-adapter.ts +81 -1
- package/pi-extensions/lib/acp/backend.ts +41 -5
- package/pi-extensions/lib/entwurf-fact-provider.ts +2 -2
- package/pi-extensions/lib/entwurf-mcp-server.ts +74 -0
- package/pi-extensions/lib/entwurf-peers-render.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-decider.ts +2 -2
- package/pi-extensions/lib/entwurf-v2-mailbox.ts +2 -2
- package/pi-extensions/lib/entwurf-v2-production.ts +3 -3
- package/pi-extensions/lib/entwurf-v2-surface.ts +3 -5
- package/pi-extensions/lib/entwurf-v2-visible-resume.ts +3 -3
- package/pi-extensions/lib/fresh-call-composition.ts +13 -6
- package/pi-extensions/lib/meta-mailbox-body.ts +1 -1
- package/pi-extensions/lib/mux-fresh-call.ts +3 -3
- package/run.sh +177 -53
- package/scripts/check-entwurf-peers-surface.ts +8 -15
- package/scripts/check-entwurf-self-address.ts +7 -11
- package/scripts/check-entwurf-v2-surface.ts +20 -112
- package/scripts/check-fresh-call-dispatch.ts +12 -13
- package/scripts/check-gate-qualification.ts +250 -5
- package/scripts/check-herdr-plugin-build.ts +159 -57
- package/scripts/check-install-surface.ts +3 -2
- package/scripts/check-mux-resume-call.ts +24 -35
- package/scripts/check-pi-bridge-sender.ts +130 -0
- package/scripts/check-pi-mcp-bridge.ts +251 -0
- package/scripts/fixtures/herdr-supply.json +8 -8
- package/scripts/lib/codex-fresh-live-protocol.ts +17 -5
- package/scripts/lib/codex-fresh-source-receipts.ts +21 -2
- package/scripts/lib/mutation-qualify.ts +269 -44
- package/scripts/lib/pi-mcp-bridge-probe.mjs +170 -0
- package/scripts/meta-bridge-doctor.sh +16 -5
- package/scripts/mutants/acp-foreground-only.json +17 -0
- package/scripts/mutants/acp-prompt-lifecycle.json +2 -2
- package/scripts/mutants/acp-vendor-rejection.json +62 -0
- package/scripts/mutants/codex-native.json +11 -11
- package/scripts/mutants/control-send-receipt.json +6 -4
- package/scripts/mutants/copilot-birth.json +2 -1
- package/scripts/mutants/fresh-call-dispatch.json +3 -14
- package/scripts/mutants/gate-qualification.json +59 -0
- package/scripts/mutants/herdr-plugin-build.json +11 -0
- package/scripts/mutants/herdr-supply.json +3 -3
- package/scripts/mutants/mux-fresh-call.json +43 -45
- package/scripts/mutants/mux-resume-call.json +11 -35
- package/scripts/mutants/pack-install.json +15 -4
- package/scripts/mutants/pi-bridge-sender.json +54 -0
- package/scripts/mutants/pi-floor.json +6 -2
- package/scripts/mutants/pi-mcp-bridge.json +36 -0
- package/scripts/mutants/pi-mcp-register.json +51 -0
- package/scripts/mutants/v2-surface.json +6 -6
- package/scripts/resident-rpc-drive.ts +5 -1
- package/scripts/smoke-acp-bundled-mcp-live.ts +3 -1
- package/scripts/smoke-acp-cortex-live.ts +3 -1
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-acp-session-reuse-live.ts +29 -12
- package/scripts/smoke-acp-socket-citizen-live.ts +3 -1
- package/scripts/smoke-acp-v2-send-live.ts +3 -1
- package/scripts/smoke-codex-fresh-live.ts +20 -1
- package/scripts/smoke-entwurf-chain-live.ts +6 -2
- package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -1
- package/scripts/smoke-herdr-fresh-call-live.ts +21 -7
- package/scripts/smoke-herdr-raw-install-live.sh +1 -1
- package/scripts/smoke-mux-lifecycle-live.ts +1 -1
- package/scripts/smoke-resident-garden-guard.sh +3 -1
|
@@ -53,6 +53,20 @@ const COMPILED_ENTRY = path.join("mcp", "entwurf-bridge", "dist", "mcp", "entwur
|
|
|
53
53
|
const COMMIT_A = "a".repeat(40);
|
|
54
54
|
const COMMIT_B = "b".repeat(40);
|
|
55
55
|
|
|
56
|
+
// Fixture inputs, not registry assertions. The npm coherence check reads this checkout's
|
|
57
|
+
// package.json, so its version is input; neither an unpublished version nor the committed
|
|
58
|
+
// carrier may choose which source these deterministic cells exercise. No network is used.
|
|
59
|
+
const FIXTURE_VERSION = (JSON.parse(fs.readFileSync(path.join(REPO, "package.json"), "utf8")) as { version: string })
|
|
60
|
+
.version;
|
|
61
|
+
const FIXTURE_NPM_LOCK = Object.freeze({
|
|
62
|
+
source: "npm",
|
|
63
|
+
name: PACKAGE,
|
|
64
|
+
version: FIXTURE_VERSION,
|
|
65
|
+
integrity: `sha512-${createHash("sha512").update("hpb-npm-fixture").digest("base64")}`,
|
|
66
|
+
});
|
|
67
|
+
const FIXTURE_OTHER_INTEGRITY = `sha512-${createHash("sha512").update("hpb-npm-other-fixture").digest("base64")}`;
|
|
68
|
+
const FIXTURE_CHECKOUT_LOCK = Object.freeze({ source: "herdr-checkout", repository: "junghan0611/entwurf" });
|
|
69
|
+
|
|
56
70
|
interface SpawnResult {
|
|
57
71
|
status: number | null;
|
|
58
72
|
stdout: string;
|
|
@@ -175,9 +189,10 @@ function tree(root: string): string[] {
|
|
|
175
189
|
*/
|
|
176
190
|
function fixtureAcquire(opts: { activate: "absent" | "incapable" | "recorder"; log?: string; fail?: boolean }) {
|
|
177
191
|
return ({ identity, prefix }: AcquireArgs) => {
|
|
192
|
+
const version = identity.version ?? FIXTURE_VERSION;
|
|
178
193
|
const root = path.join(prefix, "node_modules", PACKAGE);
|
|
179
194
|
fs.mkdirSync(path.join(root, path.dirname(COMPILED_ENTRY)), { recursive: true });
|
|
180
|
-
fs.writeFileSync(path.join(root, "package.json"), `${JSON.stringify({ name: PACKAGE, version
|
|
195
|
+
fs.writeFileSync(path.join(root, "package.json"), `${JSON.stringify({ name: PACKAGE, version })}\n`);
|
|
181
196
|
fs.writeFileSync(path.join(root, COMPILED_ENTRY), "// compiled entry fixture\n");
|
|
182
197
|
const bin = path.join(prefix, "node_modules", ".bin");
|
|
183
198
|
fs.mkdirSync(bin, { recursive: true });
|
|
@@ -208,7 +223,7 @@ function fixtureAcquire(opts: { activate: "absent" | "incapable" | "recorder"; l
|
|
|
208
223
|
.update(identity.commit ?? "x")
|
|
209
224
|
.digest("hex")}`,
|
|
210
225
|
packageName: PACKAGE,
|
|
211
|
-
packageVersion:
|
|
226
|
+
packageVersion: version,
|
|
212
227
|
};
|
|
213
228
|
};
|
|
214
229
|
}
|
|
@@ -249,6 +264,9 @@ function drive(
|
|
|
249
264
|
spawn,
|
|
250
265
|
write: (t: string) => (out += t),
|
|
251
266
|
progress,
|
|
267
|
+
// Existing gate-only seam. Production still reads the committed lock, and its
|
|
268
|
+
// real source/coherence is independently checked by check-herdr-runtime-bootstrap.
|
|
269
|
+
readRuntimeLock: () => FIXTURE_NPM_LOCK,
|
|
252
270
|
...deps,
|
|
253
271
|
});
|
|
254
272
|
return { code, refusal: null, out, progress: progressLines };
|
|
@@ -408,57 +426,121 @@ function drive(
|
|
|
408
426
|
|
|
409
427
|
// ── 6. the two-stage journey, as the RUNNER composes it ────────────────────────
|
|
410
428
|
{
|
|
411
|
-
|
|
429
|
+
// Declared axes: both closed sources × {initial A, widened A, changed B, repeated B}.
|
|
430
|
+
// npm identity is the unchanged lock, not a commit; checkout identity IS the commit.
|
|
431
|
+
const cells = [FIXTURE_NPM_LOCK, FIXTURE_CHECKOUT_LOCK].map((lock) => {
|
|
432
|
+
const env = world(`two-stage-${lock.source}`);
|
|
433
|
+
const log = path.join(env.HOME as string, "argv.log");
|
|
434
|
+
let acquisitions = 0;
|
|
435
|
+
const counted = (args: AcquireArgs) => {
|
|
436
|
+
acquisitions++;
|
|
437
|
+
return fixtureAcquire({ activate: "recorder", log })(args);
|
|
438
|
+
};
|
|
439
|
+
const stages = [COMMIT_A, COMMIT_A, COMMIT_B, COMMIT_B].map((commit, index) => {
|
|
440
|
+
const rows: Record<string, string> = { pi: "current (v8) (/home/u/.pi)" };
|
|
441
|
+
if (index > 0) rows.claude = "current (v5) (/home/u/.claude)";
|
|
442
|
+
const result = drive(env, listing(rows), {
|
|
443
|
+
acquire: counted,
|
|
444
|
+
readRuntimeLock: () => lock,
|
|
445
|
+
resolveCommit: () => commit,
|
|
446
|
+
});
|
|
447
|
+
return { code: result.code, acquisitions };
|
|
448
|
+
});
|
|
449
|
+
const calls = fs.readFileSync(log, "utf8").trim().split("\n");
|
|
450
|
+
const { artifactIdentity: identity } = JSON.parse(fs.readFileSync(journalPathOf(env), "utf8")) as {
|
|
451
|
+
artifactIdentity: Record<string, string>;
|
|
452
|
+
};
|
|
453
|
+
const expectedCounts = lock.source === "npm" ? [1, 1, 1, 1] : [1, 1, 2, 2];
|
|
454
|
+
const exactIdentity =
|
|
455
|
+
lock.source === "npm"
|
|
456
|
+
? identity.name === PACKAGE &&
|
|
457
|
+
identity.version === FIXTURE_VERSION &&
|
|
458
|
+
identity.expectedIntegrity === FIXTURE_NPM_LOCK.integrity
|
|
459
|
+
: identity.repository === FIXTURE_CHECKOUT_LOCK.repository &&
|
|
460
|
+
identity.commit === COMMIT_B &&
|
|
461
|
+
identity.packageName === PACKAGE &&
|
|
462
|
+
identity.packageVersion === FIXTURE_VERSION;
|
|
463
|
+
return {
|
|
464
|
+
source: lock.source,
|
|
465
|
+
stages,
|
|
466
|
+
calls,
|
|
467
|
+
identity,
|
|
468
|
+
passed:
|
|
469
|
+
stages.every((stage) => stage.code === 0) &&
|
|
470
|
+
JSON.stringify(stages.map((stage) => stage.acquisitions)) === JSON.stringify(expectedCounts) &&
|
|
471
|
+
JSON.stringify(calls) === JSON.stringify(["pi", "pi,claude-code", "pi,claude-code", "pi,claude-code"]) &&
|
|
472
|
+
identity.kind === lock.source &&
|
|
473
|
+
exactIdentity,
|
|
474
|
+
};
|
|
475
|
+
});
|
|
476
|
+
ok(
|
|
477
|
+
"[QK:HPB-SEQUENTIAL-TWO-STAGE] both closed sources hand down `{pi}` then the widened `{pi, claude-code}` " +
|
|
478
|
+
"on every reinstall, never OpenCode. An unchanged npm lock acquires once; checkout acquires again ONLY " +
|
|
479
|
+
"when A becomes B, and repeated B acquires nothing. Counts are exact at EACH step, not an aggregate lower " +
|
|
480
|
+
"bound. Installed identity must retain the input kind and its exact version/integrity or commit/completeness. " +
|
|
481
|
+
"The real writer bytes are HAC-TWO-STAGE-ADD-ONLY; this cell owns runner composition. " +
|
|
482
|
+
`Fixture identities do not claim registry acceptance (cells=${JSON.stringify(cells)})`,
|
|
483
|
+
cells.length === 2 && cells.every((cell) => cell.passed),
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ── 5b. the production default reader composes with the committed lock ─────────
|
|
488
|
+
{
|
|
489
|
+
// Independent input oracle: read the committed JSON directly, not the runtime reader
|
|
490
|
+
// whose invocation this cell certifies. All acquisition/activation bytes remain fixture.
|
|
491
|
+
const committed = JSON.parse(fs.readFileSync(path.join(PLUGIN_DIR, "runtime-lock.json"), "utf8")) as
|
|
492
|
+
| { source: "npm"; name: string; version: string; integrity: string }
|
|
493
|
+
| { source: "herdr-checkout"; repository: string };
|
|
494
|
+
const env = world("default-lock-reader");
|
|
412
495
|
const log = path.join(env.HOME as string, "argv.log");
|
|
413
496
|
let acquisitions = 0;
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
};
|
|
418
|
-
const stage1 = drive(env, listing({ pi: "current (v8) (/home/u/.pi)" }), {
|
|
419
|
-
acquire: counted,
|
|
420
|
-
resolveCommit: () => COMMIT_A,
|
|
421
|
-
});
|
|
422
|
-
const stage2 = drive(env, listing({ pi: "current (v8) (/home/u/.pi)", claude: "current (v5) (/home/u/.claude)" }), {
|
|
423
|
-
acquire: counted,
|
|
497
|
+
const acquire = fixtureAcquire({ activate: "recorder", log });
|
|
498
|
+
const result = drive(env, listing({ pi: "current (v8) (/home/u/.pi)" }), {
|
|
499
|
+
readRuntimeLock: undefined,
|
|
424
500
|
resolveCommit: () => COMMIT_A,
|
|
501
|
+
acquire: (args: AcquireArgs) => {
|
|
502
|
+
acquisitions++;
|
|
503
|
+
return acquire(args);
|
|
504
|
+
},
|
|
425
505
|
});
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
506
|
+
const journal = fs.existsSync(journalPathOf(env))
|
|
507
|
+
? (JSON.parse(fs.readFileSync(journalPathOf(env), "utf8")) as { artifactIdentity: Record<string, string> })
|
|
508
|
+
: null;
|
|
509
|
+
const identity = journal?.artifactIdentity;
|
|
510
|
+
const exactIdentity =
|
|
511
|
+
identity !== undefined &&
|
|
512
|
+
identity.kind === committed.source &&
|
|
513
|
+
(committed.source === "npm"
|
|
514
|
+
? identity.name === committed.name &&
|
|
515
|
+
identity.version === committed.version &&
|
|
516
|
+
identity.expectedIntegrity === committed.integrity
|
|
517
|
+
: identity.repository === committed.repository &&
|
|
518
|
+
identity.commit === COMMIT_A &&
|
|
519
|
+
identity.packageName === PACKAGE &&
|
|
520
|
+
identity.packageVersion === FIXTURE_VERSION);
|
|
434
521
|
ok(
|
|
435
|
-
"[QK:HPB-
|
|
436
|
-
"
|
|
437
|
-
"
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
acquisitions === 2 &&
|
|
445
|
-
JSON.stringify(calls) === JSON.stringify(["pi", "pi,claude-code", "pi,claude-code"]) &&
|
|
446
|
-
!calls.some((c) => c.includes("opencode")) &&
|
|
447
|
-
journal.artifactIdentity.commit === COMMIT_B,
|
|
522
|
+
"[QK:HPB-DEFAULT-LOCK-READ] without a reader override, runBuild reads this plugin's committed lock " +
|
|
523
|
+
"from PLUGIN_DIR, not the repository root or a synthetic fixture. The acquired identity retains the " +
|
|
524
|
+
"independently read source and exact artifact fields; installed activation still runs once for pi. " +
|
|
525
|
+
`This is composition proof, not registry acquisition (code=${result.code} refusal=${result.refusal} acquisitions=${acquisitions} identity=${JSON.stringify(identity)})`,
|
|
526
|
+
result.code === 0 &&
|
|
527
|
+
acquisitions === 1 &&
|
|
528
|
+
exactIdentity &&
|
|
529
|
+
fs.existsSync(log) &&
|
|
530
|
+
fs.readFileSync(log, "utf8") === "pi\n",
|
|
448
531
|
);
|
|
449
532
|
}
|
|
450
533
|
|
|
451
534
|
// ── 6a. the authority check happens BEFORE any runtime work ───────────────────
|
|
452
535
|
{
|
|
453
|
-
const IDENTITY = (
|
|
454
|
-
kind: "
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
observedDigest: `sha256-${createHash("sha256").update(commit).digest("hex")}`,
|
|
536
|
+
const IDENTITY = (seed: string) => ({
|
|
537
|
+
kind: "npm",
|
|
538
|
+
name: PACKAGE,
|
|
539
|
+
version: FIXTURE_VERSION,
|
|
540
|
+
expectedIntegrity: FIXTURE_NPM_LOCK.integrity,
|
|
541
|
+
observedDigest: `sha256-${createHash("sha256").update(seed).digest("hex")}`,
|
|
460
542
|
});
|
|
461
|
-
/** A host
|
|
543
|
+
/** A host with the explicit npm fixture installed and a ledger describing it. */
|
|
462
544
|
const seeded = (tag: string, backends: string[]): NodeJS.ProcessEnv => {
|
|
463
545
|
const env = world(tag);
|
|
464
546
|
const log = path.join(env.HOME as string, "argv.log");
|
|
@@ -468,12 +550,17 @@ function drive(
|
|
|
468
550
|
});
|
|
469
551
|
const states: Record<string, string> = {};
|
|
470
552
|
for (const b of backends) states[b] = "active";
|
|
553
|
+
const artifactIdentity = (
|
|
554
|
+
JSON.parse(fs.readFileSync(journalPathOf(env), "utf8")) as {
|
|
555
|
+
artifactIdentity: Record<string, unknown>;
|
|
556
|
+
}
|
|
557
|
+
).artifactIdentity;
|
|
471
558
|
activation.writeLedger(
|
|
472
559
|
activation.resolveActivationLayout(env),
|
|
473
560
|
activation.ledgerBody({
|
|
474
561
|
phase: "active",
|
|
475
562
|
runtimeRoot: activeDirOf(env),
|
|
476
|
-
artifactIdentity
|
|
563
|
+
artifactIdentity,
|
|
477
564
|
roots: activation.resolveComponentRoots(env),
|
|
478
565
|
states,
|
|
479
566
|
}),
|
|
@@ -518,21 +605,22 @@ function drive(
|
|
|
518
605
|
const emptiedDirSeen = treeDigest(oracleRoot) !== oracleBase;
|
|
519
606
|
const oracleHonest = sameTree && sameLengthEditSeen && restored && modeSeen && emptiedDirSeen;
|
|
520
607
|
|
|
521
|
-
// (a) a SOURCE switch: the committed lock would
|
|
608
|
+
// (a) a SOURCE switch: the committed lock would name checkout over the active npm runtime.
|
|
522
609
|
// (a) is asked of the SHARED authority directly: the runner reads its lock from the committed
|
|
523
610
|
// file, and this gate does not add a seam that would let any caller choose the source.
|
|
524
611
|
const src = seeded("plan-source", ["pi"]);
|
|
525
612
|
const srcBefore = snapshot(src);
|
|
526
613
|
const srcRefusal = refusalOf(() =>
|
|
527
614
|
runner.certifyActivationPlan(src, {
|
|
528
|
-
lock: { source: "
|
|
615
|
+
lock: { source: "herdr-checkout", repository: "junghan0611/entwurf" },
|
|
529
616
|
checkoutRoot: fixtureCheckoutRoot(),
|
|
530
617
|
requested: ["pi"],
|
|
618
|
+
resolveCommit: () => COMMIT_A,
|
|
531
619
|
}),
|
|
532
620
|
);
|
|
533
621
|
const srcUnchanged = unchanged(src, srcBefore);
|
|
534
622
|
|
|
535
|
-
// (b) a
|
|
623
|
+
// (b) a changed npm integrity whose request DROPS a backend the ledger holds.
|
|
536
624
|
const drop = seeded("plan-drop", ["pi", "claude-code"]);
|
|
537
625
|
const dropBefore = snapshot(drop);
|
|
538
626
|
let dropAcquired = 0;
|
|
@@ -541,6 +629,7 @@ function drive(
|
|
|
541
629
|
dropAcquired++;
|
|
542
630
|
return fixtureAcquire({ activate: "recorder", log: path.join(drop.HOME as string, "argv.log") })(args);
|
|
543
631
|
},
|
|
632
|
+
readRuntimeLock: () => ({ ...FIXTURE_NPM_LOCK, integrity: FIXTURE_OTHER_INTEGRITY }),
|
|
544
633
|
resolveCommit: () => COMMIT_B,
|
|
545
634
|
});
|
|
546
635
|
|
|
@@ -549,7 +638,17 @@ function drive(
|
|
|
549
638
|
const tornLedger = activation.resolveActivationLayout(torn).ledgerPath;
|
|
550
639
|
fs.writeFileSync(
|
|
551
640
|
tornLedger,
|
|
552
|
-
`${JSON.stringify(
|
|
641
|
+
`${JSON.stringify(
|
|
642
|
+
{
|
|
643
|
+
...(JSON.parse(fs.readFileSync(tornLedger, "utf8")) as Record<string, unknown>),
|
|
644
|
+
artifactIdentity: {
|
|
645
|
+
...IDENTITY(COMMIT_B),
|
|
646
|
+
expectedIntegrity: FIXTURE_OTHER_INTEGRITY,
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
null,
|
|
650
|
+
2,
|
|
651
|
+
)}\n`,
|
|
553
652
|
);
|
|
554
653
|
const tornBefore = snapshot(torn);
|
|
555
654
|
let tornAcquired = 0;
|
|
@@ -561,14 +660,13 @@ function drive(
|
|
|
561
660
|
resolveCommit: () => COMMIT_A,
|
|
562
661
|
});
|
|
563
662
|
|
|
564
|
-
// (d) the LEGAL case still passes: same source
|
|
565
|
-
// authority is asked on the PRE-install state, which is the only state it is ever asked about.
|
|
663
|
+
// (d) the LEGAL case still passes: same exact npm source and lock, request covers the ledger.
|
|
664
|
+
// The authority is asked on the PRE-install state, which is the only state it is ever asked about.
|
|
566
665
|
const legal = seeded("plan-legal", ["pi"]);
|
|
567
666
|
const legalPlan = runner.certifyActivationPlan(legal, {
|
|
568
|
-
lock:
|
|
569
|
-
checkoutRoot:
|
|
667
|
+
lock: FIXTURE_NPM_LOCK,
|
|
668
|
+
checkoutRoot: REPO,
|
|
570
669
|
requested: ["pi"],
|
|
571
|
-
resolveCommit: () => COMMIT_B,
|
|
572
670
|
});
|
|
573
671
|
const legalDrive = drive(legal, listing({ pi: "current (v8) (/home/u/.pi)" }), {
|
|
574
672
|
acquire: fixtureAcquire({ activate: "recorder", log: path.join(legal.HOME as string, "argv.log") }),
|
|
@@ -577,7 +675,7 @@ function drive(
|
|
|
577
675
|
|
|
578
676
|
ok(
|
|
579
677
|
"[QK:HPB-PRE-BOOTSTRAP-AUTHORITY] the activation authority is asked BEFORE any runtime work, through the SAME " +
|
|
580
|
-
"shipped functions the installed verb runs: a source switch, a
|
|
678
|
+
"shipped functions the installed verb runs: a source switch, a request that drops an activated " +
|
|
581
679
|
"backend, and a ledger that already disagrees with the runtime standing there are each refused with the " +
|
|
582
680
|
"runtime tree, its journal, our cache, the ledger and the activation log ALL byte-identical and the " +
|
|
583
681
|
"acquisition never called. The first cut bootstrapped first and let the installed verb refuse afterwards, " +
|
|
@@ -596,7 +694,7 @@ function drive(
|
|
|
596
694
|
tornAcquired === 0 &&
|
|
597
695
|
unchanged(torn, tornBefore) &&
|
|
598
696
|
legalDrive.code === 0 &&
|
|
599
|
-
legalPlan.disposition === "
|
|
697
|
+
legalPlan.disposition === "match",
|
|
600
698
|
);
|
|
601
699
|
}
|
|
602
700
|
|
|
@@ -607,15 +705,19 @@ function drive(
|
|
|
607
705
|
const commits = [COMMIT_A, COMMIT_B, COMMIT_B];
|
|
608
706
|
let call = 0;
|
|
609
707
|
const moving = () => commits[Math.min(call++, commits.length - 1)] as string;
|
|
708
|
+
// Candidate source is a closed, retained branch. This test seam does not expose a caller input:
|
|
709
|
+
// production `runBuild` reads only the committed lock, while the gate drives the branch whose
|
|
710
|
+
// identity can move between D1 and D2.
|
|
610
711
|
const drifted = drive(env, listing({ pi: "current (v8) (/home/u/.pi)" }), {
|
|
611
712
|
acquire: fixtureAcquire({ activate: "recorder", log }),
|
|
713
|
+
readRuntimeLock: () => FIXTURE_CHECKOUT_LOCK,
|
|
612
714
|
resolveCommit: moving,
|
|
613
715
|
});
|
|
614
716
|
const journal = JSON.parse(fs.readFileSync(journalPathOf(env), "utf8")) as { artifactIdentity: { commit: string } };
|
|
615
717
|
const activated = fs.existsSync(log) ? fs.readFileSync(log, "utf8").trim() : "";
|
|
616
718
|
ok(
|
|
617
719
|
"[QK:HPB-BOOTSTRAP-IDENTITY-EXACT] the artifact that lands must be EXACTLY the one the authority check admitted: " +
|
|
618
|
-
"when the source moves between the judgement and the install, the run refuses by name instead of handing the " +
|
|
720
|
+
"when the candidate source moves between the judgement and the install, the run refuses by name instead of handing the " +
|
|
619
721
|
"installed activation verb a different artifact than the one this build was cleared to place — the verb would " +
|
|
620
722
|
"then re-judge, and pass or fail on a question nobody asked. The refusal is honest about what did happen: the " +
|
|
621
723
|
`runtime was replaced, and NO activation ran (refusal=${drifted.refusal} landed=${journal.artifactIdentity.commit.slice(0, 8)} admitted=${COMMIT_A.slice(0, 8)} activations=${JSON.stringify(activated)})`,
|
|
@@ -709,7 +811,7 @@ function drive(
|
|
|
709
811
|
ok(
|
|
710
812
|
"[QK:HPB-PROGRESS-NAMED-SEQUENCE] a build narrates the five steps IN ORDER and names the long one — herdr pipes " +
|
|
711
813
|
"both of this process's streams into a buffer it DISCARDS on success (`src/cli/plugin.rs:1328-1373` @ c77af189), " +
|
|
712
|
-
"so an operator who has just answered the install prompt sees nothing at all through a multi-minute
|
|
814
|
+
"so an operator who has just answered the install prompt sees nothing at all through a multi-minute registry fetch " +
|
|
713
815
|
"and reads it as a hang. The sequence is reported, not logged: each step names the work about to start, the " +
|
|
714
816
|
"acquisition step says out loud that silence is expected and which source it is reaching for, and a run with " +
|
|
715
817
|
"nothing to activate takes exactly ONE step and then closes — it must not narrate work it never did. " +
|
|
@@ -721,7 +823,7 @@ function drive(
|
|
|
721
823
|
full.progress[0].includes("integration status") &&
|
|
722
824
|
full.progress[1].includes("pi, claude-code") &&
|
|
723
825
|
longStep.includes("long step") &&
|
|
724
|
-
longStep.includes(
|
|
826
|
+
longStep.includes(`${PACKAGE}@${FIXTURE_VERSION}`) &&
|
|
725
827
|
full.progress[3].includes("what landed") &&
|
|
726
828
|
full.progress[4].includes("wiring pi, claude-code") &&
|
|
727
829
|
full.progress[5].startsWith("done: ") &&
|
|
@@ -260,8 +260,9 @@ const operatorCmds = [...targets].filter(([cmd, ts]) => !isDevGate(cmd) && ts.le
|
|
|
260
260
|
.replace(/'(?:[^'\\]|\\.)*'/g, "''");
|
|
261
261
|
const RUNSH_DRIVE = /(?:\$\{?RUN\}?|run\.sh)\s+(install|setup|remove-user-scope|remove)\b/;
|
|
262
262
|
// install/setup write the operator's REAL user-scope surfaces under "$HOME"
|
|
263
|
-
// (
|
|
264
|
-
// the
|
|
263
|
+
// (settings.json registration follows PI_CODING_AGENT_DIR when set, but provider
|
|
264
|
+
// state and the rest stay HOME/XDG-rooted), so sandboxing the agent dir alone is not
|
|
265
|
+
// isolation for them; HOME is required. (setup:links died with
|
|
265
266
|
// the target registry — #50 C3.)
|
|
266
267
|
const HOME_ROOTED = new Set(["install", "setup"]);
|
|
267
268
|
|
|
@@ -212,6 +212,7 @@ function main(): void {
|
|
|
212
212
|
|
|
213
213
|
const PI_SURFACE = read("pi-extensions/entwurf-control.ts");
|
|
214
214
|
const MCP_SURFACE = read("mcp/entwurf-bridge/src/index.ts");
|
|
215
|
+
const PI_CODE = PI_SURFACE.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
|
|
215
216
|
const V2_RESUME = read("pi-extensions/lib/entwurf-v2-visible-resume.ts");
|
|
216
217
|
// IMPORTS, not mentions: the module header names the mux side deliberately (that is the
|
|
217
218
|
// architecture being documented), so an assertion on prose would forbid the explanation
|
|
@@ -222,55 +223,43 @@ function main(): void {
|
|
|
222
223
|
v2ImportSpecifiers.length > 0 && !v2ImportSpecifiers.some((s) => /mux-/.test(s)),
|
|
223
224
|
);
|
|
224
225
|
ok(
|
|
225
|
-
"
|
|
226
|
-
/mux-resume-call/.test(
|
|
227
|
-
/entwurf-v2-visible-resume/.test(PI_SURFACE) &&
|
|
228
|
-
/mux-resume-call/.test(MCP_SURFACE) &&
|
|
229
|
-
/entwurf-v2-visible-resume/.test(MCP_SURFACE),
|
|
226
|
+
"the bridge is the composition root: it imports the mux launcher AND the v2 composition and joins them itself",
|
|
227
|
+
/mux-resume-call/.test(MCP_SURFACE) && /entwurf-v2-visible-resume/.test(MCP_SURFACE),
|
|
230
228
|
);
|
|
231
|
-
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
// the tool deleted.
|
|
237
|
-
const PI_BLOCK = (PI_SURFACE.split('name: "entwurf_resume_call",')[1] ?? "").split("\n}")[0];
|
|
229
|
+
// #125: pi reaches entwurf_resume_call through Pi's built-in MCP — the bridge above IS its
|
|
230
|
+
// surface. The native pi registration, its target-only TypeBox schema and its description are
|
|
231
|
+
// retired with it (registration and target-only are held on the runtime tools/list by
|
|
232
|
+
// BRIDGEBOOT-RESUME-CALL-REGISTERED / -TARGET-ONLY, the description by DESC-CONTRACT-MCP below).
|
|
233
|
+
// What is left to say about the pi file is that it does not come back as a second root.
|
|
238
234
|
ok(
|
|
239
|
-
"
|
|
240
|
-
|
|
235
|
+
"the pi extension no longer joins the resume halves itself — no second composition root beside the bridge",
|
|
236
|
+
!/mux-resume-call/.test(PI_CODE) && !/entwurf-v2-visible-resume/.test(PI_CODE),
|
|
241
237
|
);
|
|
238
|
+
// The pass-through half the native claim used to carry, now on the one surface that has it:
|
|
239
|
+
// the handler hands the validated target to the v2 composition UNCHANGED. The schema proves the
|
|
240
|
+
// input is target-only; this proves the value that reaches the record lookup is that input.
|
|
241
|
+
const MCP_RESUME_BLOCK = (MCP_SURFACE.split('"entwurf_resume_call",')[1] ?? "").split("\n);")[0];
|
|
242
242
|
ok(
|
|
243
|
-
"[QK:MUXRESUME-
|
|
244
|
-
/
|
|
245
|
-
/visibleResume\(
|
|
246
|
-
!/model|task|prompt/.test(PI_BLOCK.split("parameters:")[1]?.split("async execute")[0] ?? ""),
|
|
243
|
+
"[QK:MUXRESUME-MCP-TARGET-PASSTHROUGH] the bridge handler passes its target straight to visibleResume — a rewritten, defaulted or substituted id would reopen a DIFFERENT citizen than the one the caller named",
|
|
244
|
+
/async \(\{ target \}\) =>/.test(MCP_RESUME_BLOCK) &&
|
|
245
|
+
/await visibleResume\(target, makeVisibleResumeDeps\(visibleResumeLaunch\)\)/.test(MCP_RESUME_BLOCK),
|
|
247
246
|
);
|
|
248
247
|
|
|
249
|
-
// ── the description contract
|
|
248
|
+
// ── the description contract ────────────────────────────────────────────────
|
|
250
249
|
// Not prose parity — three claims an operator acts on, plus the host's own cap.
|
|
251
|
-
const PI_DESC = (PI_BLOCK.split("description: `")[1] ?? "").split("`,")[0];
|
|
252
250
|
const MCP_DESC = (MCP_SURFACE.split('"entwurf_resume_call",')[1] ?? "").split("{")[0];
|
|
253
251
|
const statesTheContract = (text: string): boolean =>
|
|
254
252
|
/dormant/i.test(text) && /no turn/i.test(text) && /observation/i.test(text) && /launch/i.test(text);
|
|
255
253
|
|
|
256
|
-
for (const [which, text] of [
|
|
257
|
-
["native pi", PI_DESC],
|
|
258
|
-
["MCP bridge", MCP_DESC],
|
|
259
|
-
] as const) {
|
|
260
|
-
ok(
|
|
261
|
-
`${which}: the resume description fits the host's 2048-char tool-description cap`,
|
|
262
|
-
text.length > 0 && text.length <= 2048,
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
// The two claim tokens are written OUT, not interpolated: qualification requires each
|
|
266
|
-
// `[QK:…]` to appear literally exactly once in its gate source, so a token assembled at
|
|
267
|
-
// runtime names a claim no manifest can bind to.
|
|
268
254
|
ok(
|
|
269
|
-
"
|
|
270
|
-
|
|
255
|
+
"MCP bridge: the resume description fits the host's 2048-char tool-description cap",
|
|
256
|
+
MCP_DESC.length > 0 && MCP_DESC.length <= 2048,
|
|
271
257
|
);
|
|
258
|
+
// The claim token is written OUT, not interpolated: qualification requires each `[QK:…]` to
|
|
259
|
+
// appear literally exactly once in its gate source, so a token assembled at runtime names a
|
|
260
|
+
// claim no manifest can bind to.
|
|
272
261
|
ok(
|
|
273
|
-
"[QK:MUXRESUME-DESC-CONTRACT-MCP] MCP bridge: the description states the
|
|
262
|
+
"[QK:MUXRESUME-DESC-CONTRACT-MCP] MCP bridge: the description states the three things a caller acts on — DORMANT targets only, no turn is run, and TWO receipts whose observation is the fact that matters",
|
|
274
263
|
statesTheContract(MCP_DESC),
|
|
275
264
|
);
|
|
276
265
|
} finally {
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-pi-bridge-sender — the pi-session carrier is backed by a pi RECORD before the bridge uses it (#125).
|
|
3
|
+
*
|
|
4
|
+
* Since #125 a pi session reaches every Entwurf verb through the compiled entwurf-bridge (Pi's built-in
|
|
5
|
+
* MCP spawns it with the born gardenId as an explicit `PI_SESSION_ID`), so the bridge's pi-session
|
|
6
|
+
* rail is the main road. `resolveAuthoritativeSender` now checks, after the claims reconcile and
|
|
7
|
+
* before returning the pi branch, that the carrier names an existing, readable pi record — through
|
|
8
|
+
* the same per-entry targeted reader the sender/self policy already uses.
|
|
9
|
+
*
|
|
10
|
+
* WHY A SCRIPTS GATE AND NOT A BESIDE TEST: the subject is the bridge PROCESS answering a real
|
|
11
|
+
* `tools/call` over stdio (the same shape as check-codex-bridge-identity beside it); a vitest lane
|
|
12
|
+
* would have to boot that subprocess anyway. The store is a throwaway `ENTWURF_META_SESSIONS_DIR`, the
|
|
13
|
+
* control-socket dir a throwaway HOME: nothing on the operator host is read or written.
|
|
14
|
+
*
|
|
15
|
+
* Cells, in order (node:assert stops at the first failure, so each mutant dies at its own claim):
|
|
16
|
+
* RECORD-BACKED a pi record with NO control socket: identity is kept, replyable:false — the
|
|
17
|
+
* socket is replyability, never admission.
|
|
18
|
+
* MISSING-REFUSED a well-formed carrier with no record refuses loud; no anonymous/other-rail answer.
|
|
19
|
+
* BACKEND-REFUSED a carrier naming a record of ANOTHER backend refuses by name.
|
|
20
|
+
* UNREADABLE-CAUSE a record file that cannot be parsed refuses with ITS cause, not as "absent".
|
|
21
|
+
*/
|
|
22
|
+
import assert from "node:assert/strict";
|
|
23
|
+
import { spawn } from "node:child_process";
|
|
24
|
+
import * as fs from "node:fs";
|
|
25
|
+
import * as os from "node:os";
|
|
26
|
+
import * as path from "node:path";
|
|
27
|
+
import { createInterface } from "node:readline";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
|
|
30
|
+
import { upsertMetaSession } from "../pi-extensions/lib/meta-session.ts";
|
|
31
|
+
|
|
32
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
33
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-pi-bridge-sender-"));
|
|
34
|
+
const home = path.join(tmp, "home");
|
|
35
|
+
const sessionsDir = path.join(tmp, "meta-sessions");
|
|
36
|
+
fs.mkdirSync(home, { recursive: true });
|
|
37
|
+
|
|
38
|
+
function born(backend: "pi" | "claude-code", nativeSessionId: string): string {
|
|
39
|
+
return upsertMetaSession({
|
|
40
|
+
input: { backend, nativeSessionId, cwd: "/gate/pi-bridge-sender", model: "gate/model" },
|
|
41
|
+
dir: sessionsDir,
|
|
42
|
+
}).record.gardenId;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function selfAs(gardenId: string): Promise<{ text: string; isError: boolean }> {
|
|
46
|
+
const child = spawn("bash", [path.join(ROOT, "mcp/entwurf-bridge/start.sh")], {
|
|
47
|
+
cwd: ROOT,
|
|
48
|
+
env: {
|
|
49
|
+
PATH: process.env.PATH,
|
|
50
|
+
HOME: home,
|
|
51
|
+
PI_CODING_AGENT_DIR: path.join(home, ".pi", "agent"),
|
|
52
|
+
ENTWURF_META_SESSIONS_DIR: sessionsDir,
|
|
53
|
+
PI_SESSION_ID: gardenId,
|
|
54
|
+
PI_AGENT_ID: "gate/pi-bridge-sender",
|
|
55
|
+
},
|
|
56
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
57
|
+
});
|
|
58
|
+
let stderr = "";
|
|
59
|
+
child.stderr.setEncoding("utf8");
|
|
60
|
+
child.stderr.on("data", (chunk) => {
|
|
61
|
+
stderr += chunk;
|
|
62
|
+
});
|
|
63
|
+
const lines = createInterface({ input: child.stdout });
|
|
64
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), 10_000);
|
|
65
|
+
try {
|
|
66
|
+
const reply = await new Promise<unknown>((resolve, reject) => {
|
|
67
|
+
lines.on("line", (line) => {
|
|
68
|
+
const value = JSON.parse(line) as { id?: unknown };
|
|
69
|
+
if (value.id === 2) resolve(value);
|
|
70
|
+
});
|
|
71
|
+
child.once("error", reject);
|
|
72
|
+
child.once("exit", (code) => reject(new Error(`bridge exited ${code} before replying: ${stderr}`)));
|
|
73
|
+
const send = (msg: unknown): void => {
|
|
74
|
+
child.stdin.write(`${JSON.stringify(msg)}\n`);
|
|
75
|
+
};
|
|
76
|
+
send({
|
|
77
|
+
jsonrpc: "2.0",
|
|
78
|
+
id: 1,
|
|
79
|
+
method: "initialize",
|
|
80
|
+
params: { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "gate", version: "0" } },
|
|
81
|
+
});
|
|
82
|
+
send({ jsonrpc: "2.0", method: "notifications/initialized", params: {} });
|
|
83
|
+
send({ jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "entwurf_self", arguments: {} } });
|
|
84
|
+
});
|
|
85
|
+
const result = (reply as { result?: { content?: Array<{ text?: string }>; isError?: boolean } }).result;
|
|
86
|
+
assert.ok(result, `tools/call returned no result object (stderr: ${stderr})`);
|
|
87
|
+
return { text: result.content?.[0]?.text ?? "", isError: result.isError === true };
|
|
88
|
+
} finally {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
lines.close();
|
|
91
|
+
child.kill("SIGTERM");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const piId = born("pi", "01900000-0000-7000-8000-00000000a001");
|
|
97
|
+
const self = await selfAs(piId);
|
|
98
|
+
assert.ok(
|
|
99
|
+
!self.isError && new RegExp(`sessionId:\\s+${piId}`).test(self.text) && /replyable:\s+false/.test(self.text),
|
|
100
|
+
`[QK:PI-BRIDGE-SENDER-RECORD-BACKED] a pi record with no control socket keeps its identity and answers replyable:false — the socket is replyability, never admission (got: ${self.text})`,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const absentId = "20991231T235959-a0a0a0";
|
|
104
|
+
const absent = await selfAs(absentId);
|
|
105
|
+
assert.ok(
|
|
106
|
+
absent.isError && /no meta-record for garden id "20991231T235959-a0a0a0"/.test(absent.text),
|
|
107
|
+
`[QK:PI-BRIDGE-SENDER-MISSING-REFUSED] a well-formed PI_SESSION_ID with no record refuses loud, naming the absent record — no anonymous or other-rail answer (got: ${absent.text})`,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const claudeId = born("claude-code", "00000000-0000-4000-8000-00000000c001");
|
|
111
|
+
const foreign = await selfAs(claudeId);
|
|
112
|
+
assert.ok(
|
|
113
|
+
foreign.isError && /names a claude-code record, not a pi citizen/.test(foreign.text),
|
|
114
|
+
`[QK:PI-BRIDGE-SENDER-BACKEND-REFUSED] a PI_SESSION_ID naming another backend's record refuses by name (got: ${foreign.text})`,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const brokenId = born("pi", "01900000-0000-7000-8000-00000000a002");
|
|
118
|
+
fs.writeFileSync(path.join(sessionsDir, `${brokenId}.meta.json`), "{ not json");
|
|
119
|
+
const broken = await selfAs(brokenId);
|
|
120
|
+
assert.ok(
|
|
121
|
+
broken.isError && !/no meta-record for garden id/.test(broken.text) && /not valid JSON/.test(broken.text),
|
|
122
|
+
`[QK:PI-BRIDGE-SENDER-UNREADABLE-CAUSE] an unreadable pi record refuses with ITS own cause, never flattened into "absent" (got: ${broken.text})`,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
console.log(
|
|
126
|
+
"check-pi-bridge-sender: a pi-session carrier is served only when it names a readable pi record; missing, foreign-backend and unreadable records each refuse with their own cause, and socket absence stays replyability",
|
|
127
|
+
);
|
|
128
|
+
} finally {
|
|
129
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
130
|
+
}
|