@junghanacs/entwurf 0.36.0 → 0.37.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/CHANGELOG.md +290 -0
- package/DELIVERY.md +2 -0
- package/README.md +5 -0
- package/VERIFY.md +11 -4
- package/docs/acp-backend-rail.md +69 -5
- package/docs/setup-clean-host.md +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +16 -1
- package/package.json +5 -4
- package/pi/pi-durable/bootstrap.mjs +113 -11
- package/pi/pi-durable/carrier/harness-setup.js +4 -1
- package/pi/pi-durable/carrier/prompt.js +31 -2
- package/pi/pi-durable/carrier/runtime.js +11 -2
- package/pi/pi-durable/carrier/sessions.js +75 -5
- package/pi/pi-durable/carrier/tui.js +23 -0
- package/pi/pi-durable/overlay/upstream-pin.json +10 -10
- package/pi-extensions/meta-bridge-pi-durable.ts +20 -0
- package/run.sh +5 -2
- package/scripts/check-acp-prompt-lifecycle.ts +4 -2
- package/scripts/check-acp-session-reuse.ts +4 -2
- package/scripts/check-acp-stop-reason.ts +4 -2
- package/scripts/check-acp-stream-hooks.ts +4 -2
- package/scripts/check-acp-usage-accounting.ts +4 -2
- package/scripts/check-gate-qualification.ts +3 -3
- package/scripts/check-herdr-supply.ts +32 -7
- package/scripts/fixtures/herdr-supply.json +12 -10
- package/scripts/lib/acp-gate-home.ts +75 -0
- package/scripts/lib/codex-socket-listener.ts +429 -0
- package/scripts/mutants/acp-stop-reason.json +11 -0
- package/scripts/mutants/codex-native.json +52 -0
- package/scripts/mutants/herdr-supply.json +8 -8
- package/scripts/mutants/pi-durable.json +80 -8
- package/scripts/smoke-codex-fresh-live.ts +31 -7
- package/scripts/smoke-pi-durable-fresh-live.ts +22 -3
|
@@ -54,6 +54,7 @@ import { resolve } from "node:path";
|
|
|
54
54
|
import { pathToFileURL } from "node:url";
|
|
55
55
|
import type { Api, AssistantMessageEvent, Model } from "@earendil-works/pi-ai";
|
|
56
56
|
import { normalizeContext } from "@earendil-works/pi-ai";
|
|
57
|
+
import { assertOverlayInSandbox, sandboxAcpGateHome } from "./lib/acp-gate-home.ts";
|
|
57
58
|
|
|
58
59
|
const sonnet = { id: "claude-sonnet-5-5" } as unknown as Model<Api>;
|
|
59
60
|
|
|
@@ -118,8 +119,6 @@ const DEFAULT_RESOLVED_CONFIG: any = {
|
|
|
118
119
|
*/
|
|
119
120
|
function makeHarness(recordDir: string, wireReason: string | null) {
|
|
120
121
|
return {
|
|
121
|
-
resolveLaunch: () => ({ command: "node", args: ["fake"] }),
|
|
122
|
-
ensureOverlay: () => {},
|
|
123
122
|
spawnChild: () => makeFakeChild(),
|
|
124
123
|
createConnection: (_child: any, handlers: any) => ({
|
|
125
124
|
initialize: async () => ({ agentCapabilities: {} }),
|
|
@@ -143,6 +142,7 @@ function makeHarness(recordDir: string, wireReason: string | null) {
|
|
|
143
142
|
const TMP_EMIT = ".tmp-verify/acp-stop-reason";
|
|
144
143
|
rmSync(TMP_EMIT, { recursive: true, force: true });
|
|
145
144
|
const recordDir = mkdtempSync(resolve(tmpdir(), "acp-stop-rec-"));
|
|
145
|
+
const gateHome = sandboxAcpGateHome("acp-stop-reason-home-");
|
|
146
146
|
|
|
147
147
|
/**
|
|
148
148
|
* One driven turn, described by everything an operator could observe.
|
|
@@ -250,6 +250,7 @@ try {
|
|
|
250
250
|
const backendUrl = pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/backend.js")).href;
|
|
251
251
|
const eventUrl = pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/event-mapper.js")).href;
|
|
252
252
|
const backend = (await import(backendUrl)) as any;
|
|
253
|
+
await assertOverlayInSandbox(TMP_EMIT, gateHome);
|
|
253
254
|
const eventMod = (await import(eventUrl)) as any;
|
|
254
255
|
|
|
255
256
|
// ----------------------------------------------------------------------
|
|
@@ -354,6 +355,7 @@ try {
|
|
|
354
355
|
// non-empty or already gone — fine either way
|
|
355
356
|
}
|
|
356
357
|
rmSync(recordDir, { recursive: true, force: true });
|
|
358
|
+
gateHome.restore();
|
|
357
359
|
}
|
|
358
360
|
|
|
359
361
|
console.log(
|
|
@@ -34,6 +34,7 @@ import { resolve } from "node:path";
|
|
|
34
34
|
import { pathToFileURL } from "node:url";
|
|
35
35
|
import type { Api, AssistantMessageEvent, Message, Model, TranscriptContext } from "@earendil-works/pi-ai";
|
|
36
36
|
import { normalizeContext } from "@earendil-works/pi-ai";
|
|
37
|
+
import { assertOverlayInSandbox, sandboxAcpGateHome } from "./lib/acp-gate-home.ts";
|
|
37
38
|
|
|
38
39
|
const sonnet = { id: "claude-sonnet-5-5" } as unknown as Model<Api>;
|
|
39
40
|
|
|
@@ -108,8 +109,6 @@ function makeHarness(recordDir: string) {
|
|
|
108
109
|
children,
|
|
109
110
|
promptCalls,
|
|
110
111
|
deps: {
|
|
111
|
-
resolveLaunch: () => ({ command: "node", args: ["fake"] }),
|
|
112
|
-
ensureOverlay: () => {},
|
|
113
112
|
spawnChild: () => {
|
|
114
113
|
const c = makeFakeChild();
|
|
115
114
|
children.push(c);
|
|
@@ -187,6 +186,7 @@ const onResponse = () => {
|
|
|
187
186
|
const TMP_EMIT = ".tmp-verify/acp-stream-hooks";
|
|
188
187
|
rmSync(TMP_EMIT, { recursive: true, force: true });
|
|
189
188
|
const recordDir = mkdtempSync(resolve(tmpdir(), "acp-stream-hooks-"));
|
|
189
|
+
const gateHome = sandboxAcpGateHome("acp-stream-hooks-home-");
|
|
190
190
|
|
|
191
191
|
try {
|
|
192
192
|
execFileSync("node_modules/.bin/tsc", ["--outDir", TMP_EMIT, "--rootDir", ".", "--noEmit", "false"], {
|
|
@@ -196,6 +196,7 @@ try {
|
|
|
196
196
|
mkdirSync(promptsOut, { recursive: true });
|
|
197
197
|
copyFileSync("pi-extensions/lib/acp/prompts/engraving.md", resolve(promptsOut, "engraving.md"));
|
|
198
198
|
const backend = (await import(pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/backend.js")).href)) as any;
|
|
199
|
+
await assertOverlayInSandbox(TMP_EMIT, gateHome);
|
|
199
200
|
|
|
200
201
|
// ----------------------------------------------------------------------
|
|
201
202
|
// CELL 1 — NEW turn: the hook sees the EXACT wire params; undefined keeps them.
|
|
@@ -510,6 +511,7 @@ try {
|
|
|
510
511
|
// non-empty or already gone — fine either way
|
|
511
512
|
}
|
|
512
513
|
rmSync(recordDir, { recursive: true, force: true });
|
|
514
|
+
gateHome.restore();
|
|
513
515
|
}
|
|
514
516
|
|
|
515
517
|
console.log(
|
|
@@ -60,6 +60,7 @@ import type {
|
|
|
60
60
|
} from "@earendil-works/pi-ai";
|
|
61
61
|
import { isContextOverflow, normalizeContext } from "@earendil-works/pi-ai";
|
|
62
62
|
import { calculateContextTokens } from "@earendil-works/pi-coding-agent";
|
|
63
|
+
import { assertOverlayInSandbox, sandboxAcpGateHome } from "./lib/acp-gate-home.ts";
|
|
63
64
|
|
|
64
65
|
const claude = { id: "claude-sonnet-5-5" } as unknown as Model<Api>;
|
|
65
66
|
const cortex = { id: "cortex-auto" } as unknown as Model<Api>;
|
|
@@ -193,8 +194,6 @@ function makeHarness(recordDir: string, scripts: TurnScript[]) {
|
|
|
193
194
|
return newSessionCalls;
|
|
194
195
|
},
|
|
195
196
|
deps: {
|
|
196
|
-
resolveLaunch: () => ({ command: "node", args: ["fake"] }),
|
|
197
|
-
ensureOverlay: () => {},
|
|
198
197
|
spawnChild: () => {
|
|
199
198
|
const c = makeFakeChild();
|
|
200
199
|
children.push(c);
|
|
@@ -297,6 +296,7 @@ function reuseCtx3(a: string, b: string, c: string): TranscriptContext {
|
|
|
297
296
|
const TMP_EMIT = ".tmp-verify/acp-usage-accounting";
|
|
298
297
|
rmSync(TMP_EMIT, { recursive: true, force: true });
|
|
299
298
|
const recordDir = mkdtempSync(resolve(tmpdir(), "acp-usage-acct-"));
|
|
299
|
+
const gateHome = sandboxAcpGateHome("acp-usage-accounting-home-");
|
|
300
300
|
|
|
301
301
|
try {
|
|
302
302
|
execFileSync("node_modules/.bin/tsc", ["--outDir", TMP_EMIT, "--rootDir", ".", "--noEmit", "false"], {
|
|
@@ -306,6 +306,7 @@ try {
|
|
|
306
306
|
mkdirSync(promptsOut, { recursive: true });
|
|
307
307
|
copyFileSync("pi-extensions/lib/acp/prompts/engraving.md", resolve(promptsOut, "engraving.md"));
|
|
308
308
|
const backend = (await import(pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/backend.js")).href)) as any;
|
|
309
|
+
await assertOverlayInSandbox(TMP_EMIT, gateHome);
|
|
309
310
|
|
|
310
311
|
// ----------------------------------------------------------------------
|
|
311
312
|
// CELL 1 — the turn aggregate never reaches pi's per-request fields.
|
|
@@ -1062,6 +1063,7 @@ try {
|
|
|
1062
1063
|
// non-empty or already gone — fine either way
|
|
1063
1064
|
}
|
|
1064
1065
|
rmSync(recordDir, { recursive: true, force: true });
|
|
1066
|
+
gateHome.restore();
|
|
1065
1067
|
}
|
|
1066
1068
|
|
|
1067
1069
|
/** Distinct-but-uninteresting token partition for the cost-focused cells. */
|
|
@@ -3442,7 +3442,7 @@ let manifestCount: number;
|
|
|
3442
3442
|
"acp-overlay": 1,
|
|
3443
3443
|
"acp-prompt-lifecycle": 15,
|
|
3444
3444
|
"acp-set-model-readback": 2,
|
|
3445
|
-
"acp-stop-reason":
|
|
3445
|
+
"acp-stop-reason": 7,
|
|
3446
3446
|
"acp-stream-hooks": 10,
|
|
3447
3447
|
"acp-usage-accounting": 12,
|
|
3448
3448
|
"acp-vendor-rejection": 4,
|
|
@@ -3453,7 +3453,7 @@ let manifestCount: number;
|
|
|
3453
3453
|
"codex-app-server-launch": 10,
|
|
3454
3454
|
"codex-caller-seat": 28,
|
|
3455
3455
|
"control-send-receipt": 19,
|
|
3456
|
-
"codex-native":
|
|
3456
|
+
"codex-native": 87,
|
|
3457
3457
|
"compaction-send-guard": 7,
|
|
3458
3458
|
"control-socket-disconnect": 4,
|
|
3459
3459
|
"demo-reproducibility": 12,
|
|
@@ -3464,7 +3464,7 @@ let manifestCount: number;
|
|
|
3464
3464
|
"pi-bridge-sender": 4,
|
|
3465
3465
|
"pi-mcp-bridge": 2,
|
|
3466
3466
|
"pi-mcp-register": 3,
|
|
3467
|
-
"pi-durable":
|
|
3467
|
+
"pi-durable": 84,
|
|
3468
3468
|
"copilot-receive": 20,
|
|
3469
3469
|
"entwurf-peers": 1,
|
|
3470
3470
|
"fresh-call-dispatch": 12,
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
* HSUP-CI-REQUIRES-HERDR that floor runs with absence turned red
|
|
33
33
|
* HSUP-CI-LINUX-ONLY only the Linux check job takes this; the other jobs are untouched
|
|
34
34
|
* HSUP-SETUP-NEVER-INSTALLS the product's own setup/package never installs herdr (Rule 17)
|
|
35
|
-
* HSUP-
|
|
35
|
+
* HSUP-EXECUTION-RECEIPT-BOUND an asset recorded as executed declares a receipt bound to this
|
|
36
|
+
* pin's version and its own full digest; an unexecuted one has none
|
|
36
37
|
*/
|
|
37
38
|
|
|
38
39
|
import assert from "node:assert/strict";
|
|
@@ -62,7 +63,34 @@ interface Supply {
|
|
|
62
63
|
measuredOn: string;
|
|
63
64
|
measuredArch: string;
|
|
64
65
|
releaseUrlPrefix: string;
|
|
65
|
-
assets: Record<
|
|
66
|
+
assets: Record<
|
|
67
|
+
string,
|
|
68
|
+
{ name: string; sha256: string; executed: boolean; executedNote: string; executedReceipt?: string }
|
|
69
|
+
>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** An asset may be RECORDED as executed only with a declared receipt bound to THIS pin: the
|
|
73
|
+
* receipt BEGINS with the exact token `herdr <supply.version> ` (a substring match would let
|
|
74
|
+
* `herdr 0.9.40` stand for 0.9.4) and names the asset's FULL sha256. That checks the
|
|
75
|
+
* declaration's binding and nothing more — it is not a cryptographic receipt and cannot prove the
|
|
76
|
+
* binary ran. An unexecuted asset carries no receipt, both states keep a note, and the
|
|
77
|
+
* architecture the manifest says it was measured on must itself be recorded as executed. The
|
|
78
|
+
* architecture is never hardcoded: a future x86_64 `true` is admitted the same way, with its own. */
|
|
79
|
+
function executionReceiptsBound(supply: Supply): boolean {
|
|
80
|
+
const assets = Object.values(supply.assets);
|
|
81
|
+
const noted = (note: unknown): boolean => typeof note === "string" && note.trim().length > 0;
|
|
82
|
+
return (
|
|
83
|
+
assets.length > 0 &&
|
|
84
|
+
assets.every((a) =>
|
|
85
|
+
a.executed === true
|
|
86
|
+
? typeof a.executedReceipt === "string" &&
|
|
87
|
+
a.executedReceipt.startsWith(`herdr ${supply.version} `) &&
|
|
88
|
+
a.executedReceipt.includes(a.sha256) &&
|
|
89
|
+
noted(a.executedNote)
|
|
90
|
+
: a.executed === false && a.executedReceipt === undefined && noted(a.executedNote),
|
|
91
|
+
) &&
|
|
92
|
+
supply.assets[supply.measuredArch]?.executed === true
|
|
93
|
+
);
|
|
66
94
|
}
|
|
67
95
|
|
|
68
96
|
function main(): void {
|
|
@@ -171,11 +199,8 @@ function main(): void {
|
|
|
171
199
|
|
|
172
200
|
// ── honesty about what has actually run ──────────────────────────────────────────────
|
|
173
201
|
ok(
|
|
174
|
-
"[QK:HSUP-
|
|
175
|
-
supply
|
|
176
|
-
supply.assets.aarch64?.executed === true &&
|
|
177
|
-
supply.measuredArch === "aarch64" &&
|
|
178
|
-
(supply.assets.x86_64?.executedNote ?? "").length > 0,
|
|
202
|
+
"[QK:HSUP-EXECUTION-RECEIPT-BOUND] an asset is recorded as executed only with a DECLARED receipt bound to this pin's version and its own full digest, and an unexecuted one carries none — a pinned digest is never evidence that its cell ran, and a receipt from another pin or another asset is not this one's. This checks the declaration's binding, not that the binary ran",
|
|
203
|
+
executionReceiptsBound(supply),
|
|
179
204
|
);
|
|
180
205
|
|
|
181
206
|
console.log(`\n[check-herdr-supply] ${passed} assertions ok`);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$comment": "SSOT for the herdr binary this repository has MEASURED against. Nothing else may carry a version or a digest: the CI installer, the sandbox gate's boundary check and the launch-rail document all read or cite this file. Re-measuring a new herdr means
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"tag": "v0.9.
|
|
5
|
-
"tagCommit": "
|
|
2
|
+
"$comment": "SSOT for the herdr binary this repository has MEASURED against. Nothing else may carry a version or a digest: the CI installer, the sandbox gate's boundary check and the launch-rail document all read or cite this file. Re-measuring a new herdr means measuring it FIRST — fetched by digest and verified before it is made executable, then run against a private sandbox server (at least version, status-client protocol and integration rows) — then editing this file and repairing the measured sentences in docs/herdr-launch-rail.md in the same commit; miss one and the gate fails on version mismatch, which is the point. A private baseline is not the full gate: check-herdr-sandbox at a new pin is certified by its exact-SHA CI run unless it was also run locally, and an asset is recorded `executed` only with a receipt bound to this version and its full digest (check-herdr-supply HSUP-EXECUTION-RECEIPT-BOUND).",
|
|
3
|
+
"version": "0.9.5",
|
|
4
|
+
"tag": "v0.9.5",
|
|
5
|
+
"tagCommit": "2ecf19a92fc3a7fff75b6ce3655ef04f4625ec01",
|
|
6
6
|
"socketProtocol": 22,
|
|
7
|
-
"measuredOn": "2026-10-
|
|
7
|
+
"measuredOn": "2026-10-15",
|
|
8
8
|
"measuredArch": "aarch64",
|
|
9
9
|
"releaseUrlPrefix": "https://github.com/herdrdev/herdr/releases/download",
|
|
10
10
|
"integrity": {
|
|
@@ -15,15 +15,17 @@
|
|
|
15
15
|
"assets": {
|
|
16
16
|
"x86_64": {
|
|
17
17
|
"name": "herdr-linux-x86_64",
|
|
18
|
-
"sha256": "
|
|
19
|
-
"executed":
|
|
20
|
-
"
|
|
18
|
+
"sha256": "14d9e8bc8b4a9e0118d77c32266f76d461f09b40342eb23752960bd89f59929d",
|
|
19
|
+
"executed": true,
|
|
20
|
+
"executedReceipt": "herdr 0.9.5 linux-x86_64 sha256 14d9e8bc8b4a9e0118d77c32266f76d461f09b40342eb23752960bd89f59929d — 2026-10-15, GitHub Actions check job (x86_64), CI run 38667564413 at d8cd8f3, read from that run's check-job log: `install-herdr-ci` verified this digest before execution, and check-herdr-sandbox passed 12 assertions on private herdr 0.9.5, HS-VERSION-BOUNDARY included",
|
|
21
|
+
"executedNote": "the receipt is the full sandbox gate at the SHA that introduced this pin; it is not a claim for any later SHA. The previous pin's x86_64 asset (v0.9.4, sha256 493304fb…) ran in CI run 38479767989 at 9f33dd1, check-herdr-sandbox 12 assertions — dated history of that asset, not evidence for this one"
|
|
21
22
|
},
|
|
22
23
|
"aarch64": {
|
|
23
24
|
"name": "herdr-linux-aarch64",
|
|
24
|
-
"sha256": "
|
|
25
|
+
"sha256": "17d7536f2da50859523680ef279dffcac8e4a235f4e46fe8da6c2ec44141897c",
|
|
25
26
|
"executed": true,
|
|
26
|
-
"
|
|
27
|
+
"executedReceipt": "herdr 0.9.5 linux-aarch64 sha256 17d7536f2da50859523680ef279dffcac8e4a235f4e46fe8da6c2ec44141897c — 2026-10-15, oracle aarch64, Phase A private CLI baseline (docs/herdr-launch-rail.md, 재측정 2026-10-15): `herdr --version` 0.9.5; `status client` protocol 22, endpoint_protocol_generation 1; `integration status` 18 rows, zero diff against 0.9.4 in a fresh sandbox; over integrations seeded by 0.9.4, pi reads `current (v9)` and claude reads `outdated (v10 < v11)`; a 0.9.5 reinstall leaves the pi extension and claude settings.json byte-identical, changes the claude hook only in its `HERDR_INTEGRATION_VERSION` line (10 → 11), and then reads `current (v11)`; `server stop` leaves no socket and no process",
|
|
28
|
+
"executedNote": "the receipt is a private CLI baseline only. check-herdr-sandbox has NOT run on this architecture at this pin — its operator-untouched cell reads the operator's own panes — so the full gate's first run at this pin was the CI check job on the x86_64 asset (CI run 38667564413) — not this one. The operator host was not touched. The 0.9.4 receipt (2026-10-14) stays dated in docs/herdr-launch-rail.md"
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// In-process HOME sandbox for the ACP gates that drive the compiled `backend.streamAcpTurn`
|
|
2
|
+
// (Rule 12: offline verification never rewires the operator).
|
|
3
|
+
//
|
|
4
|
+
// The turn loop resolves the REAL routed adapter from the model id and calls its
|
|
5
|
+
// `ensureOverlay()`; there is no deps seam for it (`AcpTurnDeps`). Claude's overlay, the session
|
|
6
|
+
// cache and the operator settings path are `homedir()` constants evaluated when the compiled
|
|
7
|
+
// modules load. So the sandbox must exist BEFORE the first import of the emitted backend, and the
|
|
8
|
+
// gate proves it by reading the overlay constant from that same emitted module graph. Same env
|
|
9
|
+
// set as `pi-extensions/lib/acp/backend.vendor-rejection.test.ts`.
|
|
10
|
+
|
|
11
|
+
import { strict as assert } from "node:assert";
|
|
12
|
+
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join, resolve } from "node:path";
|
|
15
|
+
import { pathToFileURL } from "node:url";
|
|
16
|
+
|
|
17
|
+
const SANDBOXED_ENV = [
|
|
18
|
+
"HOME",
|
|
19
|
+
"XDG_CONFIG_HOME",
|
|
20
|
+
"XDG_DATA_HOME",
|
|
21
|
+
"XDG_STATE_HOME",
|
|
22
|
+
"XDG_CACHE_HOME",
|
|
23
|
+
"PI_CODING_AGENT_DIR",
|
|
24
|
+
"PI_SESSION_ID",
|
|
25
|
+
"PI_AGENT_ID",
|
|
26
|
+
"CLAUDE_AGENT_ACP_COMMAND",
|
|
27
|
+
"PI_EMACS_AGENT_SOCKET",
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
export interface AcpGateHome {
|
|
31
|
+
readonly home: string;
|
|
32
|
+
/** Restore every sandboxed variable and remove the sandbox root. */
|
|
33
|
+
restore(): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Point HOME, XDG and the pi agent dir into a fresh temp root. Call before importing backend. */
|
|
37
|
+
export function sandboxAcpGateHome(prefix: string): AcpGateHome {
|
|
38
|
+
const root = mkdtempSync(join(tmpdir(), prefix));
|
|
39
|
+
const saved = new Map<string, string | undefined>(SANDBOXED_ENV.map((key) => [key, process.env[key]]));
|
|
40
|
+
const home = join(root, "home");
|
|
41
|
+
mkdirSync(home, { recursive: true });
|
|
42
|
+
process.env.HOME = home;
|
|
43
|
+
process.env.XDG_CONFIG_HOME = join(root, "xdg-config");
|
|
44
|
+
process.env.XDG_DATA_HOME = join(root, "xdg-data");
|
|
45
|
+
process.env.XDG_STATE_HOME = join(root, "xdg-state");
|
|
46
|
+
process.env.XDG_CACHE_HOME = join(root, "xdg-cache");
|
|
47
|
+
process.env.PI_CODING_AGENT_DIR = join(home, ".pi", "agent");
|
|
48
|
+
delete process.env.PI_SESSION_ID;
|
|
49
|
+
delete process.env.PI_AGENT_ID;
|
|
50
|
+
delete process.env.CLAUDE_AGENT_ACP_COMMAND;
|
|
51
|
+
delete process.env.PI_EMACS_AGENT_SOCKET;
|
|
52
|
+
return {
|
|
53
|
+
home,
|
|
54
|
+
restore() {
|
|
55
|
+
for (const [key, value] of saved) {
|
|
56
|
+
if (value === undefined) delete process.env[key];
|
|
57
|
+
else process.env[key] = value;
|
|
58
|
+
}
|
|
59
|
+
rmSync(root, { recursive: true, force: true });
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Refuse to drive a turn unless the emitted overlay constant resolves inside the sandbox HOME. */
|
|
65
|
+
export async function assertOverlayInSandbox(emitDir: string, sandbox: AcpGateHome): Promise<void> {
|
|
66
|
+
const overlayMod = (await import(pathToFileURL(resolve(emitDir, "pi-extensions/lib/acp/overlay.js")).href)) as {
|
|
67
|
+
CLAUDE_CONFIG_OVERLAY_DIR: string;
|
|
68
|
+
};
|
|
69
|
+
assert.equal(
|
|
70
|
+
overlayMod.CLAUDE_CONFIG_OVERLAY_DIR,
|
|
71
|
+
join(sandbox.home, ".pi", "agent", "claude-config-overlay"),
|
|
72
|
+
"the compiled backend resolved the Claude overlay outside the gate's sandbox HOME, so its real " +
|
|
73
|
+
"ensureOverlay() would refresh the operator's overlay [QK:ACP-GATE-HOME-SANDBOXED]",
|
|
74
|
+
);
|
|
75
|
+
}
|