@agentforge/skills 0.17.1 → 0.18.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/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 buildMissingSkillMessage(registry, name) {
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/activation-activate-tool.ts
329
- function createActivateSkillTool(registry) {
330
- return new ToolBuilder().name("activate-skill").description(
331
- "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."
332
- ).category(ToolCategory.SKILLS).tags(["skill", "activation", "agent-skills"]).schema(activateSkillSchema).implement(async ({ name }) => {
333
- const skill = registry.get(name);
334
- if (!skill) {
335
- const { availableCount, errorMessage } = buildMissingSkillMessage(registry, name);
336
- activationLogger.warn("Skill activation failed \u2014 not found", { name, availableCount });
337
- return errorMessage;
338
- }
339
- const policyDecision = evaluateSkillActivationPolicy(skill.trustLevel);
340
- if (!policyDecision.allowed) {
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 { success: false, error: "Absolute resource paths are not allowed" };
331
+ return { kind: "access-denied", message: "Absolute resource paths are not allowed" };
388
332
  }
389
- const segments = resourcePath.split(/[/\\]/);
390
- if (segments.some((seg) => seg === "..")) {
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 rel = relative(resolvedSkillPath, resolvedPath);
396
- if (rel.startsWith("..") || resolve(resolvedSkillPath, rel) !== resolvedPath) {
397
- return { success: false, error: "Path traversal is not allowed \u2014 resource paths must stay within the skill directory" };
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
- const realSkillRoot = realpathSync(resolvedSkillPath);
401
- const realTarget = realpathSync(resolvedPath);
402
- const realRel = relative(realSkillRoot, realTarget);
403
- if (realRel.startsWith("..") || isAbsolute(realRel)) {
404
- return { success: false, error: "Symlink target escapes the skill directory \u2014 access denied" };
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
- } catch {
359
+ return {
360
+ kind: "target-read-failure",
361
+ resolvedPath,
362
+ error: formatResourceReadError(error, resolvedPath)
363
+ };
407
364
  }
408
- return { success: true, resolvedPath };
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
- // src/activation-resource-tool.ts
412
- function createReadSkillResourceTool(registry) {
413
- return new ToolBuilder().name("read-skill-resource").description(
414
- "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."
415
- ).category(ToolCategory.SKILLS).tags(["skill", "resource", "agent-skills"]).schema(readSkillResourceSchema).implement(async ({ name, path: resourcePath }) => {
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 { errorMessage } = buildMissingSkillMessage(registry, name);
419
- activationLogger.warn("Skill resource load failed \u2014 skill not found", { name, resourcePath });
420
- return errorMessage;
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 = resolveResourcePath(skill.skillPath, resourcePath);
423
- if (!pathResult.success) {
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.error;
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 = realpathSync(pathResult.resolvedPath).toLowerCase() === realpathSync(skillInstructionsPath).toLowerCase();
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 activationDecision.message;
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 policyDecision.message;
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
- try {
496
- const content = readFileSync(pathResult.resolvedPath, "utf-8");
497
- activationLogger.info("Skill resource loaded", {
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
- resolvedPath: pathResult.resolvedPath,
501
- contentLength: content.length
478
+ error: pathResult.error
502
479
  });
503
- registry.emitEvent("skill:resource-loaded" /* SKILL_RESOURCE_LOADED */, {
504
- name: skill.metadata.name,
480
+ return {
481
+ kind: pathResult.kind === "resource-not-found" ? "resource-not-found" : "read-failure",
482
+ name,
505
483
  resourcePath,
506
- resolvedPath: pathResult.resolvedPath,
507
- contentLength: content.length
508
- });
509
- return content;
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
- return `Failed to read resource "${resourcePath}" from skill "${name}": ${message}`;
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) {