agentlas 1.0.65 → 1.0.67
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 +94 -0
- package/README.md +12 -4
- package/bin/agentlas.cjs +354 -16
- package/engine/acp/server.cjs +34 -3
- package/engine/agentlas-api-agent.cjs +151 -120
- package/engine/agentlas-banner.cjs +6 -2
- package/engine/agentlas-capabilities.cjs +17 -10
- package/engine/agentlas-cloud-runtime.cjs +4 -5
- package/engine/agentlas-config.cjs +19 -54
- package/engine/agentlas-core-harness.cjs +59 -5
- package/engine/agentlas-desktop-loadout.cjs +17 -3
- package/engine/agentlas-evolution.cjs +75 -18
- package/engine/agentlas-experience-exchange.cjs +145 -14
- package/engine/agentlas-experience-intake.cjs +2 -2
- package/engine/agentlas-i18n.cjs +0 -2
- package/engine/agentlas-input.cjs +273 -20
- package/engine/agentlas-judgment.cjs +0 -0
- package/engine/agentlas-mcp-env.cjs +84 -23
- package/engine/agentlas-mcp-wrapper.cjs +36 -4
- package/engine/agentlas-memory-governance.cjs +2 -1
- package/engine/agentlas-memory-import.cjs +49 -12
- package/engine/agentlas-native-host.cjs +279 -44
- package/engine/agentlas-onboard.cjs +23 -1
- package/engine/agentlas-permissions.cjs +5 -1
- package/engine/agentlas-sqlite-policy.cjs +40 -2
- package/engine/agentlas-tasks.cjs +7 -0
- package/engine/agentlas-tools.cjs +37 -10
- package/engine/agentlas-ui.cjs +26 -4
- package/engine/agentlas-workforce.cjs +30 -5
- package/engine/agentlas-workload-routing.cjs +83 -10
- package/engine/agentlas.cjs +21 -4
- package/engine/agents/builder.cjs +35 -10
- package/engine/agents/files.cjs +81 -10
- package/engine/agents/import-local.cjs +10 -2
- package/engine/agents/registry.cjs +10 -3
- package/engine/agents/router.cjs +7 -7
- package/engine/agents/routes.cjs +31 -9
- package/engine/automation/daemon.cjs +154 -40
- package/engine/automation/launchd.cjs +395 -16
- package/engine/automation/schedule.cjs +90 -3
- package/engine/automation/store.cjs +163 -59
- package/engine/browser/cdp.cjs +190 -17
- package/engine/browser/vault.cjs +1 -0
- package/engine/cli-output.cjs +96 -30
- package/engine/cloud/auth.cjs +131 -13
- package/engine/cloud/hub-client.cjs +41 -16
- package/engine/cloud-assets/cas.cjs +18 -9
- package/engine/cloud-assets/commands.cjs +108 -30
- package/engine/cloud-assets/package.cjs +66 -3
- package/engine/cloud-assets/restore.cjs +12 -9
- package/engine/cloud-assets/state.cjs +609 -116
- package/engine/commands/acp.cjs +13 -2
- package/engine/commands/automation.cjs +75 -24
- package/engine/commands/billing.cjs +6 -1
- package/engine/commands/browser.cjs +61 -29
- package/engine/commands/build.cjs +72 -14
- package/engine/commands/call.cjs +1 -1
- package/engine/commands/cd.cjs +1 -1
- package/engine/commands/cloud.cjs +2 -1
- package/engine/commands/connect.cjs +38 -13
- package/engine/commands/creds.cjs +74 -29
- package/engine/commands/doctor.cjs +17 -1
- package/engine/commands/document.cjs +38 -10
- package/engine/commands/env.cjs +6 -1
- package/engine/commands/firm.cjs +42 -12
- package/engine/commands/graph.cjs +36 -32
- package/engine/commands/help.cjs +11 -1
- package/engine/commands/hep.cjs +1 -1
- package/engine/commands/import.cjs +5 -4
- package/engine/commands/index.cjs +1 -1
- package/engine/commands/install.cjs +6 -4
- package/engine/commands/list.cjs +56 -16
- package/engine/commands/login.cjs +5 -0
- package/engine/commands/logout.cjs +6 -1
- package/engine/commands/mcp.cjs +150 -37
- package/engine/commands/memory.cjs +3 -2
- package/engine/commands/multimodal.cjs +12 -0
- package/engine/commands/native.cjs +5 -1
- package/engine/commands/one.cjs +78 -33
- package/engine/commands/plugin.cjs +13 -2
- package/engine/commands/project.cjs +15 -12
- package/engine/commands/research.cjs +18 -7
- package/engine/commands/roles.cjs +104 -28
- package/engine/commands/route.cjs +2 -2
- package/engine/commands/run.cjs +42 -9
- package/engine/commands/search.cjs +23 -2
- package/engine/commands/setup.cjs +6 -1
- package/engine/commands/storm.cjs +44 -6
- package/engine/commands/swarm.cjs +47 -9
- package/engine/commands/telegram.cjs +6 -1
- package/engine/commands/uninstall.cjs +12 -6
- package/engine/commands/update.cjs +5 -0
- package/engine/commands/upload.cjs +2 -1
- package/engine/commands/usage.cjs +6 -1
- package/engine/commands/version.cjs +6 -1
- package/engine/commands/whoami.cjs +6 -1
- package/engine/commands/workforce.cjs +60 -16
- package/engine/core/capability-grants.cjs +7 -2
- package/engine/core/daemon-client.cjs +36 -7
- package/engine/core/db.cjs +10 -2
- package/engine/core/desktop-core-fetch.cjs +477 -77
- package/engine/core/schema-ensure.cjs +23 -11
- package/engine/experience/build.cjs +5 -1
- package/engine/experience/intents.cjs +35 -82
- package/engine/experience/runtime.cjs +7 -1
- package/engine/experience/variant.cjs +58 -4
- package/engine/firms/orchestrate.cjs +9 -6
- package/engine/graph/ask-model.cjs +22 -11
- package/engine/graph/interview.cjs +74 -23
- package/engine/graph/package.cjs +63 -8
- package/engine/hephaestus/local-core.cjs +18 -5
- package/engine/hephaestus/runtime.cjs +48 -43
- package/engine/hub/install.cjs +484 -63
- package/engine/hub/plugins.cjs +391 -29
- package/engine/mcp/consent.cjs +25 -17
- package/engine/mcp/contract.cjs +229 -24
- package/engine/mcp/inventory.cjs +46 -10
- package/engine/mcp/plan.cjs +10 -1
- package/engine/mcp/probe.cjs +24 -9
- package/engine/memory-cli/curate.cjs +74 -16
- package/engine/oberon/common.cjs +37 -12
- package/engine/oberon/manifest.cjs +25 -2
- package/engine/oberon/outputs.cjs +49 -19
- package/engine/oberon/render.cjs +665 -40
- package/engine/project/career-graph.cjs +15 -2
- package/engine/project/controller.cjs +133 -15
- package/engine/project/credentials.cjs +382 -34
- package/engine/project/env-file.cjs +93 -8
- package/engine/project/memory-context.cjs +11 -57
- package/engine/project/ontology.cjs +66 -25
- package/engine/project/paths.cjs +42 -8
- package/engine/project/seed.cjs +88 -51
- package/engine/project/state.cjs +41 -16
- package/engine/project/team.cjs +26 -1
- package/engine/runtime-refusal.cjs +4 -1
- package/engine/runtimes/acp-driver.cjs +56 -9
- package/engine/runtimes/auth-evidence.cjs +21 -2
- package/engine/runtimes/detect.cjs +28 -8
- package/engine/runtimes/kinds.cjs +12 -4
- package/engine/runtimes/overrides.cjs +4 -3
- package/engine/runtimes/resolve.cjs +8 -7
- package/engine/runtimes/roles.cjs +7 -5
- package/engine/sessions/apply-fences.cjs +396 -56
- package/engine/sessions/fences.cjs +111 -27
- package/engine/sessions/memory-turn.cjs +62 -3
- package/engine/sessions/orchestrator.cjs +11 -1
- package/engine/sessions/prompt.cjs +66 -9
- package/engine/sessions/session.cjs +70 -10
- package/engine/sessions/sink.cjs +78 -12
- package/engine/sessions/store.cjs +29 -7
- package/engine/storm/storm.cjs +26 -3
- package/engine/storm/swarm.cjs +31 -3
- package/engine/telegram/connect.cjs +185 -22
- package/engine/ui/commands-catalog.cjs +1 -1
- package/engine/ui/palette.cjs +12 -2
- package/engine/ui/repl.cjs +57 -9
- package/engine/ui/screens.cjs +2 -4
- package/engine/vendor/desktop-core.manifest.json +5 -5
- package/engine/workforce/capture.cjs +9 -27
- package/engine/workforce/deps.cjs +18 -6
- package/engine/workforce/local-core-transport.cjs +68 -5
- package/package.json +1 -1
- package/engine/commands/career-graph.cjs +0 -51
package/engine/hub/plugins.cjs
CHANGED
|
@@ -206,6 +206,11 @@ function installPluginMcpRows(db, rows) {
|
|
|
206
206
|
|
|
207
207
|
const PLUGIN_SKILL_SLUG_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
208
208
|
const PLUGIN_SKILL_FILE_MAX_BYTES = 512 * 1024;
|
|
209
|
+
const PLUGIN_SKILL_MAX_COUNT = 64;
|
|
210
|
+
const PLUGIN_SKILL_MAX_FILES_PER_SKILL = 256;
|
|
211
|
+
const PLUGIN_SKILL_TOTAL_MAX_BYTES = 8 * 1024 * 1024;
|
|
212
|
+
const PLUGIN_MARKER_MAX_BYTES = 128 * 1024;
|
|
213
|
+
const PLUGIN_LOCAL_MAX_COUNT = 256;
|
|
209
214
|
|
|
210
215
|
/** 세 채널이 공유하는 로컬 플러그인 저장소 루트. homeDir 주입은 테스트 격리용. */
|
|
211
216
|
function agentlasPluginsDir({ homeDir } = {}) {
|
|
@@ -230,7 +235,12 @@ function planPluginSkillInstall(slug, manifest) {
|
|
|
230
235
|
const skills = [];
|
|
231
236
|
const declaredOnly = [];
|
|
232
237
|
const refused = [];
|
|
233
|
-
|
|
238
|
+
let plannedBytes = 0;
|
|
239
|
+
const seenNames = new Set();
|
|
240
|
+
if (entries.length > PLUGIN_SKILL_MAX_COUNT) {
|
|
241
|
+
refused.push({ name: "(manifest)", reason: `too many skills (maximum ${PLUGIN_SKILL_MAX_COUNT})` });
|
|
242
|
+
}
|
|
243
|
+
for (const entry of entries.slice(0, PLUGIN_SKILL_MAX_COUNT)) {
|
|
234
244
|
const name = typeof entry?.name === "string" ? entry.name.trim() : "";
|
|
235
245
|
if (!name) continue;
|
|
236
246
|
const rawFiles = Array.isArray(entry?.files) ? entry.files : [];
|
|
@@ -242,25 +252,284 @@ function planPluginSkillInstall(slug, manifest) {
|
|
|
242
252
|
refused.push({ name, reason: "invalid skill name" });
|
|
243
253
|
continue;
|
|
244
254
|
}
|
|
255
|
+
const nameKey = name.toLowerCase();
|
|
256
|
+
if (seenNames.has(nameKey)) {
|
|
257
|
+
refused.push({ name, reason: "duplicate skill name" });
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
seenNames.add(nameKey);
|
|
261
|
+
if (rawFiles.length > PLUGIN_SKILL_MAX_FILES_PER_SKILL) {
|
|
262
|
+
refused.push({ name, reason: `too many files (maximum ${PLUGIN_SKILL_MAX_FILES_PER_SKILL})` });
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
245
265
|
const files = [];
|
|
266
|
+
const seenPaths = new Set();
|
|
267
|
+
let skillBytes = 0;
|
|
246
268
|
let bad = null;
|
|
247
269
|
for (const file of rawFiles) {
|
|
248
270
|
const filePath = typeof file?.path === "string" ? file.path.trim() : "";
|
|
249
271
|
const content = typeof file?.content === "string" ? file.content : "";
|
|
250
272
|
if (!pluginSkillSafeRelativePath(filePath)) { bad = `unsafe file path "${filePath}"`; break; }
|
|
273
|
+
const pathKey = filePath.toLowerCase();
|
|
274
|
+
if (seenPaths.has(pathKey)) { bad = `duplicate file path "${filePath}"`; break; }
|
|
275
|
+
seenPaths.add(pathKey);
|
|
251
276
|
if (!content.trim()) { bad = `empty content for ${filePath}`; break; }
|
|
252
|
-
|
|
277
|
+
const bytes = Buffer.byteLength(content, "utf8");
|
|
278
|
+
if (bytes > PLUGIN_SKILL_FILE_MAX_BYTES) { bad = `${filePath} exceeds the file size cap`; break; }
|
|
279
|
+
skillBytes += bytes;
|
|
253
280
|
const sha256 = typeof file?.sha256 === "string" && /^[0-9a-f]{64}$/i.test(file.sha256)
|
|
254
281
|
? file.sha256.toLowerCase()
|
|
255
282
|
: null;
|
|
256
283
|
files.push({ path: filePath, content, sha256 });
|
|
257
284
|
}
|
|
258
285
|
if (bad) { refused.push({ name, reason: bad }); continue; }
|
|
286
|
+
if (plannedBytes + skillBytes > PLUGIN_SKILL_TOTAL_MAX_BYTES) {
|
|
287
|
+
refused.push({ name, reason: `skill payloads exceed the total size cap (${PLUGIN_SKILL_TOTAL_MAX_BYTES} bytes)` });
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
plannedBytes += skillBytes;
|
|
259
291
|
skills.push({ name, description: typeof entry?.description === "string" ? entry.description : null, files });
|
|
260
292
|
}
|
|
261
293
|
return { skills, declaredOnly, refused };
|
|
262
294
|
}
|
|
263
295
|
|
|
296
|
+
function privateDirectory(dir) {
|
|
297
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
298
|
+
const stat = fs.lstatSync(dir);
|
|
299
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
300
|
+
throw new Error(`refusing unsafe plugin directory: ${dir}`);
|
|
301
|
+
}
|
|
302
|
+
fs.chmodSync(dir, 0o700);
|
|
303
|
+
const verified = fs.lstatSync(dir);
|
|
304
|
+
if (!verified.isDirectory() || verified.isSymbolicLink() || !sameDirectoryIdentity(stat, verified)) {
|
|
305
|
+
throw new Error(`plugin directory changed during setup: ${dir}`);
|
|
306
|
+
}
|
|
307
|
+
return verified;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function sameDirectoryIdentity(left, right) {
|
|
311
|
+
return left
|
|
312
|
+
&& right
|
|
313
|
+
&& left.isDirectory()
|
|
314
|
+
&& right.isDirectory()
|
|
315
|
+
&& !left.isSymbolicLink()
|
|
316
|
+
&& !right.isSymbolicLink()
|
|
317
|
+
&& left.dev === right.dev
|
|
318
|
+
&& left.ino === right.ino;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function privateDirectoryIdentity(dir) {
|
|
322
|
+
const stat = fs.lstatSync(dir);
|
|
323
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
324
|
+
throw new Error(`refusing unsafe plugin directory: ${dir}`);
|
|
325
|
+
}
|
|
326
|
+
return stat;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function assertPrivateDirectoryIdentity(dir, expected) {
|
|
330
|
+
const actual = privateDirectoryIdentity(dir);
|
|
331
|
+
if (!sameDirectoryIdentity(expected, actual)) {
|
|
332
|
+
throw new Error(`plugin directory changed during write: ${dir}`);
|
|
333
|
+
}
|
|
334
|
+
return actual;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function sameFileInode(left, right) {
|
|
338
|
+
return left
|
|
339
|
+
&& right
|
|
340
|
+
&& left.isFile()
|
|
341
|
+
&& right.isFile()
|
|
342
|
+
&& !left.isSymbolicLink()
|
|
343
|
+
&& !right.isSymbolicLink()
|
|
344
|
+
&& left.dev === right.dev
|
|
345
|
+
&& left.ino === right.ino;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function existingPluginFile(target) {
|
|
349
|
+
let stat;
|
|
350
|
+
try {
|
|
351
|
+
stat = fs.lstatSync(target);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
if (error && error.code === "ENOENT") return null;
|
|
354
|
+
throw error;
|
|
355
|
+
}
|
|
356
|
+
if (stat.isSymbolicLink()) throw new Error(`refusing symlink destination: ${target}`);
|
|
357
|
+
if (!stat.isFile()) throw new Error(`refusing non-file destination: ${target}`);
|
|
358
|
+
if (stat.nlink > 1) throw new Error(`refusing hard-linked destination: ${target}`);
|
|
359
|
+
return stat;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function writePluginFileAtomic(target, content) {
|
|
363
|
+
const parent = path.dirname(target);
|
|
364
|
+
const parentStat = privateDirectory(parent);
|
|
365
|
+
const expectedTarget = existingPluginFile(target);
|
|
366
|
+
assertPrivateDirectoryIdentity(parent, parentStat);
|
|
367
|
+
const temp = path.join(parent, `.${path.basename(target)}.agentlas-${crypto.randomUUID()}.tmp`);
|
|
368
|
+
const flags = fs.constants.O_WRONLY
|
|
369
|
+
| fs.constants.O_CREAT
|
|
370
|
+
| fs.constants.O_EXCL
|
|
371
|
+
| (fs.constants.O_NOFOLLOW || 0);
|
|
372
|
+
let fd = null;
|
|
373
|
+
let tempStat = null;
|
|
374
|
+
let tempPublished = false;
|
|
375
|
+
try {
|
|
376
|
+
assertPrivateDirectoryIdentity(parent, parentStat);
|
|
377
|
+
if (expectedTarget) {
|
|
378
|
+
// Node has no portable rename-no-replace primitive. For an existing file,
|
|
379
|
+
// update only the exact inode observed above, never a pathname successor.
|
|
380
|
+
fd = fs.openSync(target, fs.constants.O_WRONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
381
|
+
const opened = fs.fstatSync(fd);
|
|
382
|
+
if (!sameFileInode(expectedTarget, opened) || opened.nlink !== 1) {
|
|
383
|
+
throw new Error(`plugin destination changed before update: ${target}`);
|
|
384
|
+
}
|
|
385
|
+
fs.ftruncateSync(fd, 0);
|
|
386
|
+
fs.writeSync(fd, content, 0, "utf8");
|
|
387
|
+
fs.fsyncSync(fd);
|
|
388
|
+
const updated = fs.fstatSync(fd);
|
|
389
|
+
if (!sameFileInode(opened, updated) || updated.nlink !== 1) {
|
|
390
|
+
throw new Error(`plugin destination identity changed during update: ${target}`);
|
|
391
|
+
}
|
|
392
|
+
fs.closeSync(fd);
|
|
393
|
+
fd = null;
|
|
394
|
+
assertPrivateDirectoryIdentity(parent, parentStat);
|
|
395
|
+
const finalStat = fs.lstatSync(target);
|
|
396
|
+
if (!sameFileInode(updated, finalStat) || finalStat.nlink !== 1) {
|
|
397
|
+
throw new Error(`plugin destination changed after update: ${target}`);
|
|
398
|
+
}
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
fd = fs.openSync(temp, flags, 0o600);
|
|
403
|
+
fs.writeFileSync(fd, content, { encoding: "utf8" });
|
|
404
|
+
fs.fsyncSync(fd);
|
|
405
|
+
tempStat = fs.fstatSync(fd);
|
|
406
|
+
if (!tempStat.isFile() || tempStat.nlink !== 1) throw new Error(`unsafe temporary plugin file: ${target}`);
|
|
407
|
+
fs.closeSync(fd);
|
|
408
|
+
fd = null;
|
|
409
|
+
assertPrivateDirectoryIdentity(parent, parentStat);
|
|
410
|
+
// link(2) is atomic and no-replace: unlike rename(2), a successor that
|
|
411
|
+
// appears after the preflight can never be silently clobbered.
|
|
412
|
+
if (existingPluginFile(target)) {
|
|
413
|
+
throw new Error(`plugin destination appeared during write: ${target}`);
|
|
414
|
+
}
|
|
415
|
+
fs.linkSync(temp, target);
|
|
416
|
+
tempPublished = true;
|
|
417
|
+
assertPrivateDirectoryIdentity(parent, parentStat);
|
|
418
|
+
fs.unlinkSync(temp);
|
|
419
|
+
tempPublished = false;
|
|
420
|
+
const finalStat = fs.lstatSync(target);
|
|
421
|
+
if (
|
|
422
|
+
!sameFileInode(tempStat, finalStat)
|
|
423
|
+
|| finalStat.nlink !== 1
|
|
424
|
+
) {
|
|
425
|
+
throw new Error(`plugin file identity changed after write: ${target}`);
|
|
426
|
+
}
|
|
427
|
+
assertPrivateDirectoryIdentity(parent, parentStat);
|
|
428
|
+
} catch (error) {
|
|
429
|
+
if (fd !== null) {
|
|
430
|
+
try { fs.closeSync(fd); } catch { /* best effort */ }
|
|
431
|
+
}
|
|
432
|
+
try {
|
|
433
|
+
if (!tempPublished && sameDirectoryIdentity(parentStat, privateDirectoryIdentity(parent))) {
|
|
434
|
+
fs.unlinkSync(temp);
|
|
435
|
+
}
|
|
436
|
+
} catch (cleanupError) {
|
|
437
|
+
if (cleanupError && cleanupError.code !== "ENOENT") { /* best effort */ }
|
|
438
|
+
}
|
|
439
|
+
throw error;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function readPluginDirectory(dir) {
|
|
444
|
+
const stat = fs.lstatSync(dir);
|
|
445
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
446
|
+
throw new Error(`refusing unsafe plugin directory: ${dir}`);
|
|
447
|
+
}
|
|
448
|
+
return stat;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function sameFileIdentity(left, right) {
|
|
452
|
+
return left
|
|
453
|
+
&& right
|
|
454
|
+
&& left.dev === right.dev
|
|
455
|
+
&& left.ino === right.ino
|
|
456
|
+
&& left.size === right.size
|
|
457
|
+
&& left.nlink === right.nlink;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function readPluginMarker(target) {
|
|
461
|
+
const before = fs.lstatSync(target);
|
|
462
|
+
if (before.isSymbolicLink() || !before.isFile() || before.nlink !== 1 || before.size > PLUGIN_MARKER_MAX_BYTES) {
|
|
463
|
+
throw new Error(`unsafe plugin marker: ${target}`);
|
|
464
|
+
}
|
|
465
|
+
const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0);
|
|
466
|
+
let fd = null;
|
|
467
|
+
try {
|
|
468
|
+
fd = fs.openSync(target, flags);
|
|
469
|
+
const opened = fs.fstatSync(fd);
|
|
470
|
+
if (!sameFileIdentity(before, opened)) throw new Error(`plugin marker changed before read: ${target}`);
|
|
471
|
+
const buffer = Buffer.alloc(PLUGIN_MARKER_MAX_BYTES + 1);
|
|
472
|
+
let bytesRead = 0;
|
|
473
|
+
while (bytesRead < buffer.length) {
|
|
474
|
+
const count = fs.readSync(fd, buffer, bytesRead, buffer.length - bytesRead, null);
|
|
475
|
+
if (!count) break;
|
|
476
|
+
bytesRead += count;
|
|
477
|
+
}
|
|
478
|
+
if (bytesRead > PLUGIN_MARKER_MAX_BYTES) throw new Error(`plugin marker exceeds the ${PLUGIN_MARKER_MAX_BYTES}-byte cap`);
|
|
479
|
+
const after = fs.fstatSync(fd);
|
|
480
|
+
if (!sameFileIdentity(opened, after) || bytesRead !== after.size) {
|
|
481
|
+
throw new Error(`plugin marker changed during read: ${target}`);
|
|
482
|
+
}
|
|
483
|
+
const value = buffer.subarray(0, bytesRead).toString("utf8");
|
|
484
|
+
const pathnameAfter = fs.lstatSync(target);
|
|
485
|
+
if (!sameFileIdentity(before, pathnameAfter)) {
|
|
486
|
+
throw new Error(`plugin marker changed after read: ${target}`);
|
|
487
|
+
}
|
|
488
|
+
return value;
|
|
489
|
+
} finally {
|
|
490
|
+
if (fd !== null) fs.closeSync(fd);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function validMarkerText(value, max) {
|
|
495
|
+
return typeof value === "string"
|
|
496
|
+
&& value.length > 0
|
|
497
|
+
&& value.length <= max
|
|
498
|
+
&& !/[\u0000-\u001f\u007f-\u009f]/.test(value);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function validatePluginMarker(marker, directoryName) {
|
|
502
|
+
if (!marker || typeof marker !== "object" || Array.isArray(marker)) return null;
|
|
503
|
+
if (marker.schema !== "agentlas.local-plugin/v1") return null;
|
|
504
|
+
if (!validMarkerText(marker.slug, 64) || !PLUGIN_SKILL_SLUG_RE.test(marker.slug)) return null;
|
|
505
|
+
if (String(marker.slug).toLowerCase() !== String(directoryName).toLowerCase()) return null;
|
|
506
|
+
if (!validMarkerText(marker.name, 256)) return null;
|
|
507
|
+
if (!Array.isArray(marker.skills) || marker.skills.length > PLUGIN_SKILL_MAX_COUNT) return null;
|
|
508
|
+
const seenSkills = new Set();
|
|
509
|
+
for (const skill of marker.skills) {
|
|
510
|
+
if (!skill || typeof skill !== "object" || !validMarkerText(skill.name, 64) || !PLUGIN_SKILL_SLUG_RE.test(skill.name)) return null;
|
|
511
|
+
const skillKey = skill.name.toLowerCase();
|
|
512
|
+
if (seenSkills.has(skillKey)) return null;
|
|
513
|
+
seenSkills.add(skillKey);
|
|
514
|
+
if (!Array.isArray(skill.files) || skill.files.length > PLUGIN_SKILL_MAX_FILES_PER_SKILL) return null;
|
|
515
|
+
const seenFiles = new Set();
|
|
516
|
+
for (const file of skill.files) {
|
|
517
|
+
if (!file || typeof file !== "object" || !pluginSkillSafeRelativePath(file.path)) return null;
|
|
518
|
+
const fileKey = file.path.toLowerCase();
|
|
519
|
+
if (seenFiles.has(fileKey)) return null;
|
|
520
|
+
seenFiles.add(fileKey);
|
|
521
|
+
if (!/^[0-9a-f]{64}$/i.test(String(file.sha256 || "")) || typeof file.verified !== "boolean") return null;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
slug: marker.slug,
|
|
526
|
+
name: marker.name,
|
|
527
|
+
installedAt: validMarkerText(marker.installedAt, 128) ? marker.installedAt : null,
|
|
528
|
+
installedBy: validMarkerText(marker.installedBy, 128) ? marker.installedBy : null,
|
|
529
|
+
skills: marker.skills.map((skill) => String(skill.name)),
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
264
533
|
/**
|
|
265
534
|
* 계획된 스킬들을 ~/.agentlas/plugins/<slug>/skills/<name>/ 에 쓴다.
|
|
266
535
|
*
|
|
@@ -272,35 +541,122 @@ function installPluginSkills(slug, plan, { homeDir, manifestUrl, meta } = {}) {
|
|
|
272
541
|
if (!PLUGIN_SKILL_SLUG_RE.test(String(slug || ""))) {
|
|
273
542
|
return { dir: "", installed: [], failed: [{ name: String(slug || ""), reason: "invalid plugin slug" }], verified: false };
|
|
274
543
|
}
|
|
275
|
-
const
|
|
544
|
+
const pluginsRoot = agentlasPluginsDir({ homeDir });
|
|
545
|
+
const pluginDir = path.join(pluginsRoot, slug);
|
|
276
546
|
const installed = [];
|
|
277
547
|
const failed = [];
|
|
278
548
|
const markerSkills = [];
|
|
279
549
|
let allDeclared = true;
|
|
280
|
-
|
|
550
|
+
let hadFailure = false;
|
|
551
|
+
try {
|
|
552
|
+
// Every directory owned by the installer is private and must be a real
|
|
553
|
+
// directory. In particular, do not follow a pre-created .agentlas/plugins
|
|
554
|
+
// or <slug>/skills symlink into an arbitrary location.
|
|
555
|
+
privateDirectory(path.dirname(pluginsRoot));
|
|
556
|
+
privateDirectory(pluginsRoot);
|
|
557
|
+
privateDirectory(pluginDir);
|
|
558
|
+
} catch (error) {
|
|
559
|
+
return {
|
|
560
|
+
dir: pluginDir,
|
|
561
|
+
installed,
|
|
562
|
+
failed: [{ name: "plugin", reason: String((error && error.message) || error).slice(0, 160) }],
|
|
563
|
+
verified: false,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
const skills = Array.isArray(plan?.skills) ? plan.skills.slice(0, PLUGIN_SKILL_MAX_COUNT) : [];
|
|
567
|
+
if (Array.isArray(plan?.skills) && plan.skills.length > PLUGIN_SKILL_MAX_COUNT) {
|
|
568
|
+
failed.push({ name: "(manifest)", reason: `too many skills (maximum ${PLUGIN_SKILL_MAX_COUNT})` });
|
|
569
|
+
allDeclared = false;
|
|
570
|
+
hadFailure = true;
|
|
571
|
+
}
|
|
572
|
+
let installedBytes = 0;
|
|
573
|
+
const seenSkillNames = new Set();
|
|
574
|
+
for (const skill of skills) {
|
|
281
575
|
const written = [];
|
|
282
576
|
let mismatch = null;
|
|
577
|
+
let skillBytes = 0;
|
|
578
|
+
const seenPaths = new Set();
|
|
579
|
+
const skillName = typeof skill?.name === "string" ? skill.name : "";
|
|
580
|
+
if (!skill || typeof skill !== "object" || !PLUGIN_SKILL_SLUG_RE.test(skillName)) {
|
|
581
|
+
failed.push({ name: String(skill?.name || "skill"), reason: "invalid skill name" });
|
|
582
|
+
allDeclared = false;
|
|
583
|
+
hadFailure = true;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
const skillNameKey = skillName.toLowerCase();
|
|
587
|
+
if (seenSkillNames.has(skillNameKey)) {
|
|
588
|
+
failed.push({ name: skillName, reason: "duplicate skill name" });
|
|
589
|
+
allDeclared = false;
|
|
590
|
+
hadFailure = true;
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
seenSkillNames.add(skillNameKey);
|
|
594
|
+
if (!Array.isArray(skill.files) || skill.files.length > PLUGIN_SKILL_MAX_FILES_PER_SKILL) {
|
|
595
|
+
failed.push({ name: skillName, reason: `too many files (maximum ${PLUGIN_SKILL_MAX_FILES_PER_SKILL})` });
|
|
596
|
+
allDeclared = false;
|
|
597
|
+
hadFailure = true;
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
283
600
|
for (const file of skill.files) {
|
|
601
|
+
if (!file || typeof file !== "object" || !pluginSkillSafeRelativePath(file.path)) {
|
|
602
|
+
mismatch = `unsafe file path "${String(file?.path || "")}"`;
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
const pathKey = file.path.toLowerCase();
|
|
606
|
+
if (seenPaths.has(pathKey)) {
|
|
607
|
+
mismatch = `duplicate file path "${file.path}"`;
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
seenPaths.add(pathKey);
|
|
611
|
+
if (typeof file.content !== "string" || !file.content.trim()) {
|
|
612
|
+
mismatch = `empty content for ${file.path}`;
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
const bytes = Buffer.byteLength(file.content, "utf8");
|
|
616
|
+
if (bytes > PLUGIN_SKILL_FILE_MAX_BYTES) {
|
|
617
|
+
mismatch = `${file.path} exceeds the file size cap`;
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
skillBytes += bytes;
|
|
284
621
|
const actual = crypto.createHash("sha256").update(file.content, "utf8").digest("hex");
|
|
285
622
|
if (file.sha256 && file.sha256 !== actual) { mismatch = `sha256 mismatch for ${file.path}`; break; }
|
|
286
623
|
if (!file.sha256) allDeclared = false;
|
|
287
624
|
written.push({ path: file.path, sha256: actual, verified: Boolean(file.sha256) });
|
|
288
625
|
}
|
|
289
|
-
if (mismatch) {
|
|
626
|
+
if (mismatch) {
|
|
627
|
+
failed.push({ name: skillName, reason: mismatch });
|
|
628
|
+
hadFailure = true;
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
if (installedBytes + skillBytes > PLUGIN_SKILL_TOTAL_MAX_BYTES) {
|
|
632
|
+
failed.push({ name: skillName, reason: `skill payloads exceed the total size cap (${PLUGIN_SKILL_TOTAL_MAX_BYTES} bytes)` });
|
|
633
|
+
allDeclared = false;
|
|
634
|
+
hadFailure = true;
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
290
637
|
try {
|
|
291
|
-
const skillDir = path.join(pluginDir, "skills",
|
|
638
|
+
const skillDir = path.join(pluginDir, "skills", skillName);
|
|
639
|
+
privateDirectory(path.join(pluginDir, "skills"));
|
|
640
|
+
privateDirectory(skillDir);
|
|
292
641
|
for (const file of skill.files) {
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
642
|
+
const parts = file.path.split("/");
|
|
643
|
+
let parent = skillDir;
|
|
644
|
+
for (const part of parts.slice(0, -1)) {
|
|
645
|
+
parent = path.join(parent, part);
|
|
646
|
+
privateDirectory(parent);
|
|
647
|
+
}
|
|
648
|
+
writePluginFileAtomic(path.join(parent, parts[parts.length - 1]), file.content);
|
|
296
649
|
}
|
|
297
|
-
|
|
298
|
-
|
|
650
|
+
installedBytes += skillBytes;
|
|
651
|
+
installed.push(skillName);
|
|
652
|
+
markerSkills.push({ name: skillName, files: written });
|
|
299
653
|
} catch (e) {
|
|
300
|
-
failed.push({ name:
|
|
654
|
+
failed.push({ name: skillName, reason: String((e && e.message) || e).slice(0, 160) });
|
|
655
|
+
hadFailure = true;
|
|
301
656
|
}
|
|
302
657
|
}
|
|
303
|
-
const
|
|
658
|
+
const contentVerified = installed.length > 0 && allDeclared && !hadFailure;
|
|
659
|
+
let markerWritten = false;
|
|
304
660
|
if (installed.length > 0) {
|
|
305
661
|
// 마커는 마지막에 쓴다 — 마커가 있으면 스킬 파일도 있다는 뜻이어야 한다.
|
|
306
662
|
const marker = {
|
|
@@ -311,17 +667,18 @@ function installPluginSkills(slug, plan, { homeDir, manifestUrl, meta } = {}) {
|
|
|
311
667
|
version: (meta && meta.version) || null,
|
|
312
668
|
installedAt: new Date().toISOString(),
|
|
313
669
|
installedBy: "agentlas-terminal",
|
|
314
|
-
source: { manifestUrl: manifestUrl || null, contentVerification:
|
|
670
|
+
source: { manifestUrl: manifestUrl || null, contentVerification: contentVerified ? "manifest-sha256" : "none" },
|
|
315
671
|
skills: markerSkills,
|
|
316
672
|
};
|
|
317
673
|
try {
|
|
318
|
-
|
|
319
|
-
|
|
674
|
+
writePluginFileAtomic(path.join(pluginDir, "plugin.json"), `${JSON.stringify(marker, null, 2)}\n`);
|
|
675
|
+
markerWritten = true;
|
|
320
676
|
} catch (e) {
|
|
321
677
|
failed.push({ name: "plugin.json", reason: String((e && e.message) || e).slice(0, 160) });
|
|
678
|
+
hadFailure = true;
|
|
322
679
|
}
|
|
323
680
|
}
|
|
324
|
-
return { dir: pluginDir, installed, failed, verified };
|
|
681
|
+
return { dir: pluginDir, installed, failed, verified: contentVerified && markerWritten && !hadFailure };
|
|
325
682
|
}
|
|
326
683
|
|
|
327
684
|
/** ~/.agentlas/plugins/<slug>/plugin.json 마커들을 읽는다 — list의 설치 여부 표시용. */
|
|
@@ -329,25 +686,30 @@ function listInstalledLocalPlugins({ homeDir } = {}) {
|
|
|
329
686
|
const root = agentlasPluginsDir({ homeDir });
|
|
330
687
|
let names;
|
|
331
688
|
try {
|
|
332
|
-
|
|
689
|
+
const rootStat = readPluginDirectory(root);
|
|
690
|
+
names = fs.readdirSync(root, { withFileTypes: true })
|
|
691
|
+
.filter((d) => d.isDirectory() && !d.isSymbolicLink())
|
|
692
|
+
.map((d) => d.name)
|
|
693
|
+
.filter((name) => !name.startsWith("."))
|
|
694
|
+
.slice(0, PLUGIN_LOCAL_MAX_COUNT);
|
|
695
|
+
const rootAfter = readPluginDirectory(root);
|
|
696
|
+
if (rootAfter.dev !== rootStat.dev || rootAfter.ino !== rootStat.ino) return [];
|
|
333
697
|
} catch {
|
|
334
698
|
return [];
|
|
335
699
|
}
|
|
336
700
|
const out = [];
|
|
337
701
|
for (const name of names) {
|
|
338
|
-
if (name.startsWith(".")) continue;
|
|
339
702
|
try {
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
});
|
|
703
|
+
const pluginDir = path.join(root, name);
|
|
704
|
+
const pluginBefore = readPluginDirectory(pluginDir);
|
|
705
|
+
const marker = JSON.parse(readPluginMarker(path.join(pluginDir, "plugin.json")));
|
|
706
|
+
const pluginAfter = readPluginDirectory(pluginDir);
|
|
707
|
+
if (pluginAfter.dev !== pluginBefore.dev || pluginAfter.ino !== pluginBefore.ino) continue;
|
|
708
|
+
const valid = validatePluginMarker(marker, name);
|
|
709
|
+
if (!valid) continue;
|
|
710
|
+
out.push({ ...valid, dir: pluginDir });
|
|
349
711
|
} catch {
|
|
350
|
-
// 마커 없는
|
|
712
|
+
// 마커 없는 디렉터리와 unsafe/corrupt markers는 설치 상태로 광고하지 않는다.
|
|
351
713
|
}
|
|
352
714
|
}
|
|
353
715
|
return out;
|
package/engine/mcp/consent.cjs
CHANGED
|
@@ -11,16 +11,16 @@
|
|
|
11
11
|
* 기존 동의는 자동 무효가 된다. 손상된 동의 상태는 fail-closed(빈 목록).
|
|
12
12
|
*/
|
|
13
13
|
const readline = require("node:readline");
|
|
14
|
-
const fs = require("node:fs");
|
|
15
14
|
const path = require("node:path");
|
|
15
|
+
const { userDataDir: defaultUserDataDir } = require("../core/paths.cjs");
|
|
16
16
|
const {
|
|
17
17
|
MAX_APPROVED_MCP_PER_BUILD,
|
|
18
|
-
MAX_JSON_BYTES,
|
|
19
18
|
ID_RE,
|
|
20
19
|
assertExactKeys,
|
|
21
20
|
assertId,
|
|
22
21
|
assertIsoDateOrNull,
|
|
23
22
|
safeCatalogId,
|
|
23
|
+
readJsonFile,
|
|
24
24
|
writePrivateJsonAtomic,
|
|
25
25
|
withPrivateStateLock,
|
|
26
26
|
parseIdList,
|
|
@@ -36,6 +36,10 @@ const {
|
|
|
36
36
|
const MCP_CONSENT_STATE_SCHEMA = "agentlas.terminal-mcp-consents.v1";
|
|
37
37
|
const MCP_CONSENT_RECEIPT_SCHEMA = "agentlas.terminal-mcp-consent.v1";
|
|
38
38
|
|
|
39
|
+
function resolvedMcpUserDataDir(value) {
|
|
40
|
+
return typeof value === "string" && value.trim() ? value : defaultUserDataDir();
|
|
41
|
+
}
|
|
42
|
+
|
|
39
43
|
/*
|
|
40
44
|
* ── 통합 능력 승인(데스크탑 capability_grants)과의 합류 ───────────────────────
|
|
41
45
|
*
|
|
@@ -113,7 +117,7 @@ function rememberMcpCapabilityGrant(db, catalogId, decision = "allow") {
|
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
function mcpConsentStatePath(userDataDir) {
|
|
116
|
-
return path.join(userDataDir, "terminal", "mcp-consents-v1.json");
|
|
120
|
+
return path.join(resolvedMcpUserDataDir(userDataDir), "terminal", "mcp-consents-v1.json");
|
|
117
121
|
}
|
|
118
122
|
|
|
119
123
|
function emptyMcpConsentState() {
|
|
@@ -136,12 +140,13 @@ function validateMcpConsentReceipt(receipt, index) {
|
|
|
136
140
|
|
|
137
141
|
function loadMcpConsentState(userDataDir) {
|
|
138
142
|
const file = mcpConsentStatePath(userDataDir);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
+
let state;
|
|
144
|
+
try {
|
|
145
|
+
({ value: state } = readJsonFile(file, "Terminal MCP consent state"));
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (error && error.code === "ENOENT") return emptyMcpConsentState();
|
|
148
|
+
throw error;
|
|
143
149
|
}
|
|
144
|
-
const state = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
145
150
|
assertExactKeys(state, new Set(["schemaVersion", "updatedAt", "receipts"]), ["schemaVersion", "updatedAt", "receipts"], "Terminal MCP consent state");
|
|
146
151
|
if (state.schemaVersion !== MCP_CONSENT_STATE_SCHEMA || !Array.isArray(state.receipts) || state.receipts.length > 256) {
|
|
147
152
|
throw new Error("Terminal MCP consent state schema is invalid");
|
|
@@ -159,9 +164,10 @@ function withMcpConsentStateLock(userDataDir, action) {
|
|
|
159
164
|
}
|
|
160
165
|
|
|
161
166
|
function persistMcpConsentReceipts(userDataDir, servers) {
|
|
162
|
-
if (!
|
|
163
|
-
|
|
164
|
-
|
|
167
|
+
if (!(servers || []).length) return false;
|
|
168
|
+
const resolvedUserDataDir = resolvedMcpUserDataDir(userDataDir);
|
|
169
|
+
withMcpConsentStateLock(resolvedUserDataDir, () => {
|
|
170
|
+
const state = loadMcpConsentState(resolvedUserDataDir);
|
|
165
171
|
const now = new Date().toISOString();
|
|
166
172
|
for (const server of servers) {
|
|
167
173
|
if (!server || !ID_RE.test(String(server.id || "")) || !safeCatalogId(server.catalog_id) || !/^[0-9a-f]{64}$/.test(String(server.consentFingerprint || ""))) continue;
|
|
@@ -180,14 +186,15 @@ function persistMcpConsentReceipts(userDataDir, servers) {
|
|
|
180
186
|
state.receipts.sort((a, b) => String(b.consentedAt).localeCompare(String(a.consentedAt)) || a.catalogId.localeCompare(b.catalogId));
|
|
181
187
|
state.receipts = state.receipts.slice(0, 256);
|
|
182
188
|
state.updatedAt = now;
|
|
183
|
-
writePrivateJsonAtomic(mcpConsentStatePath(
|
|
189
|
+
writePrivateJsonAtomic(mcpConsentStatePath(resolvedUserDataDir), state);
|
|
184
190
|
});
|
|
185
191
|
return true;
|
|
186
192
|
}
|
|
187
193
|
|
|
188
194
|
function readConsentedSystemMcpServers(db, options = {}) {
|
|
195
|
+
const resolvedUserDataDir = resolvedMcpUserDataDir(options.userDataDir);
|
|
189
196
|
let state;
|
|
190
|
-
try { state = loadMcpConsentState(
|
|
197
|
+
try { state = loadMcpConsentState(resolvedUserDataDir); }
|
|
191
198
|
catch { return []; }
|
|
192
199
|
// 영구 거부(deny)는 옛 동의 영수증을 이긴다 — 규칙이 영수증보다 위다.
|
|
193
200
|
const partition = partitionMcpConsentByCapabilityGrants(
|
|
@@ -205,7 +212,7 @@ function readConsentedSystemMcpServers(db, options = {}) {
|
|
|
205
212
|
"SELECT id, catalog_id, name, name_en, transport, command, args_json, env_keys_json, enabled FROM mcp_servers WHERE id=? LIMIT 1",
|
|
206
213
|
).get(receipt.registryServerId);
|
|
207
214
|
} catch { continue; }
|
|
208
|
-
const server = materializeTrustedSystemMcpServer(row, options);
|
|
215
|
+
const server = materializeTrustedSystemMcpServer(row, { ...options, userDataDir: resolvedUserDataDir });
|
|
209
216
|
if (
|
|
210
217
|
!server || server.id !== receipt.registryServerId || server.catalog_id !== receipt.catalogId ||
|
|
211
218
|
server.consentFingerprint !== receipt.consentFingerprint
|
|
@@ -280,6 +287,7 @@ function askMcpConsentOnce(plan, options = {}) {
|
|
|
280
287
|
}
|
|
281
288
|
|
|
282
289
|
async function resolveApprovedMcpRuntimeAllowlist(options) {
|
|
290
|
+
const resolvedUserDataDir = resolvedMcpUserDataDir(options.userDataDir);
|
|
283
291
|
const approved = new Set(options.approvedIds || []);
|
|
284
292
|
const selectedGroups = (options.plan?.entries || []).map((entry) => {
|
|
285
293
|
if (entry.status !== "available") return null;
|
|
@@ -298,7 +306,7 @@ async function resolveApprovedMcpRuntimeAllowlist(options) {
|
|
|
298
306
|
const probe = options.probeServer || ((server, probeOptions = {}) => probeSystemMcpServerConnection(server, {
|
|
299
307
|
cwd: options.cwd,
|
|
300
308
|
env: options.env,
|
|
301
|
-
userDataDir:
|
|
309
|
+
userDataDir: resolvedUserDataDir,
|
|
302
310
|
timeoutMs: probeOptions.timeoutMs,
|
|
303
311
|
signal: probeOptions.signal,
|
|
304
312
|
}));
|
|
@@ -315,7 +323,7 @@ async function resolveApprovedMcpRuntimeAllowlist(options) {
|
|
|
315
323
|
|
|
316
324
|
const probeCandidate = async (candidate) => {
|
|
317
325
|
let server = null;
|
|
318
|
-
try { server = readApprovedSystemMcpServer(options.db, candidate, { userDataDir:
|
|
326
|
+
try { server = readApprovedSystemMcpServer(options.db, candidate, { userDataDir: resolvedUserDataDir }); }
|
|
319
327
|
catch { /* one unsafe/unwritable runtime boundary excludes only this server */ }
|
|
320
328
|
if (!server) return { candidate, server: null, status: { connected: false, reason: "registry_row_unavailable" } };
|
|
321
329
|
const remainingMs = deadline - Date.now();
|
|
@@ -382,7 +390,7 @@ async function resolveApprovedMcpRuntimeAllowlist(options) {
|
|
|
382
390
|
}
|
|
383
391
|
let consentPersisted = servers.length === 0;
|
|
384
392
|
if (servers.length) {
|
|
385
|
-
try { consentPersisted = persistMcpConsentReceipts(
|
|
393
|
+
try { consentPersisted = persistMcpConsentReceipts(resolvedUserDataDir, servers); }
|
|
386
394
|
catch { consentPersisted = false; }
|
|
387
395
|
}
|
|
388
396
|
const receipt = {
|