@agentforge/skills 0.17.1 → 0.17.2
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/README.md +21 -1
- package/dist/index.cjs +247 -121
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -4
- package/dist/index.d.ts +7 -4
- package/dist/index.js +247 -121
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -217,23 +217,12 @@ function scanAllSkillRoots(skillRoots) {
|
|
|
217
217
|
function extractBody(content) {
|
|
218
218
|
return matter(content).content.trim();
|
|
219
219
|
}
|
|
220
|
-
var activateSkillSchema = z.object({
|
|
221
|
-
name: z.string().describe('The name of the skill to activate (e.g., "code-review")')
|
|
222
|
-
});
|
|
223
|
-
var readSkillResourceSchema = z.object({
|
|
224
|
-
name: z.string().describe("The name of the skill that owns the resource"),
|
|
225
|
-
path: z.string().describe('Relative path to the resource file within the skill directory (e.g., "references/GUIDE.md", "scripts/setup.sh")')
|
|
226
|
-
});
|
|
227
220
|
var activationLogger = createLogger("agentforge:skills:activation", {
|
|
228
221
|
level: LogLevel.INFO
|
|
229
222
|
});
|
|
230
|
-
function
|
|
231
|
-
const availableNames = registry.getNames();
|
|
223
|
+
function formatMissingSkillMessage(name, availableNames) {
|
|
232
224
|
const suggestion = availableNames.length > 0 ? ` Available skills: ${availableNames.join(", ")}` : " No skills are currently registered.";
|
|
233
|
-
return {
|
|
234
|
-
availableCount: availableNames.length,
|
|
235
|
-
errorMessage: `Skill "${name}" not found.${suggestion}`
|
|
236
|
-
};
|
|
225
|
+
return `Skill "${name}" not found.${suggestion}`;
|
|
237
226
|
}
|
|
238
227
|
|
|
239
228
|
// src/trust.ts
|
|
@@ -325,114 +314,96 @@ function evaluateSkillActivationPolicy(trustLevel) {
|
|
|
325
314
|
}
|
|
326
315
|
}
|
|
327
316
|
|
|
328
|
-
// src/
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
activationLogger.warn("Skill activation blocked \u2014 trust policy", {
|
|
342
|
-
name,
|
|
343
|
-
trustLevel: skill.trustLevel,
|
|
344
|
-
reason: policyDecision.reason,
|
|
345
|
-
message: policyDecision.message
|
|
346
|
-
});
|
|
347
|
-
registry.emitEvent("trust:policy-denied" /* TRUST_POLICY_DENIED */, {
|
|
348
|
-
name: skill.metadata.name,
|
|
349
|
-
resourcePath: "SKILL.md",
|
|
350
|
-
trustLevel: skill.trustLevel,
|
|
351
|
-
reason: policyDecision.reason,
|
|
352
|
-
message: policyDecision.message
|
|
353
|
-
});
|
|
354
|
-
return policyDecision.message;
|
|
355
|
-
}
|
|
356
|
-
const skillMdPath = resolve(skill.skillPath, "SKILL.md");
|
|
357
|
-
try {
|
|
358
|
-
const content = readFileSync(skillMdPath, "utf-8");
|
|
359
|
-
const body = extractBody(content);
|
|
360
|
-
activationLogger.info("Skill activated", {
|
|
361
|
-
name: skill.metadata.name,
|
|
362
|
-
skillPath: skill.skillPath,
|
|
363
|
-
bodyLength: body.length,
|
|
364
|
-
trustLevel: skill.trustLevel,
|
|
365
|
-
activationReason: policyDecision.reason
|
|
366
|
-
});
|
|
367
|
-
registry.emitEvent("skill:activated" /* SKILL_ACTIVATED */, {
|
|
368
|
-
name: skill.metadata.name,
|
|
369
|
-
skillPath: skill.skillPath,
|
|
370
|
-
bodyLength: body.length,
|
|
371
|
-
trustLevel: skill.trustLevel
|
|
372
|
-
});
|
|
373
|
-
return body;
|
|
374
|
-
} catch (error) {
|
|
375
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
376
|
-
activationLogger.error("Skill activation failed \u2014 read error", {
|
|
377
|
-
name,
|
|
378
|
-
skillPath: skill.skillPath,
|
|
379
|
-
error: message
|
|
380
|
-
});
|
|
381
|
-
return `Failed to read skill "${name}" instructions: ${message}`;
|
|
382
|
-
}
|
|
383
|
-
}).build();
|
|
384
|
-
}
|
|
385
|
-
function resolveResourcePath(skillPath, resourcePath) {
|
|
317
|
+
// src/agent-skill-access.ts
|
|
318
|
+
var PATH_TRAVERSAL_MESSAGE = "Path traversal is not allowed \u2014 resource paths must stay within the skill directory";
|
|
319
|
+
var SYMLINK_ESCAPE_MESSAGE = "Symlink target escapes the skill directory \u2014 access denied";
|
|
320
|
+
function formatError(error) {
|
|
321
|
+
return error instanceof Error ? error.message : String(error);
|
|
322
|
+
}
|
|
323
|
+
function formatResourceReadError(error, resolvedPath) {
|
|
324
|
+
const message = formatError(error);
|
|
325
|
+
const detailIndex = message.indexOf(", ");
|
|
326
|
+
const prefix = detailIndex === -1 ? message : message.slice(0, detailIndex);
|
|
327
|
+
return `${prefix}, open '${resolvedPath}'`;
|
|
328
|
+
}
|
|
329
|
+
function resolveCanonicalResourcePath(skillPath, resourcePath) {
|
|
386
330
|
if (isAbsolute(resourcePath)) {
|
|
387
|
-
return {
|
|
331
|
+
return { kind: "access-denied", message: "Absolute resource paths are not allowed" };
|
|
388
332
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
return { success: false, error: "Path traversal is not allowed \u2014 resource paths must stay within the skill directory" };
|
|
333
|
+
if (resourcePath.split(/[/\\]/).some((segment) => segment === "..")) {
|
|
334
|
+
return { kind: "access-denied", message: PATH_TRAVERSAL_MESSAGE };
|
|
392
335
|
}
|
|
393
|
-
const resolvedPath = resolve(skillPath, resourcePath);
|
|
394
336
|
const resolvedSkillPath = resolve(skillPath);
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
337
|
+
const resolvedPath = resolve(resolvedSkillPath, resourcePath);
|
|
338
|
+
const lexicalRelativePath = relative(resolvedSkillPath, resolvedPath);
|
|
339
|
+
if (lexicalRelativePath.startsWith("..") || isAbsolute(lexicalRelativePath) || resolve(resolvedSkillPath, lexicalRelativePath) !== resolvedPath) {
|
|
340
|
+
return { kind: "access-denied", message: PATH_TRAVERSAL_MESSAGE };
|
|
398
341
|
}
|
|
342
|
+
let canonicalSkillPath;
|
|
399
343
|
try {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
344
|
+
canonicalSkillPath = realpathSync(resolvedSkillPath);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
return { kind: "read-failure", error: formatResourceReadError(error, resolvedPath) };
|
|
347
|
+
}
|
|
348
|
+
let canonicalPath;
|
|
349
|
+
try {
|
|
350
|
+
canonicalPath = realpathSync(resolvedPath);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
if (error.code === "ENOENT") {
|
|
353
|
+
return {
|
|
354
|
+
kind: "resource-not-found",
|
|
355
|
+
resolvedPath,
|
|
356
|
+
error: formatResourceReadError(error, resolvedPath)
|
|
357
|
+
};
|
|
405
358
|
}
|
|
406
|
-
|
|
359
|
+
return {
|
|
360
|
+
kind: "target-read-failure",
|
|
361
|
+
resolvedPath,
|
|
362
|
+
error: formatResourceReadError(error, resolvedPath)
|
|
363
|
+
};
|
|
407
364
|
}
|
|
408
|
-
|
|
365
|
+
const canonicalRelativePath = relative(canonicalSkillPath, canonicalPath);
|
|
366
|
+
if (canonicalRelativePath.startsWith("..") || isAbsolute(canonicalRelativePath)) {
|
|
367
|
+
return { kind: "access-denied", message: SYMLINK_ESCAPE_MESSAGE };
|
|
368
|
+
}
|
|
369
|
+
return { kind: "success", resolvedPath, canonicalPath, canonicalRelativePath };
|
|
409
370
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const skill = registry.get(name);
|
|
371
|
+
var AgentSkillAccess = class {
|
|
372
|
+
constructor(registry) {
|
|
373
|
+
this.registry = registry;
|
|
374
|
+
}
|
|
375
|
+
async readResource(name, resourcePath) {
|
|
376
|
+
const skill = this.registry.get(name);
|
|
417
377
|
if (!skill) {
|
|
418
|
-
const
|
|
419
|
-
activationLogger.warn("Skill resource load failed \u2014 skill not found", {
|
|
420
|
-
|
|
378
|
+
const availableNames = this.registry.getNames();
|
|
379
|
+
activationLogger.warn("Skill resource load failed \u2014 skill not found", {
|
|
380
|
+
name,
|
|
381
|
+
resourcePath
|
|
382
|
+
});
|
|
383
|
+
return { kind: "skill-not-found", name, availableNames };
|
|
421
384
|
}
|
|
422
|
-
const pathResult =
|
|
423
|
-
if (
|
|
385
|
+
const pathResult = resolveCanonicalResourcePath(skill.skillPath, resourcePath);
|
|
386
|
+
if (pathResult.kind === "access-denied") {
|
|
424
387
|
activationLogger.warn("Skill resource load blocked \u2014 path traversal", {
|
|
388
|
+
name,
|
|
389
|
+
resourcePath,
|
|
390
|
+
error: pathResult.message
|
|
391
|
+
});
|
|
392
|
+
return pathResult;
|
|
393
|
+
}
|
|
394
|
+
if (pathResult.kind === "read-failure") {
|
|
395
|
+
activationLogger.warn("Skill resource load failed \u2014 file not found or unreadable", {
|
|
425
396
|
name,
|
|
426
397
|
resourcePath,
|
|
427
398
|
error: pathResult.error
|
|
428
399
|
});
|
|
429
|
-
return pathResult
|
|
400
|
+
return { ...pathResult, name, resourcePath };
|
|
430
401
|
}
|
|
431
402
|
const skillInstructionsPath = resolve(skill.skillPath, "SKILL.md");
|
|
432
403
|
let isSkillInstructions = pathResult.resolvedPath.toLowerCase() === skillInstructionsPath.toLowerCase();
|
|
433
|
-
if (!isSkillInstructions) {
|
|
404
|
+
if (!isSkillInstructions && pathResult.kind === "success") {
|
|
434
405
|
try {
|
|
435
|
-
isSkillInstructions =
|
|
406
|
+
isSkillInstructions = pathResult.canonicalPath.toLowerCase() === realpathSync(skillInstructionsPath).toLowerCase();
|
|
436
407
|
} catch {
|
|
437
408
|
}
|
|
438
409
|
}
|
|
@@ -446,20 +417,24 @@ function createReadSkillResourceTool(registry) {
|
|
|
446
417
|
reason: activationDecision.reason,
|
|
447
418
|
message: activationDecision.message
|
|
448
419
|
});
|
|
449
|
-
registry.emitEvent("trust:policy-denied" /* TRUST_POLICY_DENIED */, {
|
|
420
|
+
this.registry.emitEvent("trust:policy-denied" /* TRUST_POLICY_DENIED */, {
|
|
450
421
|
name: skill.metadata.name,
|
|
451
422
|
resourcePath: "SKILL.md",
|
|
452
423
|
trustLevel: skill.trustLevel,
|
|
453
424
|
reason: activationDecision.reason,
|
|
454
425
|
message: activationDecision.message
|
|
455
426
|
});
|
|
456
|
-
return
|
|
427
|
+
return {
|
|
428
|
+
kind: "access-denied",
|
|
429
|
+
reason: activationDecision.reason,
|
|
430
|
+
message: activationDecision.message
|
|
431
|
+
};
|
|
457
432
|
}
|
|
458
433
|
}
|
|
459
434
|
const policyDecision = evaluateTrustPolicy(
|
|
460
|
-
resourcePath,
|
|
435
|
+
pathResult.kind === "success" ? pathResult.canonicalRelativePath : resourcePath,
|
|
461
436
|
skill.trustLevel,
|
|
462
|
-
registry.getAllowUntrustedScripts()
|
|
437
|
+
this.registry.getAllowUntrustedScripts()
|
|
463
438
|
);
|
|
464
439
|
if (!policyDecision.allowed) {
|
|
465
440
|
activationLogger.warn("Skill resource load blocked \u2014 trust policy", {
|
|
@@ -469,14 +444,18 @@ function createReadSkillResourceTool(registry) {
|
|
|
469
444
|
reason: policyDecision.reason,
|
|
470
445
|
message: policyDecision.message
|
|
471
446
|
});
|
|
472
|
-
registry.emitEvent("trust:policy-denied" /* TRUST_POLICY_DENIED */, {
|
|
447
|
+
this.registry.emitEvent("trust:policy-denied" /* TRUST_POLICY_DENIED */, {
|
|
473
448
|
name: skill.metadata.name,
|
|
474
449
|
resourcePath,
|
|
475
450
|
trustLevel: skill.trustLevel,
|
|
476
451
|
reason: policyDecision.reason,
|
|
477
452
|
message: policyDecision.message
|
|
478
453
|
});
|
|
479
|
-
return
|
|
454
|
+
return {
|
|
455
|
+
kind: "access-denied",
|
|
456
|
+
reason: policyDecision.reason,
|
|
457
|
+
message: policyDecision.message
|
|
458
|
+
};
|
|
480
459
|
}
|
|
481
460
|
if (policyDecision.reason !== "not-script" /* NOT_SCRIPT */) {
|
|
482
461
|
activationLogger.info("Skill resource trust policy \u2014 allowed", {
|
|
@@ -485,28 +464,29 @@ function createReadSkillResourceTool(registry) {
|
|
|
485
464
|
trustLevel: skill.trustLevel,
|
|
486
465
|
reason: policyDecision.reason
|
|
487
466
|
});
|
|
488
|
-
registry.emitEvent("trust:policy-allowed" /* TRUST_POLICY_ALLOWED */, {
|
|
467
|
+
this.registry.emitEvent("trust:policy-allowed" /* TRUST_POLICY_ALLOWED */, {
|
|
489
468
|
name: skill.metadata.name,
|
|
490
469
|
resourcePath,
|
|
491
470
|
trustLevel: skill.trustLevel,
|
|
492
471
|
reason: policyDecision.reason
|
|
493
472
|
});
|
|
494
473
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
name: skill.metadata.name,
|
|
474
|
+
if (pathResult.kind === "resource-not-found" || pathResult.kind === "target-read-failure") {
|
|
475
|
+
activationLogger.warn("Skill resource load failed \u2014 file not found or unreadable", {
|
|
476
|
+
name,
|
|
499
477
|
resourcePath,
|
|
500
|
-
|
|
501
|
-
contentLength: content.length
|
|
478
|
+
error: pathResult.error
|
|
502
479
|
});
|
|
503
|
-
|
|
504
|
-
|
|
480
|
+
return {
|
|
481
|
+
kind: pathResult.kind === "resource-not-found" ? "resource-not-found" : "read-failure",
|
|
482
|
+
name,
|
|
505
483
|
resourcePath,
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
484
|
+
error: pathResult.error
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
let content;
|
|
488
|
+
try {
|
|
489
|
+
content = readFileSync(pathResult.canonicalPath, "utf-8");
|
|
510
490
|
} catch (error) {
|
|
511
491
|
const message = error instanceof Error ? error.message : String(error);
|
|
512
492
|
activationLogger.warn("Skill resource load failed \u2014 file not found or unreadable", {
|
|
@@ -514,10 +494,156 @@ function createReadSkillResourceTool(registry) {
|
|
|
514
494
|
resourcePath,
|
|
515
495
|
error: message
|
|
516
496
|
});
|
|
517
|
-
|
|
497
|
+
if (error.code === "ENOENT") {
|
|
498
|
+
return { kind: "resource-not-found", name, resourcePath, error: message };
|
|
499
|
+
}
|
|
500
|
+
return { kind: "read-failure", name, resourcePath, error: message };
|
|
501
|
+
}
|
|
502
|
+
activationLogger.info("Skill resource loaded", {
|
|
503
|
+
name: skill.metadata.name,
|
|
504
|
+
resourcePath,
|
|
505
|
+
resolvedPath: pathResult.resolvedPath,
|
|
506
|
+
contentLength: content.length
|
|
507
|
+
});
|
|
508
|
+
this.registry.emitEvent("skill:resource-loaded" /* SKILL_RESOURCE_LOADED */, {
|
|
509
|
+
name: skill.metadata.name,
|
|
510
|
+
resourcePath,
|
|
511
|
+
resolvedPath: pathResult.resolvedPath,
|
|
512
|
+
contentLength: content.length
|
|
513
|
+
});
|
|
514
|
+
return { kind: "success", content };
|
|
515
|
+
}
|
|
516
|
+
async activate(name) {
|
|
517
|
+
const skill = this.registry.get(name);
|
|
518
|
+
if (!skill) {
|
|
519
|
+
const availableNames = this.registry.getNames();
|
|
520
|
+
activationLogger.warn("Skill activation failed \u2014 not found", {
|
|
521
|
+
name,
|
|
522
|
+
availableCount: availableNames.length
|
|
523
|
+
});
|
|
524
|
+
return { kind: "skill-not-found", name, availableNames };
|
|
525
|
+
}
|
|
526
|
+
const policyDecision = evaluateSkillActivationPolicy(skill.trustLevel);
|
|
527
|
+
if (!policyDecision.allowed) {
|
|
528
|
+
activationLogger.warn("Skill activation blocked \u2014 trust policy", {
|
|
529
|
+
name,
|
|
530
|
+
trustLevel: skill.trustLevel,
|
|
531
|
+
reason: policyDecision.reason,
|
|
532
|
+
message: policyDecision.message
|
|
533
|
+
});
|
|
534
|
+
this.registry.emitEvent("trust:policy-denied" /* TRUST_POLICY_DENIED */, {
|
|
535
|
+
name: skill.metadata.name,
|
|
536
|
+
resourcePath: "SKILL.md",
|
|
537
|
+
trustLevel: skill.trustLevel,
|
|
538
|
+
reason: policyDecision.reason,
|
|
539
|
+
message: policyDecision.message
|
|
540
|
+
});
|
|
541
|
+
return {
|
|
542
|
+
kind: "access-denied",
|
|
543
|
+
reason: policyDecision.reason,
|
|
544
|
+
message: policyDecision.message
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
let body;
|
|
548
|
+
try {
|
|
549
|
+
const content = readFileSync(resolve(skill.skillPath, "SKILL.md"), "utf-8");
|
|
550
|
+
body = extractBody(content);
|
|
551
|
+
} catch (error) {
|
|
552
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
553
|
+
activationLogger.error("Skill activation failed \u2014 read error", {
|
|
554
|
+
name,
|
|
555
|
+
skillPath: skill.skillPath,
|
|
556
|
+
error: message
|
|
557
|
+
});
|
|
558
|
+
return { kind: "read-failure", name, error: message };
|
|
559
|
+
}
|
|
560
|
+
activationLogger.info("Skill activated", {
|
|
561
|
+
name: skill.metadata.name,
|
|
562
|
+
skillPath: skill.skillPath,
|
|
563
|
+
bodyLength: body.length,
|
|
564
|
+
trustLevel: skill.trustLevel,
|
|
565
|
+
activationReason: policyDecision.reason
|
|
566
|
+
});
|
|
567
|
+
this.registry.emitEvent("skill:activated" /* SKILL_ACTIVATED */, {
|
|
568
|
+
name: skill.metadata.name,
|
|
569
|
+
skillPath: skill.skillPath,
|
|
570
|
+
bodyLength: body.length,
|
|
571
|
+
trustLevel: skill.trustLevel
|
|
572
|
+
});
|
|
573
|
+
return { kind: "success", body };
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
var activateSkillSchema = z.object({
|
|
577
|
+
name: z.string().describe('The name of the skill to activate (e.g., "code-review")')
|
|
578
|
+
});
|
|
579
|
+
var readSkillResourceSchema = z.object({
|
|
580
|
+
name: z.string().describe("The name of the skill that owns the resource"),
|
|
581
|
+
path: z.string().describe('Relative path to the resource file within the skill directory (e.g., "references/GUIDE.md", "scripts/setup.sh")')
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
// src/activation-activate-tool.ts
|
|
585
|
+
function createActivateSkillTool(registry) {
|
|
586
|
+
const access = new AgentSkillAccess(registry);
|
|
587
|
+
return new ToolBuilder().name("activate-skill").description(
|
|
588
|
+
"Activate an Agent Skill by name, loading its full instructions for trusted roots. Returns the complete SKILL.md body content for workspace or explicitly trusted skills. Use this when you see a relevant skill in <available_skills> or <untrusted_skills>; activation is blocked for untrusted roots until they are promoted."
|
|
589
|
+
).category(ToolCategory.SKILLS).tags(["skill", "activation", "agent-skills"]).schema(activateSkillSchema).implement(async ({ name }) => {
|
|
590
|
+
const result = await access.activate(name);
|
|
591
|
+
switch (result.kind) {
|
|
592
|
+
case "success":
|
|
593
|
+
return result.body;
|
|
594
|
+
case "skill-not-found":
|
|
595
|
+
return formatMissingSkillMessage(result.name, result.availableNames);
|
|
596
|
+
case "access-denied":
|
|
597
|
+
return result.message;
|
|
598
|
+
case "read-failure":
|
|
599
|
+
return `Failed to read skill "${result.name}" instructions: ${result.error}`;
|
|
600
|
+
}
|
|
601
|
+
}).build();
|
|
602
|
+
}
|
|
603
|
+
function createReadSkillResourceTool(registry) {
|
|
604
|
+
const access = new AgentSkillAccess(registry);
|
|
605
|
+
return new ToolBuilder().name("read-skill-resource").description(
|
|
606
|
+
"Read a resource file from an activated Agent Skill. Returns the content of a file within the skill directory (e.g., references/, scripts/, assets/). The path must be relative to the skill root and cannot traverse outside it. SKILL.md is only readable from workspace or trusted roots."
|
|
607
|
+
).category(ToolCategory.SKILLS).tags(["skill", "resource", "agent-skills"]).schema(readSkillResourceSchema).implement(async ({ name, path: resourcePath }) => {
|
|
608
|
+
const result = await access.readResource(name, resourcePath);
|
|
609
|
+
switch (result.kind) {
|
|
610
|
+
case "success":
|
|
611
|
+
return result.content;
|
|
612
|
+
case "skill-not-found":
|
|
613
|
+
return formatMissingSkillMessage(result.name, result.availableNames);
|
|
614
|
+
case "access-denied":
|
|
615
|
+
return result.message;
|
|
616
|
+
case "resource-not-found":
|
|
617
|
+
case "read-failure":
|
|
618
|
+
return `Failed to read resource "${result.resourcePath}" from skill "${result.name}": ${result.error}`;
|
|
518
619
|
}
|
|
519
620
|
}).build();
|
|
520
621
|
}
|
|
622
|
+
function resolveResourcePath(skillPath, resourcePath) {
|
|
623
|
+
if (isAbsolute(resourcePath)) {
|
|
624
|
+
return { success: false, error: "Absolute resource paths are not allowed" };
|
|
625
|
+
}
|
|
626
|
+
const segments = resourcePath.split(/[/\\]/);
|
|
627
|
+
if (segments.some((seg) => seg === "..")) {
|
|
628
|
+
return { success: false, error: "Path traversal is not allowed \u2014 resource paths must stay within the skill directory" };
|
|
629
|
+
}
|
|
630
|
+
const resolvedPath = resolve(skillPath, resourcePath);
|
|
631
|
+
const resolvedSkillPath = resolve(skillPath);
|
|
632
|
+
const rel = relative(resolvedSkillPath, resolvedPath);
|
|
633
|
+
if (rel.startsWith("..") || resolve(resolvedSkillPath, rel) !== resolvedPath) {
|
|
634
|
+
return { success: false, error: "Path traversal is not allowed \u2014 resource paths must stay within the skill directory" };
|
|
635
|
+
}
|
|
636
|
+
try {
|
|
637
|
+
const realSkillRoot = realpathSync(resolvedSkillPath);
|
|
638
|
+
const realTarget = realpathSync(resolvedPath);
|
|
639
|
+
const realRel = relative(realSkillRoot, realTarget);
|
|
640
|
+
if (realRel.startsWith("..") || isAbsolute(realRel)) {
|
|
641
|
+
return { success: false, error: "Symlink target escapes the skill directory \u2014 access denied" };
|
|
642
|
+
}
|
|
643
|
+
} catch {
|
|
644
|
+
}
|
|
645
|
+
return { success: true, resolvedPath };
|
|
646
|
+
}
|
|
521
647
|
|
|
522
648
|
// src/activation.ts
|
|
523
649
|
function createSkillActivationTools(registry) {
|