@junghanacs/entwurf 0.12.10 → 0.13.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 -1
- package/BASELINE.md +45 -6
- package/CHANGELOG.md +16 -0
- package/CONTRIBUTING.md +4 -2
- package/DELIVERY.md +1 -1
- package/README.md +20 -5
- package/VERIFY.md +7 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +148 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +16 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +66 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +190 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +12 -4
- package/package.json +4 -2
- package/pi/settings.reference.json +1 -1
- package/pi-extensions/acp-provider.ts +20 -10
- package/pi-extensions/lib/acp/augment.ts +60 -2
- package/pi-extensions/lib/acp/backend-adapter.ts +183 -8
- package/pi-extensions/lib/acp/backend.ts +5 -1
- package/pi-extensions/lib/acp/config.ts +19 -5
- package/pi-extensions/lib/acp/engraving.ts +3 -1
- package/pi-extensions/lib/acp/event-mapper.ts +10 -3
- package/pi-extensions/lib/acp/models.ts +69 -7
- package/pi-extensions/lib/acp/overlay.ts +234 -5
- package/pi-extensions/lib/acp/tool-surface.ts +12 -4
- package/run.sh +152 -21
- package/scripts/check-acp-cortex.ts +668 -0
- package/scripts/check-acp-provider-surface.ts +50 -6
- package/scripts/check-acp-session-reuse.ts +64 -1
- package/scripts/check-gate-qualification.ts +2 -0
- package/scripts/check-probe-cli-shim.ts +879 -0
- package/scripts/check-probe-ordering.ts +2450 -0
- package/scripts/check-shell-quote.ts +4 -4
- package/scripts/fixtures/probe-cli-shim +20 -0
- package/scripts/fixtures/probe-mcp-server.ts +168 -12
- package/scripts/lib/probe-acp-turn.ts +207 -0
- package/scripts/lib/probe-cli-shim.ts +464 -0
- package/scripts/lib/probe-cli-target.ts +165 -0
- package/scripts/lib/probe-event-log.ts +383 -0
- package/scripts/lib/probe-verdict.ts +1213 -0
- package/scripts/mutants/acp-cortex.json +196 -0
- package/scripts/mutants/probe-ordering.json +1032 -0
- package/scripts/smoke-acp-cortex-live.ts +392 -0
- package/scripts/smoke-acp-ordering-probe-live.ts +848 -0
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
// Deterministic gate for the Cortex (Snowflake Cortex Code) ACP backend — the
|
|
2
|
+
// first non-claude adapter on the rail (docs/acp-backend-rail.md §4/§6/§11-8).
|
|
3
|
+
// The cortex source lives in one `cortexAdapter` object (backend-adapter.ts) +
|
|
4
|
+
// its curated surface (models.ts) + the dual-HOME overlay (overlay.ts); the
|
|
5
|
+
// 결합 규칙 requires the gate to land WITH it, and §6 said EXTEND the
|
|
6
|
+
// `check-acp-*` family, so cortex's whole deterministic axis lives here.
|
|
7
|
+
//
|
|
8
|
+
// The contract under test is the CP0-measured one (v1.1.52, 2026-07-29), not
|
|
9
|
+
// PR #40's v1.1.8-era shape — the D-numbers refer to §11-8:
|
|
10
|
+
// 1. curated surface is the GLG-decided 4-row set, riding real registry bases;
|
|
11
|
+
// 2. `cortex-` prefix routes to cortexAdapter; prefix-strip recovers the
|
|
12
|
+
// native id; the unprefixed claude ids are never claimed;
|
|
13
|
+
// 3. resolveLaunch NEVER pins `-m` — set-model is the single model authority;
|
|
14
|
+
// `-c <connection>` rides settings/env; the CORTEX_ACP_COMMAND override
|
|
15
|
+
// single-quotes metachar tokens;
|
|
16
|
+
// 4. enforceModel sends the NATIVE id through session/set_config_option
|
|
17
|
+
// (CP0-M measured GO — same wire call as claude);
|
|
18
|
+
// 5. the dual-HOME overlay: isolated HOME layout, measured-minimum auth
|
|
19
|
+
// passthrough (credential_cache — never the whole cache, never skills),
|
|
20
|
+
// `autoUpdate:false` authored, the mcp.json projection with the
|
|
21
|
+
// entwurf-bridge-only real-HOME restore (D9/D10), non-stdio fail-loud,
|
|
22
|
+
// session-scoped dirs with exact rewrite + dead-pid sweep;
|
|
23
|
+
// 6. CORTEX_HOME ambient presence (empty string INCLUDED) refuses the spawn
|
|
24
|
+
// (D3 — upstream consumers disagree about empty);
|
|
25
|
+
// 7. system-prompt-carrier-less augment (§9-4/§11-8) — the operator engraving OVERRIDE rides the
|
|
26
|
+
// first-user augment; a carrier backend (claude) never folds it in.
|
|
27
|
+
//
|
|
28
|
+
// [QK:*] labels mark the claims kill-qualified by scripts/mutants/acp-cortex.json
|
|
29
|
+
// under check-gate-qualification (assertion counts are never evidence).
|
|
30
|
+
//
|
|
31
|
+
// Two layers, mirroring the family convention:
|
|
32
|
+
// - Layer A (direct strip-types imports): models.ts + overlay.ts + augment.ts
|
|
33
|
+
// drive against INJECTED temp roots (Hard Rule 12: no operator dir touched).
|
|
34
|
+
// - Layer B (compiled backend-adapter.js): backend-adapter.ts imports its
|
|
35
|
+
// siblings with `.js` suffixes, which plain strip-types can't resolve — so
|
|
36
|
+
// we tsc-emit the project to a temp dir and import the compiled artifact.
|
|
37
|
+
// HOME is pointed at a sandbox BEFORE the compiled import so the module's
|
|
38
|
+
// homedir()-derived roots land in the sandbox, never the operator home.
|
|
39
|
+
|
|
40
|
+
import { strict as assert } from "node:assert";
|
|
41
|
+
import { execFileSync } from "node:child_process";
|
|
42
|
+
import {
|
|
43
|
+
existsSync,
|
|
44
|
+
lstatSync,
|
|
45
|
+
mkdirSync,
|
|
46
|
+
mkdtempSync,
|
|
47
|
+
readFileSync,
|
|
48
|
+
readlinkSync,
|
|
49
|
+
rmdirSync,
|
|
50
|
+
rmSync,
|
|
51
|
+
writeFileSync,
|
|
52
|
+
} from "node:fs";
|
|
53
|
+
import { tmpdir } from "node:os";
|
|
54
|
+
import { join, resolve } from "node:path";
|
|
55
|
+
import { pathToFileURL } from "node:url";
|
|
56
|
+
import { buildPiContextAugment } from "../pi-extensions/lib/acp/augment.ts";
|
|
57
|
+
import type { AcpMcpServer } from "../pi-extensions/lib/acp/config.ts";
|
|
58
|
+
import {
|
|
59
|
+
CORTEX_MODEL_PREFIX,
|
|
60
|
+
curatedClaudeModels,
|
|
61
|
+
curatedCortexModels,
|
|
62
|
+
SUPPORTED_CORTEX_MODEL_IDS,
|
|
63
|
+
} from "../pi-extensions/lib/acp/models.ts";
|
|
64
|
+
import {
|
|
65
|
+
cortexOverlayScopeId,
|
|
66
|
+
ensureCortexDualHomeOverlay,
|
|
67
|
+
projectCortexMcpJson,
|
|
68
|
+
sweepDeadCortexOverlays,
|
|
69
|
+
} from "../pi-extensions/lib/acp/overlay.ts";
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Layer A.1 — curated cortex surface (models.ts, real registry-derived)
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
assert.equal(CORTEX_MODEL_PREFIX, "cortex-", "cortex model prefix (routing authority) drifted");
|
|
76
|
+
assert.deepEqual(
|
|
77
|
+
[...SUPPORTED_CORTEX_MODEL_IDS],
|
|
78
|
+
["cortex-auto", "cortex-claude-opus-5", "cortex-claude-sonnet-5", "cortex-openai-gpt-5.4"],
|
|
79
|
+
"supported cortex model id set drifted from the GLG-decided 4-row curation [QK:CORTEX-CURATED-FOUR-ROWS]",
|
|
80
|
+
);
|
|
81
|
+
for (const id of SUPPORTED_CORTEX_MODEL_IDS) {
|
|
82
|
+
assert.ok(id.startsWith(CORTEX_MODEL_PREFIX), `cortex id ${id} must carry the reserved cortex- prefix`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const cortexRows = curatedCortexModels();
|
|
86
|
+
const cortexRowIds = cortexRows.map((m) => m.id);
|
|
87
|
+
assert.deepEqual(cortexRowIds, [...SUPPORTED_CORTEX_MODEL_IDS], "curated cortex rows must match the supported id set");
|
|
88
|
+
// The two Claude rows ride their OWN registry bases (a fabricated row would
|
|
89
|
+
// drift from the pi-ai catalog); auto/gpt ride the sonnet base as a floor.
|
|
90
|
+
const claudeRows = curatedClaudeModels();
|
|
91
|
+
const sonnetBase = claudeRows.find((m) => m.id === "claude-sonnet-5");
|
|
92
|
+
const opusBase = claudeRows.find((m) => m.id === "claude-opus-5");
|
|
93
|
+
// Plain throw (not assert.ok): an asserts-function premise makes tsc report the
|
|
94
|
+
// downstream `base` const as circularly-typed (TS7022).
|
|
95
|
+
if (!sonnetBase || !opusBase) throw new Error("test premise: sonnet-5/opus-5 present in curated claude surface");
|
|
96
|
+
for (const row of cortexRows) {
|
|
97
|
+
// Explicit annotation: node:assert's asserts-typed helpers otherwise make tsc
|
|
98
|
+
// flag this flow-dependent const as circularly typed (TS7022).
|
|
99
|
+
const base: (typeof claudeRows)[number] = row.id === "cortex-claude-opus-5" ? opusBase : sonnetBase;
|
|
100
|
+
assert.equal(row.maxTokens, base.maxTokens, `${row.id} maxTokens must ride its registry base (real registry path)`);
|
|
101
|
+
assert.deepEqual(row.cost, base.cost, `${row.id} cost must ride its registry base`);
|
|
102
|
+
assert.equal(row.reasoning, base.reasoning, `${row.id} reasoning must ride its registry base`);
|
|
103
|
+
assert.ok(row.contextWindow > 0, `${row.id} contextWindow must be positive`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Layer A.2 — dual-HOME overlay (overlay.ts, injected temp roots)
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
const enrichedServers: AcpMcpServer[] = [
|
|
111
|
+
{
|
|
112
|
+
name: "entwurf-bridge",
|
|
113
|
+
command: "node",
|
|
114
|
+
args: ["bridge.js"],
|
|
115
|
+
env: [
|
|
116
|
+
{ name: "PI_AGENT_ID", value: "entwurf/cortex-claude-sonnet-5" },
|
|
117
|
+
{ name: "PI_SESSION_ID", value: "sess-1234" },
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
{ name: "probe-mcp", command: "node", args: ["probe.js"], env: [{ name: "PROBE_FLAG", value: "1" }] },
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
{
|
|
124
|
+
const root = mkdtempSync(join(tmpdir(), "entwurf-cortex-dualhome-"));
|
|
125
|
+
const realHome = join(root, "real-home");
|
|
126
|
+
const realSnowflake = join(realHome, ".snowflake");
|
|
127
|
+
const overlaysRoot = join(root, "overlays");
|
|
128
|
+
try {
|
|
129
|
+
// Seed a fake operator ~/.snowflake — auth surfaces + operator state that
|
|
130
|
+
// must stay hidden. config.toml is deliberately ABSENT (optional on the
|
|
131
|
+
// measured host); credential_cache sits beside leak-prone cache siblings.
|
|
132
|
+
const realCortexDir = join(realSnowflake, "cortex");
|
|
133
|
+
mkdirSync(join(realCortexDir, "cache", "credential_cache"), { recursive: true });
|
|
134
|
+
writeFileSync(join(realSnowflake, "connections.toml"), "[connections.dev]\naccount='x'\n", "utf8"); // AUTH
|
|
135
|
+
writeFileSync(join(realCortexDir, "cache", "credential_cache", "token.json"), "{}\n", "utf8");
|
|
136
|
+
mkdirSync(join(realCortexDir, "cache", "tool_outputs"), { recursive: true }); // operator work product — DENY
|
|
137
|
+
writeFileSync(join(realCortexDir, "cache", "tip_history.json"), "{}\n", "utf8"); // DENY
|
|
138
|
+
mkdirSync(join(realCortexDir, "skills"), { recursive: true }); // operator skills — DENY
|
|
139
|
+
mkdirSync(join(realCortexDir, "conversations"), { recursive: true }); // operator transcripts — DENY
|
|
140
|
+
writeFileSync(join(realCortexDir, "mcp.json"), '{"mcpServers":{"operator-ambient":{}}}\n', "utf8"); // DENY
|
|
141
|
+
|
|
142
|
+
const overlay = ensureCortexDualHomeOverlay({
|
|
143
|
+
scopeKey: "pi:sess-1234",
|
|
144
|
+
mcpServers: enrichedServers,
|
|
145
|
+
realHome,
|
|
146
|
+
overlaysRoot,
|
|
147
|
+
isPidAlive: () => true,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Dual-HOME layout: HOME is overlay-owned; SNOWFLAKE_HOME sits INSIDE it,
|
|
151
|
+
// so cortex's homedir() axis (CONFIG_DIRS, ~/.claude/skills) and its
|
|
152
|
+
// snowflake axis are BOTH contained by one isolated home (D2).
|
|
153
|
+
assert.equal(
|
|
154
|
+
overlay.snowflakeHome,
|
|
155
|
+
join(overlay.home, ".snowflake"),
|
|
156
|
+
"SNOWFLAKE_HOME must sit inside the isolated HOME",
|
|
157
|
+
);
|
|
158
|
+
assert.ok(overlay.home.startsWith(overlaysRoot), "isolated HOME must live under the overlays root");
|
|
159
|
+
const ovlCortex = join(overlay.snowflakeHome, "cortex");
|
|
160
|
+
|
|
161
|
+
// Auth passthrough — connections.toml symlinked to the REAL file (never copied).
|
|
162
|
+
const connLink = join(overlay.snowflakeHome, "connections.toml");
|
|
163
|
+
assert.ok(lstatSync(connLink).isSymbolicLink(), "connections.toml must be a symlink (never a copy)");
|
|
164
|
+
assert.equal(
|
|
165
|
+
readlinkSync(connLink),
|
|
166
|
+
join(realSnowflake, "connections.toml"),
|
|
167
|
+
"connections.toml must point at the real file",
|
|
168
|
+
);
|
|
169
|
+
// config.toml is OPTIONAL — absent operator-side means absent overlay-side
|
|
170
|
+
// (a dangling link would break cortex's TOML load).
|
|
171
|
+
assert.ok(
|
|
172
|
+
!existsSync(join(overlay.snowflakeHome, "config.toml")),
|
|
173
|
+
"absent operator config.toml must not produce an overlay entry",
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// D5/F — the measured-minimum auth surface: credential_cache alone rides
|
|
177
|
+
// through; the whole `cache` is overlay-owned (a real dir), so operator
|
|
178
|
+
// tool_outputs / tip_history never reach the child; skills and the
|
|
179
|
+
// operator mcp.json are denied outright.
|
|
180
|
+
assert.ok(
|
|
181
|
+
!lstatSync(join(ovlCortex, "cache")).isSymbolicLink(),
|
|
182
|
+
"cortex/cache must be overlay-owned (a real dir), NEVER a whole-cache symlink [QK:CORTEX-AUTH-NARROW-CREDENTIAL-CACHE]",
|
|
183
|
+
);
|
|
184
|
+
const credLink = join(ovlCortex, "cache", "credential_cache");
|
|
185
|
+
assert.ok(
|
|
186
|
+
lstatSync(credLink).isSymbolicLink() &&
|
|
187
|
+
readlinkSync(credLink) === join(realCortexDir, "cache", "credential_cache"),
|
|
188
|
+
"cortex/cache/credential_cache must symlink the real credential cache",
|
|
189
|
+
);
|
|
190
|
+
assert.ok(
|
|
191
|
+
!existsSync(join(ovlCortex, "cache", "tool_outputs")) &&
|
|
192
|
+
!existsSync(join(ovlCortex, "cache", "tip_history.json")),
|
|
193
|
+
"operator cache work product (tool_outputs/tip_history) must be unreachable through the overlay",
|
|
194
|
+
);
|
|
195
|
+
assert.ok(!existsSync(join(ovlCortex, "skills")), "operator cortex/skills must be denied (no passthrough)");
|
|
196
|
+
assert.ok(!existsSync(join(ovlCortex, "conversations")), "operator conversations must be denied");
|
|
197
|
+
|
|
198
|
+
// D4 — the overlay authors autoUpdate:false (self-replacement off, not a pin).
|
|
199
|
+
const settings = JSON.parse(readFileSync(join(ovlCortex, "settings.json"), "utf8"));
|
|
200
|
+
assert.equal(
|
|
201
|
+
settings.autoUpdate,
|
|
202
|
+
false,
|
|
203
|
+
"overlay cortex/settings.json must pin autoUpdate:false — launch-time self-update would swap the implementation mid-rail [QK:CORTEX-AUTOUPDATE-OFF]",
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// D9 — the mcp.json projection IS the tool transport (cortex ignores the
|
|
207
|
+
// wire mcpServers param). Exact-authored, operator entries never merged.
|
|
208
|
+
const mcp = JSON.parse(readFileSync(join(ovlCortex, "mcp.json"), "utf8"));
|
|
209
|
+
assert.deepEqual(
|
|
210
|
+
Object.keys(mcp.mcpServers).sort(),
|
|
211
|
+
["entwurf-bridge", "probe-mcp"],
|
|
212
|
+
"mcp.json must carry EXACTLY the explicit servers (no ambient/operator merge)",
|
|
213
|
+
);
|
|
214
|
+
const bridgeEntry = mcp.mcpServers["entwurf-bridge"];
|
|
215
|
+
assert.equal(bridgeEntry.type, "stdio", "projected entries must be stdio-typed");
|
|
216
|
+
assert.deepEqual(bridgeEntry.args, ["bridge.js"], "projected args must be exact");
|
|
217
|
+
assert.equal(bridgeEntry.env.PI_SESSION_ID, "sess-1234", "envelope PI_SESSION_ID must survive the projection");
|
|
218
|
+
assert.equal(
|
|
219
|
+
bridgeEntry.env.PI_AGENT_ID,
|
|
220
|
+
"entwurf/cortex-claude-sonnet-5",
|
|
221
|
+
"envelope PI_AGENT_ID must survive the projection",
|
|
222
|
+
);
|
|
223
|
+
// D10 — dual-HOME: the bridge child ALONE gets the real operator HOME
|
|
224
|
+
// (garden store/socket axis); every other MCP child stays isolated.
|
|
225
|
+
assert.equal(
|
|
226
|
+
bridgeEntry.env.HOME,
|
|
227
|
+
realHome,
|
|
228
|
+
"the entwurf-bridge mcp.json entry must restore the REAL operator HOME (D10 dual-HOME) [QK:CORTEX-BRIDGE-DUAL-HOME]",
|
|
229
|
+
);
|
|
230
|
+
assert.equal(
|
|
231
|
+
mcp.mcpServers["probe-mcp"].env.HOME,
|
|
232
|
+
undefined,
|
|
233
|
+
"a non-bridge MCP child must NOT receive the real HOME (isolation stays)",
|
|
234
|
+
);
|
|
235
|
+
assert.equal(mcp.mcpServers["probe-mcp"].env.PROBE_FLAG, "1", "declared server env must survive the projection");
|
|
236
|
+
|
|
237
|
+
// D9 fail-loud — an entry the file cannot represent refuses the spawn
|
|
238
|
+
// (silent drop would advertise a config the session does not have).
|
|
239
|
+
assert.throws(
|
|
240
|
+
() => projectCortexMcpJson([{ type: "http", name: "web-mcp", url: "https://x", headers: [] }], realHome),
|
|
241
|
+
/cannot represent http/,
|
|
242
|
+
"an http/sse server must fail loud BEFORE spawn — never silently dropped [QK:CORTEX-MCP-NON-STDIO-FAILLOUD]",
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// config.toml present → linked (optional passthrough, positive half).
|
|
246
|
+
writeFileSync(join(realSnowflake, "config.toml"), "default_connection_name='dev'\n", "utf8");
|
|
247
|
+
const overlay2 = ensureCortexDualHomeOverlay({
|
|
248
|
+
scopeKey: "pi:sess-1234",
|
|
249
|
+
mcpServers: enrichedServers,
|
|
250
|
+
realHome,
|
|
251
|
+
overlaysRoot,
|
|
252
|
+
isPidAlive: () => true,
|
|
253
|
+
});
|
|
254
|
+
assert.equal(overlay2.scopeDir, overlay.scopeDir, "same scope key must reuse the same scope dir");
|
|
255
|
+
assert.ok(
|
|
256
|
+
lstatSync(join(overlay2.snowflakeHome, "config.toml")).isSymbolicLink(),
|
|
257
|
+
"present operator config.toml must be symlinked through",
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// Session/child-scoped identity — two scope keys must NEVER share a dir
|
|
261
|
+
// (a static shared overlay lets two residents overwrite one mcp.json).
|
|
262
|
+
const overlayOther = ensureCortexDualHomeOverlay({
|
|
263
|
+
scopeKey: "pi:sess-OTHER",
|
|
264
|
+
mcpServers: enrichedServers,
|
|
265
|
+
realHome,
|
|
266
|
+
overlaysRoot,
|
|
267
|
+
isPidAlive: () => true,
|
|
268
|
+
});
|
|
269
|
+
assert.notEqual(
|
|
270
|
+
overlayOther.scopeDir,
|
|
271
|
+
overlay.scopeDir,
|
|
272
|
+
"different session keys must materialize DIFFERENT overlay dirs — a shared static overlay races envelopes/configs [QK:CORTEX-OVERLAY-SESSION-SCOPED]",
|
|
273
|
+
);
|
|
274
|
+
assert.ok(
|
|
275
|
+
existsSync(join(overlay.snowflakeHome, "cortex", "mcp.json")),
|
|
276
|
+
"materializing another session's overlay must not clobber a sibling scope dir",
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// Exact rewrite — state a prior child wrote into the scope (conversations,
|
|
280
|
+
// $HOME dotfiles) is discarded on the next spawn: memory containment.
|
|
281
|
+
writeFileSync(join(overlay.snowflakeHome, "cortex", "leftover-conversation.json"), "{}\n", "utf8");
|
|
282
|
+
mkdirSync(join(overlay.home, ".claude"), { recursive: true });
|
|
283
|
+
const overlay3 = ensureCortexDualHomeOverlay({
|
|
284
|
+
scopeKey: "pi:sess-1234",
|
|
285
|
+
mcpServers: enrichedServers,
|
|
286
|
+
realHome,
|
|
287
|
+
overlaysRoot,
|
|
288
|
+
isPidAlive: () => true,
|
|
289
|
+
});
|
|
290
|
+
assert.ok(
|
|
291
|
+
!existsSync(join(overlay3.snowflakeHome, "cortex", "leftover-conversation.json")) &&
|
|
292
|
+
!existsSync(join(overlay3.home, ".claude")),
|
|
293
|
+
"a respawn must exact-rewrite the scope dir — prior child state (conversations/$HOME dotfiles) is contained, not inherited",
|
|
294
|
+
);
|
|
295
|
+
// …and the exact rewrite must not have followed auth symlinks (the real
|
|
296
|
+
// credential cache survives its overlay link's deletion).
|
|
297
|
+
assert.ok(
|
|
298
|
+
existsSync(join(realCortexDir, "cache", "credential_cache", "token.json")),
|
|
299
|
+
"exact rewrite must remove LINKS, never the real auth files behind them",
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
// Dead-pid sweep — a scope dir whose host process is gone is reclaimed
|
|
303
|
+
// (process resources only); alive/foreign entries stay.
|
|
304
|
+
const deadDir = join(overlaysRoot, cortexOverlayScopeId("pi:dead", 999_999));
|
|
305
|
+
mkdirSync(deadDir, { recursive: true });
|
|
306
|
+
const foreignDir = join(overlaysRoot, "not-a-scope-entry");
|
|
307
|
+
mkdirSync(foreignDir, { recursive: true });
|
|
308
|
+
sweepDeadCortexOverlays(overlaysRoot, (pid) => pid !== 999_999);
|
|
309
|
+
assert.ok(!existsSync(deadDir), "a dead host pid's scope dir must be swept");
|
|
310
|
+
assert.ok(existsSync(foreignDir), "entries off the scope-id grammar are never touched");
|
|
311
|
+
assert.ok(existsSync(overlay3.scopeDir), "a live scope dir must survive the sweep");
|
|
312
|
+
|
|
313
|
+
// realHome must be the parent-captured ABSOLUTE path (D10 recontract).
|
|
314
|
+
assert.throws(
|
|
315
|
+
() =>
|
|
316
|
+
ensureCortexDualHomeOverlay({
|
|
317
|
+
scopeKey: "pi:sess-rel",
|
|
318
|
+
mcpServers: [],
|
|
319
|
+
realHome: "relative/home",
|
|
320
|
+
overlaysRoot,
|
|
321
|
+
isPidAlive: () => true,
|
|
322
|
+
}),
|
|
323
|
+
/absolute realHome/,
|
|
324
|
+
"a non-absolute realHome must refuse (the child must never re-derive it)",
|
|
325
|
+
);
|
|
326
|
+
} finally {
|
|
327
|
+
rmSync(root, { recursive: true, force: true });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// Layer A.3 — system-prompt-carrier-less augment (§9-4/§11-8): the operator engraving override
|
|
333
|
+
// rides the FIRST-USER AUGMENT for cortex; claude (a carrier backend) never
|
|
334
|
+
// folds it in; no override → no engraving injected.
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
{
|
|
338
|
+
const root = mkdtempSync(join(tmpdir(), "entwurf-cortex-augment-"));
|
|
339
|
+
const engravingFile = join(root, "engraving.md");
|
|
340
|
+
writeFileSync(engravingFile, "OPERATOR ENGRAVING backend={{backend}} mcp={{mcp_servers}}", "utf8");
|
|
341
|
+
const savedEngravingEnv = process.env.ENTWURF_ACP_ENGRAVING_PATH;
|
|
342
|
+
try {
|
|
343
|
+
const augParams = { cwd: root, mcpServerNames: ["zebra", "alpha"], homeDir: root } as const;
|
|
344
|
+
|
|
345
|
+
// (a) override set → cortex augment LEADS with the rendered engraving.
|
|
346
|
+
process.env.ENTWURF_ACP_ENGRAVING_PATH = engravingFile;
|
|
347
|
+
const cortexAug = buildPiContextAugment({ backend: "cortex", ...augParams });
|
|
348
|
+
const rendered = "OPERATOR ENGRAVING backend=cortex mcp=alpha, zebra";
|
|
349
|
+
assert.ok(
|
|
350
|
+
cortexAug.startsWith(rendered),
|
|
351
|
+
`carrier-less cortex augment must LEAD with the rendered operator engraving (got head: ${cortexAug.slice(0, 80)})`,
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
// (b) claude is a CARRIER backend → the override never enters its augment
|
|
355
|
+
// (it rides claude's _meta.systemPrompt carrier via loadEngraving instead).
|
|
356
|
+
const claudeAug = buildPiContextAugment({ backend: "claude", ...augParams });
|
|
357
|
+
assert.ok(
|
|
358
|
+
!claudeAug.includes("OPERATOR ENGRAVING"),
|
|
359
|
+
"a carrier backend (claude) must NOT fold the operator engraving into its augment (it rides the _meta carrier)",
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
// (c) no override → the cortex augment carries NO engraving (the shipped
|
|
363
|
+
// claude default is never injected into a carrier-less augment).
|
|
364
|
+
delete process.env.ENTWURF_ACP_ENGRAVING_PATH;
|
|
365
|
+
const cortexAugNoOverride = buildPiContextAugment({ backend: "cortex", ...augParams });
|
|
366
|
+
assert.ok(
|
|
367
|
+
!cortexAugNoOverride.includes("OPERATOR ENGRAVING"),
|
|
368
|
+
"no override configured → carrier-less cortex augment carries no engraving (shipped default never injected)",
|
|
369
|
+
);
|
|
370
|
+
assert.ok(
|
|
371
|
+
cortexAugNoOverride.includes("Backend: cortex."),
|
|
372
|
+
"cortex augment still carries the bridge-identity section when no engraving override is set",
|
|
373
|
+
);
|
|
374
|
+
} finally {
|
|
375
|
+
rmSync(root, { recursive: true, force: true });
|
|
376
|
+
if (savedEngravingEnv === undefined) delete process.env.ENTWURF_ACP_ENGRAVING_PATH;
|
|
377
|
+
else process.env.ENTWURF_ACP_ENGRAVING_PATH = savedEngravingEnv;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ---------------------------------------------------------------------------
|
|
382
|
+
// Layer B — adapter routing / launch / refusal / enforce (compiled artifact)
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
const TMP_EMIT = ".tmp-verify/acp-cortex";
|
|
386
|
+
rmSync(TMP_EMIT, { recursive: true, force: true });
|
|
387
|
+
// Sandbox + deterministic env axes for the whole compiled-layer run.
|
|
388
|
+
const savedHome = process.env.HOME;
|
|
389
|
+
const savedOverride = process.env.CORTEX_ACP_COMMAND;
|
|
390
|
+
const savedConnEnv = process.env.ENTWURF_ACP_CORTEX_CONNECTION;
|
|
391
|
+
const savedCortexHome = process.env.CORTEX_HOME;
|
|
392
|
+
const savedPiSessionId = process.env.PI_SESSION_ID;
|
|
393
|
+
delete process.env.CORTEX_ACP_COMMAND;
|
|
394
|
+
delete process.env.ENTWURF_ACP_CORTEX_CONNECTION;
|
|
395
|
+
delete process.env.CORTEX_HOME;
|
|
396
|
+
const sandboxHome = mkdtempSync(join(tmpdir(), "entwurf-cortex-adapter-home-"));
|
|
397
|
+
try {
|
|
398
|
+
execFileSync("node_modules/.bin/tsc", ["--outDir", TMP_EMIT, "--rootDir", ".", "--noEmit", "false"], {
|
|
399
|
+
stdio: "pipe",
|
|
400
|
+
});
|
|
401
|
+
// Point HOME at the sandbox BEFORE the compiled import: the module's
|
|
402
|
+
// homedir()-derived roots (CORTEX_OVERLAYS_ROOT) bind at import time, so the
|
|
403
|
+
// adapter's real ensureOverlay below writes ONLY under the sandbox
|
|
404
|
+
// (Hard Rule 12 — offline verification never rewires the operator).
|
|
405
|
+
process.env.HOME = sandboxHome;
|
|
406
|
+
const adapterUrl = pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/backend-adapter.js")).href;
|
|
407
|
+
const mod = (await import(adapterUrl)) as {
|
|
408
|
+
cortexAdapter: {
|
|
409
|
+
backend: string;
|
|
410
|
+
routeModel: (id: string) => { nativeModelId: string } | undefined;
|
|
411
|
+
resolveLaunch: (p: { cwd: string; modelId: string; nativeModelId: string; config: unknown }) => {
|
|
412
|
+
command: string;
|
|
413
|
+
args: string[];
|
|
414
|
+
};
|
|
415
|
+
ensureOverlay: (p: {
|
|
416
|
+
cwd: string;
|
|
417
|
+
modelId: string;
|
|
418
|
+
nativeModelId: string;
|
|
419
|
+
config: unknown;
|
|
420
|
+
sessionKey: string;
|
|
421
|
+
}) => {
|
|
422
|
+
envOverrides: Record<string, string>;
|
|
423
|
+
};
|
|
424
|
+
enforceModel: (p: {
|
|
425
|
+
connection: unknown;
|
|
426
|
+
acpSessionId: string;
|
|
427
|
+
modelId: string;
|
|
428
|
+
nativeModelId: string;
|
|
429
|
+
}) => Promise<void>;
|
|
430
|
+
configSignatureFields: (adapterSettings: unknown) => Record<string, unknown>;
|
|
431
|
+
};
|
|
432
|
+
resolveAcpBackendAdapter: (id: string) => { adapter: { backend: string }; nativeModelId: string };
|
|
433
|
+
allCuratedModels: () => Array<{ id: string }>;
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
// (1) cortex curated models register through the real registry path —
|
|
437
|
+
// allCuratedModels folds cortex rows in beside claude and self-validates that
|
|
438
|
+
// every id routes to exactly one adapter (throws on 0/2+ matches). Its not
|
|
439
|
+
// throwing IS the anti-collision proof; we also assert the rows are present.
|
|
440
|
+
const registered = mod.allCuratedModels().map((m) => m.id);
|
|
441
|
+
for (const want of [
|
|
442
|
+
"claude-sonnet-5",
|
|
443
|
+
"cortex-auto",
|
|
444
|
+
"cortex-claude-opus-5",
|
|
445
|
+
"cortex-claude-sonnet-5",
|
|
446
|
+
"cortex-openai-gpt-5.4",
|
|
447
|
+
]) {
|
|
448
|
+
assert.ok(registered.includes(want), `allCuratedModels must register ${want} (got: ${registered.join(", ")})`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// (2) the cortex- prefix routes to cortexAdapter; strip recovers the native id.
|
|
452
|
+
const routedSonnet = mod.resolveAcpBackendAdapter("cortex-claude-sonnet-5");
|
|
453
|
+
assert.equal(routedSonnet.adapter.backend, "cortex", "cortex-claude-sonnet-5 must route to the cortex adapter");
|
|
454
|
+
assert.equal(routedSonnet.nativeModelId, "claude-sonnet-5", "prefix strip must recover the native model id");
|
|
455
|
+
assert.equal(
|
|
456
|
+
mod.resolveAcpBackendAdapter("claude-sonnet-5").adapter.backend,
|
|
457
|
+
"claude",
|
|
458
|
+
"the unprefixed claude id must still route to claude (prefix keeps the namespaces apart)",
|
|
459
|
+
);
|
|
460
|
+
assert.equal(
|
|
461
|
+
mod.cortexAdapter.routeModel("claude-sonnet-5"),
|
|
462
|
+
undefined,
|
|
463
|
+
"cortex must NOT claim the unprefixed claude id",
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
// (3) resolveLaunch NEVER pins the model. set-model is the single model
|
|
467
|
+
// authority (enforceModel below); a launch `-m` would be a second authority
|
|
468
|
+
// that silently drifts from the per-turn enforcement.
|
|
469
|
+
const launchSonnet = mod.cortexAdapter.resolveLaunch({
|
|
470
|
+
cwd: process.cwd(),
|
|
471
|
+
modelId: "cortex-claude-sonnet-5",
|
|
472
|
+
nativeModelId: "claude-sonnet-5",
|
|
473
|
+
config: { adapterSettings: { cortexConnection: null } },
|
|
474
|
+
});
|
|
475
|
+
assert.equal(launchSonnet.command, "cortex", "cortex launch command must be the `cortex` CLI (the ACP server)");
|
|
476
|
+
assert.deepEqual(
|
|
477
|
+
launchSonnet.args,
|
|
478
|
+
["acp", "serve"],
|
|
479
|
+
"cortex launch must be `acp serve` with NO -m — the model rides set-model, never a launch pin [QK:CORTEX-LAUNCH-NO-MODEL-PIN]",
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
// `-c <connection>` rides the resolved settings.
|
|
483
|
+
const launchConn = mod.cortexAdapter.resolveLaunch({
|
|
484
|
+
cwd: process.cwd(),
|
|
485
|
+
modelId: "cortex-auto",
|
|
486
|
+
nativeModelId: "auto",
|
|
487
|
+
config: { adapterSettings: { cortexConnection: "XD00000" } },
|
|
488
|
+
});
|
|
489
|
+
assert.deepEqual(launchConn.args, ["acp", "serve", "-c", "XD00000"], "a pinned connection must ride -c");
|
|
490
|
+
|
|
491
|
+
// (4) CORTEX_ACP_COMMAND override quoting — connection tokens with shell
|
|
492
|
+
// metacharacters must be single-quoted into the `bash -lc` string.
|
|
493
|
+
process.env.CORTEX_ACP_COMMAND = "my-cortex --debug";
|
|
494
|
+
const launchOverride = mod.cortexAdapter.resolveLaunch({
|
|
495
|
+
cwd: process.cwd(),
|
|
496
|
+
modelId: "cortex-claude-sonnet-5",
|
|
497
|
+
nativeModelId: "claude-sonnet-5",
|
|
498
|
+
config: { adapterSettings: { cortexConnection: "danger; rm -rf" } },
|
|
499
|
+
});
|
|
500
|
+
assert.equal(launchOverride.command, "bash", "override path must run via bash");
|
|
501
|
+
assert.equal(launchOverride.args[0], "-lc", "override path must use bash -lc");
|
|
502
|
+
const overrideCmd = launchOverride.args[1];
|
|
503
|
+
assert.ok(overrideCmd.startsWith("my-cortex --debug "), "override string must lead with the operator command");
|
|
504
|
+
assert.ok(
|
|
505
|
+
overrideCmd.includes("'danger; rm -rf'"),
|
|
506
|
+
`override must single-quote the connection with metacharacters (got: ${overrideCmd})`,
|
|
507
|
+
);
|
|
508
|
+
assert.ok(!overrideCmd.includes("-m"), "the override path must not smuggle a -m model pin back in");
|
|
509
|
+
// Single-quote inside a token is escaped as '\'' (POSIX-safe), never left bare.
|
|
510
|
+
const launchQuote = mod.cortexAdapter.resolveLaunch({
|
|
511
|
+
cwd: process.cwd(),
|
|
512
|
+
modelId: "cortex-claude-sonnet-5",
|
|
513
|
+
nativeModelId: "claude-sonnet-5",
|
|
514
|
+
config: { adapterSettings: { cortexConnection: "o'brien" } },
|
|
515
|
+
});
|
|
516
|
+
assert.ok(
|
|
517
|
+
launchQuote.args[1].includes(`'o'\\''brien'`),
|
|
518
|
+
`override must POSIX-escape an embedded single quote (got: ${launchQuote.args[1]})`,
|
|
519
|
+
);
|
|
520
|
+
delete process.env.CORTEX_ACP_COMMAND;
|
|
521
|
+
|
|
522
|
+
// (5) D3 — CORTEX_HOME ambient presence refuses the spawn, EMPTY STRING
|
|
523
|
+
// INCLUDED: upstream's own resolver fallthroughs on empty while other
|
|
524
|
+
// consumers treat set-but-empty as set; the adapter refuses the ambiguity.
|
|
525
|
+
const overlayParams = {
|
|
526
|
+
cwd: process.cwd(),
|
|
527
|
+
modelId: "cortex-claude-sonnet-5",
|
|
528
|
+
nativeModelId: "claude-sonnet-5",
|
|
529
|
+
config: { mcpServers: enrichedServers, adapterSettings: { cortexConnection: null } },
|
|
530
|
+
sessionKey: "pi:gate-sess-42",
|
|
531
|
+
};
|
|
532
|
+
process.env.CORTEX_HOME = "";
|
|
533
|
+
assert.throws(
|
|
534
|
+
() => mod.cortexAdapter.ensureOverlay(overlayParams),
|
|
535
|
+
/CORTEX_HOME is present/,
|
|
536
|
+
"an ambient CORTEX_HOME (even empty) must refuse the spawn — it bypasses SNOWFLAKE_HOME inside cortex [QK:CORTEX-HOME-PRESENCE-REFUSED]",
|
|
537
|
+
);
|
|
538
|
+
process.env.CORTEX_HOME = "/tmp/elsewhere";
|
|
539
|
+
assert.throws(
|
|
540
|
+
() => mod.cortexAdapter.ensureOverlay(overlayParams),
|
|
541
|
+
/CORTEX_HOME is present/,
|
|
542
|
+
"a set CORTEX_HOME must refuse the spawn",
|
|
543
|
+
);
|
|
544
|
+
delete process.env.CORTEX_HOME;
|
|
545
|
+
|
|
546
|
+
// …and in the ambient-clean state the adapter materializes the session-scoped
|
|
547
|
+
// overlay end to end: envOverrides carry the isolated HOME/SNOWFLAKE_HOME and
|
|
548
|
+
// the projected mcp.json carries envelope + dual-HOME (all under the sandbox).
|
|
549
|
+
process.env.PI_SESSION_ID = "gate-sess-42";
|
|
550
|
+
const overlayResult = mod.cortexAdapter.ensureOverlay(overlayParams);
|
|
551
|
+
const childHome = overlayResult.envOverrides.HOME;
|
|
552
|
+
const childSnowflake = overlayResult.envOverrides.SNOWFLAKE_HOME;
|
|
553
|
+
assert.ok(childHome && childSnowflake, "ensureOverlay must return HOME + SNOWFLAKE_HOME env overrides");
|
|
554
|
+
assert.equal(
|
|
555
|
+
childSnowflake,
|
|
556
|
+
join(childHome, ".snowflake"),
|
|
557
|
+
"SNOWFLAKE_HOME override must sit inside the HOME override",
|
|
558
|
+
);
|
|
559
|
+
assert.ok(
|
|
560
|
+
childHome.startsWith(join(sandboxHome, ".pi", "agent", "cortex-overlays")),
|
|
561
|
+
`the adapter overlay must land under <home>/.pi/agent/cortex-overlays (got: ${childHome})`,
|
|
562
|
+
);
|
|
563
|
+
const projected = JSON.parse(readFileSync(join(childSnowflake, "cortex", "mcp.json"), "utf8"));
|
|
564
|
+
assert.equal(
|
|
565
|
+
projected.mcpServers["entwurf-bridge"].env.PI_SESSION_ID,
|
|
566
|
+
"gate-sess-42",
|
|
567
|
+
"the adapter must envelope-enrich the projection with the live PI_SESSION_ID",
|
|
568
|
+
);
|
|
569
|
+
assert.equal(
|
|
570
|
+
projected.mcpServers["entwurf-bridge"].env.PI_AGENT_ID,
|
|
571
|
+
"entwurf/cortex-claude-sonnet-5",
|
|
572
|
+
"the adapter must envelope-enrich the projection with PI_AGENT_ID = entwurf/<curated id>",
|
|
573
|
+
);
|
|
574
|
+
assert.equal(
|
|
575
|
+
projected.mcpServers["entwurf-bridge"].env.HOME,
|
|
576
|
+
sandboxHome,
|
|
577
|
+
"the adapter must restore the parent-captured real HOME on the bridge entry (dual-HOME)",
|
|
578
|
+
);
|
|
579
|
+
delete process.env.PI_SESSION_ID;
|
|
580
|
+
|
|
581
|
+
// P0-1 (GPT review 2026-07-29) — the adapter scopes on the AUTHORITATIVE
|
|
582
|
+
// params.sessionKey, never an ambient re-derivation: backend.ts's key is
|
|
583
|
+
// opts.sessionId → PI_SESSION_ID → cwd, so two sessions in one process/cwd
|
|
584
|
+
// with different opts.sessionId (and no PI_SESSION_ID) are separate in the
|
|
585
|
+
// registry and MUST get separate overlays — an ambient re-derivation would
|
|
586
|
+
// alias them onto one mcp.json.
|
|
587
|
+
const overlayK1 = mod.cortexAdapter.ensureOverlay({ ...overlayParams, sessionKey: "pi:key-one" });
|
|
588
|
+
const overlayK2 = mod.cortexAdapter.ensureOverlay({ ...overlayParams, sessionKey: "pi:key-two" });
|
|
589
|
+
assert.notEqual(
|
|
590
|
+
overlayK1.envOverrides.HOME,
|
|
591
|
+
overlayK2.envOverrides.HOME,
|
|
592
|
+
"two session keys (same cwd, no PI_SESSION_ID) must materialize DIFFERENT overlay homes — the adapter scopes on params.sessionKey alone [QK:CORTEX-OVERLAY-KEY-IS-SESSION-KEY]",
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
// (6) CP0-M — enforceModel sends the NATIVE id through the SAME wire call the
|
|
596
|
+
// claude adapter makes (session/set_config_option, configId "model").
|
|
597
|
+
const setCalls: Array<Record<string, unknown>> = [];
|
|
598
|
+
await mod.cortexAdapter.enforceModel({
|
|
599
|
+
connection: {
|
|
600
|
+
setSessionConfigOption: async (p: Record<string, unknown>) => {
|
|
601
|
+
setCalls.push(p);
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
acpSessionId: "acp-1",
|
|
605
|
+
modelId: "cortex-claude-sonnet-5",
|
|
606
|
+
nativeModelId: "claude-sonnet-5",
|
|
607
|
+
});
|
|
608
|
+
assert.deepEqual(
|
|
609
|
+
setCalls,
|
|
610
|
+
[{ sessionId: "acp-1", configId: "model", value: "claude-sonnet-5" }],
|
|
611
|
+
"cortex enforceModel must send the NATIVE id via session/set_config_option — the single model authority [QK:CORTEX-ENFORCE-SET-MODEL]",
|
|
612
|
+
);
|
|
613
|
+
await assert.rejects(
|
|
614
|
+
mod.cortexAdapter.enforceModel({
|
|
615
|
+
connection: {},
|
|
616
|
+
acpSessionId: "acp-1",
|
|
617
|
+
modelId: "cortex-auto",
|
|
618
|
+
nativeModelId: "auto",
|
|
619
|
+
}),
|
|
620
|
+
/setSessionConfigOption unsupported/,
|
|
621
|
+
"a connection without set_config_option must fail loud (a silent default would lie about the model)",
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
// (7) a connection change invalidates a reused session via the signature seam.
|
|
625
|
+
assert.deepEqual(
|
|
626
|
+
mod.cortexAdapter.configSignatureFields({ cortexConnection: "XD00000" }),
|
|
627
|
+
{ cortexConnection: "XD00000" },
|
|
628
|
+
"configSignatureFields must fold the connection id (reuse invalidation)",
|
|
629
|
+
);
|
|
630
|
+
assert.deepEqual(
|
|
631
|
+
mod.cortexAdapter.configSignatureFields(undefined),
|
|
632
|
+
{ cortexConnection: null },
|
|
633
|
+
"no settings → null connection in the signature (stable shape)",
|
|
634
|
+
);
|
|
635
|
+
} finally {
|
|
636
|
+
rmSync(TMP_EMIT, { recursive: true, force: true });
|
|
637
|
+
try {
|
|
638
|
+
// The emit created `.tmp-verify/` as TMP_EMIT's parent. This gate runs inside
|
|
639
|
+
// check-gate-qualification's PURITY-checked snapshot (the first tsc-emit gate
|
|
640
|
+
// on a mutant lane), and the snapshot tree manifest walks ignored paths too —
|
|
641
|
+
// a leftover empty parent dir reads as IMPURE drift. Remove it when empty;
|
|
642
|
+
// a concurrent sibling gate's emit keeps it alive and the rmdir just fails.
|
|
643
|
+
rmdirSync(".tmp-verify");
|
|
644
|
+
} catch {
|
|
645
|
+
// non-empty or already gone — fine either way
|
|
646
|
+
}
|
|
647
|
+
rmSync(sandboxHome, { recursive: true, force: true });
|
|
648
|
+
if (savedHome === undefined) delete process.env.HOME;
|
|
649
|
+
else process.env.HOME = savedHome;
|
|
650
|
+
if (savedOverride === undefined) delete process.env.CORTEX_ACP_COMMAND;
|
|
651
|
+
else process.env.CORTEX_ACP_COMMAND = savedOverride;
|
|
652
|
+
if (savedConnEnv === undefined) delete process.env.ENTWURF_ACP_CORTEX_CONNECTION;
|
|
653
|
+
else process.env.ENTWURF_ACP_CORTEX_CONNECTION = savedConnEnv;
|
|
654
|
+
if (savedCortexHome === undefined) delete process.env.CORTEX_HOME;
|
|
655
|
+
else process.env.CORTEX_HOME = savedCortexHome;
|
|
656
|
+
if (savedPiSessionId === undefined) delete process.env.PI_SESSION_ID;
|
|
657
|
+
else process.env.PI_SESSION_ID = savedPiSessionId;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
console.log(
|
|
661
|
+
`[check-acp-cortex] ok — 4-row curation (${cortexRowIds.join(", ")}) rides real registry bases and routes via the ` +
|
|
662
|
+
`cortex- prefix; launch is \`cortex acp serve\` with NO -m (set-model is the single model authority, native id ` +
|
|
663
|
+
`on the wire); CORTEX_HOME presence (empty included) refuses the spawn; the dual-HOME overlay isolates HOME + ` +
|
|
664
|
+
`SNOWFLAKE_HOME per session, passes through only connections.toml/config.toml/credential_cache, authors ` +
|
|
665
|
+
`autoUpdate:false, and projects the envelope-enriched explicit servers into cortex/mcp.json with the real HOME ` +
|
|
666
|
+
`restored on entwurf-bridge alone (non-stdio fails loud); the carrier-less augment leads with the operator ` +
|
|
667
|
+
`engraving override; CORTEX_ACP_COMMAND override single-quotes metachar tokens`,
|
|
668
|
+
);
|