@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/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/README.md
CHANGED
|
@@ -20,10 +20,30 @@ yarn add @agentforge/skills
|
|
|
20
20
|
|
|
21
21
|
## Overview
|
|
22
22
|
|
|
23
|
-
`@agentforge/skills` provides
|
|
23
|
+
`@agentforge/skills` provides Agent Skill discovery, registration, access, and trust policy enforcement for AgentForge agents. Within Agent Skill access, Skill activation loads trusted instructions while resource access loads supporting files. It implements the [Agent Skills Specification](https://agentskills.io) for composable, modular agent capabilities.
|
|
24
24
|
|
|
25
25
|
Full source code and API will be available after ST-07002 (Move Skills Source Files). See the [AgentForge docs](https://tvscoundrel.github.io/agentforge/) for usage guides and tutorials.
|
|
26
26
|
|
|
27
|
+
## Agent Skill Access
|
|
28
|
+
|
|
29
|
+
Access Agent Skill instructions and supporting resources through the Tools bound
|
|
30
|
+
to a `SkillRegistry`:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { SkillRegistry } from '@agentforge/skills';
|
|
34
|
+
|
|
35
|
+
const registry = new SkillRegistry({
|
|
36
|
+
enabled: true,
|
|
37
|
+
skillRoots: [{ path: '.agentskills', trust: 'workspace' }],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const [activateSkill, readSkillResource] = registry.toActivationTools();
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`resolveResourcePath` remains exported for compatibility, but it is deprecated
|
|
44
|
+
and planned for removal in the next major release. Use the
|
|
45
|
+
`read-skill-resource` Tool for Agent Skill resource access instead.
|
|
46
|
+
|
|
27
47
|
## License
|
|
28
48
|
|
|
29
49
|
MIT — see [LICENSE](../../LICENSE) for details.
|
package/dist/index.cjs
CHANGED
|
@@ -223,23 +223,12 @@ function scanAllSkillRoots(skillRoots) {
|
|
|
223
223
|
function extractBody(content) {
|
|
224
224
|
return matter__default.default(content).content.trim();
|
|
225
225
|
}
|
|
226
|
-
var activateSkillSchema = zod.z.object({
|
|
227
|
-
name: zod.z.string().describe('The name of the skill to activate (e.g., "code-review")')
|
|
228
|
-
});
|
|
229
|
-
var readSkillResourceSchema = zod.z.object({
|
|
230
|
-
name: zod.z.string().describe("The name of the skill that owns the resource"),
|
|
231
|
-
path: zod.z.string().describe('Relative path to the resource file within the skill directory (e.g., "references/GUIDE.md", "scripts/setup.sh")')
|
|
232
|
-
});
|
|
233
226
|
var activationLogger = core.createLogger("agentforge:skills:activation", {
|
|
234
227
|
level: core.LogLevel.INFO
|
|
235
228
|
});
|
|
236
|
-
function
|
|
237
|
-
const availableNames = registry.getNames();
|
|
229
|
+
function formatMissingSkillMessage(name, availableNames) {
|
|
238
230
|
const suggestion = availableNames.length > 0 ? ` Available skills: ${availableNames.join(", ")}` : " No skills are currently registered.";
|
|
239
|
-
return {
|
|
240
|
-
availableCount: availableNames.length,
|
|
241
|
-
errorMessage: `Skill "${name}" not found.${suggestion}`
|
|
242
|
-
};
|
|
231
|
+
return `Skill "${name}" not found.${suggestion}`;
|
|
243
232
|
}
|
|
244
233
|
|
|
245
234
|
// src/trust.ts
|
|
@@ -331,114 +320,96 @@ function evaluateSkillActivationPolicy(trustLevel) {
|
|
|
331
320
|
}
|
|
332
321
|
}
|
|
333
322
|
|
|
334
|
-
// src/
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
activationLogger.warn("Skill activation blocked \u2014 trust policy", {
|
|
348
|
-
name,
|
|
349
|
-
trustLevel: skill.trustLevel,
|
|
350
|
-
reason: policyDecision.reason,
|
|
351
|
-
message: policyDecision.message
|
|
352
|
-
});
|
|
353
|
-
registry.emitEvent("trust:policy-denied" /* TRUST_POLICY_DENIED */, {
|
|
354
|
-
name: skill.metadata.name,
|
|
355
|
-
resourcePath: "SKILL.md",
|
|
356
|
-
trustLevel: skill.trustLevel,
|
|
357
|
-
reason: policyDecision.reason,
|
|
358
|
-
message: policyDecision.message
|
|
359
|
-
});
|
|
360
|
-
return policyDecision.message;
|
|
361
|
-
}
|
|
362
|
-
const skillMdPath = path.resolve(skill.skillPath, "SKILL.md");
|
|
363
|
-
try {
|
|
364
|
-
const content = fs.readFileSync(skillMdPath, "utf-8");
|
|
365
|
-
const body = extractBody(content);
|
|
366
|
-
activationLogger.info("Skill activated", {
|
|
367
|
-
name: skill.metadata.name,
|
|
368
|
-
skillPath: skill.skillPath,
|
|
369
|
-
bodyLength: body.length,
|
|
370
|
-
trustLevel: skill.trustLevel,
|
|
371
|
-
activationReason: policyDecision.reason
|
|
372
|
-
});
|
|
373
|
-
registry.emitEvent("skill:activated" /* SKILL_ACTIVATED */, {
|
|
374
|
-
name: skill.metadata.name,
|
|
375
|
-
skillPath: skill.skillPath,
|
|
376
|
-
bodyLength: body.length,
|
|
377
|
-
trustLevel: skill.trustLevel
|
|
378
|
-
});
|
|
379
|
-
return body;
|
|
380
|
-
} catch (error) {
|
|
381
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
382
|
-
activationLogger.error("Skill activation failed \u2014 read error", {
|
|
383
|
-
name,
|
|
384
|
-
skillPath: skill.skillPath,
|
|
385
|
-
error: message
|
|
386
|
-
});
|
|
387
|
-
return `Failed to read skill "${name}" instructions: ${message}`;
|
|
388
|
-
}
|
|
389
|
-
}).build();
|
|
390
|
-
}
|
|
391
|
-
function resolveResourcePath(skillPath, resourcePath) {
|
|
323
|
+
// src/agent-skill-access.ts
|
|
324
|
+
var PATH_TRAVERSAL_MESSAGE = "Path traversal is not allowed \u2014 resource paths must stay within the skill directory";
|
|
325
|
+
var SYMLINK_ESCAPE_MESSAGE = "Symlink target escapes the skill directory \u2014 access denied";
|
|
326
|
+
function formatError(error) {
|
|
327
|
+
return error instanceof Error ? error.message : String(error);
|
|
328
|
+
}
|
|
329
|
+
function formatResourceReadError(error, resolvedPath) {
|
|
330
|
+
const message = formatError(error);
|
|
331
|
+
const detailIndex = message.indexOf(", ");
|
|
332
|
+
const prefix = detailIndex === -1 ? message : message.slice(0, detailIndex);
|
|
333
|
+
return `${prefix}, open '${resolvedPath}'`;
|
|
334
|
+
}
|
|
335
|
+
function resolveCanonicalResourcePath(skillPath, resourcePath) {
|
|
392
336
|
if (path.isAbsolute(resourcePath)) {
|
|
393
|
-
return {
|
|
337
|
+
return { kind: "access-denied", message: "Absolute resource paths are not allowed" };
|
|
394
338
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
return { success: false, error: "Path traversal is not allowed \u2014 resource paths must stay within the skill directory" };
|
|
339
|
+
if (resourcePath.split(/[/\\]/).some((segment) => segment === "..")) {
|
|
340
|
+
return { kind: "access-denied", message: PATH_TRAVERSAL_MESSAGE };
|
|
398
341
|
}
|
|
399
|
-
const resolvedPath = path.resolve(skillPath, resourcePath);
|
|
400
342
|
const resolvedSkillPath = path.resolve(skillPath);
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
343
|
+
const resolvedPath = path.resolve(resolvedSkillPath, resourcePath);
|
|
344
|
+
const lexicalRelativePath = path.relative(resolvedSkillPath, resolvedPath);
|
|
345
|
+
if (lexicalRelativePath.startsWith("..") || path.isAbsolute(lexicalRelativePath) || path.resolve(resolvedSkillPath, lexicalRelativePath) !== resolvedPath) {
|
|
346
|
+
return { kind: "access-denied", message: PATH_TRAVERSAL_MESSAGE };
|
|
404
347
|
}
|
|
348
|
+
let canonicalSkillPath;
|
|
405
349
|
try {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
350
|
+
canonicalSkillPath = fs.realpathSync(resolvedSkillPath);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
return { kind: "read-failure", error: formatResourceReadError(error, resolvedPath) };
|
|
353
|
+
}
|
|
354
|
+
let canonicalPath;
|
|
355
|
+
try {
|
|
356
|
+
canonicalPath = fs.realpathSync(resolvedPath);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
if (error.code === "ENOENT") {
|
|
359
|
+
return {
|
|
360
|
+
kind: "resource-not-found",
|
|
361
|
+
resolvedPath,
|
|
362
|
+
error: formatResourceReadError(error, resolvedPath)
|
|
363
|
+
};
|
|
411
364
|
}
|
|
412
|
-
|
|
365
|
+
return {
|
|
366
|
+
kind: "target-read-failure",
|
|
367
|
+
resolvedPath,
|
|
368
|
+
error: formatResourceReadError(error, resolvedPath)
|
|
369
|
+
};
|
|
413
370
|
}
|
|
414
|
-
|
|
371
|
+
const canonicalRelativePath = path.relative(canonicalSkillPath, canonicalPath);
|
|
372
|
+
if (canonicalRelativePath.startsWith("..") || path.isAbsolute(canonicalRelativePath)) {
|
|
373
|
+
return { kind: "access-denied", message: SYMLINK_ESCAPE_MESSAGE };
|
|
374
|
+
}
|
|
375
|
+
return { kind: "success", resolvedPath, canonicalPath, canonicalRelativePath };
|
|
415
376
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const skill = registry.get(name);
|
|
377
|
+
var AgentSkillAccess = class {
|
|
378
|
+
constructor(registry) {
|
|
379
|
+
this.registry = registry;
|
|
380
|
+
}
|
|
381
|
+
async readResource(name, resourcePath) {
|
|
382
|
+
const skill = this.registry.get(name);
|
|
423
383
|
if (!skill) {
|
|
424
|
-
const
|
|
425
|
-
activationLogger.warn("Skill resource load failed \u2014 skill not found", {
|
|
426
|
-
|
|
384
|
+
const availableNames = this.registry.getNames();
|
|
385
|
+
activationLogger.warn("Skill resource load failed \u2014 skill not found", {
|
|
386
|
+
name,
|
|
387
|
+
resourcePath
|
|
388
|
+
});
|
|
389
|
+
return { kind: "skill-not-found", name, availableNames };
|
|
427
390
|
}
|
|
428
|
-
const pathResult =
|
|
429
|
-
if (
|
|
391
|
+
const pathResult = resolveCanonicalResourcePath(skill.skillPath, resourcePath);
|
|
392
|
+
if (pathResult.kind === "access-denied") {
|
|
430
393
|
activationLogger.warn("Skill resource load blocked \u2014 path traversal", {
|
|
394
|
+
name,
|
|
395
|
+
resourcePath,
|
|
396
|
+
error: pathResult.message
|
|
397
|
+
});
|
|
398
|
+
return pathResult;
|
|
399
|
+
}
|
|
400
|
+
if (pathResult.kind === "read-failure") {
|
|
401
|
+
activationLogger.warn("Skill resource load failed \u2014 file not found or unreadable", {
|
|
431
402
|
name,
|
|
432
403
|
resourcePath,
|
|
433
404
|
error: pathResult.error
|
|
434
405
|
});
|
|
435
|
-
return pathResult
|
|
406
|
+
return { ...pathResult, name, resourcePath };
|
|
436
407
|
}
|
|
437
408
|
const skillInstructionsPath = path.resolve(skill.skillPath, "SKILL.md");
|
|
438
409
|
let isSkillInstructions = pathResult.resolvedPath.toLowerCase() === skillInstructionsPath.toLowerCase();
|
|
439
|
-
if (!isSkillInstructions) {
|
|
410
|
+
if (!isSkillInstructions && pathResult.kind === "success") {
|
|
440
411
|
try {
|
|
441
|
-
isSkillInstructions =
|
|
412
|
+
isSkillInstructions = pathResult.canonicalPath.toLowerCase() === fs.realpathSync(skillInstructionsPath).toLowerCase();
|
|
442
413
|
} catch {
|
|
443
414
|
}
|
|
444
415
|
}
|
|
@@ -452,20 +423,24 @@ function createReadSkillResourceTool(registry) {
|
|
|
452
423
|
reason: activationDecision.reason,
|
|
453
424
|
message: activationDecision.message
|
|
454
425
|
});
|
|
455
|
-
registry.emitEvent("trust:policy-denied" /* TRUST_POLICY_DENIED */, {
|
|
426
|
+
this.registry.emitEvent("trust:policy-denied" /* TRUST_POLICY_DENIED */, {
|
|
456
427
|
name: skill.metadata.name,
|
|
457
428
|
resourcePath: "SKILL.md",
|
|
458
429
|
trustLevel: skill.trustLevel,
|
|
459
430
|
reason: activationDecision.reason,
|
|
460
431
|
message: activationDecision.message
|
|
461
432
|
});
|
|
462
|
-
return
|
|
433
|
+
return {
|
|
434
|
+
kind: "access-denied",
|
|
435
|
+
reason: activationDecision.reason,
|
|
436
|
+
message: activationDecision.message
|
|
437
|
+
};
|
|
463
438
|
}
|
|
464
439
|
}
|
|
465
440
|
const policyDecision = evaluateTrustPolicy(
|
|
466
|
-
resourcePath,
|
|
441
|
+
pathResult.kind === "success" ? pathResult.canonicalRelativePath : resourcePath,
|
|
467
442
|
skill.trustLevel,
|
|
468
|
-
registry.getAllowUntrustedScripts()
|
|
443
|
+
this.registry.getAllowUntrustedScripts()
|
|
469
444
|
);
|
|
470
445
|
if (!policyDecision.allowed) {
|
|
471
446
|
activationLogger.warn("Skill resource load blocked \u2014 trust policy", {
|
|
@@ -475,14 +450,18 @@ function createReadSkillResourceTool(registry) {
|
|
|
475
450
|
reason: policyDecision.reason,
|
|
476
451
|
message: policyDecision.message
|
|
477
452
|
});
|
|
478
|
-
registry.emitEvent("trust:policy-denied" /* TRUST_POLICY_DENIED */, {
|
|
453
|
+
this.registry.emitEvent("trust:policy-denied" /* TRUST_POLICY_DENIED */, {
|
|
479
454
|
name: skill.metadata.name,
|
|
480
455
|
resourcePath,
|
|
481
456
|
trustLevel: skill.trustLevel,
|
|
482
457
|
reason: policyDecision.reason,
|
|
483
458
|
message: policyDecision.message
|
|
484
459
|
});
|
|
485
|
-
return
|
|
460
|
+
return {
|
|
461
|
+
kind: "access-denied",
|
|
462
|
+
reason: policyDecision.reason,
|
|
463
|
+
message: policyDecision.message
|
|
464
|
+
};
|
|
486
465
|
}
|
|
487
466
|
if (policyDecision.reason !== "not-script" /* NOT_SCRIPT */) {
|
|
488
467
|
activationLogger.info("Skill resource trust policy \u2014 allowed", {
|
|
@@ -491,28 +470,29 @@ function createReadSkillResourceTool(registry) {
|
|
|
491
470
|
trustLevel: skill.trustLevel,
|
|
492
471
|
reason: policyDecision.reason
|
|
493
472
|
});
|
|
494
|
-
registry.emitEvent("trust:policy-allowed" /* TRUST_POLICY_ALLOWED */, {
|
|
473
|
+
this.registry.emitEvent("trust:policy-allowed" /* TRUST_POLICY_ALLOWED */, {
|
|
495
474
|
name: skill.metadata.name,
|
|
496
475
|
resourcePath,
|
|
497
476
|
trustLevel: skill.trustLevel,
|
|
498
477
|
reason: policyDecision.reason
|
|
499
478
|
});
|
|
500
479
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
name: skill.metadata.name,
|
|
480
|
+
if (pathResult.kind === "resource-not-found" || pathResult.kind === "target-read-failure") {
|
|
481
|
+
activationLogger.warn("Skill resource load failed \u2014 file not found or unreadable", {
|
|
482
|
+
name,
|
|
505
483
|
resourcePath,
|
|
506
|
-
|
|
507
|
-
contentLength: content.length
|
|
484
|
+
error: pathResult.error
|
|
508
485
|
});
|
|
509
|
-
|
|
510
|
-
|
|
486
|
+
return {
|
|
487
|
+
kind: pathResult.kind === "resource-not-found" ? "resource-not-found" : "read-failure",
|
|
488
|
+
name,
|
|
511
489
|
resourcePath,
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
490
|
+
error: pathResult.error
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
let content;
|
|
494
|
+
try {
|
|
495
|
+
content = fs.readFileSync(pathResult.canonicalPath, "utf-8");
|
|
516
496
|
} catch (error) {
|
|
517
497
|
const message = error instanceof Error ? error.message : String(error);
|
|
518
498
|
activationLogger.warn("Skill resource load failed \u2014 file not found or unreadable", {
|
|
@@ -520,10 +500,156 @@ function createReadSkillResourceTool(registry) {
|
|
|
520
500
|
resourcePath,
|
|
521
501
|
error: message
|
|
522
502
|
});
|
|
523
|
-
|
|
503
|
+
if (error.code === "ENOENT") {
|
|
504
|
+
return { kind: "resource-not-found", name, resourcePath, error: message };
|
|
505
|
+
}
|
|
506
|
+
return { kind: "read-failure", name, resourcePath, error: message };
|
|
507
|
+
}
|
|
508
|
+
activationLogger.info("Skill resource loaded", {
|
|
509
|
+
name: skill.metadata.name,
|
|
510
|
+
resourcePath,
|
|
511
|
+
resolvedPath: pathResult.resolvedPath,
|
|
512
|
+
contentLength: content.length
|
|
513
|
+
});
|
|
514
|
+
this.registry.emitEvent("skill:resource-loaded" /* SKILL_RESOURCE_LOADED */, {
|
|
515
|
+
name: skill.metadata.name,
|
|
516
|
+
resourcePath,
|
|
517
|
+
resolvedPath: pathResult.resolvedPath,
|
|
518
|
+
contentLength: content.length
|
|
519
|
+
});
|
|
520
|
+
return { kind: "success", content };
|
|
521
|
+
}
|
|
522
|
+
async activate(name) {
|
|
523
|
+
const skill = this.registry.get(name);
|
|
524
|
+
if (!skill) {
|
|
525
|
+
const availableNames = this.registry.getNames();
|
|
526
|
+
activationLogger.warn("Skill activation failed \u2014 not found", {
|
|
527
|
+
name,
|
|
528
|
+
availableCount: availableNames.length
|
|
529
|
+
});
|
|
530
|
+
return { kind: "skill-not-found", name, availableNames };
|
|
531
|
+
}
|
|
532
|
+
const policyDecision = evaluateSkillActivationPolicy(skill.trustLevel);
|
|
533
|
+
if (!policyDecision.allowed) {
|
|
534
|
+
activationLogger.warn("Skill activation blocked \u2014 trust policy", {
|
|
535
|
+
name,
|
|
536
|
+
trustLevel: skill.trustLevel,
|
|
537
|
+
reason: policyDecision.reason,
|
|
538
|
+
message: policyDecision.message
|
|
539
|
+
});
|
|
540
|
+
this.registry.emitEvent("trust:policy-denied" /* TRUST_POLICY_DENIED */, {
|
|
541
|
+
name: skill.metadata.name,
|
|
542
|
+
resourcePath: "SKILL.md",
|
|
543
|
+
trustLevel: skill.trustLevel,
|
|
544
|
+
reason: policyDecision.reason,
|
|
545
|
+
message: policyDecision.message
|
|
546
|
+
});
|
|
547
|
+
return {
|
|
548
|
+
kind: "access-denied",
|
|
549
|
+
reason: policyDecision.reason,
|
|
550
|
+
message: policyDecision.message
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
let body;
|
|
554
|
+
try {
|
|
555
|
+
const content = fs.readFileSync(path.resolve(skill.skillPath, "SKILL.md"), "utf-8");
|
|
556
|
+
body = extractBody(content);
|
|
557
|
+
} catch (error) {
|
|
558
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
559
|
+
activationLogger.error("Skill activation failed \u2014 read error", {
|
|
560
|
+
name,
|
|
561
|
+
skillPath: skill.skillPath,
|
|
562
|
+
error: message
|
|
563
|
+
});
|
|
564
|
+
return { kind: "read-failure", name, error: message };
|
|
565
|
+
}
|
|
566
|
+
activationLogger.info("Skill activated", {
|
|
567
|
+
name: skill.metadata.name,
|
|
568
|
+
skillPath: skill.skillPath,
|
|
569
|
+
bodyLength: body.length,
|
|
570
|
+
trustLevel: skill.trustLevel,
|
|
571
|
+
activationReason: policyDecision.reason
|
|
572
|
+
});
|
|
573
|
+
this.registry.emitEvent("skill:activated" /* SKILL_ACTIVATED */, {
|
|
574
|
+
name: skill.metadata.name,
|
|
575
|
+
skillPath: skill.skillPath,
|
|
576
|
+
bodyLength: body.length,
|
|
577
|
+
trustLevel: skill.trustLevel
|
|
578
|
+
});
|
|
579
|
+
return { kind: "success", body };
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
var activateSkillSchema = zod.z.object({
|
|
583
|
+
name: zod.z.string().describe('The name of the skill to activate (e.g., "code-review")')
|
|
584
|
+
});
|
|
585
|
+
var readSkillResourceSchema = zod.z.object({
|
|
586
|
+
name: zod.z.string().describe("The name of the skill that owns the resource"),
|
|
587
|
+
path: zod.z.string().describe('Relative path to the resource file within the skill directory (e.g., "references/GUIDE.md", "scripts/setup.sh")')
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
// src/activation-activate-tool.ts
|
|
591
|
+
function createActivateSkillTool(registry) {
|
|
592
|
+
const access = new AgentSkillAccess(registry);
|
|
593
|
+
return new core.ToolBuilder().name("activate-skill").description(
|
|
594
|
+
"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."
|
|
595
|
+
).category(core.ToolCategory.SKILLS).tags(["skill", "activation", "agent-skills"]).schema(activateSkillSchema).implement(async ({ name }) => {
|
|
596
|
+
const result = await access.activate(name);
|
|
597
|
+
switch (result.kind) {
|
|
598
|
+
case "success":
|
|
599
|
+
return result.body;
|
|
600
|
+
case "skill-not-found":
|
|
601
|
+
return formatMissingSkillMessage(result.name, result.availableNames);
|
|
602
|
+
case "access-denied":
|
|
603
|
+
return result.message;
|
|
604
|
+
case "read-failure":
|
|
605
|
+
return `Failed to read skill "${result.name}" instructions: ${result.error}`;
|
|
606
|
+
}
|
|
607
|
+
}).build();
|
|
608
|
+
}
|
|
609
|
+
function createReadSkillResourceTool(registry) {
|
|
610
|
+
const access = new AgentSkillAccess(registry);
|
|
611
|
+
return new core.ToolBuilder().name("read-skill-resource").description(
|
|
612
|
+
"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."
|
|
613
|
+
).category(core.ToolCategory.SKILLS).tags(["skill", "resource", "agent-skills"]).schema(readSkillResourceSchema).implement(async ({ name, path: resourcePath }) => {
|
|
614
|
+
const result = await access.readResource(name, resourcePath);
|
|
615
|
+
switch (result.kind) {
|
|
616
|
+
case "success":
|
|
617
|
+
return result.content;
|
|
618
|
+
case "skill-not-found":
|
|
619
|
+
return formatMissingSkillMessage(result.name, result.availableNames);
|
|
620
|
+
case "access-denied":
|
|
621
|
+
return result.message;
|
|
622
|
+
case "resource-not-found":
|
|
623
|
+
case "read-failure":
|
|
624
|
+
return `Failed to read resource "${result.resourcePath}" from skill "${result.name}": ${result.error}`;
|
|
524
625
|
}
|
|
525
626
|
}).build();
|
|
526
627
|
}
|
|
628
|
+
function resolveResourcePath(skillPath, resourcePath) {
|
|
629
|
+
if (path.isAbsolute(resourcePath)) {
|
|
630
|
+
return { success: false, error: "Absolute resource paths are not allowed" };
|
|
631
|
+
}
|
|
632
|
+
const segments = resourcePath.split(/[/\\]/);
|
|
633
|
+
if (segments.some((seg) => seg === "..")) {
|
|
634
|
+
return { success: false, error: "Path traversal is not allowed \u2014 resource paths must stay within the skill directory" };
|
|
635
|
+
}
|
|
636
|
+
const resolvedPath = path.resolve(skillPath, resourcePath);
|
|
637
|
+
const resolvedSkillPath = path.resolve(skillPath);
|
|
638
|
+
const rel = path.relative(resolvedSkillPath, resolvedPath);
|
|
639
|
+
if (rel.startsWith("..") || path.resolve(resolvedSkillPath, rel) !== resolvedPath) {
|
|
640
|
+
return { success: false, error: "Path traversal is not allowed \u2014 resource paths must stay within the skill directory" };
|
|
641
|
+
}
|
|
642
|
+
try {
|
|
643
|
+
const realSkillRoot = fs.realpathSync(resolvedSkillPath);
|
|
644
|
+
const realTarget = fs.realpathSync(resolvedPath);
|
|
645
|
+
const realRel = path.relative(realSkillRoot, realTarget);
|
|
646
|
+
if (realRel.startsWith("..") || path.isAbsolute(realRel)) {
|
|
647
|
+
return { success: false, error: "Symlink target escapes the skill directory \u2014 access denied" };
|
|
648
|
+
}
|
|
649
|
+
} catch {
|
|
650
|
+
}
|
|
651
|
+
return { success: true, resolvedPath };
|
|
652
|
+
}
|
|
527
653
|
|
|
528
654
|
// src/activation.ts
|
|
529
655
|
function createSkillActivationTools(registry) {
|