agent-skillboard 0.1.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/CONTRIBUTING.md +83 -0
- package/LICENSE +21 -0
- package/README.md +645 -0
- package/bin/skillboard.mjs +4 -0
- package/docs/adapters.md +127 -0
- package/docs/capabilities.md +107 -0
- package/docs/install.md +346 -0
- package/docs/plans/20260625-080025-skillboard-mvp-review.md +189 -0
- package/docs/plans/README.md +20 -0
- package/docs/policy-model.md +251 -0
- package/docs/positioning.md +94 -0
- package/docs/profiles.md +166 -0
- package/docs/rollout-runbook.md +60 -0
- package/docs/user-flow.md +231 -0
- package/docs/versioning.md +201 -0
- package/examples/multi-source-skills/anthropic/docx/SKILL.md +8 -0
- package/examples/multi-source-skills/anthropic/skill-creator/SKILL.md +8 -0
- package/examples/multi-source-skills/matt/grill-me/SKILL.md +8 -0
- package/examples/multi-source-skills/matt/tdd/SKILL.md +8 -0
- package/examples/multi-source-skills/omo/review-work/SKILL.md +8 -0
- package/examples/multi-source-skills/omo/ulw-plan/SKILL.md +8 -0
- package/examples/multi-source-skills/private/tdd-work-continuity/SKILL.md +8 -0
- package/examples/multi-source-skills/private/workflow-router/SKILL.md +8 -0
- package/examples/multi-source-skills/wshobson/python-testing/SKILL.md +8 -0
- package/examples/multi-source-skills/wshobson/security-review/SKILL.md +8 -0
- package/examples/multi-source.config.yaml +271 -0
- package/examples/skillboard.config.yaml +194 -0
- package/examples/skills/grill-me/SKILL.md +9 -0
- package/examples/skills/grill-with-docs/SKILL.md +9 -0
- package/examples/skills/requirement-intake/SKILL.md +9 -0
- package/examples/skills/tdd/SKILL.md +8 -0
- package/package.json +58 -0
- package/profiles/anthropics-skills.yaml +17 -0
- package/profiles/mattpocock-skills.yaml +26 -0
- package/profiles/oh-my-openagent.yaml +32 -0
- package/profiles/voltagent-awesome-agent-skills.yaml +18 -0
- package/profiles/wshobson-agents.yaml +19 -0
- package/src/advisor/action-core.mjs +74 -0
- package/src/advisor/actions.mjs +256 -0
- package/src/advisor/application-commands.mjs +59 -0
- package/src/advisor/apply-action.mjs +183 -0
- package/src/advisor/schema.mjs +112 -0
- package/src/advisor/setup-actions.mjs +42 -0
- package/src/advisor/skills.mjs +191 -0
- package/src/advisor/sort.mjs +15 -0
- package/src/advisor/sources.mjs +160 -0
- package/src/advisor/trust-policy.mjs +65 -0
- package/src/advisor/workflow.mjs +41 -0
- package/src/advisor.mjs +134 -0
- package/src/agent-inventory-platforms.mjs +100 -0
- package/src/agent-inventory.mjs +804 -0
- package/src/brief-cli.mjs +40 -0
- package/src/brief-renderer.mjs +362 -0
- package/src/change-plan.mjs +169 -0
- package/src/cli.mjs +1171 -0
- package/src/config-helpers.mjs +72 -0
- package/src/control/can-use-guard.mjs +91 -0
- package/src/control/config-write.mjs +163 -0
- package/src/control/skill-crud.mjs +394 -0
- package/src/control/source-trust.mjs +161 -0
- package/src/control/workflow-crud.mjs +104 -0
- package/src/control.mjs +197 -0
- package/src/doctor.mjs +299 -0
- package/src/domain/constants.mjs +47 -0
- package/src/domain/indexes.mjs +29 -0
- package/src/domain/rules/capabilities.mjs +33 -0
- package/src/domain/rules/harnesses.mjs +16 -0
- package/src/domain/rules/install-units.mjs +95 -0
- package/src/domain/rules/skills.mjs +105 -0
- package/src/domain/rules/workflows.mjs +105 -0
- package/src/domain/skill-state-matrix.mjs +79 -0
- package/src/domain/source-classes.mjs +99 -0
- package/src/hook-plan.mjs +152 -0
- package/src/impact.mjs +41 -0
- package/src/index.mjs +82 -0
- package/src/init.mjs +136 -0
- package/src/install-output-detector.mjs +337 -0
- package/src/install-units.mjs +62 -0
- package/src/inventory-refresh.mjs +45 -0
- package/src/lifecycle-cli.mjs +166 -0
- package/src/lifecycle-content.mjs +87 -0
- package/src/policy.mjs +42 -0
- package/src/reconcile.mjs +111 -0
- package/src/report.mjs +151 -0
- package/src/review.mjs +88 -0
- package/src/rollout.mjs +453 -0
- package/src/skill-paths.mjs +17 -0
- package/src/source-cache.mjs +178 -0
- package/src/source-profile-loader.mjs +160 -0
- package/src/source-profiles.mjs +299 -0
- package/src/source-verification.mjs +252 -0
- package/src/uninstall.mjs +258 -0
- package/src/workspace.mjs +219 -0
- package/tsconfig.lsp.json +15 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import YAML from "yaml";
|
|
2
|
+
import {
|
|
3
|
+
EXPOSURE_VALUES,
|
|
4
|
+
INVOCATION_VALUES,
|
|
5
|
+
NON_CALLABLE_WORKFLOW_INVOCATIONS,
|
|
6
|
+
NON_CALLABLE_WORKFLOW_STATUSES,
|
|
7
|
+
STATUS_VALUES
|
|
8
|
+
} from "../domain/constants.mjs";
|
|
9
|
+
import { normalizeSkillPath } from "../skill-paths.mjs";
|
|
10
|
+
import {
|
|
11
|
+
addUnique,
|
|
12
|
+
ensureMapAt,
|
|
13
|
+
ensureSeq,
|
|
14
|
+
loadConfig,
|
|
15
|
+
mapValues,
|
|
16
|
+
nodeScalarValue,
|
|
17
|
+
optionalMap,
|
|
18
|
+
optionalRootMap,
|
|
19
|
+
optionalSeq,
|
|
20
|
+
readMapString,
|
|
21
|
+
removeValue,
|
|
22
|
+
requireMapAt,
|
|
23
|
+
requireYamlMap,
|
|
24
|
+
sequenceIncludes,
|
|
25
|
+
uniqueValues,
|
|
26
|
+
writeCheckedConfig
|
|
27
|
+
} from "./config-write.mjs";
|
|
28
|
+
import { canUseSkill } from "./can-use-guard.mjs";
|
|
29
|
+
|
|
30
|
+
const WRITABLE_INVOCATIONS = new Set(["manual-only", "router-only", "workflow-auto", "global-auto"]);
|
|
31
|
+
|
|
32
|
+
export async function activateSkill(options) {
|
|
33
|
+
const { document, originalText } = await loadConfig(options.configPath);
|
|
34
|
+
const skill = requireConfigSkill(document, options.skillId);
|
|
35
|
+
const workflow = requireConfigWorkflow(document, options.workflow);
|
|
36
|
+
const mode = options.mode ?? readMapString(skill, "invocation", "manual-only");
|
|
37
|
+
|
|
38
|
+
if (!WRITABLE_INVOCATIONS.has(mode) || mode === "global-auto") {
|
|
39
|
+
throw new Error(`activate requires --mode manual-only, router-only, or workflow-auto; got ${mode}`);
|
|
40
|
+
}
|
|
41
|
+
skill.set("status", "active");
|
|
42
|
+
skill.set("invocation", mode);
|
|
43
|
+
addUnique(ensureSeq(workflow, "active_skills", document), options.skillId);
|
|
44
|
+
removeValue(ensureSeq(workflow, "blocked_skills", document), options.skillId);
|
|
45
|
+
|
|
46
|
+
return await writeCheckedConfig(
|
|
47
|
+
document,
|
|
48
|
+
originalText,
|
|
49
|
+
{ ...options, validateUse: { skillId: options.skillId, workflow: options.workflow } },
|
|
50
|
+
`Activated ${options.skillId} in ${options.workflow} as ${mode}`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function addSkill(options) {
|
|
55
|
+
const { document, originalText } = await loadConfig(options.configPath);
|
|
56
|
+
const skills = ensureMapAt(document, ["skills"], "skills");
|
|
57
|
+
if (skills.get(options.skillId, true) !== undefined) {
|
|
58
|
+
throw new Error(`Skill already exists: ${options.skillId}`);
|
|
59
|
+
}
|
|
60
|
+
const skillPath = normalizeSkillPath(options.path, "skill path");
|
|
61
|
+
const status = options.status ?? (options.workflow === undefined ? "candidate" : "active");
|
|
62
|
+
const invocation = options.invocation ?? "manual-only";
|
|
63
|
+
const exposure = options.exposure ?? "exported";
|
|
64
|
+
validateSkillState(status, invocation, exposure);
|
|
65
|
+
skills.set(options.skillId, document.createNode(stripUndefined({
|
|
66
|
+
path: skillPath,
|
|
67
|
+
status,
|
|
68
|
+
invocation,
|
|
69
|
+
exposure,
|
|
70
|
+
category: options.category ?? "user",
|
|
71
|
+
owner_install_unit: options.ownerInstallUnit
|
|
72
|
+
})));
|
|
73
|
+
if (options.workflow !== undefined) {
|
|
74
|
+
const workflow = requireConfigWorkflow(document, options.workflow);
|
|
75
|
+
addUnique(ensureSeq(workflow, "active_skills", document), options.skillId);
|
|
76
|
+
removeValue(ensureSeq(workflow, "blocked_skills", document), options.skillId);
|
|
77
|
+
}
|
|
78
|
+
const validation = options.workflow === undefined
|
|
79
|
+
? options
|
|
80
|
+
: { ...options, validateUse: { skillId: options.skillId, workflow: options.workflow } };
|
|
81
|
+
return await writeCheckedConfig(document, originalText, validation, `Added ${options.skillId}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function blockSkill(options) {
|
|
85
|
+
const { document, originalText } = await loadConfig(options.configPath);
|
|
86
|
+
requireConfigSkill(document, options.skillId);
|
|
87
|
+
const workflow = requireConfigWorkflow(document, options.workflow);
|
|
88
|
+
|
|
89
|
+
addUnique(ensureSeq(workflow, "blocked_skills", document), options.skillId);
|
|
90
|
+
removeValue(ensureSeq(workflow, "active_skills", document), options.skillId);
|
|
91
|
+
removeSkillFromRequiredCapabilities(workflow, options.skillId, document);
|
|
92
|
+
downgradeIfUnscopedWorkflowAuto(document, options.skillId);
|
|
93
|
+
|
|
94
|
+
return await writeCheckedConfig(document, originalText, options, `Blocked ${options.skillId} in ${options.workflow}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function quarantineSkill(options) {
|
|
98
|
+
const { document, originalText } = await loadConfig(options.configPath);
|
|
99
|
+
const skill = requireConfigSkill(document, options.skillId);
|
|
100
|
+
|
|
101
|
+
skill.set("status", "quarantined");
|
|
102
|
+
skill.set("invocation", "blocked");
|
|
103
|
+
const workflows = requireMapAt(document, ["workflows"], "workflows");
|
|
104
|
+
for (const workflow of mapValues(workflows, "workflow")) {
|
|
105
|
+
removeValue(ensureSeq(workflow, "active_skills", document), options.skillId);
|
|
106
|
+
removeSkillFromRequiredCapabilities(workflow, options.skillId, document);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return await writeCheckedConfig(document, originalText, options, `Quarantined ${options.skillId}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function removeSkill(options) {
|
|
113
|
+
const { document, originalText } = await loadConfig(options.configPath);
|
|
114
|
+
const skills = requireMapAt(document, ["skills"], "skills");
|
|
115
|
+
requireConfigSkill(document, options.skillId);
|
|
116
|
+
const references = configSkillReferences(document, options.skillId);
|
|
117
|
+
if (references.length > 0 && options.force !== true) {
|
|
118
|
+
throw new Error(`Skill ${options.skillId} is still referenced: ${references.join(", ")}. Re-run with --force to remove config references first.`);
|
|
119
|
+
}
|
|
120
|
+
removeSkillReferences(document, options.skillId);
|
|
121
|
+
skills.delete(options.skillId);
|
|
122
|
+
return await writeCheckedConfig(document, originalText, options, `Removed ${options.skillId}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function preferSkill(options) {
|
|
126
|
+
const { document, originalText } = await loadConfig(options.configPath);
|
|
127
|
+
const skill = requireConfigSkill(document, options.skillId);
|
|
128
|
+
const workflow = requireConfigWorkflow(document, options.workflow);
|
|
129
|
+
const capabilities = requireMapAt(document, ["capabilities"], "capabilities");
|
|
130
|
+
const capabilityDefinition = capabilities.get(options.capability, true);
|
|
131
|
+
if (capabilityDefinition === undefined) {
|
|
132
|
+
throw new Error(`Unknown capability: ${options.capability}`);
|
|
133
|
+
}
|
|
134
|
+
const required = ensureRequiredCapability(workflow, options.capability, document);
|
|
135
|
+
const previousPreferred = readMapString(required, "preferred", "");
|
|
136
|
+
if (previousPreferred.length > 0 && previousPreferred !== options.skillId) {
|
|
137
|
+
addUnique(ensureSeq(required, "fallback", document), previousPreferred);
|
|
138
|
+
}
|
|
139
|
+
required.set("preferred", options.skillId);
|
|
140
|
+
removeValue(ensureSeq(required, "fallback", document), options.skillId);
|
|
141
|
+
addUnique(ensureSeq(workflow, "active_skills", document), options.skillId);
|
|
142
|
+
removeValue(ensureSeq(workflow, "blocked_skills", document), options.skillId);
|
|
143
|
+
const status = readMapString(skill, "status", "vendor");
|
|
144
|
+
const invocation = readMapString(skill, "invocation", "manual-only");
|
|
145
|
+
if (status === "quarantined" || status === "blocked") {
|
|
146
|
+
skill.set("status", "active");
|
|
147
|
+
}
|
|
148
|
+
if (invocation === "blocked" || invocation === "deprecated") {
|
|
149
|
+
const requiredPolicy = readMapString(required, "policy", "");
|
|
150
|
+
const defaultPolicy = YAML.isMap(capabilityDefinition) ? readMapString(capabilityDefinition, "default_policy", "manual-only") : "manual-only";
|
|
151
|
+
skill.set("invocation", requiredPolicy.length > 0 ? requiredPolicy : defaultPolicy);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return await writeCheckedConfig(
|
|
155
|
+
document,
|
|
156
|
+
originalText,
|
|
157
|
+
{ ...options, validateUse: { skillId: options.skillId, workflow: options.workflow } },
|
|
158
|
+
`Preferred ${options.skillId} for ${options.capability} in ${options.workflow}`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function requireConfigSkill(document, skillId) {
|
|
163
|
+
const skills = requireMapAt(document, ["skills"], "skills");
|
|
164
|
+
const skill = skills.get(skillId, true);
|
|
165
|
+
if (skill === undefined) {
|
|
166
|
+
throw new Error(`Unknown skill: ${skillId}`);
|
|
167
|
+
}
|
|
168
|
+
return requireYamlMap(skill, `skills.${skillId}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function requireConfigWorkflow(document, workflowName) {
|
|
172
|
+
const workflows = requireMapAt(document, ["workflows"], "workflows");
|
|
173
|
+
const workflow = workflows.get(workflowName, true);
|
|
174
|
+
if (workflow === undefined) {
|
|
175
|
+
throw new Error(`Unknown workflow: ${workflowName}`);
|
|
176
|
+
}
|
|
177
|
+
const raw = requireYamlMap(workflow, `workflows.${workflowName}`);
|
|
178
|
+
ensureSeq(raw, "active_skills", document);
|
|
179
|
+
ensureSeq(raw, "blocked_skills", document);
|
|
180
|
+
const requiredCapabilities = raw.get("required_capabilities", true);
|
|
181
|
+
if (requiredCapabilities !== undefined) {
|
|
182
|
+
requireYamlMap(requiredCapabilities, `workflows.${workflowName}.required_capabilities`);
|
|
183
|
+
}
|
|
184
|
+
return raw;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function ensureRequiredCapability(workflow, capabilityName, document) {
|
|
188
|
+
let capabilities = workflow.get("required_capabilities", true);
|
|
189
|
+
if (capabilities === undefined) {
|
|
190
|
+
capabilities = document.createNode({});
|
|
191
|
+
workflow.set("required_capabilities", capabilities);
|
|
192
|
+
}
|
|
193
|
+
const capabilityMap = requireYamlMap(capabilities, "required_capabilities");
|
|
194
|
+
if (capabilityMap.get(capabilityName, true) === undefined) {
|
|
195
|
+
capabilityMap.set(capabilityName, document.createNode({
|
|
196
|
+
preferred: "",
|
|
197
|
+
fallback: [],
|
|
198
|
+
policy: "manual-only"
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
const capability = requireYamlMap(capabilityMap.get(capabilityName, true), `required_capabilities.${capabilityName}`);
|
|
202
|
+
ensureSeq(capability, "fallback", document);
|
|
203
|
+
return capability;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function removeSkillFromRequiredCapabilities(workflow, skillId, document) {
|
|
207
|
+
const capabilities = optionalMap(workflow, "required_capabilities");
|
|
208
|
+
if (capabilities === undefined) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
for (const capability of mapValues(capabilities, "required_capability")) {
|
|
212
|
+
if (readMapString(capability, "preferred", "") === skillId) {
|
|
213
|
+
capability.set("preferred", "");
|
|
214
|
+
}
|
|
215
|
+
removeValue(ensureSeq(capability, "fallback", document), skillId);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function configSkillReferences(document, skillId) {
|
|
220
|
+
return [
|
|
221
|
+
...workflowReferences(document, skillId),
|
|
222
|
+
...capabilityReferences(document, skillId),
|
|
223
|
+
...installUnitReferences(document, skillId)
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function workflowReferences(document, skillId) {
|
|
228
|
+
const workflows = optionalRootMap(document, "workflows");
|
|
229
|
+
if (workflows === undefined) {
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
const references = [];
|
|
233
|
+
for (const pair of workflows.items) {
|
|
234
|
+
const name = nodeScalarValue(pair.key);
|
|
235
|
+
const workflow = requireYamlMap(pair.value, `workflows.${name}`);
|
|
236
|
+
const activeSkills = optionalSeq(workflow, "active_skills");
|
|
237
|
+
const blockedSkills = optionalSeq(workflow, "blocked_skills");
|
|
238
|
+
if (activeSkills !== undefined && sequenceIncludes(activeSkills, skillId)) {
|
|
239
|
+
references.push(`workflow ${name}.active_skills`);
|
|
240
|
+
}
|
|
241
|
+
if (blockedSkills !== undefined && sequenceIncludes(blockedSkills, skillId)) {
|
|
242
|
+
references.push(`workflow ${name}.blocked_skills`);
|
|
243
|
+
}
|
|
244
|
+
const requiredCapabilities = optionalMap(workflow, "required_capabilities");
|
|
245
|
+
if (requiredCapabilities === undefined) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
for (const capabilityPair of requiredCapabilities.items) {
|
|
249
|
+
const capabilityName = nodeScalarValue(capabilityPair.key);
|
|
250
|
+
const requirement = requireYamlMap(capabilityPair.value, `workflows.${name}.required_capabilities.${capabilityName}`);
|
|
251
|
+
if (readMapString(requirement, "preferred", "") === skillId) {
|
|
252
|
+
references.push(`workflow ${name}.required_capabilities.${capabilityName}.preferred`);
|
|
253
|
+
}
|
|
254
|
+
const fallback = optionalSeq(requirement, "fallback");
|
|
255
|
+
if (fallback !== undefined && sequenceIncludes(fallback, skillId)) {
|
|
256
|
+
references.push(`workflow ${name}.required_capabilities.${capabilityName}.fallback`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return references;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function capabilityReferences(document, skillId) {
|
|
264
|
+
const capabilities = optionalRootMap(document, "capabilities");
|
|
265
|
+
if (capabilities === undefined) {
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
268
|
+
const references = [];
|
|
269
|
+
for (const pair of capabilities.items) {
|
|
270
|
+
const name = nodeScalarValue(pair.key);
|
|
271
|
+
const capability = requireYamlMap(pair.value, `capabilities.${name}`);
|
|
272
|
+
if (readMapString(capability, "canonical", "") === skillId) {
|
|
273
|
+
references.push(`capability ${name}.canonical`);
|
|
274
|
+
}
|
|
275
|
+
const alternatives = optionalSeq(capability, "alternatives");
|
|
276
|
+
if (alternatives !== undefined && sequenceIncludes(alternatives, skillId)) {
|
|
277
|
+
references.push(`capability ${name}.alternatives`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return references;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function installUnitReferences(document, skillId) {
|
|
284
|
+
const installUnits = optionalRootMap(document, "install_units");
|
|
285
|
+
if (installUnits === undefined) {
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
const references = [];
|
|
289
|
+
for (const pair of installUnits.items) {
|
|
290
|
+
const id = nodeScalarValue(pair.key);
|
|
291
|
+
const unit = requireYamlMap(pair.value, `install_units.${id}`);
|
|
292
|
+
const components = optionalMap(unit, "components");
|
|
293
|
+
const skills = components === undefined ? undefined : optionalSeq(components, "skills");
|
|
294
|
+
if (skills !== undefined && sequenceIncludes(skills, skillId)) {
|
|
295
|
+
references.push(`install_unit ${id}.components.skills`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return references;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function removeSkillReferences(document, skillId) {
|
|
302
|
+
const workflows = optionalRootMap(document, "workflows");
|
|
303
|
+
if (workflows !== undefined) {
|
|
304
|
+
for (const workflow of mapValues(workflows, "workflow")) {
|
|
305
|
+
const activeSkills = optionalSeq(workflow, "active_skills");
|
|
306
|
+
const blockedSkills = optionalSeq(workflow, "blocked_skills");
|
|
307
|
+
if (activeSkills !== undefined) {
|
|
308
|
+
removeValue(activeSkills, skillId);
|
|
309
|
+
}
|
|
310
|
+
if (blockedSkills !== undefined) {
|
|
311
|
+
removeValue(blockedSkills, skillId);
|
|
312
|
+
}
|
|
313
|
+
removeSkillFromRequiredCapabilities(workflow, skillId, document);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const capabilities = optionalRootMap(document, "capabilities");
|
|
317
|
+
if (capabilities !== undefined) {
|
|
318
|
+
for (const capability of mapValues(capabilities, "capability")) {
|
|
319
|
+
if (readMapString(capability, "canonical", "") === skillId) {
|
|
320
|
+
capability.set("canonical", "");
|
|
321
|
+
}
|
|
322
|
+
const alternatives = optionalSeq(capability, "alternatives");
|
|
323
|
+
if (alternatives !== undefined) {
|
|
324
|
+
removeValue(alternatives, skillId);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const installUnits = optionalRootMap(document, "install_units");
|
|
329
|
+
if (installUnits !== undefined) {
|
|
330
|
+
for (const unit of mapValues(installUnits, "install_unit")) {
|
|
331
|
+
const components = optionalMap(unit, "components");
|
|
332
|
+
const skills = components === undefined ? undefined : optionalSeq(components, "skills");
|
|
333
|
+
if (skills !== undefined) {
|
|
334
|
+
removeValue(skills, skillId);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function validateSkillState(status, invocation, exposure) {
|
|
341
|
+
if (!STATUS_VALUES.has(status)) {
|
|
342
|
+
throw new Error(`Unsupported status: ${status}`);
|
|
343
|
+
}
|
|
344
|
+
if (!INVOCATION_VALUES.has(invocation)) {
|
|
345
|
+
throw new Error(`Unsupported invocation: ${invocation}`);
|
|
346
|
+
}
|
|
347
|
+
if (!EXPOSURE_VALUES.has(exposure)) {
|
|
348
|
+
throw new Error(`Unsupported exposure: ${exposure}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function ensureCallableWorkflowSkill(skillId, skill) {
|
|
353
|
+
const status = readMapString(skill, "status", "vendor");
|
|
354
|
+
const invocation = readMapString(skill, "invocation", "manual-only");
|
|
355
|
+
if (NON_CALLABLE_WORKFLOW_STATUSES.has(status)) {
|
|
356
|
+
throw new Error(`Cannot attach non-callable skill ${skillId} with status: ${status}`);
|
|
357
|
+
}
|
|
358
|
+
if (NON_CALLABLE_WORKFLOW_INVOCATIONS.has(invocation)) {
|
|
359
|
+
throw new Error(`Cannot attach non-callable skill ${skillId} with invocation: ${invocation}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function stripUndefined(value) {
|
|
364
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function downgradeIfUnscopedWorkflowAuto(document, skillId) {
|
|
368
|
+
const skill = requireConfigSkill(document, skillId);
|
|
369
|
+
if (readMapString(skill, "invocation", "") !== "workflow-auto") {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const workflows = requireMapAt(document, ["workflows"], "workflows");
|
|
373
|
+
for (const raw of mapValues(workflows, "workflow")) {
|
|
374
|
+
const activeSkills = optionalSeq(raw, "active_skills");
|
|
375
|
+
if (activeSkills !== undefined && sequenceIncludes(activeSkills, skillId)) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const capabilities = optionalMap(raw, "required_capabilities");
|
|
379
|
+
if (capabilities === undefined) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
for (const requirement of mapValues(capabilities, "required_capability")) {
|
|
383
|
+
const fallback = optionalSeq(requirement, "fallback");
|
|
384
|
+
if (readMapString(requirement, "preferred", "") === skillId || (fallback !== undefined && sequenceIncludes(fallback, skillId))) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
skill.set("invocation", "manual-only");
|
|
390
|
+
const status = readMapString(skill, "status", "");
|
|
391
|
+
if (status === "active" || status === "active-auto") {
|
|
392
|
+
skill.set("status", "candidate");
|
|
393
|
+
}
|
|
394
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasRuntimeComponents,
|
|
3
|
+
installUnitPriority,
|
|
4
|
+
installUnitSourceClass,
|
|
5
|
+
isModelSelectableInvocation,
|
|
6
|
+
isUserControlledSource
|
|
7
|
+
} from "../domain/source-classes.mjs";
|
|
8
|
+
|
|
9
|
+
export function classifySkillSource(workspace, skill) {
|
|
10
|
+
const unit = skill.ownerInstallUnit === undefined
|
|
11
|
+
? undefined
|
|
12
|
+
: workspace.installUnits.find((candidate) => candidate.id === skill.ownerInstallUnit);
|
|
13
|
+
if (unit === undefined) {
|
|
14
|
+
return {
|
|
15
|
+
class: "user",
|
|
16
|
+
priority: 100,
|
|
17
|
+
ownerInstallUnit: skill.ownerInstallUnit ?? null,
|
|
18
|
+
detail: "declared directly in the workspace policy"
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const sourceClass = installUnitSourceClass(unit);
|
|
22
|
+
const priority = installUnitPriority(unit);
|
|
23
|
+
return {
|
|
24
|
+
class: sourceClass,
|
|
25
|
+
priority,
|
|
26
|
+
ownerInstallUnit: unit.id,
|
|
27
|
+
detail: unit.source || unit.kind
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function classifySkillTrust(workspace, skill) {
|
|
32
|
+
const unit = skill.ownerInstallUnit === undefined
|
|
33
|
+
? undefined
|
|
34
|
+
: workspace.installUnits.find((candidate) => candidate.id === skill.ownerInstallUnit);
|
|
35
|
+
if (unit === undefined) {
|
|
36
|
+
return {
|
|
37
|
+
level: "trusted",
|
|
38
|
+
reviewed: true,
|
|
39
|
+
signed: false,
|
|
40
|
+
pinned: false,
|
|
41
|
+
ownerInstallUnit: skill.ownerInstallUnit ?? null,
|
|
42
|
+
reason: "declared directly in workspace policy"
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const sourceClass = installUnitSourceClass(unit);
|
|
46
|
+
const level = unit.trustLevel ?? (isUserControlledSource(unit) ? "trusted" : "unreviewed");
|
|
47
|
+
return {
|
|
48
|
+
level,
|
|
49
|
+
reviewed: level === "trusted" || level === "reviewed",
|
|
50
|
+
signed: unit.signature !== undefined && unit.signature.length > 0,
|
|
51
|
+
pinned: unit.sourceDigest !== undefined && unit.sourceDigest.length > 0,
|
|
52
|
+
verifiedAt: unit.verifiedAt ?? null,
|
|
53
|
+
ownerInstallUnit: unit.id,
|
|
54
|
+
sourceClass,
|
|
55
|
+
permissionRisk: unit.permissionRisk
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function auditSources(workspace) {
|
|
60
|
+
const skillsByOwner = new Map();
|
|
61
|
+
for (const skill of workspace.skills) {
|
|
62
|
+
if (skill.ownerInstallUnit === undefined) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const skills = skillsByOwner.get(skill.ownerInstallUnit) ?? [];
|
|
66
|
+
skills.push(skill);
|
|
67
|
+
skillsByOwner.set(skill.ownerInstallUnit, skills);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const units = workspace.installUnits.map((unit) => {
|
|
71
|
+
const sourceClass = installUnitSourceClass(unit);
|
|
72
|
+
const ownedSkills = skillsByOwner.get(unit.id) ?? [];
|
|
73
|
+
const automaticSkills = ownedSkills
|
|
74
|
+
.filter((skill) => isModelSelectableInvocation(skill.invocation))
|
|
75
|
+
.map((skill) => skill.id)
|
|
76
|
+
.sort((left, right) => left.localeCompare(right));
|
|
77
|
+
const findings = sourceAuditFindings(unit, sourceClass, automaticSkills);
|
|
78
|
+
return {
|
|
79
|
+
id: unit.id,
|
|
80
|
+
kind: unit.kind,
|
|
81
|
+
sourceClass,
|
|
82
|
+
enabled: unit.enabled,
|
|
83
|
+
trustLevel: unit.trustLevel,
|
|
84
|
+
permissionRisk: unit.permissionRisk,
|
|
85
|
+
signed: unit.signature !== undefined && unit.signature.length > 0,
|
|
86
|
+
pinned: unit.sourceDigest !== undefined && unit.sourceDigest.length > 0,
|
|
87
|
+
verifiedAt: unit.verifiedAt ?? null,
|
|
88
|
+
automaticSkills,
|
|
89
|
+
findings
|
|
90
|
+
};
|
|
91
|
+
}).sort((left, right) => left.id.localeCompare(right.id));
|
|
92
|
+
const errors = units.flatMap((unit) => unit.findings.filter((finding) => finding.severity === "error").map((finding) => `${unit.id}: ${finding.message}`));
|
|
93
|
+
const warnings = units.flatMap((unit) => unit.findings.filter((finding) => finding.severity === "warning").map((finding) => `${unit.id}: ${finding.message}`));
|
|
94
|
+
return {
|
|
95
|
+
ok: errors.length === 0,
|
|
96
|
+
units,
|
|
97
|
+
errors,
|
|
98
|
+
warnings
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function workflowSkillRole(workflow, skillId) {
|
|
103
|
+
const roles = [];
|
|
104
|
+
const capabilityRoles = [];
|
|
105
|
+
if (workflow.activeSkills.includes(skillId)) {
|
|
106
|
+
roles.push("active");
|
|
107
|
+
}
|
|
108
|
+
if (workflow.blockedSkills.includes(skillId)) {
|
|
109
|
+
roles.push("blocked");
|
|
110
|
+
}
|
|
111
|
+
for (const capability of workflow.requiredCapabilities) {
|
|
112
|
+
if (capability.preferred === skillId) {
|
|
113
|
+
roles.push("preferred");
|
|
114
|
+
capabilityRoles.push({ capability: capability.name, role: "preferred", policy: capability.policy });
|
|
115
|
+
}
|
|
116
|
+
if (capability.fallback.includes(skillId)) {
|
|
117
|
+
roles.push("fallback");
|
|
118
|
+
capabilityRoles.push({ capability: capability.name, role: "fallback", policy: capability.policy });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { workflow: workflow.name, roles: [...new Set(roles)], capabilityRoles };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function skillSummary(workspace, skill, workflow) {
|
|
125
|
+
const source = classifySkillSource(workspace, skill);
|
|
126
|
+
return {
|
|
127
|
+
id: skill.id,
|
|
128
|
+
path: skill.path,
|
|
129
|
+
status: skill.status,
|
|
130
|
+
invocation: skill.invocation,
|
|
131
|
+
exposure: skill.exposure,
|
|
132
|
+
category: skill.category,
|
|
133
|
+
sourceClass: source.class,
|
|
134
|
+
sourcePriority: source.priority,
|
|
135
|
+
ownerInstallUnit: source.ownerInstallUnit,
|
|
136
|
+
workflowRoles: workflow === undefined ? [] : workflowSkillRole(workflow, skill.id).roles
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function sourceAuditFindings(unit, sourceClass, automaticSkills) {
|
|
141
|
+
const findings = [];
|
|
142
|
+
if (unit.enabled && unit.trustLevel === "blocked") {
|
|
143
|
+
findings.push({ severity: "error", message: "enabled install unit is trust-blocked" });
|
|
144
|
+
}
|
|
145
|
+
if (!unit.enabled && automaticSkills.length > 0) {
|
|
146
|
+
findings.push({ severity: "warning", message: `disabled source owns model-selectable skills: ${automaticSkills.join(", ")}` });
|
|
147
|
+
}
|
|
148
|
+
if (unit.enabled && !isUserControlledSource(unit) && unit.trustLevel === "unreviewed" && automaticSkills.length > 0) {
|
|
149
|
+
findings.push({ severity: "error", message: `unreviewed source owns model-selectable skills: ${automaticSkills.join(", ")}` });
|
|
150
|
+
}
|
|
151
|
+
if (unit.enabled && unit.permissionRisk === "high" && !["trusted", "reviewed"].includes(unit.trustLevel)) {
|
|
152
|
+
findings.push({ severity: "warning", message: "high-risk source is not reviewed or trusted" });
|
|
153
|
+
}
|
|
154
|
+
if (unit.enabled && hasRuntimeComponents(unit) && unit.trustLevel === "unreviewed") {
|
|
155
|
+
findings.push({ severity: "warning", message: "runtime extension source is unreviewed" });
|
|
156
|
+
}
|
|
157
|
+
if (unit.enabled && sourceClass !== "user" && unit.permissionRisk !== "low" && unit.sourceDigest === undefined && unit.signature === undefined) {
|
|
158
|
+
findings.push({ severity: "warning", message: "source is not pinned by digest or signature" });
|
|
159
|
+
}
|
|
160
|
+
return findings;
|
|
161
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { HARNESS_STATUS_VALUES } from "../domain/constants.mjs";
|
|
2
|
+
import {
|
|
3
|
+
addUnique,
|
|
4
|
+
ensureMapAt,
|
|
5
|
+
ensureSeq,
|
|
6
|
+
loadConfig,
|
|
7
|
+
readMapString,
|
|
8
|
+
removeValue,
|
|
9
|
+
requireMapAt,
|
|
10
|
+
requireYamlMap,
|
|
11
|
+
uniqueValues,
|
|
12
|
+
writeCheckedConfig
|
|
13
|
+
} from "./config-write.mjs";
|
|
14
|
+
import { ensureCallableWorkflowSkill } from "./skill-crud.mjs";
|
|
15
|
+
|
|
16
|
+
export async function addHarness(options) {
|
|
17
|
+
if (options.harness === undefined) {
|
|
18
|
+
throw new Error("addHarness requires a harness name");
|
|
19
|
+
}
|
|
20
|
+
const { document, originalText } = await loadConfig(options.configPath);
|
|
21
|
+
const harnesses = ensureMapAt(document, ["harnesses"], "harnesses");
|
|
22
|
+
if (harnesses.get(options.harness, true) !== undefined) {
|
|
23
|
+
throw new Error(`Harness already exists: ${options.harness}`);
|
|
24
|
+
}
|
|
25
|
+
const status = options.status ?? "configured";
|
|
26
|
+
validateHarnessStatus(status);
|
|
27
|
+
harnesses.set(options.harness, document.createNode({
|
|
28
|
+
status,
|
|
29
|
+
workflows: [],
|
|
30
|
+
commands: options.commands ?? []
|
|
31
|
+
}));
|
|
32
|
+
return await writeCheckedConfig(document, originalText, options, `Added harness ${options.harness}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function addWorkflow(options) {
|
|
36
|
+
if (options.workflow === undefined) {
|
|
37
|
+
throw new Error("addWorkflow requires a workflow name");
|
|
38
|
+
}
|
|
39
|
+
if (options.harness === undefined) {
|
|
40
|
+
throw new Error("addWorkflow requires a harness name");
|
|
41
|
+
}
|
|
42
|
+
const { document, originalText } = await loadConfig(options.configPath);
|
|
43
|
+
const workflows = ensureMapAt(document, ["workflows"], "workflows");
|
|
44
|
+
const harnesses = ensureMapAt(document, ["harnesses"], "harnesses");
|
|
45
|
+
if (workflows.get(options.workflow, true) !== undefined) {
|
|
46
|
+
throw new Error(`Workflow already exists: ${options.workflow}`);
|
|
47
|
+
}
|
|
48
|
+
const skillIds = uniqueValues(options.skills ?? []);
|
|
49
|
+
const activeSkills = [];
|
|
50
|
+
const validateUses = [];
|
|
51
|
+
for (const skillId of skillIds) {
|
|
52
|
+
const skill = requireConfigSkill(document, skillId);
|
|
53
|
+
ensureCallableWorkflowSkill(skillId, skill);
|
|
54
|
+
if (readMapString(skill, "status", "vendor") === "candidate" && readMapString(skill, "invocation", "manual-only") === "manual-only") {
|
|
55
|
+
skill.set("status", "active");
|
|
56
|
+
}
|
|
57
|
+
activeSkills.push(skillId);
|
|
58
|
+
validateUses.push({ skillId, workflow: options.workflow });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const harness = harnesses.get(options.harness, true);
|
|
62
|
+
if (harness === undefined) {
|
|
63
|
+
if (options.requireExistingHarness === true) {
|
|
64
|
+
throw new Error(`Unknown harness: ${options.harness}`);
|
|
65
|
+
}
|
|
66
|
+
const harnessStatus = options.harnessStatus ?? "configured";
|
|
67
|
+
validateHarnessStatus(harnessStatus);
|
|
68
|
+
harnesses.set(options.harness, document.createNode({
|
|
69
|
+
status: harnessStatus,
|
|
70
|
+
workflows: [options.workflow],
|
|
71
|
+
commands: []
|
|
72
|
+
}));
|
|
73
|
+
} else {
|
|
74
|
+
addUnique(ensureSeq(requireYamlMap(harness, `harnesses.${options.harness}`), "workflows", document), options.workflow);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
workflows.set(options.workflow, document.createNode({
|
|
78
|
+
harness: options.harness,
|
|
79
|
+
active_skills: activeSkills,
|
|
80
|
+
blocked_skills: []
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
return await writeCheckedConfig(
|
|
84
|
+
document,
|
|
85
|
+
originalText,
|
|
86
|
+
{ ...options, validateUses },
|
|
87
|
+
`Added workflow ${options.workflow}`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function requireConfigSkill(document, skillId) {
|
|
92
|
+
const skills = requireMapAt(document, ["skills"], "skills");
|
|
93
|
+
const skill = skills.get(skillId, true);
|
|
94
|
+
if (skill === undefined) {
|
|
95
|
+
throw new Error(`Unknown skill: ${skillId}`);
|
|
96
|
+
}
|
|
97
|
+
return requireYamlMap(skill, `skills.${skillId}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function validateHarnessStatus(status) {
|
|
101
|
+
if (!HARNESS_STATUS_VALUES.has(status)) {
|
|
102
|
+
throw new Error(`Unsupported harness status: ${status}`);
|
|
103
|
+
}
|
|
104
|
+
}
|