@sanqianx/project-knowledge 5.0.26 → 5.0.27
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/.claude-plugin/marketplace.json +1 -1
- package/_site/_test/helpers/qa-r44-env.js +418 -0
- package/_site/_test/helpers/qa-r44-report.js +656 -0
- package/_site/_test/helpers/qa-r44-ui.js +347 -0
- package/_site/_test/helpers/qa-r49-wb-host.js +58 -0
- package/_site/_test/qa-r44-permission-question.js +756 -0
- package/_site/_test/qa-r49-sys003-real-blank-probe.js +374 -0
- package/module-runtime-manifest.json +28 -26
- package/package.json +2 -2
- package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.mcp.json +1 -1
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// _site/_test/helpers/qa-r44-env.js
|
|
3
|
+
//
|
|
4
|
+
// The R44 lane isolation helper (WB-006 / WB-007).
|
|
5
|
+
//
|
|
6
|
+
// Contract (from the lane dispatch):
|
|
7
|
+
// * the driver's FIRST action is to capture the real environment and the
|
|
8
|
+
// real tool paths, and then to harden the driver's OWN process.env, so
|
|
9
|
+
// that every child process it spawns inherits the isolation. The product
|
|
10
|
+
// is never bootstrapped under the real environment first;
|
|
11
|
+
// * HOME / USERPROFILE / HOMEDRIVE / HOMEPATH / APPDATA / LOCALAPPDATA /
|
|
12
|
+
// CODEX_HOME / CLAUDE_CONFIG_DIR / TEMP / TMP / TMPDIR are redirected;
|
|
13
|
+
// * KB_DATA_DIR and KB_KNOWLEDGE_DATA_DIR are redirected;
|
|
14
|
+
// * the Workbench data dir and the Bridge home are redirected;
|
|
15
|
+
// * the temp root lives OUTSIDE the repository;
|
|
16
|
+
// * the ambient module addresses and every ambient model credential are
|
|
17
|
+
// dropped, so a call can only reach the profile the product resolves
|
|
18
|
+
// itself, in memory;
|
|
19
|
+
// * the live product ports are refused.
|
|
20
|
+
//
|
|
21
|
+
// The real Chrome executable and the real Claude Code executable are captured
|
|
22
|
+
// BEFORE the environment is hardened (the R33 lesson: Chrome and the CLI are
|
|
23
|
+
// resolved from LOCALAPPDATA / APPDATA, and those are exactly the variables
|
|
24
|
+
// this lane isolates). They are re-published to the isolated environment as
|
|
25
|
+
// KB_CHROME_PATH and CLAUDE_CODE_EXECPATH, so the isolation never turns into
|
|
26
|
+
// a "tool not found" failure.
|
|
27
|
+
//
|
|
28
|
+
// Credential contract: no credential value is read, copied, logged, written
|
|
29
|
+
// to a fixture or reported. Only variable NAMES are ever recorded.
|
|
30
|
+
//
|
|
31
|
+
// R44C: the lane no longer depends on the older batch fixture. It uses the
|
|
32
|
+
// CANONICAL tracked fixture (helpers/parallel-workbench-integration-5025-
|
|
33
|
+
// fixture.js) — the same one the tracked runtime harness uses — so the lane's
|
|
34
|
+
// layout and the harness' child environment are built by one contract. The two
|
|
35
|
+
// directory-digest helpers the lane needs are implemented here, in the lane's
|
|
36
|
+
// own file, so the old fixture leaves the dependency closure entirely.
|
|
37
|
+
const fs = require('node:fs');
|
|
38
|
+
const os = require('node:os');
|
|
39
|
+
const path = require('node:path');
|
|
40
|
+
const crypto = require('node:crypto');
|
|
41
|
+
const { execFileSync } = require('node:child_process');
|
|
42
|
+
const fixture = require('./parallel-workbench-integration-5025-fixture');
|
|
43
|
+
const { findChromeDetailed } = require('./find-chrome');
|
|
44
|
+
|
|
45
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
|
|
46
|
+
const LANE_NAME = 'wb-permission-question-r44';
|
|
47
|
+
|
|
48
|
+
// Ambient module addresses that would point a lane at a live instance.
|
|
49
|
+
const AMBIENT_MODULE_URLS = [
|
|
50
|
+
'KB_KNOWLEDGE_ENGINE_URL',
|
|
51
|
+
'KB_VECTORHUB_URL',
|
|
52
|
+
'KB_TERMINAL_URL',
|
|
53
|
+
'KB_EVENTBRIDGE_URL',
|
|
54
|
+
'KB_EVENT_BRIDGE_URL',
|
|
55
|
+
'KB_WORKBENCH_RUNTIME_TOKEN',
|
|
56
|
+
'KB_MODULES_AUTOSTART',
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
// Ambient model credentials. A lane may only use the credential the product
|
|
60
|
+
// resolves from its own read-only profile store, in memory.
|
|
61
|
+
const AMBIENT_MODEL_ENV = [
|
|
62
|
+
'ANTHROPIC_API_KEY',
|
|
63
|
+
'ANTHROPIC_AUTH_TOKEN',
|
|
64
|
+
'ANTHROPIC_BASE_URL',
|
|
65
|
+
'ANTHROPIC_MODEL',
|
|
66
|
+
'ANTHROPIC_DEFAULT_THINKING_MODEL',
|
|
67
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
68
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
69
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
70
|
+
'API_TIMEOUT_MS',
|
|
71
|
+
'CLAUDE_CODE_OAUTH_TOKEN',
|
|
72
|
+
'CLAUDE_CODE_USE_BEDROCK',
|
|
73
|
+
'CLAUDE_CODE_USE_VERTEX',
|
|
74
|
+
'CLAUDE_CODE_USE_FOUNDRY',
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
// The variables this lane redirects. Every one of them must point inside the
|
|
78
|
+
// lane's own run root once the driver is hardened.
|
|
79
|
+
const REDIRECTED_VARS = [
|
|
80
|
+
'HOME',
|
|
81
|
+
'USERPROFILE',
|
|
82
|
+
'HOMEDRIVE',
|
|
83
|
+
'HOMEPATH',
|
|
84
|
+
'APPDATA',
|
|
85
|
+
'LOCALAPPDATA',
|
|
86
|
+
'CODEX_HOME',
|
|
87
|
+
'CLAUDE_CONFIG_DIR',
|
|
88
|
+
'TEMP',
|
|
89
|
+
'TMP',
|
|
90
|
+
'TMPDIR',
|
|
91
|
+
'KB_DATA_DIR',
|
|
92
|
+
'KB_KNOWLEDGE_DATA_DIR',
|
|
93
|
+
'AGENT_TERMINAL_DATA_DIR',
|
|
94
|
+
'CLAUDE_AI_WORKBENCH_DATA_DIR',
|
|
95
|
+
'AI_CODING_EVENT_BRIDGE_HOME',
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
function isInside(candidate, parent) {
|
|
99
|
+
const relative = path.relative(path.resolve(parent), path.resolve(candidate));
|
|
100
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// The real profile store of the product. It is resolved while the REAL
|
|
104
|
+
// environment is still in place: this module is loaded before the driver
|
|
105
|
+
// hardens its own environment, and os.homedir() reads the REDIRECTED
|
|
106
|
+
// USERPROFILE once the isolation is applied. The value is therefore captured
|
|
107
|
+
// at load time and refreshed by the capture, never re-derived later.
|
|
108
|
+
let REAL_PROFILE_DIR = path.join(os.homedir(), '.project-knowledge-v5', 'workbench');
|
|
109
|
+
function realProfileDir() { return REAL_PROFILE_DIR; }
|
|
110
|
+
|
|
111
|
+
// The lane's own temp root, outside the repository and outside the real
|
|
112
|
+
// home. Portable: it follows the repository location, never a fixed drive.
|
|
113
|
+
function resolveLaneTempRoot() {
|
|
114
|
+
const override = String(process.env.QA_R44_TEMP_ROOT || '').trim();
|
|
115
|
+
const base = override
|
|
116
|
+
? path.resolve(override)
|
|
117
|
+
: path.join(path.dirname(REPO_ROOT), '.project-knowledge-test-temp');
|
|
118
|
+
const root = path.join(base, LANE_NAME);
|
|
119
|
+
if (isInside(root, REPO_ROOT)) throw new Error(`refusing a lane temp root inside the repository: ${root}`);
|
|
120
|
+
fs.mkdirSync(root, { recursive: true });
|
|
121
|
+
return root;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// The real Claude Code executable, resolved the way the product resolves it
|
|
125
|
+
// but WITHOUT loading the product module (loading it would boot its own
|
|
126
|
+
// session housekeeping inside the driver).
|
|
127
|
+
function findClaudeExecutable() {
|
|
128
|
+
const rejected = [];
|
|
129
|
+
const push = (file, source) => {
|
|
130
|
+
if (!file) return;
|
|
131
|
+
try {
|
|
132
|
+
if (!fs.statSync(file).isFile()) { rejected.push({ path: file, reason: 'not a file' }); return; }
|
|
133
|
+
} catch { rejected.push({ path: file, reason: 'missing' }); return; }
|
|
134
|
+
return { path: path.resolve(file), source };
|
|
135
|
+
};
|
|
136
|
+
const override = String(process.env.CLAUDE_CODE_EXECPATH || '').trim();
|
|
137
|
+
if (override) {
|
|
138
|
+
const found = push(override, 'environment');
|
|
139
|
+
if (found) return found;
|
|
140
|
+
}
|
|
141
|
+
const packageDirs = [];
|
|
142
|
+
if (process.env.APPDATA) packageDirs.push(path.join(process.env.APPDATA, 'npm', 'node_modules', '@anthropic-ai', 'claude-code'));
|
|
143
|
+
for (const dir of packageDirs) {
|
|
144
|
+
try {
|
|
145
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
146
|
+
const bin = typeof pkg.bin === 'string' ? pkg.bin : (pkg.bin && pkg.bin.claude) || null;
|
|
147
|
+
if (bin) {
|
|
148
|
+
const entry = path.join(dir, ...String(bin).split(/[\\/]+/).filter(Boolean));
|
|
149
|
+
const found = push(entry, 'npm-global-package');
|
|
150
|
+
if (found) return found;
|
|
151
|
+
}
|
|
152
|
+
} catch { /* the package is simply not there */ }
|
|
153
|
+
const found = push(path.join(dir, 'bin', 'claude.exe'), 'npm-global-package');
|
|
154
|
+
if (found) return found;
|
|
155
|
+
}
|
|
156
|
+
const userProfile = process.env.USERPROFILE || '';
|
|
157
|
+
if (userProfile) {
|
|
158
|
+
const found = push(path.join(userProfile, '.local', 'bin', 'claude.exe'), 'user-local-bin');
|
|
159
|
+
if (found) return found;
|
|
160
|
+
}
|
|
161
|
+
if (process.env.LOCALAPPDATA) {
|
|
162
|
+
for (const candidate of [
|
|
163
|
+
path.join(process.env.LOCALAPPDATA, 'Programs', 'claude-code', 'claude.exe'),
|
|
164
|
+
path.join(process.env.LOCALAPPDATA, 'claude-code', 'claude.exe'),
|
|
165
|
+
]) {
|
|
166
|
+
const found = push(candidate, 'localappdata');
|
|
167
|
+
if (found) return found;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
for (const dir of String(process.env.PATH || '').split(path.delimiter)) {
|
|
171
|
+
if (!dir) continue;
|
|
172
|
+
const found = push(path.join(dir, 'claude.exe'), 'path');
|
|
173
|
+
if (found) return found;
|
|
174
|
+
}
|
|
175
|
+
const whereExe = process.env.WINDIR ? path.join(process.env.WINDIR, 'System32', 'where.exe') : 'where.exe';
|
|
176
|
+
try {
|
|
177
|
+
const stdout = execFileSync(whereExe, ['claude'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true, timeout: 5000 });
|
|
178
|
+
for (const hit of String(stdout).split(/\r?\n/).map(entry => entry.trim()).filter(Boolean)) {
|
|
179
|
+
const found = push(hit, 'where');
|
|
180
|
+
if (found) return found;
|
|
181
|
+
}
|
|
182
|
+
} catch { /* where.exe found nothing */ }
|
|
183
|
+
throw new Error(`Claude Code executable not found for the ${LANE_NAME} lane; rejected: ${JSON.stringify(rejected)}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// The real environment, captured BEFORE anything is hardened.
|
|
187
|
+
function captureRealEnvironment() {
|
|
188
|
+
const realHome = os.homedir();
|
|
189
|
+
const realUserProfile = process.env.USERPROFILE || realHome;
|
|
190
|
+
const chrome = findChromeDetailed();
|
|
191
|
+
const claude = findClaudeExecutable();
|
|
192
|
+
const present = (name) => Object.keys(process.env).some(key => key.toUpperCase() === name && String(process.env[key] || '').trim() !== '');
|
|
193
|
+
REAL_PROFILE_DIR = path.join(realHome, '.project-knowledge-v5', 'workbench');
|
|
194
|
+
return {
|
|
195
|
+
realHome,
|
|
196
|
+
realUserProfile,
|
|
197
|
+
realProfileDir: REAL_PROFILE_DIR,
|
|
198
|
+
chrome: { path: chrome.path, source: chrome.source },
|
|
199
|
+
claude: { path: claude.path, source: claude.source },
|
|
200
|
+
ambientModuleUrlsPresent: AMBIENT_MODULE_URLS.filter(present),
|
|
201
|
+
ambientCredentialsPresent: AMBIENT_MODEL_ENV.filter(present),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// The lane's own isolation layout. It reuses the canonical fixture layout so
|
|
206
|
+
// the frozen host helper can host it unchanged, and adds the directories this
|
|
207
|
+
// lane has to isolate on its own.
|
|
208
|
+
function createLaneLayout() {
|
|
209
|
+
const laneTempRoot = resolveLaneTempRoot();
|
|
210
|
+
// The canonical fixture resolves its temp root from this variable, so the
|
|
211
|
+
// lane root is published BEFORE the layout is created: the whole run then
|
|
212
|
+
// lives under one lane-owned directory outside the repository.
|
|
213
|
+
process.env.PK_TEST_TEMP_ROOT = laneTempRoot;
|
|
214
|
+
const layout = fixture.createIsolationLayout('qa-r44');
|
|
215
|
+
layout.laneTempRoot = laneTempRoot;
|
|
216
|
+
layout.logs = path.join(laneTempRoot, 'logs');
|
|
217
|
+
fs.mkdirSync(layout.logs, { recursive: true });
|
|
218
|
+
// Chrome refuses remote debugging when the profile root it resolves is
|
|
219
|
+
// missing, so the empty scaffold is created (the R33 lesson). The canonical
|
|
220
|
+
// fixture already creates the home-shaped one; the LOCALAPPDATA-shaped one
|
|
221
|
+
// is kept because the lane redirects LOCALAPPDATA as well.
|
|
222
|
+
for (const scaffold of [
|
|
223
|
+
path.join(layout.localAppData, 'Google', 'Chrome', 'User Data'),
|
|
224
|
+
path.join(layout.home, 'AppData', 'Local', 'Google', 'Chrome', 'User Data'),
|
|
225
|
+
]) {
|
|
226
|
+
fs.mkdirSync(scaffold, { recursive: true });
|
|
227
|
+
}
|
|
228
|
+
return layout;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Harden the DRIVER's own environment. Every child process this driver
|
|
232
|
+
// spawns afterwards inherits it; nothing is bootstrapped under the real
|
|
233
|
+
// environment first.
|
|
234
|
+
function hardenDriverEnv(layout, captured) {
|
|
235
|
+
const parsed = path.parse(layout.home);
|
|
236
|
+
const redirected = {
|
|
237
|
+
HOME: layout.home,
|
|
238
|
+
USERPROFILE: layout.home,
|
|
239
|
+
HOMEDRIVE: parsed.root,
|
|
240
|
+
HOMEPATH: parsed.root === path.sep ? layout.home.slice(1) : layout.home.slice(parsed.root.length),
|
|
241
|
+
APPDATA: layout.appData,
|
|
242
|
+
LOCALAPPDATA: layout.localAppData,
|
|
243
|
+
CODEX_HOME: layout.codexHome,
|
|
244
|
+
CLAUDE_CONFIG_DIR: layout.claudeConfigDir,
|
|
245
|
+
TEMP: layout.temp,
|
|
246
|
+
TMP: layout.temp,
|
|
247
|
+
TMPDIR: layout.temp,
|
|
248
|
+
KB_DATA_DIR: layout.knowledgeDataDir,
|
|
249
|
+
KB_KNOWLEDGE_DATA_DIR: layout.knowledgeDataDir,
|
|
250
|
+
AGENT_TERMINAL_DATA_DIR: layout.dataDir,
|
|
251
|
+
CLAUDE_AI_WORKBENCH_DATA_DIR: layout.dataDir,
|
|
252
|
+
AI_CODING_EVENT_BRIDGE_HOME: layout.bridgeHome,
|
|
253
|
+
};
|
|
254
|
+
const droppedModuleUrls = [];
|
|
255
|
+
const droppedCredentials = [];
|
|
256
|
+
for (const name of AMBIENT_MODULE_URLS) {
|
|
257
|
+
if (process.env[name] !== undefined) { delete process.env[name]; droppedModuleUrls.push(name); }
|
|
258
|
+
}
|
|
259
|
+
for (const name of AMBIENT_MODEL_ENV) {
|
|
260
|
+
if (process.env[name] !== undefined) { delete process.env[name]; droppedCredentials.push(name); }
|
|
261
|
+
}
|
|
262
|
+
for (const [name, value] of Object.entries(redirected)) process.env[name] = value;
|
|
263
|
+
// The isolated fixture layout is the temp root of the frozen helper too.
|
|
264
|
+
process.env.PK_TEST_TEMP_ROOT = layout.laneTempRoot;
|
|
265
|
+
process.env.KB_MODULES_AUTOSTART = '0';
|
|
266
|
+
// The real tools are captured before the isolation and re-published as
|
|
267
|
+
// absolute paths, so isolating APPDATA / LOCALAPPDATA never hides them.
|
|
268
|
+
process.env.KB_CHROME_PATH = captured.chrome.path;
|
|
269
|
+
process.env.CLAUDE_CODE_EXECPATH = captured.claude.path;
|
|
270
|
+
// The canonical harness refuses to discover a browser from an isolated
|
|
271
|
+
// environment: it wants the path that was captured BEFORE the isolation.
|
|
272
|
+
// It is published under both names its callers accept.
|
|
273
|
+
process.env.PK_WB_CHROME = captured.chrome.path;
|
|
274
|
+
// The product resolves the ORIGINAL profile store from the real home; the
|
|
275
|
+
// isolated HOME would otherwise hide it. The path is a location, not a
|
|
276
|
+
// credential: the store itself is opened read-only by the product.
|
|
277
|
+
process.env.PK_WB_REAL_PROFILE_DIR = captured.realProfileDir;
|
|
278
|
+
return {
|
|
279
|
+
redirected: Object.keys(redirected),
|
|
280
|
+
redirectedValues: redirected,
|
|
281
|
+
droppedModuleUrls,
|
|
282
|
+
droppedCredentials,
|
|
283
|
+
republishedTools: { KB_CHROME_PATH: captured.chrome.path, PK_WB_CHROME: captured.chrome.path, CLAUDE_CODE_EXECPATH: captured.claude.path },
|
|
284
|
+
preservedRealProfileDir: captured.realProfileDir,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Fail-closed check of the hardened environment.
|
|
289
|
+
function assertIsolatedEnv(env, layout, captured) {
|
|
290
|
+
const problems = [];
|
|
291
|
+
for (const name of REDIRECTED_VARS) {
|
|
292
|
+
const value = env[name];
|
|
293
|
+
if (!value) { problems.push(`${name} is not set`); continue; }
|
|
294
|
+
// HOMEDRIVE and HOMEPATH are one Windows pair: they are checked as a
|
|
295
|
+
// whole below, not as two independent directories.
|
|
296
|
+
if (name === 'HOMEDRIVE' || name === 'HOMEPATH') continue;
|
|
297
|
+
if (!isInside(value, layout.root)) problems.push(`${name} points outside the lane run root: ${value}`);
|
|
298
|
+
}
|
|
299
|
+
if (env.HOMEDRIVE && env.HOMEPATH) {
|
|
300
|
+
const combined = path.resolve(`${env.HOMEDRIVE}${env.HOMEPATH}`);
|
|
301
|
+
if (!isInside(combined, layout.root)) problems.push(`HOMEDRIVE+HOMEPATH points outside the lane run root: ${combined}`);
|
|
302
|
+
else if (env.HOME && path.resolve(combined).toLowerCase() !== path.resolve(env.HOME).toLowerCase()) {
|
|
303
|
+
problems.push(`HOMEDRIVE+HOMEPATH and HOME disagree: ${combined} vs ${env.HOME}`);
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
problems.push('HOMEDRIVE and HOMEPATH are not both set');
|
|
307
|
+
}
|
|
308
|
+
if (env.USERPROFILE && captured.realUserProfile && path.resolve(env.USERPROFILE).toLowerCase() === path.resolve(captured.realUserProfile).toLowerCase()) {
|
|
309
|
+
problems.push('USERPROFILE still points at the real user profile');
|
|
310
|
+
}
|
|
311
|
+
if (env.HOME && captured.realHome && path.resolve(env.HOME).toLowerCase() === path.resolve(captured.realHome).toLowerCase()) {
|
|
312
|
+
problems.push('HOME still points at the real home');
|
|
313
|
+
}
|
|
314
|
+
for (const name of AMBIENT_MODEL_ENV) {
|
|
315
|
+
if (env[name] !== undefined && String(env[name] || '').trim() !== '') problems.push(`credential variable survived: ${name}`);
|
|
316
|
+
}
|
|
317
|
+
for (const name of AMBIENT_MODULE_URLS) {
|
|
318
|
+
if (name === 'KB_MODULES_AUTOSTART') continue;
|
|
319
|
+
if (env[name] !== undefined && String(env[name] || '').trim() !== '') problems.push(`ambient module address survived: ${name}`);
|
|
320
|
+
}
|
|
321
|
+
if (path.resolve(env.PK_TEST_TEMP_ROOT || '') === path.resolve(REPO_ROOT)) problems.push('the lane temp root is the repository root');
|
|
322
|
+
if (isInside(env.PK_TEST_TEMP_ROOT || '', REPO_ROOT)) problems.push('the lane temp root is inside the repository');
|
|
323
|
+
if (problems.length) throw new Error(`the lane environment is not isolated:\n - ${problems.join('\n - ')}`);
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// A report-friendly summary. It carries paths and variable NAMES only; no
|
|
328
|
+
// credential value is ever read, copied or printed.
|
|
329
|
+
function isolationContract(layout, captured, hardened) {
|
|
330
|
+
return {
|
|
331
|
+
lane: LANE_NAME,
|
|
332
|
+
repoRoot: REPO_ROOT,
|
|
333
|
+
runRoot: layout.root,
|
|
334
|
+
laneTempRoot: layout.laneTempRoot,
|
|
335
|
+
tempRootOutsideRepository: !isInside(layout.laneTempRoot, REPO_ROOT),
|
|
336
|
+
redirectedVariables: hardened.redirected,
|
|
337
|
+
redirectedValues: hardened.redirectedValues,
|
|
338
|
+
droppedAmbientModuleUrls: hardened.droppedModuleUrls,
|
|
339
|
+
droppedAmbientCredentials: hardened.droppedCredentials,
|
|
340
|
+
ambientModuleUrlsPresentBefore: captured.ambientModuleUrlsPresent,
|
|
341
|
+
ambientCredentialsPresentBefore: captured.ambientCredentialsPresent,
|
|
342
|
+
republishedTools: hardened.republishedTools,
|
|
343
|
+
realProfileDirectory: captured.realProfileDir,
|
|
344
|
+
realProfileOpened: 'read-only by the product; the credential stays in the host process memory',
|
|
345
|
+
forbiddenPorts: Array.from(fixture.FORBIDDEN_PORTS),
|
|
346
|
+
realHomeUsed: false,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Explicit containment: every path a lane tool touches must live under
|
|
351
|
+
// `root`. No globbing, no traversal, no absolute path outside the root.
|
|
352
|
+
function containedPath(root, rel) {
|
|
353
|
+
const target = path.resolve(root, ...String(rel).split(/[\\/]+/).filter(Boolean));
|
|
354
|
+
const relative = path.relative(path.resolve(root), target);
|
|
355
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
356
|
+
throw new Error(`path escapes its root: ${rel}`);
|
|
357
|
+
}
|
|
358
|
+
return target;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// The byte-level digest of every file of a directory. Only the NAMES and the
|
|
362
|
+
// digests are kept; the content is never copied anywhere. The comparison is
|
|
363
|
+
// done in memory and the digests of a real profile store are never printed.
|
|
364
|
+
function digestDirectory(dir) {
|
|
365
|
+
if (!dir || !fs.existsSync(dir)) return null;
|
|
366
|
+
const entries = {};
|
|
367
|
+
for (const name of fs.readdirSync(dir)) {
|
|
368
|
+
try {
|
|
369
|
+
const file = path.join(dir, name);
|
|
370
|
+
if (!fs.statSync(file).isFile()) continue;
|
|
371
|
+
entries[name] = crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
372
|
+
} catch { entries[name] = null; }
|
|
373
|
+
}
|
|
374
|
+
return entries;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function compareDigests(before, after) {
|
|
378
|
+
const names = new Set([...Object.keys(before || {}), ...Object.keys(after || {})]);
|
|
379
|
+
const changed = [];
|
|
380
|
+
const added = [];
|
|
381
|
+
const removed = [];
|
|
382
|
+
const unreadable = [];
|
|
383
|
+
for (const name of names) {
|
|
384
|
+
const a = before ? before[name] : undefined;
|
|
385
|
+
const b = after ? after[name] : undefined;
|
|
386
|
+
if (a === undefined && b !== undefined) added.push(name);
|
|
387
|
+
else if (a !== undefined && b === undefined) removed.push(name);
|
|
388
|
+
else if (a === null || b === null) unreadable.push(name);
|
|
389
|
+
else if (a !== b) changed.push(name);
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
count: names.size,
|
|
393
|
+
byteIdentical: changed.length === 0 && added.length === 0 && removed.length === 0 && unreadable.length === 0,
|
|
394
|
+
changedFiles: changed,
|
|
395
|
+
addedFiles: added,
|
|
396
|
+
removedFiles: removed,
|
|
397
|
+
unreadableFiles: unreadable,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
module.exports = {
|
|
402
|
+
LANE_NAME,
|
|
403
|
+
REPO_ROOT,
|
|
404
|
+
REDIRECTED_VARS,
|
|
405
|
+
AMBIENT_MODULE_URLS,
|
|
406
|
+
AMBIENT_MODEL_ENV,
|
|
407
|
+
isInside,
|
|
408
|
+
realProfileDir,
|
|
409
|
+
resolveLaneTempRoot,
|
|
410
|
+
captureRealEnvironment,
|
|
411
|
+
createLaneLayout,
|
|
412
|
+
hardenDriverEnv,
|
|
413
|
+
assertIsolatedEnv,
|
|
414
|
+
isolationContract,
|
|
415
|
+
containedPath,
|
|
416
|
+
digestDirectory,
|
|
417
|
+
compareDigests,
|
|
418
|
+
};
|