@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +290 -0
  2. package/DELIVERY.md +2 -0
  3. package/README.md +5 -0
  4. package/VERIFY.md +11 -4
  5. package/docs/acp-backend-rail.md +69 -5
  6. package/docs/setup-clean-host.md +1 -0
  7. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +16 -1
  8. package/package.json +5 -4
  9. package/pi/pi-durable/bootstrap.mjs +113 -11
  10. package/pi/pi-durable/carrier/harness-setup.js +4 -1
  11. package/pi/pi-durable/carrier/prompt.js +31 -2
  12. package/pi/pi-durable/carrier/runtime.js +11 -2
  13. package/pi/pi-durable/carrier/sessions.js +75 -5
  14. package/pi/pi-durable/carrier/tui.js +23 -0
  15. package/pi/pi-durable/overlay/upstream-pin.json +10 -10
  16. package/pi-extensions/meta-bridge-pi-durable.ts +20 -0
  17. package/run.sh +5 -2
  18. package/scripts/check-acp-prompt-lifecycle.ts +4 -2
  19. package/scripts/check-acp-session-reuse.ts +4 -2
  20. package/scripts/check-acp-stop-reason.ts +4 -2
  21. package/scripts/check-acp-stream-hooks.ts +4 -2
  22. package/scripts/check-acp-usage-accounting.ts +4 -2
  23. package/scripts/check-gate-qualification.ts +3 -3
  24. package/scripts/check-herdr-supply.ts +32 -7
  25. package/scripts/fixtures/herdr-supply.json +12 -10
  26. package/scripts/lib/acp-gate-home.ts +75 -0
  27. package/scripts/lib/codex-socket-listener.ts +429 -0
  28. package/scripts/mutants/acp-stop-reason.json +11 -0
  29. package/scripts/mutants/codex-native.json +52 -0
  30. package/scripts/mutants/herdr-supply.json +8 -8
  31. package/scripts/mutants/pi-durable.json +80 -8
  32. package/scripts/smoke-codex-fresh-live.ts +31 -7
  33. 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": 6,
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": 83,
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": 78,
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-X86-UNEXECUTED the x86_64 asset is pinned but recorded as never executed
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<string, { name: string; sha256: string; executed: boolean; executedNote: string }>;
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-X86-UNEXECUTED] the x86_64 asset is pinned but RECORDED AS NEVER EXECUTED — every measurement in this lane ran on aarch64, so a pinned digest must not read as evidence that the cell passes on the other architecture",
175
- supply.assets.x86_64?.executed === false &&
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 running check-herdr-sandbox against it FIRST, then editing this file, then 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.",
3
- "version": "0.9.3",
4
- "tag": "v0.9.3",
5
- "tagCommit": "7b116c05bfda646af39d2524c54e70c751f57ee8",
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-03",
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": "18a8dc65f1c2fa485884344356dea1cfd911c6f06cf46fa78e193f4087f4dba7",
19
- "executed": false,
20
- "executedNote": "pending the first CI run on an x86_64 runner — the digest is pinned (and named by GitHub's release attestation for pkg:github/herdrdev/herdr@v0.9.3), but no cell of this rail has ever run on this binary"
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": "4de7aa3e25678812e92960de64f7c2aaa1bca1f0f80a3c5e559837e231e1f5c0",
25
+ "sha256": "17d7536f2da50859523680ef279dffcac8e4a235f4e46fe8da6c2ec44141897c",
25
26
  "executed": true,
26
- "executedNote": "the #116 rail was re-measured on this exact binary on 2026-10-03 (#126), fetched by digest through scripts/install-herdr-ci.sh into a temp dir and run from an isolated snapshot whose manifest named it: `check-herdr-sandbox` green (12 assertions, private server), `herdr status client` reports protocol 22 (unchanged from 0.9.1), and `integration status` prints the same 18 rows as 0.9.1 in a sandbox HOME once that root path is normalized; pi (v9) and claude (v10) integrations installed by 0.9.1 read `current` under 0.9.3 and a 0.9.3 reinstall leaves their bytes identical. The operator host still runs the 0.9.1 asset at that date and was not touched. The 0.9.1 receipt (2026-09-17) stays dated in docs/herdr-launch-rail.md"
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
+ }