@junghanacs/entwurf 0.18.2 → 0.20.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 +2 -2
- package/BASELINE.md +23 -5
- package/CHANGELOG.md +415 -0
- package/DELIVERY.md +59 -6
- package/README.md +43 -12
- package/VERIFY.md +10 -2
- package/docs/acp-backend-rail.md +9 -1
- package/docs/external-mcp-host.md +8 -5
- package/docs/setup-clean-host.md +93 -7
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +18 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +3 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +6 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +76 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-placement.js +54 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/resolve-tmux-session.js +114 -0
- package/mcp/entwurf-bridge/src/index.ts +22 -7
- package/package.json +2 -2
- package/pi-extensions/acp-provider.ts +1 -1
- package/pi-extensions/entwurf-control.ts +27 -3
- package/pi-extensions/lib/acp/models.ts +3 -3
- package/pi-extensions/lib/acp/overlay.ts +6 -2
- package/pi-extensions/lib/meta-session.ts +25 -0
- package/pi-extensions/lib/mux-fresh-call.ts +98 -11
- package/pi-extensions/lib/mux-placement.ts +65 -10
- package/pi-extensions/lib/resolve-tmux-session.ts +137 -0
- package/run.sh +71 -26
- package/scripts/agy-bridge.sh +1 -1
- package/scripts/agy-imprint.sh +10 -7
- package/scripts/check-acp-carrier-augment.ts +6 -5
- package/scripts/check-acp-cortex.ts +41 -0
- package/scripts/check-acp-provider-surface.ts +9 -6
- package/scripts/check-copilot-birth-hook.ts +3 -1
- package/scripts/check-copilot-receive-arm.ts +169 -21
- package/scripts/check-copilot-statusline.ts +3 -1
- package/scripts/check-fresh-cut-gate.sh +54 -2
- package/scripts/check-gate-qualification.ts +13 -12
- package/scripts/check-hook-launch-topology.ts +6 -5
- package/scripts/check-install-surface.ts +2 -1
- package/scripts/check-meta-facts.ts +6 -4
- package/scripts/check-meta-hook-session-switch.ts +4 -3
- package/scripts/check-meta-identity-consumers.ts +8 -7
- package/scripts/check-meta-receiver-marker.ts +2 -1
- package/scripts/check-mux-launch-tmux.ts +77 -2
- package/scripts/check-mux-launch.ts +17 -0
- package/scripts/check-mux-placement-tmux.ts +61 -1
- package/scripts/check-mux-placement.ts +33 -0
- package/scripts/check-omp-birth-hook.ts +3 -2
- package/scripts/check-probe-bridge-command.ts +3 -1
- package/scripts/check-setup-qualification.sh +21 -0
- package/scripts/copilot-bridge-doctor.sh +15 -5
- package/scripts/copilot-bridge-install.sh +6 -2
- package/scripts/copilot-launch.sh +16 -2
- package/scripts/copilot-receive-bridge.sh +80 -13
- package/scripts/lib/reclaim-on-exit.ts +86 -0
- package/scripts/meta-bridge-doctor.sh +56 -15
- package/scripts/meta-bridge-install.sh +13 -7
- package/scripts/meta-bridge-uninstall.sh +6 -4
- package/scripts/mutants/acp-cortex.json +26 -2
- package/scripts/mutants/copilot-receive.json +26 -0
- package/scripts/mutants/fresh-cut.json +26 -0
- package/scripts/mutants/mux-boundary.json +24 -0
- package/scripts/mutants/mux-fresh-call.json +81 -0
- package/scripts/mutants/omp-birth.json +33 -3
- package/scripts/mutants/setup-verdict.json +13 -0
- package/scripts/omp-bridge-doctor.sh +110 -31
- package/scripts/omp-bridge-install.sh +6 -2
- package/scripts/omp-receive-install.sh +7 -2
- package/scripts/raw-async-delivery/README.md +10 -1
- package/scripts/raw-codex-measure/README.md +689 -0
- package/scripts/raw-codex-measure/source-audit.md +243 -0
- package/scripts/raw-macos-measure/README.md +148 -0
- package/scripts/raw-macos-measure/probe.sh +389 -0
- package/scripts/smoke-meta-async-drift.sh +7 -5
- package/scripts/smoke-meta-install-state.sh +70 -19
- package/scripts/smoke-mux-fresh-call-live.ts +175 -3
- package/scripts/smoke-omp-bridge-state.sh +43 -2
- package/scripts/smoke-setup-verdict.sh +100 -0
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
upsertMetaSession,
|
|
61
61
|
writeMetaSenderMarker,
|
|
62
62
|
} from "../pi-extensions/lib/meta-session.ts";
|
|
63
|
+
import { killOnExit, reclaimNow, reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
63
64
|
|
|
64
65
|
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
65
66
|
const UNIT = "entwurf-receive";
|
|
@@ -90,7 +91,7 @@ async function waitUntil(fn: () => boolean, ms = 15000): Promise<boolean> {
|
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
const root = mkdtempSync(path.join(tmpdir(), "entwurf-copilot-receive."));
|
|
94
|
+
const root = reclaimOnExit(mkdtempSync(path.join(tmpdir(), "entwurf-copilot-receive.")));
|
|
94
95
|
const home = path.join(root, "home");
|
|
95
96
|
const store = path.join(root, "agent");
|
|
96
97
|
const extRoot = path.join(root, "extensions");
|
|
@@ -260,6 +261,30 @@ writeFileSync(
|
|
|
260
261
|
path.join(stubDir, "register.mjs"),
|
|
261
262
|
`import { register } from "node:module";
|
|
262
263
|
register("./loader.mjs", import.meta.url);
|
|
264
|
+
// The vendor half this fixture MUST model. Production children are started by the CLI's
|
|
265
|
+
// own bootstrap (\`preloads/extension_bootstrap.mjs\`), which exits the extension when its
|
|
266
|
+
// parent disappears — checked at startup and then once a second — and the receiver's
|
|
267
|
+
// owner-pid contract is built on that. A stub that omitted it made the fixture MORE
|
|
268
|
+
// permissive than production: the stub's referenced timer kept every child alive after
|
|
269
|
+
// the gate died, so each red run reparented its children to pid 1 forever (measured
|
|
270
|
+
// 2026-09-07/08 on \`oracle\`: 360 such node processes, 720 temp roots). The check is
|
|
271
|
+
// \`unref\`ed so it never becomes the reason a child stays up.
|
|
272
|
+
//
|
|
273
|
+
// KNOWN-OPEN, deliberately: under a subreaper (\`PR_SET_CHILD_SUBREAPER\`, systemd-run)
|
|
274
|
+
// a parent that dies before this module's FIRST tick is already reparented to a LIVE
|
|
275
|
+
// subreaper, so \`parentAtStart\` records that pid and never changes again. The obvious
|
|
276
|
+
// patch — probing the recorded pid with signal 0 — closes nothing: Linux reparents in
|
|
277
|
+
// \`forget_original_parent()\` BEFORE the old pid becomes unreachable, and a zombie
|
|
278
|
+
// parent still answers signal 0, so the probe is false at startup in exactly the case
|
|
279
|
+
// it was written for. The honest close is to watch the DECLARED parent
|
|
280
|
+
// (\`COPILOT_EXTENSION_PARENT_PID\`), which this gate deliberately sets to a pid that is
|
|
281
|
+
// not the child's real parent, so it is left documented rather than half-fixed. The
|
|
282
|
+
// window is microseconds and no measured orphan on this host came through it.
|
|
283
|
+
const parentAtStart = process.ppid;
|
|
284
|
+
if (parentAtStart === 1) process.exit(0);
|
|
285
|
+
setInterval(() => {
|
|
286
|
+
if (process.ppid !== parentAtStart) process.exit(0);
|
|
287
|
+
}, 1000).unref();
|
|
263
288
|
`,
|
|
264
289
|
);
|
|
265
290
|
|
|
@@ -274,23 +299,25 @@ interface Harness {
|
|
|
274
299
|
function launch(label: string, sessionId: string, extra: NodeJS.ProcessEnv = {}): Harness {
|
|
275
300
|
const box = path.join(root, `box-${label}`);
|
|
276
301
|
mkdirSync(box, { recursive: true });
|
|
277
|
-
const child =
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
302
|
+
const child = killOnExit(
|
|
303
|
+
spawn(process.execPath, ["--import", path.join(stubDir, "register.mjs"), entry], {
|
|
304
|
+
env: {
|
|
305
|
+
...process.env,
|
|
306
|
+
HOME: home,
|
|
307
|
+
PI_CODING_AGENT_DIR: store,
|
|
308
|
+
STUB_BOX: box,
|
|
309
|
+
STUB_SDK: path.join(stubDir, "extension.js"),
|
|
310
|
+
STUB_SESSION_ID: sessionId,
|
|
311
|
+
SESSION_ID: sessionId,
|
|
312
|
+
// The vendor's carrier. This gate process IS the fork parent, which is also
|
|
313
|
+
// what makes the sender-marker join real rather than mocked: the marker below
|
|
314
|
+
// is written under the very pid the child will look itself up by.
|
|
315
|
+
COPILOT_EXTENSION_PARENT_PID: String(process.pid),
|
|
316
|
+
...extra,
|
|
317
|
+
},
|
|
318
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
319
|
+
}),
|
|
320
|
+
);
|
|
294
321
|
child.stdout?.resume();
|
|
295
322
|
child.stderr?.resume();
|
|
296
323
|
return {
|
|
@@ -699,6 +726,68 @@ ok(
|
|
|
699
726
|
opaque.kill("SIGKILL");
|
|
700
727
|
}
|
|
701
728
|
}
|
|
729
|
+
{
|
|
730
|
+
// AN UNREADABLE argv IS "UNKNOWN", NOT "ABSENT" (#78 B2). Identity is the FIRST
|
|
731
|
+
// predicate of the scan, so a candidate whose `cmdline` will not open cannot be
|
|
732
|
+
// classified at all — and a blanket `except OSError: return None` classified it
|
|
733
|
+
// anyway, as "not a Copilot CLI". Every candidate taking that branch left all
|
|
734
|
+
// counters at zero, which is the benign "no live GitHub Copilot CLI process" note:
|
|
735
|
+
// a doctor claiming ABSENCE while its identity input was unavailable. That is the
|
|
736
|
+
// same false-success class as the platform branch above, so it gets the same verdict.
|
|
737
|
+
//
|
|
738
|
+
// THE DENIAL IS INJECTED, AND THAT IS SAID OUT LOUD BECAUSE IT MATTERS. The environ
|
|
739
|
+
// cell above makes its file really unreadable with prctl(PR_SET_DUMPABLE, 0), and
|
|
740
|
+
// measured on this kernel that trick does NOT reach argv: `/proc/<pid>/cmdline` stays
|
|
741
|
+
// mode 0444 and readable even for a non-dumpable child (a full /proc census read
|
|
742
|
+
// 340/340 cmdlines). A real EACCES there needs a `hidepid` /proc mount, which an
|
|
743
|
+
// unprivileged gate cannot create. So the refusal is raised exactly where the kernel
|
|
744
|
+
// would raise it — at the scanner's own read, with EACCES — through a `sitecustomize`
|
|
745
|
+
// on PYTHONPATH, and what this cell pins is the CLASSIFICATION of a refused read,
|
|
746
|
+
// which is the entire contract. The fixture process is real, and the control run
|
|
747
|
+
// asserts it WOULD be identified as a native CLI when its argv can be read, so
|
|
748
|
+
// neither half of the pin is vacuous.
|
|
749
|
+
const denyDir = path.join(root, "deny-cmdline");
|
|
750
|
+
mkdirSync(denyDir, { recursive: true });
|
|
751
|
+
writeFileSync(
|
|
752
|
+
path.join(denyDir, "sitecustomize.py"),
|
|
753
|
+
[
|
|
754
|
+
"import errno, os, pathlib",
|
|
755
|
+
"_pid = os.environ.get('ENTWURF_TEST_DENY_CMDLINE_PID')",
|
|
756
|
+
"if _pid:",
|
|
757
|
+
" _target = '/proc/' + _pid + '/cmdline'",
|
|
758
|
+
" _real = pathlib.Path.read_bytes",
|
|
759
|
+
" def _denied(self):",
|
|
760
|
+
" if str(self) == _target:",
|
|
761
|
+
" raise PermissionError(errno.EACCES, 'Permission denied', str(self))",
|
|
762
|
+
" return _real(self)",
|
|
763
|
+
" pathlib.Path.read_bytes = _denied",
|
|
764
|
+
].join("\n") + "\n",
|
|
765
|
+
);
|
|
766
|
+
const opaqueArgv = spawn(
|
|
767
|
+
"python3",
|
|
768
|
+
["-c", "import time; time.sleep(60)", path.join(root, "node_modules", "@github", "copilot", "npm-loader.js")],
|
|
769
|
+
{ stdio: "ignore" },
|
|
770
|
+
);
|
|
771
|
+
running.push(opaqueArgv);
|
|
772
|
+
try {
|
|
773
|
+
await sleep(400);
|
|
774
|
+
const identified = doctor({ ENTWURF_COPILOT_RECEIVE_PIDS: String(opaqueArgv.pid) });
|
|
775
|
+
const blind = doctor({
|
|
776
|
+
ENTWURF_COPILOT_RECEIVE_PIDS: String(opaqueArgv.pid),
|
|
777
|
+
PYTHONPATH: denyDir,
|
|
778
|
+
ENTWURF_TEST_DENY_CMDLINE_PID: String(opaqueArgv.pid),
|
|
779
|
+
});
|
|
780
|
+
ok(
|
|
781
|
+
"[QK:COPILOT-RECEIVE-DOCTOR-UNREADABLE-ARGV-IS-UNVERIFIABLE] a live process whose argv is REFUSED is UNKNOWN, not absent — the doctor declines to claim that no CLI is running and ends RED, where the same process with a readable argv is judged",
|
|
782
|
+
identified.includes("lack COPILOT_CLI_ENABLED_FEATURE_FLAGS") &&
|
|
783
|
+
blind.includes("UNVERIFIABLE — the argv of 1 live process(es) could not be read") &&
|
|
784
|
+
!blind.includes("no live GitHub Copilot CLI process") &&
|
|
785
|
+
blind.trimEnd().endsWith("[copilot-receive-doctor] FAIL"),
|
|
786
|
+
);
|
|
787
|
+
} finally {
|
|
788
|
+
opaqueArgv.kill("SIGKILL");
|
|
789
|
+
}
|
|
790
|
+
}
|
|
702
791
|
{
|
|
703
792
|
// THE SCANNER'S OWN FAILURE IS THIS DOCTOR'S VERDICT, NOT ITS DEATH. Under
|
|
704
793
|
// `set -euo pipefail`, `x="$(python3 …)"` hands the interpreter's status to the
|
|
@@ -725,10 +814,69 @@ ok(
|
|
|
725
814
|
);
|
|
726
815
|
}
|
|
727
816
|
|
|
728
|
-
// ── 10. the
|
|
817
|
+
// ── 10. the fixture dies with its parent, like the vendor's child does ───────
|
|
818
|
+
// This gate spawns real Node children that hold a referenced timer, so "the gate
|
|
819
|
+
// stopped" and "the gate's children stopped" are two different facts. Production gets
|
|
820
|
+
// the second one from the CLI's bootstrap; the stub loader has to supply it here, or a
|
|
821
|
+
// red run (which ends by throwing, long before any cleanup statement) leaves a process
|
|
822
|
+
// per launch on the host forever. Orphan one deliberately and require it to leave on
|
|
823
|
+
// its own. The intermediate `bash` holds the child for two seconds first: a child whose
|
|
824
|
+
// parent is ALREADY gone when its loader runs leaves at the watchdog's startup check,
|
|
825
|
+
// which proves nothing about a session that was live when the gate died.
|
|
826
|
+
{
|
|
827
|
+
const orphanBox = path.join(root, "box-orphan");
|
|
828
|
+
mkdirSync(orphanBox, { recursive: true });
|
|
829
|
+
const pidFile = path.join(root, "orphan.pid");
|
|
830
|
+
const ORPHAN_SESSION = "cop-recv-orphan";
|
|
831
|
+
spawnSync(
|
|
832
|
+
"bash",
|
|
833
|
+
[
|
|
834
|
+
"-c",
|
|
835
|
+
`${JSON.stringify(process.execPath)} --import ${JSON.stringify(path.join(stubDir, "register.mjs"))} ` +
|
|
836
|
+
`${JSON.stringify(entry)} </dev/null >/dev/null 2>&1 & echo $! > ${JSON.stringify(pidFile)}; sleep 2`,
|
|
837
|
+
],
|
|
838
|
+
{
|
|
839
|
+
env: {
|
|
840
|
+
...process.env,
|
|
841
|
+
HOME: home,
|
|
842
|
+
PI_CODING_AGENT_DIR: store,
|
|
843
|
+
STUB_BOX: orphanBox,
|
|
844
|
+
STUB_SDK: path.join(stubDir, "extension.js"),
|
|
845
|
+
STUB_SESSION_ID: ORPHAN_SESSION,
|
|
846
|
+
SESSION_ID: ORPHAN_SESSION,
|
|
847
|
+
COPILOT_EXTENSION_PARENT_PID: String(process.pid),
|
|
848
|
+
},
|
|
849
|
+
stdio: "ignore",
|
|
850
|
+
},
|
|
851
|
+
);
|
|
852
|
+
const orphanPid = Number(readFileSync(pidFile, "utf8").trim());
|
|
853
|
+
// Non-vacuity: it is only evidence if the process it names actually reached the
|
|
854
|
+
// same live state every other child in this gate reaches. Its log line is written
|
|
855
|
+
// during boot and outlives it, so reading it after the exit is not a race.
|
|
856
|
+
const booted = await waitUntil(() => {
|
|
857
|
+
const f = path.join(store, "meta-bridge-receive-copilot.log");
|
|
858
|
+
return existsSync(f) && readFileSync(f, "utf8").includes(`joined session=${ORPHAN_SESSION}`);
|
|
859
|
+
});
|
|
860
|
+
ok("the orphaned child booted the real unit (so its exit below is not a startup crash)", booted);
|
|
861
|
+
const gone = await waitUntil(() => {
|
|
862
|
+
try {
|
|
863
|
+
process.kill(orphanPid, 0);
|
|
864
|
+
return false;
|
|
865
|
+
} catch {
|
|
866
|
+
return true; // ESRCH — the pid is gone
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
ok(
|
|
870
|
+
"[QK:COPILOT-RECEIVE-STUB-CHILD-DIES-WITH-PARENT] a fixture child whose parent disappeared exits by " +
|
|
871
|
+
"itself — no pid-1 receiver survives a red or interrupted run",
|
|
872
|
+
gone,
|
|
873
|
+
);
|
|
874
|
+
if (!gone) process.kill(orphanPid, "SIGKILL");
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// ── 11. the installer's honest inverse ───────────────────────────────────────
|
|
729
878
|
execFileSync("bash", [path.join(REPO, "run.sh"), "uninstall-copilot-receive"], { env: installEnv, stdio: "pipe" });
|
|
730
879
|
ok("uninstall removes the unit it installed and clears its state", !existsSync(unitDir) && !existsSync(stateFile));
|
|
731
880
|
|
|
732
|
-
|
|
733
|
-
rmSync(root, { recursive: true, force: true });
|
|
881
|
+
reclaimNow();
|
|
734
882
|
console.log(`\n[check-copilot-receive-arm] PASS (${passed} assertions)`);
|
|
@@ -13,6 +13,7 @@ import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
|
13
13
|
import { tmpdir } from "node:os";
|
|
14
14
|
import * as path from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
16
17
|
|
|
17
18
|
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
18
19
|
const SCRIPT = path.join(REPO, "scripts", "copilot-statusline.sh");
|
|
@@ -27,7 +28,8 @@ function ok(label: string, cond: boolean): void {
|
|
|
27
28
|
passed++;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
const root = mkdtempSync(path.join(tmpdir(), "entwurf-copilot-statusline."));
|
|
31
|
+
const root = reclaimOnExit(mkdtempSync(path.join(tmpdir(), "entwurf-copilot-statusline.")));
|
|
32
|
+
|
|
31
33
|
const home = path.join(root, "home");
|
|
32
34
|
const store = path.join(root, "meta-sessions");
|
|
33
35
|
mkdirSync(home);
|
|
@@ -403,12 +403,44 @@ fi
|
|
|
403
403
|
# #86 A5 credential tripwire: the retired sync_auth mutation (OAuth alias copy +
|
|
404
404
|
# auth.json.bak) must never resurrect as an invocable surface in run.sh. Comments
|
|
405
405
|
# documenting the removal are allowed; a function definition or call is not.
|
|
406
|
-
|
|
406
|
+
# THE PREDICATE IS A FUNCTION so the self-test below can run the SAME code against a planted
|
|
407
|
+
# fixture. A self-test that re-typed this pipeline would keep passing while the real one went
|
|
408
|
+
# blind, which is precisely the failure this cell exists to prevent.
|
|
409
|
+
#
|
|
410
|
+
# `grep -c`, never `grep -q`. This file runs under pipefail, and a `-q` grep exits at the FIRST
|
|
411
|
+
# match, closing the pipe while `awk` is still writing the remaining ~200KB of a 433KB run.sh.
|
|
412
|
+
# `awk` dies of SIGPIPE (141), pipefail promotes that to the pipeline's status, and the `if`
|
|
413
|
+
# takes the ELSE branch — so this tripwire went blind EXACTLY when a sync_auth surface existed
|
|
414
|
+
# and reported "no credential surface remains". Unlike the omp doctor's load-dependent race this
|
|
415
|
+
# one was deterministic, because the producer is far larger than the pipe buffer.
|
|
416
|
+
sync_auth_surface_count() {
|
|
417
|
+
awk '!/^[[:space:]]*#/' "$1" | grep -c 'sync_auth' || true
|
|
418
|
+
}
|
|
419
|
+
sync_auth_hits="$(sync_auth_surface_count "$REPO/run.sh")"
|
|
420
|
+
if [ "${sync_auth_hits:-0}" -gt 0 ]; then
|
|
407
421
|
bad "D4b the retired sync_auth credential mutation reappeared as code in run.sh"
|
|
408
422
|
else
|
|
409
423
|
ok "D4b no sync_auth credential surface remains in run.sh (comments only)"
|
|
410
424
|
fi
|
|
411
425
|
|
|
426
|
+
# D4b-self. The cell above is green on a clean run.sh — which is exactly the state in which a
|
|
427
|
+
# blind tripwire and a working one are indistinguishable. This plants the surface it exists to
|
|
428
|
+
# catch and requires the SAME predicate to see it. The plant goes NEAR THE TOP of the real,
|
|
429
|
+
# large run.sh on purpose: the defect only appears when the consumer can exit while the producer
|
|
430
|
+
# still has bulk left to write, so a small synthetic fixture would pass even with `grep -q` and
|
|
431
|
+
# would prove nothing. It is a copy under mktemp; run.sh is never written.
|
|
432
|
+
fc_probe="$(mktemp -t fresh-cut-syncauth-probe.XXXXXX)"
|
|
433
|
+
awk 'NR==2{print "sync_auth() { cp \"$HOME/.claude/.credentials.json\" \"$HOME/.claude/auth.json.bak\"; }"}1' \
|
|
434
|
+
"$REPO/run.sh" >"$fc_probe"
|
|
435
|
+
fc_planted="$(sync_auth_surface_count "$fc_probe")"
|
|
436
|
+
fc_clean="$(sync_auth_surface_count "$REPO/run.sh")"
|
|
437
|
+
rm -f "$fc_probe"
|
|
438
|
+
if [ "${fc_planted:-0}" -gt 0 ] && [ "${fc_clean:-0}" -eq 0 ]; then
|
|
439
|
+
ok "D4b-self the tripwire actually FIRES on a planted sync_auth surface (planted=$fc_planted clean=$fc_clean)"
|
|
440
|
+
else
|
|
441
|
+
bad "[QK:FRESHCUT-SYNCAUTH-TRIPWIRE-FIRES] D4b's predicate did not see a planted sync_auth surface (planted=$fc_planted clean=$fc_clean) — the credential tripwire is blind and a green D4b means nothing"
|
|
442
|
+
fi
|
|
443
|
+
|
|
412
444
|
ilp_gate=$(awk '/^install_local_package\(\)/,/^}/{ if ($0 ~ /preflight_v3_store install/) { print NR; exit } }' "$REPO/run.sh")
|
|
413
445
|
ilp_write=$(awk '/^install_local_package\(\)/,/^}/{ if ($0 ~ /register-pi-package.py|mkdir -p "\$project_dir/) { print NR; exit } }' "$REPO/run.sh")
|
|
414
446
|
if [ -n "$ilp_gate" ] && [ -n "$ilp_write" ] && [ "$ilp_gate" -lt "$ilp_write" ]; then
|
|
@@ -425,12 +457,32 @@ else
|
|
|
425
457
|
bad "D6 meta-bridge-install.sh's doctor call is missing or sits after the state snapshot (doctor=$mb_gate prepare=$mb_write)"
|
|
426
458
|
fi
|
|
427
459
|
|
|
428
|
-
|
|
460
|
+
# Same hazard as D4b. This producer happens to emit less than one pipe buffer today, so
|
|
461
|
+
# no SIGPIPE window exists — but that is a size coincidence, not a property, and it moves
|
|
462
|
+
# the moment run.sh grows another match. Counted rather than short-circuited.
|
|
463
|
+
preflight_cut_count() {
|
|
464
|
+
grep -n 'preflight_v3_store()' -A 40 "$1" | grep -cE 'run_ts scripts/meta-bridge-fresh-cut\.ts' || true
|
|
465
|
+
}
|
|
466
|
+
cut_in_preflight="$(preflight_cut_count "$REPO/run.sh")"
|
|
467
|
+
if [ "${cut_in_preflight:-0}" -gt 0 ]; then
|
|
429
468
|
bad "D7 the preflight invokes fresh-cut — an install must never cut a generation by itself"
|
|
430
469
|
else
|
|
431
470
|
ok "D7 the preflight only ever asks the doctor (never runs the cut)"
|
|
432
471
|
fi
|
|
433
472
|
|
|
473
|
+
# D7-self, same argument as D4b-self: a forbidding cell that is green on a clean tree proves
|
|
474
|
+
# nothing until it is shown catching the thing it forbids. A synthetic fixture is enough here
|
|
475
|
+
# because this producer has no SIGPIPE window to reproduce — what is being proven is detection.
|
|
476
|
+
d7_probe="$(mktemp -t fresh-cut-preflight-probe.XXXXXX)"
|
|
477
|
+
printf '%s\n' 'preflight_v3_store() {' ' run_ts scripts/meta-bridge-fresh-cut.ts' '}' >"$d7_probe"
|
|
478
|
+
d7_planted="$(preflight_cut_count "$d7_probe")"
|
|
479
|
+
rm -f "$d7_probe"
|
|
480
|
+
if [ "${d7_planted:-0}" -gt 0 ] && [ "${cut_in_preflight:-0}" -eq 0 ]; then
|
|
481
|
+
ok "D7-self the cell actually FIRES on a preflight that does call fresh-cut (planted=$d7_planted real=$cut_in_preflight)"
|
|
482
|
+
else
|
|
483
|
+
bad "[QK:FRESHCUT-PREFLIGHT-CUT-DETECTED] D7's predicate did not see a planted fresh-cut call in a preflight (planted=$d7_planted real=$cut_in_preflight)"
|
|
484
|
+
fi
|
|
485
|
+
|
|
434
486
|
if [ -e "$FRESH_CUT_GATE_CLAUDE_SENTINEL" ]; then
|
|
435
487
|
bad "D8 an offline gate drive crossed the store refusal and invoked Claude" "$(cat "$FRESH_CUT_GATE_CLAUDE_SENTINEL")"
|
|
436
488
|
else
|
|
@@ -66,6 +66,7 @@ import {
|
|
|
66
66
|
validateManifest,
|
|
67
67
|
validateManifestSet,
|
|
68
68
|
} from "./lib/mutation-qualify.ts";
|
|
69
|
+
import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
69
70
|
|
|
70
71
|
const REPO_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
71
72
|
const MUTANTS_DIR = path.join(REPO_DIR, "scripts", "mutants");
|
|
@@ -135,7 +136,7 @@ function checkVitestAttribution(): void {
|
|
|
135
136
|
],
|
|
136
137
|
});
|
|
137
138
|
|
|
138
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-attribution-selftest-"));
|
|
139
|
+
const dir = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-attribution-selftest-")));
|
|
139
140
|
const reportPath = path.join(dir, "vitest-report.json");
|
|
140
141
|
try {
|
|
141
142
|
fs.writeFileSync(reportPath, report);
|
|
@@ -422,10 +423,10 @@ if (process.argv.includes("--attribution-self-test")) {
|
|
|
422
423
|
// ═══ Phase 1c — the pipeline over a synthetic fixture repo ══════════════════
|
|
423
424
|
|
|
424
425
|
function buildFixtureOrigin(): { dir: string; externalTarget: string } {
|
|
425
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-qualify-fixture-"));
|
|
426
|
+
const dir = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-qualify-fixture-")));
|
|
426
427
|
// A file OUTSIDE the fixture repo, reachable only through a tracked symlink — the
|
|
427
428
|
// P0-1 escape shape: mutating `linked.txt` would write THIS file.
|
|
428
|
-
const externalDir = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-qualify-external-"));
|
|
429
|
+
const externalDir = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-qualify-external-")));
|
|
429
430
|
const externalTarget = path.join(externalDir, "external-target.txt");
|
|
430
431
|
fs.writeFileSync(externalTarget, "original-line outside the sandbox\n");
|
|
431
432
|
const write = (rel: string, body: string, mode = 0o644): void => {
|
|
@@ -745,7 +746,7 @@ const quiet = (): void => {};
|
|
|
745
746
|
|
|
746
747
|
// pgroup kill: the grandchild really dies on this host (hardening #6, Linux axis).
|
|
747
748
|
{
|
|
748
|
-
const invocationDir = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-qualify-pgk-"));
|
|
749
|
+
const invocationDir = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-qualify-pgk-")));
|
|
749
750
|
const pidFile = path.join(invocationDir, "grandchild.pid");
|
|
750
751
|
const script = path.join(invocationDir, "spawner.sh");
|
|
751
752
|
fs.writeFileSync(script, `#!/usr/bin/env bash\nsleep 300 & echo $! > ${JSON.stringify(pidFile)}\nwait\n`);
|
|
@@ -773,7 +774,7 @@ const quiet = (): void => {};
|
|
|
773
774
|
|
|
774
775
|
// Stale-snapshot sweep: dead-pid residue is reclaimed, a live runner's dir survives.
|
|
775
776
|
{
|
|
776
|
-
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-qualify-sweep-"));
|
|
777
|
+
const tmpRoot = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-qualify-sweep-")));
|
|
777
778
|
const deadDir = path.join(tmpRoot, "entwurf-qualify-dead1");
|
|
778
779
|
fs.mkdirSync(deadDir);
|
|
779
780
|
fs.writeFileSync(path.join(deadDir, "runner.json"), JSON.stringify({ pid: 999_999_999, startedAt: 0 }));
|
|
@@ -820,7 +821,7 @@ let manifestCount: number;
|
|
|
820
821
|
// Extend this inventory and the manifests TOGETHER, never silently.
|
|
821
822
|
const EXPECTED_LANE_MUTANTS: Record<string, number> = {
|
|
822
823
|
"acp-augment": 10,
|
|
823
|
-
"acp-cortex":
|
|
824
|
+
"acp-cortex": 14,
|
|
824
825
|
"acp-launch-namespace": 2,
|
|
825
826
|
"acp-overlay": 1,
|
|
826
827
|
"acp-prompt-lifecycle": 15,
|
|
@@ -833,21 +834,21 @@ let manifestCount: number;
|
|
|
833
834
|
"capability-cache": 3,
|
|
834
835
|
"copilot-birth": 19,
|
|
835
836
|
"copilot-launch": 14,
|
|
836
|
-
"copilot-receive":
|
|
837
|
-
"fresh-cut":
|
|
837
|
+
"copilot-receive": 20,
|
|
838
|
+
"fresh-cut": 3,
|
|
838
839
|
"gate-qualification": 2,
|
|
839
840
|
"meta-facts": 4,
|
|
840
841
|
"meta-hook-session-switch": 17,
|
|
841
842
|
"meta-identity": 4,
|
|
842
843
|
"meta-retire": 3,
|
|
843
|
-
"mux-boundary":
|
|
844
|
-
"mux-fresh-call":
|
|
844
|
+
"mux-boundary": 16,
|
|
845
|
+
"mux-fresh-call": 44,
|
|
845
846
|
"mux-launcher-fence": 7,
|
|
846
847
|
"mux-parent-artifact": 3,
|
|
847
848
|
"pack-install": 2,
|
|
848
849
|
"pi-package-ownership": 6,
|
|
849
850
|
"mux-resume-call": 12,
|
|
850
|
-
"omp-birth":
|
|
851
|
+
"omp-birth": 13,
|
|
851
852
|
"omp-fresh": 24,
|
|
852
853
|
"omp-receive": 11,
|
|
853
854
|
"probe-ordering": 1,
|
|
@@ -855,7 +856,7 @@ let manifestCount: number;
|
|
|
855
856
|
"resume-args": 6,
|
|
856
857
|
"resume-launch-identity": 6,
|
|
857
858
|
"self-address": 5,
|
|
858
|
-
"setup-verdict":
|
|
859
|
+
"setup-verdict": 14,
|
|
859
860
|
"source-install": 2,
|
|
860
861
|
"v2-surface": 7,
|
|
861
862
|
"v2-visible-resume": 17,
|
|
@@ -45,6 +45,7 @@ import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSyn
|
|
|
45
45
|
import { tmpdir } from "node:os";
|
|
46
46
|
import * as path from "node:path";
|
|
47
47
|
import { fileURLToPath } from "node:url";
|
|
48
|
+
import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
48
49
|
|
|
49
50
|
let passed = 0;
|
|
50
51
|
function ok(label: string, cond: boolean): void {
|
|
@@ -272,7 +273,7 @@ function hookEnv(agentRoot: string, pluginRoot: string): NodeJS.ProcessEnv {
|
|
|
272
273
|
}
|
|
273
274
|
|
|
274
275
|
function driveHook(label: string, pluginRoot: string, nativeSessionId: string): void {
|
|
275
|
-
const agentRoot = mkdtempSync(path.join(tmpdir(), "psa-hook-topology-"));
|
|
276
|
+
const agentRoot = reclaimOnExit(mkdtempSync(path.join(tmpdir(), "psa-hook-topology-")));
|
|
276
277
|
const leaf = leafOf("SessionStart");
|
|
277
278
|
const exe = resolveEl(leaf.command ?? "", pluginRoot);
|
|
278
279
|
const argv = (leaf.args ?? []).map((a) => resolveEl(a, pluginRoot));
|
|
@@ -317,7 +318,7 @@ function driveHook(label: string, pluginRoot: string, nativeSessionId: string):
|
|
|
317
318
|
}
|
|
318
319
|
|
|
319
320
|
{
|
|
320
|
-
const plain = mkdtempSync(path.join(tmpdir(), "psa-hook-bundle-"));
|
|
321
|
+
const plain = reclaimOnExit(mkdtempSync(path.join(tmpdir(), "psa-hook-bundle-")));
|
|
321
322
|
makeBundle(plain);
|
|
322
323
|
driveHook("exec direct topology", plain, "native-exec-direct");
|
|
323
324
|
}
|
|
@@ -329,7 +330,7 @@ function driveHook(label: string, pluginRoot: string, nativeSessionId: string):
|
|
|
329
330
|
// path was pasted into a command string and would have been word-split or
|
|
330
331
|
// command-substituted; under exec form each element is one opaque argv slot.
|
|
331
332
|
{
|
|
332
|
-
const holder = mkdtempSync(path.join(tmpdir(), "psa-hook-meta-"));
|
|
333
|
+
const holder = reclaimOnExit(mkdtempSync(path.join(tmpdir(), "psa-hook-meta-")));
|
|
333
334
|
const nasty = path.join(holder, "plug in $HOME `id` ;& dir");
|
|
334
335
|
makeBundle(nasty);
|
|
335
336
|
ok("metachar fixture root really contains space/$/backtick/;&", /[ $`;&]/.test(path.basename(nasty)));
|
|
@@ -345,9 +346,9 @@ function driveHook(label: string, pluginRoot: string, nativeSessionId: string):
|
|
|
345
346
|
// it. The launcher's provenance token is what makes this fail CLOSED — the record still
|
|
346
347
|
// lands (best-effort, as always), but no presence is claimed that cannot be backed.
|
|
347
348
|
{
|
|
348
|
-
const pluginRoot = mkdtempSync(path.join(tmpdir(), "psa-hook-oldcmd-"));
|
|
349
|
+
const pluginRoot = reclaimOnExit(mkdtempSync(path.join(tmpdir(), "psa-hook-oldcmd-")));
|
|
349
350
|
makeBundle(pluginRoot);
|
|
350
|
-
const agentRoot = mkdtempSync(path.join(tmpdir(), "psa-hook-oldcmd-agent-"));
|
|
351
|
+
const agentRoot = reclaimOnExit(mkdtempSync(path.join(tmpdir(), "psa-hook-oldcmd-agent-")));
|
|
351
352
|
const result = spawnSync(process.execPath, [path.join(pluginRoot, "meta-bridge-hook.ts")], {
|
|
352
353
|
encoding: "utf8",
|
|
353
354
|
input: JSON.stringify({
|
|
@@ -510,12 +510,13 @@ const operatorCmds = [...targets].filter(([cmd, ts]) => !isDevGate(cmd) && ts.le
|
|
|
510
510
|
`${skillRel}: missing name, four authority modes, prerelease contract, or English/ASCII-only surface`,
|
|
511
511
|
);
|
|
512
512
|
ok(
|
|
513
|
-
"S7g: the executable ASCII CI oracle binds headSha and all
|
|
513
|
+
"S7g: the executable ASCII CI oracle binds headSha and all four required jobs",
|
|
514
514
|
ciOracle === null ||
|
|
515
515
|
(ciOracle.includes("headSha") &&
|
|
516
516
|
ciOracle.includes('"check"') &&
|
|
517
517
|
ciOracle.includes('"install-surface"') &&
|
|
518
518
|
ciOracle.includes('"artifact-consumer"') &&
|
|
519
|
+
ciOracle.includes('"macos-install-surface"') &&
|
|
519
520
|
ciOracleMode === "100755" &&
|
|
520
521
|
oracleIsAscii),
|
|
521
522
|
`${ciOracleRel}: missing executable mode, exact-SHA classification, required jobs, or ASCII-only content`,
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
type MetaIdentity,
|
|
34
34
|
serializeMetaIdentity,
|
|
35
35
|
} from "../pi-extensions/lib/meta-session.ts";
|
|
36
|
+
import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
36
37
|
|
|
37
38
|
let passed = 0;
|
|
38
39
|
function ok(label: string, cond: boolean): void {
|
|
@@ -62,7 +63,7 @@ function record(gardenId: string, over: Partial<MetaIdentity> = {}): string {
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
function store(files: Record<string, string | Buffer>): string {
|
|
65
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-meta-facts-"));
|
|
66
|
+
const dir = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-meta-facts-")));
|
|
66
67
|
for (const [name, body] of Object.entries(files)) fs.writeFileSync(path.join(dir, name), body);
|
|
67
68
|
return dir;
|
|
68
69
|
}
|
|
@@ -148,7 +149,7 @@ function parseProjection(run: CliRun, label: string): Projection {
|
|
|
148
149
|
|
|
149
150
|
// ── drift / symlink / invalid UTF-8 are in-band defects, exit 0 ─────────────
|
|
150
151
|
{
|
|
151
|
-
const outside = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-meta-facts-outside-"));
|
|
152
|
+
const outside = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-meta-facts-outside-")));
|
|
152
153
|
fs.writeFileSync(path.join(outside, "foreign.meta.json"), record(GID_C));
|
|
153
154
|
const dir = store({
|
|
154
155
|
[`${GID_A}.meta.json`]: record(GID_A),
|
|
@@ -206,7 +207,8 @@ function parseProjection(run: CliRun, label: string): Projection {
|
|
|
206
207
|
|
|
207
208
|
// ── missing store = readable EMPTY store ────────────────────────────────────
|
|
208
209
|
{
|
|
209
|
-
const
|
|
210
|
+
const goneRoot = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-meta-facts-")));
|
|
211
|
+
const gone = path.join(goneRoot, "never-created");
|
|
210
212
|
const facts = parseProjection(cli([gone]), "missing store");
|
|
211
213
|
ok(
|
|
212
214
|
"a store that does not exist is a readable empty store (ENOENT only)",
|
|
@@ -216,7 +218,7 @@ function parseProjection(run: CliRun, label: string): Projection {
|
|
|
216
218
|
|
|
217
219
|
// ── unreadable store = exit 3, NO JSON ──────────────────────────────────────
|
|
218
220
|
{
|
|
219
|
-
const base = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-meta-facts-"));
|
|
221
|
+
const base = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-meta-facts-")));
|
|
220
222
|
const file = path.join(base, "not-a-dir");
|
|
221
223
|
fs.writeFileSync(file, "plain file");
|
|
222
224
|
const run = cli([path.join(file, "store")]); // ENOTDIR: the path runs THROUGH a regular file
|
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
upsertMetaSession,
|
|
71
71
|
writeMetaReceiverMarker,
|
|
72
72
|
} from "../pi-extensions/lib/meta-session.ts";
|
|
73
|
+
import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
73
74
|
|
|
74
75
|
let passed = 0;
|
|
75
76
|
function ok(label: string, cond: boolean): void {
|
|
@@ -112,9 +113,9 @@ function resolveEl(value: string, pluginRoot: string): string {
|
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
// ── one sandbox for the whole switch story (the pid is what ties it together) ──
|
|
115
|
-
const AGENT_ROOT = mkdtempSync(path.join(tmpdir(), "psa-hook-switch-agent-"));
|
|
116
|
-
const PLUGIN_ROOT = mkdtempSync(path.join(tmpdir(), "psa-hook-switch-plugin-"));
|
|
117
|
-
const CWD = mkdtempSync(path.join(tmpdir(), "psa-hook-switch-cwd-"));
|
|
116
|
+
const AGENT_ROOT = reclaimOnExit(mkdtempSync(path.join(tmpdir(), "psa-hook-switch-agent-")));
|
|
117
|
+
const PLUGIN_ROOT = reclaimOnExit(mkdtempSync(path.join(tmpdir(), "psa-hook-switch-plugin-")));
|
|
118
|
+
const CWD = reclaimOnExit(mkdtempSync(path.join(tmpdir(), "psa-hook-switch-cwd-")));
|
|
118
119
|
makeBundle(PLUGIN_ROOT);
|
|
119
120
|
|
|
120
121
|
const ROOTS = {
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
serializeMetaIdentity,
|
|
53
53
|
} from "../pi-extensions/lib/meta-session.ts";
|
|
54
54
|
import { birthPiCitizen } from "../pi-extensions/lib/pi-citizen-birth.ts";
|
|
55
|
+
import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
55
56
|
|
|
56
57
|
let passed = 0;
|
|
57
58
|
function ok(label: string, cond: boolean): void {
|
|
@@ -103,7 +104,7 @@ const v2Body = `${JSON.stringify(
|
|
|
103
104
|
|
|
104
105
|
/** Run `body` against a store dir seeded by `seed`, then remove it. */
|
|
105
106
|
function withStore(seed: (dir: string) => void, body: (dir: string) => void): void {
|
|
106
|
-
const dir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-case-"));
|
|
107
|
+
const dir = reclaimOnExit(fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-case-")));
|
|
107
108
|
try {
|
|
108
109
|
seed(dir);
|
|
109
110
|
body(dir);
|
|
@@ -129,7 +130,7 @@ const record = (dir: string, gardenId: string, nativeSessionId: string): void =>
|
|
|
129
130
|
write(dir, `${gardenId}.meta.json`, serializeMetaIdentity(identity(gardenId, nativeSessionId)));
|
|
130
131
|
const recordCount = (dir: string): number => fs.readdirSync(dir).filter((f) => f.endsWith(".meta.json")).length;
|
|
131
132
|
|
|
132
|
-
const dir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-"));
|
|
133
|
+
const dir = reclaimOnExit(fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-")));
|
|
133
134
|
try {
|
|
134
135
|
record(dir, GID_A, "native-a");
|
|
135
136
|
record(dir, GID_B, "native-b");
|
|
@@ -355,7 +356,7 @@ withStore(
|
|
|
355
356
|
// entries with the right kind. A pure-only proof would have passed while the fs path
|
|
356
357
|
// still did a bare-name readdir and followed symlinks — which is exactly what shipped.
|
|
357
358
|
{
|
|
358
|
-
const tmp = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-addr-"));
|
|
359
|
+
const tmp = reclaimOnExit(fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-addr-")));
|
|
359
360
|
try {
|
|
360
361
|
// A symlink whose target lives OUTSIDE the store and holds the same native id.
|
|
361
362
|
const dir = path.join(tmp, "symlink-store");
|
|
@@ -444,7 +445,7 @@ withStore(
|
|
|
444
445
|
// into foreign bytes while every "is it a symlink" test on a settled store stayed green.
|
|
445
446
|
// A plain already-a-symlink read cannot fail that way, so it cannot prove this.
|
|
446
447
|
{
|
|
447
|
-
const tmp = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-toctou-"));
|
|
448
|
+
const tmp = reclaimOnExit(fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-toctou-")));
|
|
448
449
|
try {
|
|
449
450
|
const dir = path.join(tmp, "store");
|
|
450
451
|
const outside = path.join(tmp, "outside");
|
|
@@ -700,7 +701,7 @@ withStore(
|
|
|
700
701
|
// where they live and a probe of this directory cannot certify them.
|
|
701
702
|
withStore(
|
|
702
703
|
(dir) => {
|
|
703
|
-
const outside = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-outside-"));
|
|
704
|
+
const outside = reclaimOnExit(fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-outside-")));
|
|
704
705
|
fs.writeFileSync(path.join(outside, "real.json"), serializeMetaIdentity(identity(GID_C, "native-symlinked")));
|
|
705
706
|
fs.symlinkSync(path.join(outside, "real.json"), path.join(dir, `${GID_C}.meta.json`));
|
|
706
707
|
},
|
|
@@ -889,7 +890,7 @@ withStore(
|
|
|
889
890
|
// clean "no such citizen" — the fail-open shape (2026-07-25 fresh-eyes review). ENOENT
|
|
890
891
|
// alone is absence; a broken store SHAPE (ENOTDIR here) is a failure to inspect.
|
|
891
892
|
{
|
|
892
|
-
const tmp = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-shape-"));
|
|
893
|
+
const tmp = reclaimOnExit(fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-shape-")));
|
|
893
894
|
try {
|
|
894
895
|
const brokenStore = path.join(tmp, "store-is-actually-a-file");
|
|
895
896
|
fs.writeFileSync(brokenStore, "not a store\n");
|
|
@@ -927,7 +928,7 @@ withStore(
|
|
|
927
928
|
// — the doctor and the install preflight would call an unreadable host clean. ENOENT
|
|
928
929
|
// alone is an absent store (a host that never had a generation is not broken).
|
|
929
930
|
{
|
|
930
|
-
const tmp = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-unreadable-"));
|
|
931
|
+
const tmp = reclaimOnExit(fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "psa-idcons-unreadable-")));
|
|
931
932
|
try {
|
|
932
933
|
ok(
|
|
933
934
|
"an absent store still certifies as empty (ENOENT alone)",
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
startKeyScheme,
|
|
42
42
|
writeMetaReceiverMarker,
|
|
43
43
|
} from "../pi-extensions/lib/meta-session.ts";
|
|
44
|
+
import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
44
45
|
|
|
45
46
|
let passed = 0;
|
|
46
47
|
function ok(label: string, cond: boolean): void {
|
|
@@ -50,7 +51,7 @@ function ok(label: string, cond: boolean): void {
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
const REPO_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
53
|
-
const DIR = mkdtempSync(path.join(tmpdir(), "psa-meta-receivers-"));
|
|
54
|
+
const DIR = reclaimOnExit(mkdtempSync(path.join(tmpdir(), "psa-meta-receivers-")));
|
|
54
55
|
const GARDEN = "20260614T120000-aaaaaa";
|
|
55
56
|
|
|
56
57
|
// ── write → read round-trip, garden-id keyed ────────────────────────────────
|