@botbotgo/agent-harness 0.0.16 → 0.0.17
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.
|
@@ -6,6 +6,11 @@ export type InventoryToolRecord = {
|
|
|
6
6
|
export type InventorySkillRecord = {
|
|
7
7
|
name: string;
|
|
8
8
|
path: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
license?: string;
|
|
11
|
+
compatibility?: string;
|
|
12
|
+
metadata?: Record<string, string>;
|
|
13
|
+
allowedTools?: string[];
|
|
9
14
|
};
|
|
10
15
|
export type InventoryAgentRecord = {
|
|
11
16
|
id: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readSkillMetadata } from "./support/skill-metadata.js";
|
|
2
2
|
function listHostBindings(workspace) {
|
|
3
3
|
return Array.from(workspace.bindings.values()).filter((binding) => binding.harnessRuntime.hostFacing);
|
|
4
4
|
}
|
|
@@ -13,10 +13,18 @@ function dedupeTools(tools) {
|
|
|
13
13
|
return Array.from(deduped.values());
|
|
14
14
|
}
|
|
15
15
|
function toSkillRecords(skillPaths) {
|
|
16
|
-
return Array.from(new Set(skillPaths)).map((skillPath) =>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
return Array.from(new Set(skillPaths)).map((skillPath) => {
|
|
17
|
+
const metadata = readSkillMetadata(skillPath);
|
|
18
|
+
return {
|
|
19
|
+
name: metadata.name,
|
|
20
|
+
path: skillPath,
|
|
21
|
+
description: metadata.description,
|
|
22
|
+
license: metadata.license,
|
|
23
|
+
compatibility: metadata.compatibility,
|
|
24
|
+
metadata: metadata.metadata,
|
|
25
|
+
allowedTools: metadata.allowedTools,
|
|
26
|
+
};
|
|
27
|
+
});
|
|
20
28
|
}
|
|
21
29
|
export function listAgentTools(workspace, agentId) {
|
|
22
30
|
const binding = findAgentBinding(workspace, agentId);
|
|
@@ -1 +1,11 @@
|
|
|
1
|
+
export type SkillMetadata = {
|
|
2
|
+
name: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
license?: string;
|
|
5
|
+
compatibility?: string;
|
|
6
|
+
metadata?: Record<string, string>;
|
|
7
|
+
allowedTools?: string[];
|
|
8
|
+
};
|
|
9
|
+
export declare function readSkillMetadata(skillPath: string): SkillMetadata;
|
|
10
|
+
export declare function validateSkillMetadata(skillPath: string): SkillMetadata;
|
|
1
11
|
export declare function readSkillName(skillPath: string): string;
|
|
@@ -1,34 +1,147 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import { parse } from "yaml";
|
|
4
|
+
const skillMetadataCache = new Map();
|
|
5
|
+
const skillValidationCache = new Map();
|
|
6
|
+
const SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
7
|
+
function parseFrontmatterSource(document) {
|
|
5
8
|
const match = document.match(/^---\s*\n([\s\S]*?)\n---\s*(?:\n|$)/);
|
|
6
|
-
|
|
9
|
+
return match?.[1];
|
|
10
|
+
}
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function normalizeMetadata(value, strict) {
|
|
15
|
+
if (value === undefined) {
|
|
7
16
|
return undefined;
|
|
8
17
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (!nameLine) {
|
|
18
|
+
if (!isRecord(value)) {
|
|
19
|
+
if (strict) {
|
|
20
|
+
throw new Error("metadata must be a string key-value map");
|
|
21
|
+
}
|
|
14
22
|
return undefined;
|
|
15
23
|
}
|
|
16
|
-
const
|
|
17
|
-
|
|
24
|
+
const normalized = Object.entries(value).reduce((acc, [key, item]) => {
|
|
25
|
+
if (typeof item === "string") {
|
|
26
|
+
acc[key] = item;
|
|
27
|
+
}
|
|
28
|
+
return acc;
|
|
29
|
+
}, {});
|
|
30
|
+
if (strict && Object.keys(normalized).length !== Object.keys(value).length) {
|
|
31
|
+
throw new Error("metadata values must all be strings");
|
|
32
|
+
}
|
|
33
|
+
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
18
34
|
}
|
|
19
|
-
|
|
20
|
-
|
|
35
|
+
function toAllowedTools(value) {
|
|
36
|
+
if (typeof value !== "string") {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const tools = value.split(/\s+/).filter(Boolean);
|
|
40
|
+
return tools.length > 0 ? tools : undefined;
|
|
41
|
+
}
|
|
42
|
+
function parseFrontmatter(document, strict = false) {
|
|
43
|
+
const source = parseFrontmatterSource(document);
|
|
44
|
+
if (!source) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
const parsed = parse(source);
|
|
48
|
+
if (!isRecord(parsed)) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
name: typeof parsed.name === "string" ? parsed.name : "",
|
|
53
|
+
description: typeof parsed.description === "string" ? parsed.description : undefined,
|
|
54
|
+
license: typeof parsed.license === "string" ? parsed.license : undefined,
|
|
55
|
+
compatibility: typeof parsed.compatibility === "string" ? parsed.compatibility : undefined,
|
|
56
|
+
metadata: normalizeMetadata(parsed.metadata, strict),
|
|
57
|
+
allowedTools: toAllowedTools(parsed["allowed-tools"]),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function isLegacyBuiltinSkillName(name) {
|
|
61
|
+
return /^builtin\.[a-z0-9-]+$/.test(name);
|
|
62
|
+
}
|
|
63
|
+
function validateSkillName(name, skillPath) {
|
|
64
|
+
if (!name || name.length > 64) {
|
|
65
|
+
throw new Error(`Skill ${skillPath} must define a name between 1 and 64 characters`);
|
|
66
|
+
}
|
|
67
|
+
if (!SKILL_NAME_PATTERN.test(name) && !isLegacyBuiltinSkillName(name)) {
|
|
68
|
+
throw new Error(`Skill ${skillPath} has invalid name ${name}. Standard skill names must use lowercase letters, numbers, and single hyphens`);
|
|
69
|
+
}
|
|
70
|
+
if (SKILL_NAME_PATTERN.test(name) && path.basename(skillPath) !== name) {
|
|
71
|
+
throw new Error(`Skill ${skillPath} must use a directory name that matches its frontmatter name ${name}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function validateSkillDescription(description, skillPath) {
|
|
75
|
+
if (!description || !description.trim() || description.length > 1024) {
|
|
76
|
+
throw new Error(`Skill ${skillPath} must define a description between 1 and 1024 characters`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function validateCompatibility(value, skillPath) {
|
|
80
|
+
if (value !== undefined && (!value.trim() || value.length > 500)) {
|
|
81
|
+
throw new Error(`Skill ${skillPath} compatibility must be between 1 and 500 characters when provided`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function validateMetadata(metadata, skillPath) {
|
|
85
|
+
if (!metadata) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (Object.keys(metadata).length === 0) {
|
|
89
|
+
throw new Error(`Skill ${skillPath} metadata must contain string key-value pairs when provided`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function parseSkillMetadataFromDocument(document, skillPath, strict) {
|
|
93
|
+
const parsed = parseFrontmatter(document, strict);
|
|
94
|
+
if (!parsed) {
|
|
95
|
+
if (strict) {
|
|
96
|
+
throw new Error(`Skill ${skillPath} must contain YAML frontmatter in SKILL.md`);
|
|
97
|
+
}
|
|
98
|
+
return { name: path.basename(skillPath) };
|
|
99
|
+
}
|
|
100
|
+
const metadata = {
|
|
101
|
+
name: strict ? parsed.name : parsed.name || path.basename(skillPath),
|
|
102
|
+
description: parsed.description,
|
|
103
|
+
license: parsed.license,
|
|
104
|
+
compatibility: parsed.compatibility,
|
|
105
|
+
metadata: parsed.metadata,
|
|
106
|
+
allowedTools: parsed.allowedTools,
|
|
107
|
+
};
|
|
108
|
+
if (strict) {
|
|
109
|
+
validateSkillName(metadata.name, skillPath);
|
|
110
|
+
validateSkillDescription(metadata.description, skillPath);
|
|
111
|
+
validateCompatibility(metadata.compatibility, skillPath);
|
|
112
|
+
validateMetadata(metadata.metadata, skillPath);
|
|
113
|
+
}
|
|
114
|
+
return metadata;
|
|
115
|
+
}
|
|
116
|
+
export function readSkillMetadata(skillPath) {
|
|
117
|
+
const cached = skillMetadataCache.get(skillPath);
|
|
21
118
|
if (cached) {
|
|
22
119
|
return cached;
|
|
23
120
|
}
|
|
24
|
-
|
|
121
|
+
const metadata = {
|
|
122
|
+
name: path.basename(skillPath),
|
|
123
|
+
};
|
|
25
124
|
try {
|
|
26
125
|
const document = readFileSync(path.join(skillPath, "SKILL.md"), "utf8");
|
|
27
|
-
|
|
126
|
+
Object.assign(metadata, parseSkillMetadataFromDocument(document, skillPath, false));
|
|
28
127
|
}
|
|
29
128
|
catch {
|
|
30
129
|
// Fall back to the directory name when the skill doc cannot be read.
|
|
31
130
|
}
|
|
32
|
-
|
|
33
|
-
return
|
|
131
|
+
skillMetadataCache.set(skillPath, metadata);
|
|
132
|
+
return metadata;
|
|
133
|
+
}
|
|
134
|
+
export function validateSkillMetadata(skillPath) {
|
|
135
|
+
const cached = skillValidationCache.get(skillPath);
|
|
136
|
+
if (cached) {
|
|
137
|
+
return cached;
|
|
138
|
+
}
|
|
139
|
+
const document = readFileSync(path.join(skillPath, "SKILL.md"), "utf8");
|
|
140
|
+
const metadata = parseSkillMetadataFromDocument(document, skillPath, true);
|
|
141
|
+
skillValidationCache.set(skillPath, metadata);
|
|
142
|
+
skillMetadataCache.set(skillPath, metadata);
|
|
143
|
+
return metadata;
|
|
144
|
+
}
|
|
145
|
+
export function readSkillName(skillPath) {
|
|
146
|
+
return readSkillMetadata(skillPath).name;
|
|
34
147
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { defaultResourceConfigRoot, defaultResourceSkillsRoot } from "../../resource/resource.js";
|
|
4
|
+
import { validateSkillMetadata } from "../../runtime/support/skill-metadata.js";
|
|
4
5
|
import { ensureExternalResourceSource, isDirectoryPath, isExternalSourceLocator, resolveExternalResourcePath, resolveResourcePackageRoot, } from "../../resource/sources.js";
|
|
5
6
|
import { parseAgentItem, readYamlItems } from "../object-loader.js";
|
|
6
7
|
function resolveBuiltinPath(kind, ref) {
|
|
@@ -93,6 +94,7 @@ export function discoverSkillPaths(rootRefs, workspaceRoot) {
|
|
|
93
94
|
throw new Error(`Skill discovery root ${rootRef} does not exist`);
|
|
94
95
|
}
|
|
95
96
|
if (existsSync(path.join(root, "SKILL.md"))) {
|
|
97
|
+
validateSkillMetadata(root);
|
|
96
98
|
discovered.set(path.basename(root), root);
|
|
97
99
|
continue;
|
|
98
100
|
}
|
|
@@ -102,6 +104,7 @@ export function discoverSkillPaths(rootRefs, workspaceRoot) {
|
|
|
102
104
|
}
|
|
103
105
|
const skillRoot = path.join(root, entry.name);
|
|
104
106
|
if (existsSync(path.join(skillRoot, "SKILL.md"))) {
|
|
107
|
+
validateSkillMetadata(skillRoot);
|
|
105
108
|
discovered.set(entry.name, skillRoot);
|
|
106
109
|
}
|
|
107
110
|
}
|