guardskills 1.0.0 → 1.2.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 +125 -69
- package/dist/cli.cjs +1368 -190
- package/dist/cli.js +1368 -190
- package/package.json +3 -1
package/dist/cli.js
CHANGED
|
@@ -176,9 +176,13 @@ function enforceSourcePolicy(repoInput, policy) {
|
|
|
176
176
|
|
|
177
177
|
// src/install/skills.ts
|
|
178
178
|
import { execa } from "execa";
|
|
179
|
-
async function
|
|
179
|
+
async function runProviderInstall(provider, repo, skill) {
|
|
180
|
+
if (provider === "playbooks" && !skill) {
|
|
181
|
+
return 30;
|
|
182
|
+
}
|
|
183
|
+
const args = provider === "skills" ? skill ? ["skills", "add", repo, "--skill", skill] : ["skills", "add", repo] : provider === "playbooks" ? ["playbooks", "add", "skill", repo, "--skill", skill] : provider === "skillkit" ? skill ? ["skillkit", "install", repo, "--skill", skill] : ["skillkit", "install", repo] : skill ? ["openskills", "install", repo, skill] : ["openskills", "install", repo];
|
|
180
184
|
try {
|
|
181
|
-
await execa("npx",
|
|
185
|
+
await execa("npx", args, {
|
|
182
186
|
stdio: "inherit"
|
|
183
187
|
});
|
|
184
188
|
return 0;
|
|
@@ -284,6 +288,73 @@ function printHumanLocalReport(report) {
|
|
|
284
288
|
function printJsonLocalReport(report) {
|
|
285
289
|
console.log(JSON.stringify(report, null, 2));
|
|
286
290
|
}
|
|
291
|
+
function printHumanClawHubReport(report) {
|
|
292
|
+
console.log(`Command: ${report.command}`);
|
|
293
|
+
console.log(`Identifier: ${report.identifier}`);
|
|
294
|
+
console.log(`Registry: ${report.registry}`);
|
|
295
|
+
console.log(`Mapped Repo: ${report.repo}`);
|
|
296
|
+
console.log(`Skill: ${report.skill}`);
|
|
297
|
+
if (report.version) {
|
|
298
|
+
console.log(`Version: ${report.version}`);
|
|
299
|
+
}
|
|
300
|
+
console.log(`Mode: ${report.strict ? "strict" : "standard"}`);
|
|
301
|
+
if (report.configPath) {
|
|
302
|
+
console.log(`Config: ${report.configPath}`);
|
|
303
|
+
}
|
|
304
|
+
if (report.skillDir) {
|
|
305
|
+
console.log(`Skill Dir: ${report.skillDir}`);
|
|
306
|
+
}
|
|
307
|
+
if (report.commitSha) {
|
|
308
|
+
console.log(`Commit: ${report.commitSha}`);
|
|
309
|
+
}
|
|
310
|
+
if (report.moderation) {
|
|
311
|
+
const parts = [];
|
|
312
|
+
if (report.moderation.isSuspicious) {
|
|
313
|
+
parts.push("suspicious");
|
|
314
|
+
}
|
|
315
|
+
if (report.moderation.isMalwareBlocked) {
|
|
316
|
+
parts.push("malware-blocked");
|
|
317
|
+
}
|
|
318
|
+
if (report.moderation.isRemoved) {
|
|
319
|
+
parts.push("removed");
|
|
320
|
+
}
|
|
321
|
+
if (parts.length > 0) {
|
|
322
|
+
console.log(`ClawHub Moderation: ${parts.join(", ")}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
console.log(`Files Scanned: ${report.scanFiles.length}`);
|
|
326
|
+
if (report.decision.riskScore === null) {
|
|
327
|
+
console.log("Result: UNVERIFIABLE");
|
|
328
|
+
} else {
|
|
329
|
+
console.log(`Risk Score: ${report.decision.riskScore.toFixed(1)}/100`);
|
|
330
|
+
console.log(`Decision: ${report.decision.level}`);
|
|
331
|
+
}
|
|
332
|
+
if (report.unverifiableReasons && report.unverifiableReasons.length > 0) {
|
|
333
|
+
console.log("Unverifiable Reasons:");
|
|
334
|
+
for (const reason of report.unverifiableReasons) {
|
|
335
|
+
console.log(`- ${reason}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (report.decision.chainMatches.length > 0) {
|
|
339
|
+
console.log("Attack Chains:");
|
|
340
|
+
for (const chain of report.decision.chainMatches) {
|
|
341
|
+
console.log(`- ${chain.id} (+${chain.bonus}): ${chain.description}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (report.decision.findings.length > 0) {
|
|
345
|
+
console.log("Findings:");
|
|
346
|
+
for (const finding of report.decision.findings.slice(0, 10)) {
|
|
347
|
+
const fileText = finding.file ? ` (${finding.file})` : "";
|
|
348
|
+
console.log(`- [${finding.severity}/${finding.confidence}] ${finding.title}${fileText}`);
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
console.log("Findings: none");
|
|
352
|
+
}
|
|
353
|
+
console.log(`Note: ${report.note}`);
|
|
354
|
+
}
|
|
355
|
+
function printJsonClawHubReport(report) {
|
|
356
|
+
console.log(JSON.stringify(report, null, 2));
|
|
357
|
+
}
|
|
287
358
|
|
|
288
359
|
// src/resolver/github.ts
|
|
289
360
|
import path2 from "path";
|
|
@@ -976,6 +1047,22 @@ var cliAddOptionsSchema = z2.object({
|
|
|
976
1047
|
maxAuxFiles: z2.coerce.number().int().min(1).max(200).optional(),
|
|
977
1048
|
maxTotalFiles: z2.coerce.number().int().min(1).max(400).optional()
|
|
978
1049
|
});
|
|
1050
|
+
var cliBulkAddOptionsSchema = z2.object({
|
|
1051
|
+
config: z2.string().optional(),
|
|
1052
|
+
strict: z2.boolean().optional(),
|
|
1053
|
+
ci: z2.boolean().optional(),
|
|
1054
|
+
json: z2.boolean().optional(),
|
|
1055
|
+
yes: z2.boolean().optional(),
|
|
1056
|
+
dryRun: z2.boolean().optional(),
|
|
1057
|
+
force: z2.boolean().optional(),
|
|
1058
|
+
allowUnverifiable: z2.boolean().optional(),
|
|
1059
|
+
githubTimeoutMs: z2.coerce.number().int().min(1e3).max(12e4).optional(),
|
|
1060
|
+
githubRetries: z2.coerce.number().int().min(0).max(6).optional(),
|
|
1061
|
+
githubRetryBaseMs: z2.coerce.number().int().min(50).max(5e3).optional(),
|
|
1062
|
+
maxFileBytes: z2.coerce.number().int().min(4096).max(5e6).optional(),
|
|
1063
|
+
maxAuxFiles: z2.coerce.number().int().min(1).max(200).optional(),
|
|
1064
|
+
maxTotalFiles: z2.coerce.number().int().min(1).max(400).optional()
|
|
1065
|
+
});
|
|
979
1066
|
var effectiveAddOptionsSchema = z2.object({
|
|
980
1067
|
skill: z2.string().min(1),
|
|
981
1068
|
strict: z2.boolean(),
|
|
@@ -1095,9 +1182,11 @@ function evaluateGate(level, options) {
|
|
|
1095
1182
|
gateNote: level === "WARNING" ? "WARNING accepted via --yes." : "SAFE to proceed."
|
|
1096
1183
|
};
|
|
1097
1184
|
}
|
|
1098
|
-
async function runAddCommand(repo, rawOptions) {
|
|
1185
|
+
async function runAddCommand(repo, rawOptions, context = {}) {
|
|
1099
1186
|
const cliOptions = cliAddOptionsSchema.parse(rawOptions);
|
|
1100
1187
|
const loadedConfig = loadGuardSkillsConfig(cliOptions.config);
|
|
1188
|
+
const provider = context.provider ?? "skills";
|
|
1189
|
+
const commandName = context.commandName ?? "guardskills add";
|
|
1101
1190
|
const options = resolveEffectiveAddOptions(cliOptions, loadedConfig.config);
|
|
1102
1191
|
enforceSourcePolicy(repo, loadedConfig.config.policy);
|
|
1103
1192
|
enforceOptionPolicy(options, loadedConfig.config);
|
|
@@ -1118,7 +1207,7 @@ async function runAddCommand(repo, rawOptions) {
|
|
|
1118
1207
|
const gate = evaluateGate(decision.level, options);
|
|
1119
1208
|
const configNote = loadedConfig.path ? ` Config: ${loadedConfig.path}` : "";
|
|
1120
1209
|
const report = {
|
|
1121
|
-
command:
|
|
1210
|
+
command: commandName,
|
|
1122
1211
|
repo,
|
|
1123
1212
|
skill: options.skill,
|
|
1124
1213
|
strict: options.strict,
|
|
@@ -1143,12 +1232,14 @@ async function runAddCommand(repo, rawOptions) {
|
|
|
1143
1232
|
if (options.dryRun || options.ci) {
|
|
1144
1233
|
return 0;
|
|
1145
1234
|
}
|
|
1146
|
-
return
|
|
1235
|
+
return runProviderInstall(provider, repo, options.skill);
|
|
1147
1236
|
}
|
|
1148
1237
|
|
|
1149
|
-
// src/commands/
|
|
1238
|
+
// src/commands/openskills-install.ts
|
|
1150
1239
|
import fs2 from "fs";
|
|
1240
|
+
import os from "os";
|
|
1151
1241
|
import path4 from "path";
|
|
1242
|
+
import { execa as execa2 } from "execa";
|
|
1152
1243
|
import { z as z3 } from "zod";
|
|
1153
1244
|
var ALLOWED_TEXT_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
1154
1245
|
".md",
|
|
@@ -1170,68 +1261,82 @@ var ALLOWED_TEXT_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
|
1170
1261
|
".cfg"
|
|
1171
1262
|
]);
|
|
1172
1263
|
var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".turbo"]);
|
|
1173
|
-
var
|
|
1264
|
+
var cliOpenSkillsOptionsSchema = z3.object({
|
|
1174
1265
|
config: z3.string().optional(),
|
|
1175
1266
|
strict: z3.boolean().optional(),
|
|
1267
|
+
ci: z3.boolean().optional(),
|
|
1176
1268
|
json: z3.boolean().optional(),
|
|
1177
|
-
|
|
1269
|
+
yes: z3.boolean().optional(),
|
|
1270
|
+
dryRun: z3.boolean().optional(),
|
|
1271
|
+
force: z3.boolean().optional(),
|
|
1272
|
+
allowUnverifiable: z3.boolean().optional(),
|
|
1273
|
+
githubTimeoutMs: z3.coerce.number().int().min(1e3).max(12e4).optional(),
|
|
1274
|
+
githubRetries: z3.coerce.number().int().min(0).max(6).optional(),
|
|
1275
|
+
githubRetryBaseMs: z3.coerce.number().int().min(50).max(5e3).optional(),
|
|
1178
1276
|
maxFileBytes: z3.coerce.number().int().min(4096).max(5e6).optional(),
|
|
1277
|
+
maxAuxFiles: z3.coerce.number().int().min(1).max(200).optional(),
|
|
1179
1278
|
maxTotalFiles: z3.coerce.number().int().min(1).max(400).optional()
|
|
1180
1279
|
});
|
|
1181
|
-
var effectiveScanLocalOptionsSchema = z3.object({
|
|
1182
|
-
strict: z3.boolean(),
|
|
1183
|
-
json: z3.boolean(),
|
|
1184
|
-
skill: z3.string().min(1).optional(),
|
|
1185
|
-
maxFileBytes: z3.number().int().min(4096).max(5e6),
|
|
1186
|
-
maxTotalFiles: z3.number().int().min(1).max(400)
|
|
1187
|
-
});
|
|
1188
1280
|
var DEFAULT_OPTIONS2 = {
|
|
1189
1281
|
strict: false,
|
|
1282
|
+
ci: false,
|
|
1190
1283
|
json: false,
|
|
1191
|
-
|
|
1284
|
+
yes: false,
|
|
1285
|
+
dryRun: false,
|
|
1286
|
+
force: false,
|
|
1287
|
+
allowUnverifiable: false,
|
|
1288
|
+
githubTimeoutMs: 15e3,
|
|
1289
|
+
githubRetries: 2,
|
|
1290
|
+
githubRetryBaseMs: 300,
|
|
1192
1291
|
maxFileBytes: 25e4,
|
|
1292
|
+
maxAuxFiles: 40,
|
|
1193
1293
|
maxTotalFiles: 120
|
|
1194
1294
|
};
|
|
1195
|
-
function
|
|
1196
|
-
|
|
1295
|
+
function resolveEffectiveOptions(cliOptions, config) {
|
|
1296
|
+
const defaults = config.defaults ?? {};
|
|
1297
|
+
const resolver = config.resolver ?? {};
|
|
1298
|
+
return {
|
|
1299
|
+
strict: cliOptions.strict ?? defaults.strict ?? DEFAULT_OPTIONS2.strict,
|
|
1300
|
+
ci: cliOptions.ci ?? defaults.ci ?? DEFAULT_OPTIONS2.ci,
|
|
1301
|
+
json: cliOptions.json ?? defaults.json ?? DEFAULT_OPTIONS2.json,
|
|
1302
|
+
yes: cliOptions.yes ?? defaults.yes ?? DEFAULT_OPTIONS2.yes,
|
|
1303
|
+
dryRun: cliOptions.dryRun ?? defaults.dryRun ?? DEFAULT_OPTIONS2.dryRun,
|
|
1304
|
+
force: cliOptions.force ?? defaults.force ?? DEFAULT_OPTIONS2.force,
|
|
1305
|
+
allowUnverifiable: cliOptions.allowUnverifiable ?? defaults.allowUnverifiable ?? DEFAULT_OPTIONS2.allowUnverifiable,
|
|
1306
|
+
githubTimeoutMs: cliOptions.githubTimeoutMs ?? resolver.githubTimeoutMs ?? DEFAULT_OPTIONS2.githubTimeoutMs,
|
|
1307
|
+
githubRetries: cliOptions.githubRetries ?? resolver.githubRetries ?? DEFAULT_OPTIONS2.githubRetries,
|
|
1308
|
+
githubRetryBaseMs: cliOptions.githubRetryBaseMs ?? resolver.githubRetryBaseMs ?? DEFAULT_OPTIONS2.githubRetryBaseMs,
|
|
1309
|
+
maxFileBytes: cliOptions.maxFileBytes ?? resolver.maxFileBytes ?? DEFAULT_OPTIONS2.maxFileBytes,
|
|
1310
|
+
maxAuxFiles: cliOptions.maxAuxFiles ?? resolver.maxAuxFiles ?? DEFAULT_OPTIONS2.maxAuxFiles,
|
|
1311
|
+
maxTotalFiles: cliOptions.maxTotalFiles ?? resolver.maxTotalFiles ?? DEFAULT_OPTIONS2.maxTotalFiles
|
|
1312
|
+
};
|
|
1197
1313
|
}
|
|
1198
|
-
function
|
|
1199
|
-
const
|
|
1200
|
-
if (!
|
|
1201
|
-
return
|
|
1314
|
+
function enforceOptionPolicy2(options, config) {
|
|
1315
|
+
const policy = config.policy;
|
|
1316
|
+
if (!policy) {
|
|
1317
|
+
return;
|
|
1202
1318
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
}
|
|
1319
|
+
if (options.force && policy.allowForce === false) {
|
|
1320
|
+
throw new GuardSkillsError(
|
|
1321
|
+
"POLICY_VIOLATION",
|
|
1322
|
+
"Policy blocks --force overrides (allowForce=false)."
|
|
1323
|
+
);
|
|
1209
1324
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
function isScannableTextFile(filePath) {
|
|
1216
|
-
if (isSkillFile(filePath)) {
|
|
1217
|
-
return true;
|
|
1325
|
+
if (options.allowUnverifiable && policy.allowUnverifiableOverride === false) {
|
|
1326
|
+
throw new GuardSkillsError(
|
|
1327
|
+
"POLICY_VIOLATION",
|
|
1328
|
+
"Policy blocks --allow-unverifiable overrides (allowUnverifiableOverride=false)."
|
|
1329
|
+
);
|
|
1218
1330
|
}
|
|
1219
|
-
const ext = path4.extname(filePath).toLowerCase();
|
|
1220
|
-
return ALLOWED_TEXT_EXTENSIONS2.has(ext);
|
|
1221
1331
|
}
|
|
1222
1332
|
function findSkillDirs(rootDir) {
|
|
1223
1333
|
const found = /* @__PURE__ */ new Set();
|
|
1224
1334
|
const stack = [{ dir: rootDir, depth: 0 }];
|
|
1225
|
-
let seen = 0;
|
|
1226
1335
|
while (stack.length > 0) {
|
|
1227
1336
|
const current = stack.pop();
|
|
1228
1337
|
if (!current) {
|
|
1229
1338
|
continue;
|
|
1230
1339
|
}
|
|
1231
|
-
seen += 1;
|
|
1232
|
-
if (seen > 5e3) {
|
|
1233
|
-
break;
|
|
1234
|
-
}
|
|
1235
1340
|
const skillFile = path4.join(current.dir, "SKILL.md");
|
|
1236
1341
|
if (fs2.existsSync(skillFile) && fs2.statSync(skillFile).isFile()) {
|
|
1237
1342
|
found.add(current.dir);
|
|
@@ -1258,82 +1363,6 @@ function findSkillDirs(rootDir) {
|
|
|
1258
1363
|
}
|
|
1259
1364
|
return [...found].sort();
|
|
1260
1365
|
}
|
|
1261
|
-
function formatCandidates(candidates) {
|
|
1262
|
-
return candidates.map((candidate) => `- ${toPosixPath(candidate)}`).join("\n");
|
|
1263
|
-
}
|
|
1264
|
-
function resolveSkillDirectory(inputPath, preferredSkillName) {
|
|
1265
|
-
const absoluteInput = path4.resolve(inputPath);
|
|
1266
|
-
if (!fs2.existsSync(absoluteInput)) {
|
|
1267
|
-
const suggestions = getNearbyPathSuggestions(absoluteInput);
|
|
1268
|
-
const suggestionText = suggestions.length > 0 ? `
|
|
1269
|
-
Nearby paths:
|
|
1270
|
-
${formatCandidates(suggestions)}` : "";
|
|
1271
|
-
throw new GuardSkillsError(
|
|
1272
|
-
"INVALID_LOCAL_PATH",
|
|
1273
|
-
`Local path not found: ${toPosixPath(absoluteInput)}${suggestionText}`
|
|
1274
|
-
);
|
|
1275
|
-
}
|
|
1276
|
-
const stat = fs2.statSync(absoluteInput);
|
|
1277
|
-
if (stat.isFile()) {
|
|
1278
|
-
if (!isSkillFile(absoluteInput)) {
|
|
1279
|
-
throw new GuardSkillsError(
|
|
1280
|
-
"INVALID_LOCAL_PATH",
|
|
1281
|
-
"Local scan expects a directory or a SKILL.md file path."
|
|
1282
|
-
);
|
|
1283
|
-
}
|
|
1284
|
-
return {
|
|
1285
|
-
skillDir: path4.dirname(absoluteInput),
|
|
1286
|
-
note: "Using parent directory of provided SKILL.md file."
|
|
1287
|
-
};
|
|
1288
|
-
}
|
|
1289
|
-
const directSkillFile = path4.join(absoluteInput, "SKILL.md");
|
|
1290
|
-
if (fs2.existsSync(directSkillFile) && fs2.statSync(directSkillFile).isFile()) {
|
|
1291
|
-
return { skillDir: absoluteInput };
|
|
1292
|
-
}
|
|
1293
|
-
const discovered = findSkillDirs(absoluteInput);
|
|
1294
|
-
if (discovered.length === 0) {
|
|
1295
|
-
throw new GuardSkillsError(
|
|
1296
|
-
"INVALID_LOCAL_PATH",
|
|
1297
|
-
`No SKILL.md found under: ${toPosixPath(absoluteInput)}`
|
|
1298
|
-
);
|
|
1299
|
-
}
|
|
1300
|
-
if (preferredSkillName) {
|
|
1301
|
-
const matches = discovered.filter(
|
|
1302
|
-
(directory) => path4.basename(directory).toLowerCase() === preferredSkillName.toLowerCase()
|
|
1303
|
-
);
|
|
1304
|
-
if (matches.length === 1) {
|
|
1305
|
-
const selected = matches[0];
|
|
1306
|
-
if (!selected) {
|
|
1307
|
-
throw new GuardSkillsError("INVALID_LOCAL_PATH", "Unable to resolve selected local skill.");
|
|
1308
|
-
}
|
|
1309
|
-
return {
|
|
1310
|
-
skillDir: selected,
|
|
1311
|
-
note: `Auto-selected skill '${preferredSkillName}' under the provided path.`
|
|
1312
|
-
};
|
|
1313
|
-
}
|
|
1314
|
-
const available = discovered.map((directory) => path4.basename(directory));
|
|
1315
|
-
throw new GuardSkillsError(
|
|
1316
|
-
"INVALID_LOCAL_PATH",
|
|
1317
|
-
`Requested --skill '${preferredSkillName}' was not found.
|
|
1318
|
-
Available skills: ${available.join(", ")}`
|
|
1319
|
-
);
|
|
1320
|
-
}
|
|
1321
|
-
if (discovered.length === 1) {
|
|
1322
|
-
const selected = discovered[0];
|
|
1323
|
-
if (!selected) {
|
|
1324
|
-
throw new GuardSkillsError("INVALID_LOCAL_PATH", "Unable to resolve discovered local skill.");
|
|
1325
|
-
}
|
|
1326
|
-
return {
|
|
1327
|
-
skillDir: selected,
|
|
1328
|
-
note: "Auto-selected the only SKILL.md found under the provided path."
|
|
1329
|
-
};
|
|
1330
|
-
}
|
|
1331
|
-
throw new GuardSkillsError(
|
|
1332
|
-
"INVALID_LOCAL_PATH",
|
|
1333
|
-
`Multiple skills found under path. Provide --skill <name>.
|
|
1334
|
-
${formatCandidates(discovered)}`
|
|
1335
|
-
);
|
|
1336
|
-
}
|
|
1337
1366
|
function collectLocalFiles(skillDir, options) {
|
|
1338
1367
|
const files = [];
|
|
1339
1368
|
const unverifiableReasons = [];
|
|
@@ -1347,7 +1376,7 @@ function collectLocalFiles(skillDir, options) {
|
|
|
1347
1376
|
try {
|
|
1348
1377
|
entries = fs2.readdirSync(currentDir, { withFileTypes: true });
|
|
1349
1378
|
} catch {
|
|
1350
|
-
unverifiableReasons.push(`Cannot read directory: ${
|
|
1379
|
+
unverifiableReasons.push(`Cannot read directory: ${currentDir}`);
|
|
1351
1380
|
continue;
|
|
1352
1381
|
}
|
|
1353
1382
|
for (const entry of entries) {
|
|
@@ -1361,8 +1390,10 @@ function collectLocalFiles(skillDir, options) {
|
|
|
1361
1390
|
if (!entry.isFile()) {
|
|
1362
1391
|
continue;
|
|
1363
1392
|
}
|
|
1364
|
-
const relativePath =
|
|
1365
|
-
|
|
1393
|
+
const relativePath = path4.relative(skillDir, fullPath).replace(/\\/g, "/");
|
|
1394
|
+
const ext = path4.extname(relativePath).toLowerCase();
|
|
1395
|
+
const isSkillFile2 = path4.basename(relativePath).toLowerCase() === "skill.md";
|
|
1396
|
+
if (!isSkillFile2 && !ALLOWED_TEXT_EXTENSIONS2.has(ext)) {
|
|
1366
1397
|
continue;
|
|
1367
1398
|
}
|
|
1368
1399
|
if (files.length >= options.maxTotalFiles) {
|
|
@@ -1396,86 +1427,1233 @@ function collectLocalFiles(skillDir, options) {
|
|
|
1396
1427
|
}
|
|
1397
1428
|
return { files, unverifiableReasons };
|
|
1398
1429
|
}
|
|
1399
|
-
function
|
|
1400
|
-
const
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
strict: cliOptions.strict ?? defaults.strict ?? DEFAULT_OPTIONS2.strict,
|
|
1404
|
-
json: cliOptions.json ?? defaults.json ?? DEFAULT_OPTIONS2.json,
|
|
1405
|
-
skill: cliOptions.skill ?? DEFAULT_OPTIONS2.skill,
|
|
1406
|
-
maxFileBytes: cliOptions.maxFileBytes ?? resolver.maxFileBytes ?? DEFAULT_OPTIONS2.maxFileBytes,
|
|
1407
|
-
maxTotalFiles: cliOptions.maxTotalFiles ?? resolver.maxTotalFiles ?? DEFAULT_OPTIONS2.maxTotalFiles
|
|
1408
|
-
});
|
|
1409
|
-
}
|
|
1410
|
-
async function runScanLocalCommand(inputPath, rawOptions) {
|
|
1411
|
-
const cliOptions = cliScanLocalOptionsSchema.parse(rawOptions);
|
|
1412
|
-
const loadedConfig = loadGuardSkillsConfig(cliOptions.config);
|
|
1413
|
-
const options = resolveEffectiveScanLocalOptions(cliOptions, loadedConfig.config);
|
|
1414
|
-
const target = resolveSkillDirectory(inputPath, options.skill);
|
|
1415
|
-
const { files, unverifiableReasons } = collectLocalFiles(target.skillDir, options);
|
|
1416
|
-
if (files.length === 0) {
|
|
1417
|
-
throw new GuardSkillsError(
|
|
1418
|
-
"INVALID_LOCAL_PATH",
|
|
1419
|
-
`No scannable text files found in: ${toPosixPath(target.skillDir)}`
|
|
1420
|
-
);
|
|
1430
|
+
function resolveSource(source) {
|
|
1431
|
+
const maybePath = path4.resolve(source);
|
|
1432
|
+
if (fs2.existsSync(maybePath)) {
|
|
1433
|
+
return { kind: "local", path: maybePath };
|
|
1421
1434
|
}
|
|
1422
|
-
const
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
repo: "local",
|
|
1426
|
-
defaultBranch: "local",
|
|
1427
|
-
commitSha: "local",
|
|
1428
|
-
skillName: options.skill ?? path4.basename(target.skillDir),
|
|
1429
|
-
skillDir: toPosixPath(target.skillDir),
|
|
1430
|
-
skillFilePath: "SKILL.md",
|
|
1431
|
-
files,
|
|
1432
|
-
unverifiableReasons
|
|
1433
|
-
};
|
|
1434
|
-
const scan = scanResolvedSkill(resolvedSkill);
|
|
1435
|
-
const decision = calculateRiskScore(scan.findings, {
|
|
1436
|
-
strict: options.strict,
|
|
1437
|
-
trustCredits: 0,
|
|
1438
|
-
hasUnverifiableContent: scan.hasUnverifiableContent
|
|
1439
|
-
});
|
|
1440
|
-
const noteParts = ["Local scan complete."];
|
|
1441
|
-
if (target.note) {
|
|
1442
|
-
noteParts.push(target.note);
|
|
1435
|
+
const shorthand = source.match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/);
|
|
1436
|
+
if (shorthand) {
|
|
1437
|
+
return { kind: "git", cloneUrl: `https://github.com/${shorthand[1]}/${shorthand[2]}.git` };
|
|
1443
1438
|
}
|
|
1444
|
-
|
|
1445
|
-
|
|
1439
|
+
try {
|
|
1440
|
+
const parsed = new URL(source);
|
|
1441
|
+
if (parsed.hostname === "github.com" || parsed.hostname === "www.github.com") {
|
|
1442
|
+
return { kind: "git", cloneUrl: source.endsWith(".git") ? source : `${source}.git` };
|
|
1443
|
+
}
|
|
1444
|
+
} catch {
|
|
1446
1445
|
}
|
|
1447
|
-
|
|
1448
|
-
|
|
1446
|
+
return { kind: "git", cloneUrl: source };
|
|
1447
|
+
}
|
|
1448
|
+
function aggregateLevel(levels) {
|
|
1449
|
+
if (levels.includes("UNVERIFIABLE")) {
|
|
1450
|
+
return "UNVERIFIABLE";
|
|
1449
1451
|
}
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
inputPath,
|
|
1453
|
-
strict: options.strict,
|
|
1454
|
-
configPath: loadedConfig.path ?? void 0,
|
|
1455
|
-
decision,
|
|
1456
|
-
scanFiles: resolvedSkill.files.map((file) => file.path),
|
|
1457
|
-
skillDir: resolvedSkill.skillDir,
|
|
1458
|
-
unverifiableReasons: scan.unverifiableReasons,
|
|
1459
|
-
note: noteParts.join(" ")
|
|
1460
|
-
};
|
|
1461
|
-
if (options.json) {
|
|
1462
|
-
printJsonLocalReport(report);
|
|
1463
|
-
} else {
|
|
1464
|
-
printHumanLocalReport(report);
|
|
1452
|
+
if (levels.includes("CRITICAL")) {
|
|
1453
|
+
return "CRITICAL";
|
|
1465
1454
|
}
|
|
1466
|
-
|
|
1455
|
+
if (levels.includes("UNSAFE")) {
|
|
1456
|
+
return "UNSAFE";
|
|
1457
|
+
}
|
|
1458
|
+
if (levels.includes("WARNING")) {
|
|
1459
|
+
return "WARNING";
|
|
1460
|
+
}
|
|
1461
|
+
return "SAFE";
|
|
1462
|
+
}
|
|
1463
|
+
async function runInteractiveInstallCommand(provider, source, skillName, rawOptions) {
|
|
1464
|
+
if (provider !== "openskills" && provider !== "skills" && provider !== "skillkit") {
|
|
1465
|
+
throw new GuardSkillsError(
|
|
1466
|
+
"INVALID_OPTIONS",
|
|
1467
|
+
`Interactive install flow is not supported for provider '${provider}'.`
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
const cliOptions = cliOpenSkillsOptionsSchema.parse(rawOptions);
|
|
1471
|
+
const loadedConfig = loadGuardSkillsConfig(cliOptions.config);
|
|
1472
|
+
const options = resolveEffectiveOptions(cliOptions, loadedConfig.config);
|
|
1473
|
+
enforceSourcePolicy(source, loadedConfig.config.policy);
|
|
1474
|
+
enforceOptionPolicy2(options, loadedConfig.config);
|
|
1475
|
+
const resolvedSource = resolveSource(source);
|
|
1476
|
+
const tempDir = resolvedSource.kind === "git" ? fs2.mkdtempSync(path4.join(os.tmpdir(), `guardskills-${provider}-`)) : null;
|
|
1477
|
+
const scanRoot = resolvedSource.kind === "git" ? tempDir ?? "" : resolvedSource.path;
|
|
1478
|
+
try {
|
|
1479
|
+
if (resolvedSource.kind === "git") {
|
|
1480
|
+
try {
|
|
1481
|
+
await execa2("git", ["clone", "--depth", "1", resolvedSource.cloneUrl, scanRoot], {
|
|
1482
|
+
timeout: options.githubTimeoutMs
|
|
1483
|
+
});
|
|
1484
|
+
} catch (error) {
|
|
1485
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1486
|
+
throw new GuardSkillsError(
|
|
1487
|
+
"GITHUB_UNKNOWN",
|
|
1488
|
+
`Failed to clone source '${source}' for ${provider} scan: ${message}`,
|
|
1489
|
+
{ cause: error }
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
const discovered = findSkillDirs(scanRoot);
|
|
1494
|
+
if (discovered.length === 0) {
|
|
1495
|
+
throw new GuardSkillsError("SKILL_NOT_FOUND", "No SKILL.md files were found in the source.");
|
|
1496
|
+
}
|
|
1497
|
+
const selectedDirs = skillName ? discovered.filter((dir) => path4.basename(dir).toLowerCase() === skillName.toLowerCase()) : discovered;
|
|
1498
|
+
if (selectedDirs.length === 0) {
|
|
1499
|
+
throw new GuardSkillsError(
|
|
1500
|
+
"SKILL_NOT_FOUND",
|
|
1501
|
+
`Skill '${skillName}' was not found. Available: ${discovered.map((dir) => path4.basename(dir)).join(", ")}`
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
const levels = [];
|
|
1505
|
+
const summaries = [];
|
|
1506
|
+
for (const skillDir of selectedDirs) {
|
|
1507
|
+
const skill = path4.basename(skillDir);
|
|
1508
|
+
const { files, unverifiableReasons } = collectLocalFiles(skillDir, options);
|
|
1509
|
+
const resolvedSkill = {
|
|
1510
|
+
source: `local:${skillDir}`,
|
|
1511
|
+
owner: "local",
|
|
1512
|
+
repo: "local",
|
|
1513
|
+
defaultBranch: "local",
|
|
1514
|
+
commitSha: "local",
|
|
1515
|
+
skillName: skill,
|
|
1516
|
+
skillDir: skillDir.replace(/\\/g, "/"),
|
|
1517
|
+
skillFilePath: "SKILL.md",
|
|
1518
|
+
files,
|
|
1519
|
+
unverifiableReasons
|
|
1520
|
+
};
|
|
1521
|
+
const scan = scanResolvedSkill(resolvedSkill);
|
|
1522
|
+
const decision = calculateRiskScore(scan.findings, {
|
|
1523
|
+
strict: options.strict,
|
|
1524
|
+
trustCredits: 0,
|
|
1525
|
+
hasUnverifiableContent: scan.hasUnverifiableContent
|
|
1526
|
+
});
|
|
1527
|
+
levels.push(decision.level);
|
|
1528
|
+
summaries.push({ skill, level: decision.level, riskScore: decision.riskScore });
|
|
1529
|
+
}
|
|
1530
|
+
const overall = aggregateLevel(levels);
|
|
1531
|
+
const gate = evaluateGate(overall, { ...options, skill: skillName ?? "ALL_SKILLS" });
|
|
1532
|
+
const label = provider === "openskills" ? "OpenSkills" : provider === "skillkit" ? "skillkit" : "skills.sh";
|
|
1533
|
+
const note = `${gate.gateNote} ${label} flow: ${skillName ? "single skill" : "interactive skill selection"}.`;
|
|
1534
|
+
if (options.json) {
|
|
1535
|
+
console.log(
|
|
1536
|
+
JSON.stringify(
|
|
1537
|
+
{
|
|
1538
|
+
command: `guardskills ${provider} ${provider === "skills" ? "add" : "install"}`,
|
|
1539
|
+
source,
|
|
1540
|
+
skill: skillName ?? null,
|
|
1541
|
+
scannedSkills: summaries.length,
|
|
1542
|
+
overallLevel: overall,
|
|
1543
|
+
skills: summaries,
|
|
1544
|
+
note
|
|
1545
|
+
},
|
|
1546
|
+
null,
|
|
1547
|
+
2
|
|
1548
|
+
)
|
|
1549
|
+
);
|
|
1550
|
+
} else {
|
|
1551
|
+
console.log(`Command: guardskills ${provider} ${provider === "skills" ? "add" : "install"}`);
|
|
1552
|
+
console.log(`Source: ${source}`);
|
|
1553
|
+
console.log(`Selection: ${skillName ?? "interactive (all scanned)"}`);
|
|
1554
|
+
console.log(`Scanned Skills: ${summaries.length}`);
|
|
1555
|
+
console.log(`Decision: ${overall}`);
|
|
1556
|
+
console.log("Per-skill results:");
|
|
1557
|
+
for (const summary of summaries) {
|
|
1558
|
+
const score = summary.riskScore === null ? "n/a" : summary.riskScore.toFixed(1);
|
|
1559
|
+
console.log(`- ${summary.skill}: ${summary.level} (risk ${score})`);
|
|
1560
|
+
}
|
|
1561
|
+
console.log(`Note: ${note}`);
|
|
1562
|
+
}
|
|
1563
|
+
if (!gate.canInstall) {
|
|
1564
|
+
return gate.exitCode;
|
|
1565
|
+
}
|
|
1566
|
+
if (options.dryRun || options.ci) {
|
|
1567
|
+
return 0;
|
|
1568
|
+
}
|
|
1569
|
+
return runProviderInstall(provider, source, skillName);
|
|
1570
|
+
} finally {
|
|
1571
|
+
if (tempDir) {
|
|
1572
|
+
fs2.rmSync(tempDir, { recursive: true, force: true });
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// src/commands/scan-clawhub.ts
|
|
1578
|
+
import { z as z4 } from "zod";
|
|
1579
|
+
|
|
1580
|
+
// src/resolver/clawhub.ts
|
|
1581
|
+
import path5 from "path";
|
|
1582
|
+
import JSZip from "jszip";
|
|
1583
|
+
var DEFAULT_REGISTRY_BASE_URL = "https://clawhub.ai";
|
|
1584
|
+
var DEFAULT_ARCHIVE_BASE_URL = "https://auth.clawdhub.com";
|
|
1585
|
+
var DEFAULT_REQUEST_TIMEOUT_MS2 = 15e3;
|
|
1586
|
+
var DEFAULT_MAX_FILE_SIZE_BYTES = 25e4;
|
|
1587
|
+
var DEFAULT_MAX_TOTAL_FILES = 120;
|
|
1588
|
+
var RETRYABLE_STATUS2 = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
1589
|
+
var ALLOWED_TEXT_EXTENSIONS3 = /* @__PURE__ */ new Set([
|
|
1590
|
+
".md",
|
|
1591
|
+
".txt",
|
|
1592
|
+
".sh",
|
|
1593
|
+
".bash",
|
|
1594
|
+
".zsh",
|
|
1595
|
+
".ps1",
|
|
1596
|
+
".py",
|
|
1597
|
+
".js",
|
|
1598
|
+
".ts",
|
|
1599
|
+
".mjs",
|
|
1600
|
+
".cjs",
|
|
1601
|
+
".json",
|
|
1602
|
+
".yaml",
|
|
1603
|
+
".yml",
|
|
1604
|
+
".toml",
|
|
1605
|
+
".ini",
|
|
1606
|
+
".cfg"
|
|
1607
|
+
]);
|
|
1608
|
+
function normalizeRegistryBaseUrl(input) {
|
|
1609
|
+
const raw = input?.trim() || DEFAULT_REGISTRY_BASE_URL;
|
|
1610
|
+
const withProtocol = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
|
|
1611
|
+
const url = new URL(withProtocol);
|
|
1612
|
+
const pathname = url.pathname.endsWith("/") ? url.pathname.slice(0, -1) : url.pathname;
|
|
1613
|
+
return `${url.origin}${pathname}`;
|
|
1614
|
+
}
|
|
1615
|
+
function toApiCandidates(registryBaseUrl, identifier) {
|
|
1616
|
+
const encodedIdentifier = encodeURIComponent(identifier);
|
|
1617
|
+
const slugOnly = identifier.split("/").filter(Boolean).at(-1) ?? identifier;
|
|
1618
|
+
const encodedSlug = encodeURIComponent(slugOnly);
|
|
1619
|
+
return [
|
|
1620
|
+
`${registryBaseUrl}/api/v1/package/${encodedIdentifier}`,
|
|
1621
|
+
`${registryBaseUrl}/api/package/${encodedIdentifier}`,
|
|
1622
|
+
`${registryBaseUrl}/api/v1/skills/${encodedSlug}`
|
|
1623
|
+
];
|
|
1624
|
+
}
|
|
1625
|
+
function tryParseClawHubUrl(input) {
|
|
1626
|
+
try {
|
|
1627
|
+
const parsed = new URL(input.trim());
|
|
1628
|
+
const host = parsed.hostname.toLowerCase();
|
|
1629
|
+
if (host !== "clawhub.ai" && host !== "www.clawhub.ai") {
|
|
1630
|
+
return null;
|
|
1631
|
+
}
|
|
1632
|
+
return parsed;
|
|
1633
|
+
} catch {
|
|
1634
|
+
return null;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
function extractIdentifierCandidates(input) {
|
|
1638
|
+
const trimmed = input.trim();
|
|
1639
|
+
if (!trimmed) {
|
|
1640
|
+
return [];
|
|
1641
|
+
}
|
|
1642
|
+
const candidates = [];
|
|
1643
|
+
const pushUnique = (value) => {
|
|
1644
|
+
if (!value) {
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
const normalized = value.trim();
|
|
1648
|
+
if (!normalized) {
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
if (!candidates.includes(normalized)) {
|
|
1652
|
+
candidates.push(normalized);
|
|
1653
|
+
}
|
|
1654
|
+
};
|
|
1655
|
+
try {
|
|
1656
|
+
const parsed = new URL(trimmed);
|
|
1657
|
+
const host = parsed.hostname.toLowerCase();
|
|
1658
|
+
if (host !== "clawhub.ai" && host !== "www.clawhub.ai") {
|
|
1659
|
+
return [trimmed];
|
|
1660
|
+
}
|
|
1661
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
1662
|
+
if (segments.length >= 2 && segments[0] && segments[1]) {
|
|
1663
|
+
pushUnique(`${segments[0]}/${segments[1]}`);
|
|
1664
|
+
}
|
|
1665
|
+
if (segments.length >= 3 && segments[0]?.toLowerCase() === "skills" && segments[1] && segments[2]) {
|
|
1666
|
+
pushUnique(`${segments[1]}/${segments[2]}`);
|
|
1667
|
+
}
|
|
1668
|
+
if (segments.length > 0) {
|
|
1669
|
+
pushUnique(segments.join("/"));
|
|
1670
|
+
const last = segments.at(-1);
|
|
1671
|
+
pushUnique(last);
|
|
1672
|
+
}
|
|
1673
|
+
} catch {
|
|
1674
|
+
pushUnique(trimmed);
|
|
1675
|
+
}
|
|
1676
|
+
return candidates;
|
|
1677
|
+
}
|
|
1678
|
+
function mapClawHubError(error, operation) {
|
|
1679
|
+
if (error instanceof GuardSkillsError) {
|
|
1680
|
+
return error;
|
|
1681
|
+
}
|
|
1682
|
+
const status = typeof error === "object" && error !== null && "status" in error ? error.status : void 0;
|
|
1683
|
+
const message = typeof error === "object" && error !== null && "message" in error ? String(error.message) : String(error);
|
|
1684
|
+
const lowerMessage = message.toLowerCase();
|
|
1685
|
+
if (status === 401 || status === 403) {
|
|
1686
|
+
return new GuardSkillsError(
|
|
1687
|
+
"CLAWHUB_AUTH",
|
|
1688
|
+
`${operation} failed: authentication/authorization error from ClawHub.`,
|
|
1689
|
+
{ status, cause: error }
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
if (status === 404) {
|
|
1693
|
+
return new GuardSkillsError("CLAWHUB_NOT_FOUND", `${operation} failed: resource not found.`, {
|
|
1694
|
+
status,
|
|
1695
|
+
cause: error
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
if (status !== void 0 && RETRYABLE_STATUS2.has(status)) {
|
|
1699
|
+
return new GuardSkillsError(
|
|
1700
|
+
status === 429 ? "CLAWHUB_RATE_LIMIT" : "CLAWHUB_TRANSIENT",
|
|
1701
|
+
`${operation} failed with retryable ClawHub status ${status}.`,
|
|
1702
|
+
{ status, retryable: true, cause: error }
|
|
1703
|
+
);
|
|
1704
|
+
}
|
|
1705
|
+
if (lowerMessage.includes("timeout") || lowerMessage.includes("timed out") || lowerMessage.includes("abort")) {
|
|
1706
|
+
return new GuardSkillsError("CLAWHUB_TIMEOUT", `${operation} timed out while calling ClawHub API.`, {
|
|
1707
|
+
retryable: true,
|
|
1708
|
+
cause: error
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
return new GuardSkillsError("CLAWHUB_UNKNOWN", `${operation} failed: ${message}`, {
|
|
1712
|
+
status,
|
|
1713
|
+
cause: error
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
function unwrapResponsePayload(payload) {
|
|
1717
|
+
if (!payload || typeof payload !== "object") {
|
|
1718
|
+
throw new GuardSkillsError("CLAWHUB_UNKNOWN", "ClawHub returned a non-object JSON payload.");
|
|
1719
|
+
}
|
|
1720
|
+
const objectPayload = payload;
|
|
1721
|
+
if (objectPayload.data && typeof objectPayload.data === "object") {
|
|
1722
|
+
return objectPayload.data;
|
|
1723
|
+
}
|
|
1724
|
+
return objectPayload;
|
|
1725
|
+
}
|
|
1726
|
+
async function fetchJson(url, timeoutMs) {
|
|
1727
|
+
const controller = new AbortController();
|
|
1728
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1729
|
+
try {
|
|
1730
|
+
const response = await fetch(url, {
|
|
1731
|
+
method: "GET",
|
|
1732
|
+
headers: { accept: "application/json" },
|
|
1733
|
+
signal: controller.signal
|
|
1734
|
+
});
|
|
1735
|
+
if (!response.ok) {
|
|
1736
|
+
throw { status: response.status, message: `${response.status} ${response.statusText}` };
|
|
1737
|
+
}
|
|
1738
|
+
const payload = await response.json();
|
|
1739
|
+
return unwrapResponsePayload(payload);
|
|
1740
|
+
} catch (error) {
|
|
1741
|
+
throw mapClawHubError(error, `ClawHub metadata fetch (${url})`);
|
|
1742
|
+
} finally {
|
|
1743
|
+
clearTimeout(timeout);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
async function fetchText(url, timeoutMs) {
|
|
1747
|
+
const controller = new AbortController();
|
|
1748
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1749
|
+
try {
|
|
1750
|
+
const response = await fetch(url, {
|
|
1751
|
+
method: "GET",
|
|
1752
|
+
headers: { accept: "text/html,application/xhtml+xml" },
|
|
1753
|
+
signal: controller.signal
|
|
1754
|
+
});
|
|
1755
|
+
if (!response.ok) {
|
|
1756
|
+
throw { status: response.status, message: `${response.status} ${response.statusText}` };
|
|
1757
|
+
}
|
|
1758
|
+
return await response.text();
|
|
1759
|
+
} catch (error) {
|
|
1760
|
+
throw mapClawHubError(error, `ClawHub page fetch (${url})`);
|
|
1761
|
+
} finally {
|
|
1762
|
+
clearTimeout(timeout);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
async function fetchArrayBuffer(url, timeoutMs) {
|
|
1766
|
+
const controller = new AbortController();
|
|
1767
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1768
|
+
try {
|
|
1769
|
+
const response = await fetch(url, {
|
|
1770
|
+
method: "GET",
|
|
1771
|
+
signal: controller.signal
|
|
1772
|
+
});
|
|
1773
|
+
if (!response.ok) {
|
|
1774
|
+
throw { status: response.status, message: `${response.status} ${response.statusText}` };
|
|
1775
|
+
}
|
|
1776
|
+
return await response.arrayBuffer();
|
|
1777
|
+
} catch (error) {
|
|
1778
|
+
throw mapClawHubError(error, `ClawHub archive fetch (${url})`);
|
|
1779
|
+
} finally {
|
|
1780
|
+
clearTimeout(timeout);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
function normalizeRepoRef(candidate) {
|
|
1784
|
+
const trimmed = candidate.trim();
|
|
1785
|
+
if (!trimmed) {
|
|
1786
|
+
return null;
|
|
1787
|
+
}
|
|
1788
|
+
const shorthand = trimmed.match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/);
|
|
1789
|
+
if (shorthand && shorthand[1] && shorthand[2]) {
|
|
1790
|
+
return `${shorthand[1]}/${shorthand[2].replace(/\.git$/i, "")}`;
|
|
1791
|
+
}
|
|
1792
|
+
try {
|
|
1793
|
+
const parsed = new URL(trimmed);
|
|
1794
|
+
if (!(parsed.hostname === "github.com" || parsed.hostname === "www.github.com")) {
|
|
1795
|
+
return null;
|
|
1796
|
+
}
|
|
1797
|
+
const parts = parsed.pathname.split("/").filter(Boolean);
|
|
1798
|
+
if (parts.length < 2 || !parts[0] || !parts[1]) {
|
|
1799
|
+
return null;
|
|
1800
|
+
}
|
|
1801
|
+
return `${parts[0]}/${parts[1].replace(/\.git$/i, "")}`;
|
|
1802
|
+
} catch {
|
|
1803
|
+
return null;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
function getNestedString(obj, ...keys) {
|
|
1807
|
+
let cursor = obj;
|
|
1808
|
+
for (const key of keys) {
|
|
1809
|
+
if (!cursor || typeof cursor !== "object" || !(key in cursor)) {
|
|
1810
|
+
return void 0;
|
|
1811
|
+
}
|
|
1812
|
+
cursor = cursor[key];
|
|
1813
|
+
}
|
|
1814
|
+
return typeof cursor === "string" && cursor.trim() ? cursor.trim() : void 0;
|
|
1815
|
+
}
|
|
1816
|
+
function getNestedBoolean(obj, ...keys) {
|
|
1817
|
+
let cursor = obj;
|
|
1818
|
+
for (const key of keys) {
|
|
1819
|
+
if (!cursor || typeof cursor !== "object" || !(key in cursor)) {
|
|
1820
|
+
return void 0;
|
|
1821
|
+
}
|
|
1822
|
+
cursor = cursor[key];
|
|
1823
|
+
}
|
|
1824
|
+
return typeof cursor === "boolean" ? cursor : void 0;
|
|
1825
|
+
}
|
|
1826
|
+
function isLikelyTextFile2(filePath) {
|
|
1827
|
+
const lower = filePath.toLowerCase();
|
|
1828
|
+
if (lower.endsWith("/skill.md") || lower === "skill.md") {
|
|
1829
|
+
return true;
|
|
1830
|
+
}
|
|
1831
|
+
const ext = path5.posix.extname(lower);
|
|
1832
|
+
return ALLOWED_TEXT_EXTENSIONS3.has(ext);
|
|
1833
|
+
}
|
|
1834
|
+
function isBinaryContent2(content) {
|
|
1835
|
+
return content.includes("\0");
|
|
1836
|
+
}
|
|
1837
|
+
function collectGitHubRepos(value, collected, seen) {
|
|
1838
|
+
if (value === null || value === void 0) {
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
if (typeof value === "string") {
|
|
1842
|
+
const repo = normalizeRepoRef(value);
|
|
1843
|
+
if (repo) {
|
|
1844
|
+
collected.add(repo);
|
|
1845
|
+
}
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
if (typeof value !== "object") {
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
if (seen.has(value)) {
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
seen.add(value);
|
|
1855
|
+
if (Array.isArray(value)) {
|
|
1856
|
+
for (const item of value) {
|
|
1857
|
+
collectGitHubRepos(item, collected, seen);
|
|
1858
|
+
}
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
for (const nested of Object.values(value)) {
|
|
1862
|
+
collectGitHubRepos(nested, collected, seen);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
function collectPossibleSkillNames(identifier, metadata, override) {
|
|
1866
|
+
const names = /* @__PURE__ */ new Set();
|
|
1867
|
+
if (override?.trim()) {
|
|
1868
|
+
names.add(override.trim());
|
|
1869
|
+
}
|
|
1870
|
+
const lastSegment = identifier.split(/[/:@]/).filter(Boolean).at(-1);
|
|
1871
|
+
if (lastSegment) {
|
|
1872
|
+
names.add(lastSegment);
|
|
1873
|
+
}
|
|
1874
|
+
const maybePush = (value) => {
|
|
1875
|
+
if (typeof value === "string" && value.trim()) {
|
|
1876
|
+
names.add(value.trim());
|
|
1877
|
+
}
|
|
1878
|
+
};
|
|
1879
|
+
maybePush(metadata.skill);
|
|
1880
|
+
maybePush(metadata.slug);
|
|
1881
|
+
maybePush(metadata.name);
|
|
1882
|
+
maybePush(metadata.id);
|
|
1883
|
+
return [...names];
|
|
1884
|
+
}
|
|
1885
|
+
function parseArchiveMetadata(metadata, identifier, requestedVersion) {
|
|
1886
|
+
const ownerFromMetadata = getNestedString(metadata, "owner", "handle") ?? getNestedString(metadata, "owner") ?? getNestedString(metadata, "author") ?? getNestedString(metadata, "publisher");
|
|
1887
|
+
const slugFromMetadata = getNestedString(metadata, "skill", "slug") ?? getNestedString(metadata, "slug") ?? getNestedString(metadata, "name");
|
|
1888
|
+
const versionFromMetadata = requestedVersion ?? getNestedString(metadata, "latestVersion", "version") ?? getNestedString(metadata, "version");
|
|
1889
|
+
const identifierParts = identifier.split("/").filter(Boolean);
|
|
1890
|
+
const ownerFromIdentifier = identifierParts.length >= 2 ? identifierParts[0] : void 0;
|
|
1891
|
+
const slugFromIdentifier = identifierParts.length > 0 ? identifierParts.at(-1) : void 0;
|
|
1892
|
+
const owner = ownerFromMetadata ?? ownerFromIdentifier;
|
|
1893
|
+
const slug = slugFromMetadata ?? slugFromIdentifier;
|
|
1894
|
+
if (!owner || !slug) {
|
|
1895
|
+
return null;
|
|
1896
|
+
}
|
|
1897
|
+
return { owner, slug, version: versionFromMetadata };
|
|
1898
|
+
}
|
|
1899
|
+
function parseClawHubModeration(metadata) {
|
|
1900
|
+
const isSuspicious = getNestedBoolean(metadata, "moderation", "isSuspicious") ?? getNestedBoolean(metadata, "isSuspicious");
|
|
1901
|
+
const isMalwareBlocked = getNestedBoolean(metadata, "moderation", "isMalwareBlocked") ?? getNestedBoolean(metadata, "isMalwareBlocked");
|
|
1902
|
+
const isRemoved = getNestedBoolean(metadata, "moderation", "isRemoved") ?? getNestedBoolean(metadata, "isRemoved");
|
|
1903
|
+
if (isSuspicious === void 0 && isMalwareBlocked === void 0 && isRemoved === void 0) {
|
|
1904
|
+
return void 0;
|
|
1905
|
+
}
|
|
1906
|
+
return { isSuspicious, isMalwareBlocked, isRemoved };
|
|
1907
|
+
}
|
|
1908
|
+
async function resolveSkillFromArchive(metadata, identifier, options) {
|
|
1909
|
+
const archiveMeta = parseArchiveMetadata(metadata, identifier, options.version);
|
|
1910
|
+
if (!archiveMeta) {
|
|
1911
|
+
return null;
|
|
1912
|
+
}
|
|
1913
|
+
const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS2;
|
|
1914
|
+
const maxFileSizeBytes = options.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE_BYTES;
|
|
1915
|
+
const maxTotalFiles = options.maxTotalFiles ?? DEFAULT_MAX_TOTAL_FILES;
|
|
1916
|
+
const params = new URLSearchParams({ slug: archiveMeta.slug });
|
|
1917
|
+
if (archiveMeta.version) {
|
|
1918
|
+
params.set("version", archiveMeta.version);
|
|
1919
|
+
}
|
|
1920
|
+
const downloadUrl = `${DEFAULT_ARCHIVE_BASE_URL}/api/v1/download?${params.toString()}`;
|
|
1921
|
+
const archiveBuffer = await fetchArrayBuffer(downloadUrl, timeoutMs);
|
|
1922
|
+
const zip = await JSZip.loadAsync(Buffer.from(archiveBuffer));
|
|
1923
|
+
const files = new Array();
|
|
1924
|
+
const unverifiableReasons = [];
|
|
1925
|
+
let skillFilePath = null;
|
|
1926
|
+
const entries = Object.values(zip.files);
|
|
1927
|
+
for (const entry of entries) {
|
|
1928
|
+
if (entry.dir) {
|
|
1929
|
+
continue;
|
|
1930
|
+
}
|
|
1931
|
+
const normalizedPath = entry.name.replace(/\\/g, "/");
|
|
1932
|
+
if (!isLikelyTextFile2(normalizedPath)) {
|
|
1933
|
+
continue;
|
|
1934
|
+
}
|
|
1935
|
+
if (files.length >= maxTotalFiles) {
|
|
1936
|
+
unverifiableReasons.push(
|
|
1937
|
+
`Resolved archive file count exceeds maxTotalFiles=${maxTotalFiles}; scan truncated.`
|
|
1938
|
+
);
|
|
1939
|
+
break;
|
|
1940
|
+
}
|
|
1941
|
+
const contentBuffer = await entry.async("nodebuffer");
|
|
1942
|
+
if (contentBuffer.length > maxFileSizeBytes) {
|
|
1943
|
+
unverifiableReasons.push(
|
|
1944
|
+
`File too large to scan safely: ${normalizedPath}`
|
|
1945
|
+
);
|
|
1946
|
+
continue;
|
|
1947
|
+
}
|
|
1948
|
+
const content = contentBuffer.toString("utf8");
|
|
1949
|
+
if (isBinaryContent2(content)) {
|
|
1950
|
+
unverifiableReasons.push(`Binary content detected: ${normalizedPath}`);
|
|
1951
|
+
continue;
|
|
1952
|
+
}
|
|
1953
|
+
files.push({ path: normalizedPath, content });
|
|
1954
|
+
if (!skillFilePath && normalizedPath.toLowerCase().endsWith("/skill.md")) {
|
|
1955
|
+
skillFilePath = normalizedPath;
|
|
1956
|
+
}
|
|
1957
|
+
if (!skillFilePath && normalizedPath.toLowerCase() === "skill.md") {
|
|
1958
|
+
skillFilePath = normalizedPath;
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
if (!skillFilePath) {
|
|
1962
|
+
skillFilePath = files.find((file) => file.path.toLowerCase().endsWith("skill.md"))?.path ?? null;
|
|
1963
|
+
}
|
|
1964
|
+
if (!skillFilePath || files.length === 0) {
|
|
1965
|
+
return null;
|
|
1966
|
+
}
|
|
1967
|
+
const moderation = parseClawHubModeration(metadata);
|
|
1968
|
+
return {
|
|
1969
|
+
source: `clawhub:${archiveMeta.owner}/${archiveMeta.slug}${archiveMeta.version ? `@${archiveMeta.version}` : ""}`,
|
|
1970
|
+
owner: archiveMeta.owner,
|
|
1971
|
+
repo: "clawhub-archive",
|
|
1972
|
+
defaultBranch: "archive",
|
|
1973
|
+
commitSha: archiveMeta.version ?? "archive",
|
|
1974
|
+
skillName: options.skillNameOverride ?? archiveMeta.slug,
|
|
1975
|
+
skillDir: path5.posix.dirname(skillFilePath),
|
|
1976
|
+
skillFilePath,
|
|
1977
|
+
files,
|
|
1978
|
+
unverifiableReasons: [...unverifiableReasons],
|
|
1979
|
+
sourceMetadata: moderation ? {
|
|
1980
|
+
clawhubModeration: moderation
|
|
1981
|
+
} : void 0
|
|
1982
|
+
};
|
|
1983
|
+
}
|
|
1984
|
+
function extractMetadataFromClawHubPage(html, pageUrl) {
|
|
1985
|
+
const metadata = {};
|
|
1986
|
+
const githubUrlMatch = html.match(/https?:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)/i);
|
|
1987
|
+
if (githubUrlMatch?.[1] && githubUrlMatch[2]) {
|
|
1988
|
+
metadata.repository = `https://github.com/${githubUrlMatch[1]}/${githubUrlMatch[2]}`;
|
|
1989
|
+
}
|
|
1990
|
+
const directRepoMatch = html.match(/"repository"\s*:\s*"([^"]+)"/i);
|
|
1991
|
+
if (directRepoMatch?.[1]) {
|
|
1992
|
+
metadata.repository = directRepoMatch[1];
|
|
1993
|
+
}
|
|
1994
|
+
const pathnameParts = pageUrl.pathname.split("/").filter(Boolean);
|
|
1995
|
+
if (pathnameParts.length >= 2 && pathnameParts[1]) {
|
|
1996
|
+
metadata.skill = pathnameParts[1];
|
|
1997
|
+
}
|
|
1998
|
+
return Object.keys(metadata).length > 0 ? metadata : null;
|
|
1999
|
+
}
|
|
2000
|
+
async function resolveFromClawHubPageUrl(input, timeoutMs) {
|
|
2001
|
+
const parsed = tryParseClawHubUrl(input);
|
|
2002
|
+
if (!parsed) {
|
|
2003
|
+
return null;
|
|
2004
|
+
}
|
|
2005
|
+
const html = await fetchText(parsed.toString(), timeoutMs);
|
|
2006
|
+
return extractMetadataFromClawHubPage(html, parsed);
|
|
2007
|
+
}
|
|
2008
|
+
async function resolveSkillFromClawHub(identifier, options = {}) {
|
|
2009
|
+
const identifierCandidates = extractIdentifierCandidates(identifier);
|
|
2010
|
+
if (identifierCandidates.length === 0) {
|
|
2011
|
+
throw new GuardSkillsError("CLAWHUB_UNKNOWN", "ClawHub identifier is required.");
|
|
2012
|
+
}
|
|
2013
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS2;
|
|
2014
|
+
const registryBaseUrl = normalizeRegistryBaseUrl(options.registryBaseUrl);
|
|
2015
|
+
let metadata = null;
|
|
2016
|
+
let resolvedIdentifier = null;
|
|
2017
|
+
let lastError = null;
|
|
2018
|
+
for (const identifierCandidate of identifierCandidates) {
|
|
2019
|
+
const candidateUrls = toApiCandidates(registryBaseUrl, identifierCandidate);
|
|
2020
|
+
for (const url of candidateUrls) {
|
|
2021
|
+
try {
|
|
2022
|
+
metadata = await fetchJson(url, requestTimeoutMs);
|
|
2023
|
+
resolvedIdentifier = identifierCandidate;
|
|
2024
|
+
break;
|
|
2025
|
+
} catch (error) {
|
|
2026
|
+
const mapped = mapClawHubError(error, "ClawHub package lookup");
|
|
2027
|
+
lastError = mapped;
|
|
2028
|
+
if (mapped.code !== "CLAWHUB_NOT_FOUND") {
|
|
2029
|
+
break;
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
if (metadata) {
|
|
2034
|
+
break;
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
if (!metadata) {
|
|
2038
|
+
try {
|
|
2039
|
+
metadata = await resolveFromClawHubPageUrl(identifier, requestTimeoutMs);
|
|
2040
|
+
if (metadata) {
|
|
2041
|
+
resolvedIdentifier = identifierCandidates[0] ?? identifier;
|
|
2042
|
+
}
|
|
2043
|
+
} catch (error) {
|
|
2044
|
+
lastError = mapClawHubError(error, "ClawHub skill page fallback");
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
if (!metadata) {
|
|
2048
|
+
if (lastError) {
|
|
2049
|
+
throw lastError;
|
|
2050
|
+
}
|
|
2051
|
+
throw new GuardSkillsError(
|
|
2052
|
+
"CLAWHUB_NOT_FOUND",
|
|
2053
|
+
`Unable to resolve '${identifier}' from ClawHub. Candidates tried: ${identifierCandidates.join(", ")}.`
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
const canonicalIdentifier = resolvedIdentifier ?? identifierCandidates[0] ?? identifier;
|
|
2057
|
+
const repos = /* @__PURE__ */ new Set();
|
|
2058
|
+
collectGitHubRepos(metadata, repos, /* @__PURE__ */ new Set());
|
|
2059
|
+
const skillCandidates = collectPossibleSkillNames(canonicalIdentifier, metadata, options.skillNameOverride);
|
|
2060
|
+
if (skillCandidates.length === 0) {
|
|
2061
|
+
throw new GuardSkillsError(
|
|
2062
|
+
"CLAWHUB_UNKNOWN",
|
|
2063
|
+
`Could not infer skill name for ClawHub package '${canonicalIdentifier}'. Use --skill.`
|
|
2064
|
+
);
|
|
2065
|
+
}
|
|
2066
|
+
let resolved = null;
|
|
2067
|
+
let resolveError;
|
|
2068
|
+
const githubOptions = {
|
|
2069
|
+
requestTimeoutMs: options.requestTimeoutMs,
|
|
2070
|
+
maxFileSizeBytes: options.maxFileSizeBytes,
|
|
2071
|
+
maxAuxFiles: options.maxAuxFiles,
|
|
2072
|
+
maxTotalFiles: options.maxTotalFiles,
|
|
2073
|
+
retries: options.retries,
|
|
2074
|
+
retryBaseDelayMs: options.retryBaseDelayMs
|
|
2075
|
+
};
|
|
2076
|
+
if (repos.size > 0) {
|
|
2077
|
+
for (const repo of repos) {
|
|
2078
|
+
for (const skillName of skillCandidates) {
|
|
2079
|
+
try {
|
|
2080
|
+
resolved = await resolveSkillFromGitHub(repo, skillName, githubOptions);
|
|
2081
|
+
break;
|
|
2082
|
+
} catch (error) {
|
|
2083
|
+
resolveError = error;
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
if (resolved) {
|
|
2087
|
+
break;
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
if (!resolved) {
|
|
2092
|
+
const archiveResolved = await resolveSkillFromArchive(metadata, canonicalIdentifier, options);
|
|
2093
|
+
if (archiveResolved) {
|
|
2094
|
+
return archiveResolved;
|
|
2095
|
+
}
|
|
2096
|
+
if (repos.size === 0) {
|
|
2097
|
+
throw new GuardSkillsError(
|
|
2098
|
+
"CLAWHUB_UNKNOWN",
|
|
2099
|
+
`ClawHub package '${canonicalIdentifier}' did not expose a resolvable GitHub source or archive payload.`
|
|
2100
|
+
);
|
|
2101
|
+
}
|
|
2102
|
+
throw new GuardSkillsError(
|
|
2103
|
+
"SKILL_NOT_FOUND",
|
|
2104
|
+
`Unable to map ClawHub package '${canonicalIdentifier}' to a GitHub skill. Repos tried: ${[...repos].join(", ")}. Skill names tried: ${skillCandidates.join(", ")}.`,
|
|
2105
|
+
{ cause: resolveError }
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
const packageVersion = options.version ?? (typeof metadata.version === "string" ? metadata.version : void 0);
|
|
2109
|
+
const sourceSuffix = packageVersion ? `${canonicalIdentifier}@${packageVersion}` : canonicalIdentifier;
|
|
2110
|
+
const moderation = parseClawHubModeration(metadata);
|
|
2111
|
+
return {
|
|
2112
|
+
...resolved,
|
|
2113
|
+
source: `clawhub:${sourceSuffix}`,
|
|
2114
|
+
unverifiableReasons: [...resolved.unverifiableReasons],
|
|
2115
|
+
sourceMetadata: moderation ? {
|
|
2116
|
+
...resolved.sourceMetadata ?? {},
|
|
2117
|
+
clawhubModeration: moderation
|
|
2118
|
+
} : resolved.sourceMetadata
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
// src/commands/scan-clawhub.ts
|
|
2123
|
+
var cliScanClawHubOptionsSchema = z4.object({
|
|
2124
|
+
config: z4.string().optional(),
|
|
2125
|
+
strict: z4.boolean().optional(),
|
|
2126
|
+
json: z4.boolean().optional(),
|
|
2127
|
+
skill: z4.string().min(1).optional(),
|
|
2128
|
+
version: z4.string().min(1).optional(),
|
|
2129
|
+
clawhubRegistry: z4.string().min(1).optional(),
|
|
2130
|
+
githubTimeoutMs: z4.coerce.number().int().min(1e3).max(12e4).optional(),
|
|
2131
|
+
githubRetries: z4.coerce.number().int().min(0).max(6).optional(),
|
|
2132
|
+
githubRetryBaseMs: z4.coerce.number().int().min(50).max(5e3).optional(),
|
|
2133
|
+
maxFileBytes: z4.coerce.number().int().min(4096).max(5e6).optional(),
|
|
2134
|
+
maxAuxFiles: z4.coerce.number().int().min(1).max(200).optional(),
|
|
2135
|
+
maxTotalFiles: z4.coerce.number().int().min(1).max(400).optional()
|
|
2136
|
+
});
|
|
2137
|
+
var effectiveScanClawHubOptionsSchema = z4.object({
|
|
2138
|
+
strict: z4.boolean(),
|
|
2139
|
+
json: z4.boolean(),
|
|
2140
|
+
skill: z4.string().min(1).optional(),
|
|
2141
|
+
version: z4.string().min(1).optional(),
|
|
2142
|
+
clawhubRegistry: z4.string().min(1),
|
|
2143
|
+
githubTimeoutMs: z4.number().int().min(1e3).max(12e4),
|
|
2144
|
+
githubRetries: z4.number().int().min(0).max(6),
|
|
2145
|
+
githubRetryBaseMs: z4.number().int().min(50).max(5e3),
|
|
2146
|
+
maxFileBytes: z4.number().int().min(4096).max(5e6),
|
|
2147
|
+
maxAuxFiles: z4.number().int().min(1).max(200),
|
|
2148
|
+
maxTotalFiles: z4.number().int().min(1).max(400)
|
|
2149
|
+
});
|
|
2150
|
+
var DEFAULT_OPTIONS3 = {
|
|
2151
|
+
strict: false,
|
|
2152
|
+
json: false,
|
|
2153
|
+
skill: void 0,
|
|
2154
|
+
version: void 0,
|
|
2155
|
+
clawhubRegistry: "https://clawhub.ai",
|
|
2156
|
+
githubTimeoutMs: 15e3,
|
|
2157
|
+
githubRetries: 2,
|
|
2158
|
+
githubRetryBaseMs: 300,
|
|
2159
|
+
maxFileBytes: 25e4,
|
|
2160
|
+
maxAuxFiles: 40,
|
|
2161
|
+
maxTotalFiles: 120
|
|
2162
|
+
};
|
|
2163
|
+
function alignWithClawHubModeration(baseDecision, moderation) {
|
|
2164
|
+
if (!moderation) {
|
|
2165
|
+
return { decision: baseDecision };
|
|
2166
|
+
}
|
|
2167
|
+
if (moderation.isMalwareBlocked) {
|
|
2168
|
+
return {
|
|
2169
|
+
decision: {
|
|
2170
|
+
...baseDecision,
|
|
2171
|
+
riskScore: 100,
|
|
2172
|
+
safetyScore: 0,
|
|
2173
|
+
level: "CRITICAL",
|
|
2174
|
+
reason: "ClawHub moderation marked this skill as malware-blocked."
|
|
2175
|
+
},
|
|
2176
|
+
moderationNote: "Aligned with ClawHub moderation: malware-blocked."
|
|
2177
|
+
};
|
|
2178
|
+
}
|
|
2179
|
+
if (moderation.isSuspicious && baseDecision.level === "SAFE") {
|
|
2180
|
+
const adjustedRisk = Math.max(baseDecision.riskScore ?? 0, 30);
|
|
2181
|
+
return {
|
|
2182
|
+
decision: {
|
|
2183
|
+
...baseDecision,
|
|
2184
|
+
riskScore: adjustedRisk,
|
|
2185
|
+
safetyScore: 100 - adjustedRisk,
|
|
2186
|
+
level: "WARNING",
|
|
2187
|
+
reason: "ClawHub moderation marked this skill as suspicious."
|
|
2188
|
+
},
|
|
2189
|
+
moderationNote: "Aligned with ClawHub moderation: suspicious."
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
return { decision: baseDecision };
|
|
2193
|
+
}
|
|
2194
|
+
function resolveEffectiveScanClawHubOptions(cliOptions, config) {
|
|
2195
|
+
const defaults = config.defaults ?? {};
|
|
2196
|
+
const resolver = config.resolver ?? {};
|
|
2197
|
+
return effectiveScanClawHubOptionsSchema.parse({
|
|
2198
|
+
strict: cliOptions.strict ?? defaults.strict ?? DEFAULT_OPTIONS3.strict,
|
|
2199
|
+
json: cliOptions.json ?? defaults.json ?? DEFAULT_OPTIONS3.json,
|
|
2200
|
+
skill: cliOptions.skill ?? DEFAULT_OPTIONS3.skill,
|
|
2201
|
+
version: cliOptions.version ?? DEFAULT_OPTIONS3.version,
|
|
2202
|
+
clawhubRegistry: cliOptions.clawhubRegistry ?? DEFAULT_OPTIONS3.clawhubRegistry,
|
|
2203
|
+
githubTimeoutMs: cliOptions.githubTimeoutMs ?? resolver.githubTimeoutMs ?? DEFAULT_OPTIONS3.githubTimeoutMs,
|
|
2204
|
+
githubRetries: cliOptions.githubRetries ?? resolver.githubRetries ?? DEFAULT_OPTIONS3.githubRetries,
|
|
2205
|
+
githubRetryBaseMs: cliOptions.githubRetryBaseMs ?? resolver.githubRetryBaseMs ?? DEFAULT_OPTIONS3.githubRetryBaseMs,
|
|
2206
|
+
maxFileBytes: cliOptions.maxFileBytes ?? resolver.maxFileBytes ?? DEFAULT_OPTIONS3.maxFileBytes,
|
|
2207
|
+
maxAuxFiles: cliOptions.maxAuxFiles ?? resolver.maxAuxFiles ?? DEFAULT_OPTIONS3.maxAuxFiles,
|
|
2208
|
+
maxTotalFiles: cliOptions.maxTotalFiles ?? resolver.maxTotalFiles ?? DEFAULT_OPTIONS3.maxTotalFiles
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
2211
|
+
async function runScanClawHubCommand(identifier, rawOptions) {
|
|
2212
|
+
const cliOptions = cliScanClawHubOptionsSchema.parse(rawOptions);
|
|
2213
|
+
const loadedConfig = loadGuardSkillsConfig(cliOptions.config);
|
|
2214
|
+
const options = resolveEffectiveScanClawHubOptions(cliOptions, loadedConfig.config);
|
|
2215
|
+
const resolved = await resolveSkillFromClawHub(identifier, {
|
|
2216
|
+
registryBaseUrl: options.clawhubRegistry,
|
|
2217
|
+
version: options.version,
|
|
2218
|
+
skillNameOverride: options.skill,
|
|
2219
|
+
requestTimeoutMs: options.githubTimeoutMs,
|
|
2220
|
+
retries: options.githubRetries,
|
|
2221
|
+
retryBaseDelayMs: options.githubRetryBaseMs,
|
|
2222
|
+
maxFileSizeBytes: options.maxFileBytes,
|
|
2223
|
+
maxAuxFiles: options.maxAuxFiles,
|
|
2224
|
+
maxTotalFiles: options.maxTotalFiles
|
|
2225
|
+
});
|
|
2226
|
+
const scan = scanResolvedSkill(resolved);
|
|
2227
|
+
const baseDecision = calculateRiskScore(scan.findings, {
|
|
2228
|
+
strict: options.strict,
|
|
2229
|
+
trustCredits: 0,
|
|
2230
|
+
hasUnverifiableContent: scan.hasUnverifiableContent
|
|
2231
|
+
});
|
|
2232
|
+
const moderation = resolved.sourceMetadata?.clawhubModeration;
|
|
2233
|
+
const { decision, moderationNote } = alignWithClawHubModeration(baseDecision, moderation);
|
|
2234
|
+
const noteParts = ["ClawHub scan complete."];
|
|
2235
|
+
if (moderationNote) {
|
|
2236
|
+
noteParts.push(moderationNote);
|
|
2237
|
+
}
|
|
2238
|
+
if (decision.level === "UNSAFE" || decision.level === "CRITICAL" || decision.level === "UNVERIFIABLE") {
|
|
2239
|
+
noteParts.push("Blocked-level risk detected.");
|
|
2240
|
+
}
|
|
2241
|
+
if (loadedConfig.path) {
|
|
2242
|
+
noteParts.push(`Config: ${loadedConfig.path}`);
|
|
2243
|
+
}
|
|
2244
|
+
const report = {
|
|
2245
|
+
command: "guardskills scan-clawhub",
|
|
2246
|
+
identifier,
|
|
2247
|
+
registry: options.clawhubRegistry,
|
|
2248
|
+
strict: options.strict,
|
|
2249
|
+
configPath: loadedConfig.path ?? void 0,
|
|
2250
|
+
decision,
|
|
2251
|
+
scanFiles: resolved.files.map((file) => file.path),
|
|
2252
|
+
skillDir: resolved.skillDir,
|
|
2253
|
+
repo: `${resolved.owner}/${resolved.repo}`,
|
|
2254
|
+
skill: resolved.skillName,
|
|
2255
|
+
version: options.version,
|
|
2256
|
+
commitSha: resolved.commitSha,
|
|
2257
|
+
moderation,
|
|
2258
|
+
unverifiableReasons: scan.unverifiableReasons,
|
|
2259
|
+
note: noteParts.join(" ")
|
|
2260
|
+
};
|
|
2261
|
+
if (options.json) {
|
|
2262
|
+
printJsonClawHubReport(report);
|
|
2263
|
+
} else {
|
|
2264
|
+
printHumanClawHubReport(report);
|
|
2265
|
+
}
|
|
2266
|
+
return decision.level === "UNSAFE" || decision.level === "CRITICAL" || decision.level === "UNVERIFIABLE" ? 20 : 0;
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
// src/commands/scan-local.ts
|
|
2270
|
+
import fs3 from "fs";
|
|
2271
|
+
import path6 from "path";
|
|
2272
|
+
import { z as z5 } from "zod";
|
|
2273
|
+
var ALLOWED_TEXT_EXTENSIONS4 = /* @__PURE__ */ new Set([
|
|
2274
|
+
".md",
|
|
2275
|
+
".txt",
|
|
2276
|
+
".sh",
|
|
2277
|
+
".bash",
|
|
2278
|
+
".zsh",
|
|
2279
|
+
".ps1",
|
|
2280
|
+
".py",
|
|
2281
|
+
".js",
|
|
2282
|
+
".ts",
|
|
2283
|
+
".mjs",
|
|
2284
|
+
".cjs",
|
|
2285
|
+
".json",
|
|
2286
|
+
".yaml",
|
|
2287
|
+
".yml",
|
|
2288
|
+
".toml",
|
|
2289
|
+
".ini",
|
|
2290
|
+
".cfg"
|
|
2291
|
+
]);
|
|
2292
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".turbo"]);
|
|
2293
|
+
var cliScanLocalOptionsSchema = z5.object({
|
|
2294
|
+
config: z5.string().optional(),
|
|
2295
|
+
strict: z5.boolean().optional(),
|
|
2296
|
+
json: z5.boolean().optional(),
|
|
2297
|
+
skill: z5.string().min(1).optional(),
|
|
2298
|
+
maxFileBytes: z5.coerce.number().int().min(4096).max(5e6).optional(),
|
|
2299
|
+
maxTotalFiles: z5.coerce.number().int().min(1).max(400).optional()
|
|
2300
|
+
});
|
|
2301
|
+
var effectiveScanLocalOptionsSchema = z5.object({
|
|
2302
|
+
strict: z5.boolean(),
|
|
2303
|
+
json: z5.boolean(),
|
|
2304
|
+
skill: z5.string().min(1).optional(),
|
|
2305
|
+
maxFileBytes: z5.number().int().min(4096).max(5e6),
|
|
2306
|
+
maxTotalFiles: z5.number().int().min(1).max(400)
|
|
2307
|
+
});
|
|
2308
|
+
var DEFAULT_OPTIONS4 = {
|
|
2309
|
+
strict: false,
|
|
2310
|
+
json: false,
|
|
2311
|
+
skill: void 0,
|
|
2312
|
+
maxFileBytes: 25e4,
|
|
2313
|
+
maxTotalFiles: 120
|
|
2314
|
+
};
|
|
2315
|
+
function toPosixPath(filePath) {
|
|
2316
|
+
return filePath.replace(/\\/g, "/");
|
|
2317
|
+
}
|
|
2318
|
+
function getNearbyPathSuggestions(targetPath) {
|
|
2319
|
+
const parent = path6.dirname(targetPath);
|
|
2320
|
+
if (!fs3.existsSync(parent)) {
|
|
2321
|
+
return [];
|
|
2322
|
+
}
|
|
2323
|
+
const needle = path6.basename(targetPath).toLowerCase();
|
|
2324
|
+
const suggestions = [];
|
|
2325
|
+
for (const entry of fs3.readdirSync(parent, { withFileTypes: true })) {
|
|
2326
|
+
if (entry.name.toLowerCase().includes(needle)) {
|
|
2327
|
+
suggestions.push(path6.join(parent, entry.name));
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
return suggestions.slice(0, 5);
|
|
2331
|
+
}
|
|
2332
|
+
function isSkillFile(filePath) {
|
|
2333
|
+
return path6.basename(filePath).toLowerCase() === "skill.md";
|
|
2334
|
+
}
|
|
2335
|
+
function isScannableTextFile(filePath) {
|
|
2336
|
+
if (isSkillFile(filePath)) {
|
|
2337
|
+
return true;
|
|
2338
|
+
}
|
|
2339
|
+
const ext = path6.extname(filePath).toLowerCase();
|
|
2340
|
+
return ALLOWED_TEXT_EXTENSIONS4.has(ext);
|
|
2341
|
+
}
|
|
2342
|
+
function findSkillDirs2(rootDir) {
|
|
2343
|
+
const found = /* @__PURE__ */ new Set();
|
|
2344
|
+
const stack = [{ dir: rootDir, depth: 0 }];
|
|
2345
|
+
let seen = 0;
|
|
2346
|
+
while (stack.length > 0) {
|
|
2347
|
+
const current = stack.pop();
|
|
2348
|
+
if (!current) {
|
|
2349
|
+
continue;
|
|
2350
|
+
}
|
|
2351
|
+
seen += 1;
|
|
2352
|
+
if (seen > 5e3) {
|
|
2353
|
+
break;
|
|
2354
|
+
}
|
|
2355
|
+
const skillFile = path6.join(current.dir, "SKILL.md");
|
|
2356
|
+
if (fs3.existsSync(skillFile) && fs3.statSync(skillFile).isFile()) {
|
|
2357
|
+
found.add(current.dir);
|
|
2358
|
+
continue;
|
|
2359
|
+
}
|
|
2360
|
+
if (current.depth >= 8) {
|
|
2361
|
+
continue;
|
|
2362
|
+
}
|
|
2363
|
+
let entries;
|
|
2364
|
+
try {
|
|
2365
|
+
entries = fs3.readdirSync(current.dir, { withFileTypes: true });
|
|
2366
|
+
} catch {
|
|
2367
|
+
continue;
|
|
2368
|
+
}
|
|
2369
|
+
for (const entry of entries) {
|
|
2370
|
+
if (!entry.isDirectory()) {
|
|
2371
|
+
continue;
|
|
2372
|
+
}
|
|
2373
|
+
if (SKIP_DIRS2.has(entry.name)) {
|
|
2374
|
+
continue;
|
|
2375
|
+
}
|
|
2376
|
+
stack.push({ dir: path6.join(current.dir, entry.name), depth: current.depth + 1 });
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
return [...found].sort();
|
|
2380
|
+
}
|
|
2381
|
+
function formatCandidates(candidates) {
|
|
2382
|
+
return candidates.map((candidate) => `- ${toPosixPath(candidate)}`).join("\n");
|
|
2383
|
+
}
|
|
2384
|
+
function resolveSkillDirectory(inputPath, preferredSkillName) {
|
|
2385
|
+
const absoluteInput = path6.resolve(inputPath);
|
|
2386
|
+
if (!fs3.existsSync(absoluteInput)) {
|
|
2387
|
+
const suggestions = getNearbyPathSuggestions(absoluteInput);
|
|
2388
|
+
const suggestionText = suggestions.length > 0 ? `
|
|
2389
|
+
Nearby paths:
|
|
2390
|
+
${formatCandidates(suggestions)}` : "";
|
|
2391
|
+
throw new GuardSkillsError(
|
|
2392
|
+
"INVALID_LOCAL_PATH",
|
|
2393
|
+
`Local path not found: ${toPosixPath(absoluteInput)}${suggestionText}`
|
|
2394
|
+
);
|
|
2395
|
+
}
|
|
2396
|
+
const stat = fs3.statSync(absoluteInput);
|
|
2397
|
+
if (stat.isFile()) {
|
|
2398
|
+
if (!isSkillFile(absoluteInput)) {
|
|
2399
|
+
throw new GuardSkillsError(
|
|
2400
|
+
"INVALID_LOCAL_PATH",
|
|
2401
|
+
"Local scan expects a directory or a SKILL.md file path."
|
|
2402
|
+
);
|
|
2403
|
+
}
|
|
2404
|
+
return {
|
|
2405
|
+
skillDir: path6.dirname(absoluteInput),
|
|
2406
|
+
note: "Using parent directory of provided SKILL.md file."
|
|
2407
|
+
};
|
|
2408
|
+
}
|
|
2409
|
+
const directSkillFile = path6.join(absoluteInput, "SKILL.md");
|
|
2410
|
+
if (fs3.existsSync(directSkillFile) && fs3.statSync(directSkillFile).isFile()) {
|
|
2411
|
+
return { skillDir: absoluteInput };
|
|
2412
|
+
}
|
|
2413
|
+
const discovered = findSkillDirs2(absoluteInput);
|
|
2414
|
+
if (discovered.length === 0) {
|
|
2415
|
+
throw new GuardSkillsError(
|
|
2416
|
+
"INVALID_LOCAL_PATH",
|
|
2417
|
+
`No SKILL.md found under: ${toPosixPath(absoluteInput)}`
|
|
2418
|
+
);
|
|
2419
|
+
}
|
|
2420
|
+
if (preferredSkillName) {
|
|
2421
|
+
const matches = discovered.filter(
|
|
2422
|
+
(directory) => path6.basename(directory).toLowerCase() === preferredSkillName.toLowerCase()
|
|
2423
|
+
);
|
|
2424
|
+
if (matches.length === 1) {
|
|
2425
|
+
const selected = matches[0];
|
|
2426
|
+
if (!selected) {
|
|
2427
|
+
throw new GuardSkillsError("INVALID_LOCAL_PATH", "Unable to resolve selected local skill.");
|
|
2428
|
+
}
|
|
2429
|
+
return {
|
|
2430
|
+
skillDir: selected,
|
|
2431
|
+
note: `Auto-selected skill '${preferredSkillName}' under the provided path.`
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
const available = discovered.map((directory) => path6.basename(directory));
|
|
2435
|
+
throw new GuardSkillsError(
|
|
2436
|
+
"INVALID_LOCAL_PATH",
|
|
2437
|
+
`Requested --skill '${preferredSkillName}' was not found.
|
|
2438
|
+
Available skills: ${available.join(", ")}`
|
|
2439
|
+
);
|
|
2440
|
+
}
|
|
2441
|
+
if (discovered.length === 1) {
|
|
2442
|
+
const selected = discovered[0];
|
|
2443
|
+
if (!selected) {
|
|
2444
|
+
throw new GuardSkillsError("INVALID_LOCAL_PATH", "Unable to resolve discovered local skill.");
|
|
2445
|
+
}
|
|
2446
|
+
return {
|
|
2447
|
+
skillDir: selected,
|
|
2448
|
+
note: "Auto-selected the only SKILL.md found under the provided path."
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
throw new GuardSkillsError(
|
|
2452
|
+
"INVALID_LOCAL_PATH",
|
|
2453
|
+
`Multiple skills found under path. Provide --skill <name>.
|
|
2454
|
+
${formatCandidates(discovered)}`
|
|
2455
|
+
);
|
|
2456
|
+
}
|
|
2457
|
+
function collectLocalFiles2(skillDir, options) {
|
|
2458
|
+
const files = [];
|
|
2459
|
+
const unverifiableReasons = [];
|
|
2460
|
+
const stack = [skillDir];
|
|
2461
|
+
while (stack.length > 0) {
|
|
2462
|
+
const currentDir = stack.pop();
|
|
2463
|
+
if (!currentDir) {
|
|
2464
|
+
continue;
|
|
2465
|
+
}
|
|
2466
|
+
let entries;
|
|
2467
|
+
try {
|
|
2468
|
+
entries = fs3.readdirSync(currentDir, { withFileTypes: true });
|
|
2469
|
+
} catch {
|
|
2470
|
+
unverifiableReasons.push(`Cannot read directory: ${toPosixPath(currentDir)}`);
|
|
2471
|
+
continue;
|
|
2472
|
+
}
|
|
2473
|
+
for (const entry of entries) {
|
|
2474
|
+
const fullPath = path6.join(currentDir, entry.name);
|
|
2475
|
+
if (entry.isDirectory()) {
|
|
2476
|
+
if (!SKIP_DIRS2.has(entry.name)) {
|
|
2477
|
+
stack.push(fullPath);
|
|
2478
|
+
}
|
|
2479
|
+
continue;
|
|
2480
|
+
}
|
|
2481
|
+
if (!entry.isFile()) {
|
|
2482
|
+
continue;
|
|
2483
|
+
}
|
|
2484
|
+
const relativePath = toPosixPath(path6.relative(skillDir, fullPath));
|
|
2485
|
+
if (!isScannableTextFile(relativePath)) {
|
|
2486
|
+
continue;
|
|
2487
|
+
}
|
|
2488
|
+
if (files.length >= options.maxTotalFiles) {
|
|
2489
|
+
unverifiableReasons.push(
|
|
2490
|
+
`Reached maxTotalFiles=${options.maxTotalFiles}. Remaining files were not scanned.`
|
|
2491
|
+
);
|
|
2492
|
+
return { files, unverifiableReasons };
|
|
2493
|
+
}
|
|
2494
|
+
let sizeBytes = 0;
|
|
2495
|
+
try {
|
|
2496
|
+
sizeBytes = fs3.statSync(fullPath).size;
|
|
2497
|
+
} catch {
|
|
2498
|
+
unverifiableReasons.push(`Cannot stat file: ${relativePath}`);
|
|
2499
|
+
continue;
|
|
2500
|
+
}
|
|
2501
|
+
if (sizeBytes > options.maxFileBytes) {
|
|
2502
|
+
unverifiableReasons.push(
|
|
2503
|
+
`Skipped oversized file (${sizeBytes} bytes > ${options.maxFileBytes}): ${relativePath}`
|
|
2504
|
+
);
|
|
2505
|
+
continue;
|
|
2506
|
+
}
|
|
2507
|
+
try {
|
|
2508
|
+
files.push({
|
|
2509
|
+
path: relativePath,
|
|
2510
|
+
content: fs3.readFileSync(fullPath, "utf8")
|
|
2511
|
+
});
|
|
2512
|
+
} catch {
|
|
2513
|
+
unverifiableReasons.push(`Cannot read text content: ${relativePath}`);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
return { files, unverifiableReasons };
|
|
2518
|
+
}
|
|
2519
|
+
function resolveEffectiveScanLocalOptions(cliOptions, config) {
|
|
2520
|
+
const defaults = config.defaults ?? {};
|
|
2521
|
+
const resolver = config.resolver ?? {};
|
|
2522
|
+
return effectiveScanLocalOptionsSchema.parse({
|
|
2523
|
+
strict: cliOptions.strict ?? defaults.strict ?? DEFAULT_OPTIONS4.strict,
|
|
2524
|
+
json: cliOptions.json ?? defaults.json ?? DEFAULT_OPTIONS4.json,
|
|
2525
|
+
skill: cliOptions.skill ?? DEFAULT_OPTIONS4.skill,
|
|
2526
|
+
maxFileBytes: cliOptions.maxFileBytes ?? resolver.maxFileBytes ?? DEFAULT_OPTIONS4.maxFileBytes,
|
|
2527
|
+
maxTotalFiles: cliOptions.maxTotalFiles ?? resolver.maxTotalFiles ?? DEFAULT_OPTIONS4.maxTotalFiles
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
async function runScanLocalCommand(inputPath, rawOptions) {
|
|
2531
|
+
const cliOptions = cliScanLocalOptionsSchema.parse(rawOptions);
|
|
2532
|
+
const loadedConfig = loadGuardSkillsConfig(cliOptions.config);
|
|
2533
|
+
const options = resolveEffectiveScanLocalOptions(cliOptions, loadedConfig.config);
|
|
2534
|
+
const target = resolveSkillDirectory(inputPath, options.skill);
|
|
2535
|
+
const { files, unverifiableReasons } = collectLocalFiles2(target.skillDir, options);
|
|
2536
|
+
if (files.length === 0) {
|
|
2537
|
+
throw new GuardSkillsError(
|
|
2538
|
+
"INVALID_LOCAL_PATH",
|
|
2539
|
+
`No scannable text files found in: ${toPosixPath(target.skillDir)}`
|
|
2540
|
+
);
|
|
2541
|
+
}
|
|
2542
|
+
const resolvedSkill = {
|
|
2543
|
+
source: `local:${toPosixPath(target.skillDir)}`,
|
|
2544
|
+
owner: "local",
|
|
2545
|
+
repo: "local",
|
|
2546
|
+
defaultBranch: "local",
|
|
2547
|
+
commitSha: "local",
|
|
2548
|
+
skillName: options.skill ?? path6.basename(target.skillDir),
|
|
2549
|
+
skillDir: toPosixPath(target.skillDir),
|
|
2550
|
+
skillFilePath: "SKILL.md",
|
|
2551
|
+
files,
|
|
2552
|
+
unverifiableReasons
|
|
2553
|
+
};
|
|
2554
|
+
const scan = scanResolvedSkill(resolvedSkill);
|
|
2555
|
+
const decision = calculateRiskScore(scan.findings, {
|
|
2556
|
+
strict: options.strict,
|
|
2557
|
+
trustCredits: 0,
|
|
2558
|
+
hasUnverifiableContent: scan.hasUnverifiableContent
|
|
2559
|
+
});
|
|
2560
|
+
const noteParts = ["Local scan complete."];
|
|
2561
|
+
if (target.note) {
|
|
2562
|
+
noteParts.push(target.note);
|
|
2563
|
+
}
|
|
2564
|
+
if (decision.level === "UNSAFE" || decision.level === "CRITICAL" || decision.level === "UNVERIFIABLE") {
|
|
2565
|
+
noteParts.push("Blocked-level risk detected.");
|
|
2566
|
+
}
|
|
2567
|
+
if (loadedConfig.path) {
|
|
2568
|
+
noteParts.push(`Config: ${loadedConfig.path}`);
|
|
2569
|
+
}
|
|
2570
|
+
const report = {
|
|
2571
|
+
command: "guardskills scan-local",
|
|
2572
|
+
inputPath,
|
|
2573
|
+
strict: options.strict,
|
|
2574
|
+
configPath: loadedConfig.path ?? void 0,
|
|
2575
|
+
decision,
|
|
2576
|
+
scanFiles: resolvedSkill.files.map((file) => file.path),
|
|
2577
|
+
skillDir: resolvedSkill.skillDir,
|
|
2578
|
+
unverifiableReasons: scan.unverifiableReasons,
|
|
2579
|
+
note: noteParts.join(" ")
|
|
2580
|
+
};
|
|
2581
|
+
if (options.json) {
|
|
2582
|
+
printJsonLocalReport(report);
|
|
2583
|
+
} else {
|
|
2584
|
+
printHumanLocalReport(report);
|
|
2585
|
+
}
|
|
2586
|
+
return decision.level === "UNSAFE" || decision.level === "CRITICAL" || decision.level === "UNVERIFIABLE" ? 20 : 0;
|
|
1467
2587
|
}
|
|
1468
2588
|
|
|
1469
2589
|
// src/cli.ts
|
|
2590
|
+
function withCommonAddOptions(command, requireSkill) {
|
|
2591
|
+
const configured = command.option("--config <path>", "Path to guardskills.config.json").option("--strict", "Use stricter risk thresholds").option("--ci", "Deterministic CI mode: scan + gate only, no install handoff").option("--json", "Output machine-readable JSON").option("--yes", "Auto-confirm warnings").option("--dry-run", "Scan only, do not install").option("--force", "Override UNSAFE outcome").option("--allow-unverifiable", "Override UNVERIFIABLE outcome").option("--github-timeout-ms <ms>", "GitHub API request timeout in milliseconds").option("--github-retries <count>", "Retry count for retryable GitHub errors").option("--github-retry-base-ms <ms>", "Base backoff delay for GitHub retries").option("--max-file-bytes <bytes>", "Max file size to scan").option("--max-aux-files <count>", "Max auxiliary files from scripts/src folders").option("--max-total-files <count>", "Max total resolved files to scan");
|
|
2592
|
+
return requireSkill ? configured.requiredOption("--skill <name>", "Skill name to install") : configured.option("--skill <name>", "Skill name to install");
|
|
2593
|
+
}
|
|
1470
2594
|
async function main() {
|
|
1471
2595
|
const program = new Command();
|
|
1472
|
-
program.name("guardskills").description("Security wrapper around
|
|
1473
|
-
program.command("add").description("Scan a skill source and conditionally install it via skills CLI").argument("<repo>", "GitHub repository URL or owner/repo")
|
|
1474
|
-
|
|
2596
|
+
program.name("guardskills").description("Security wrapper around skill installation CLIs").version("1.1.0");
|
|
2597
|
+
const legacyAdd = program.command("add").description("Scan a skill source and conditionally install it via skills CLI").argument("<repo>", "GitHub repository URL or owner/repo");
|
|
2598
|
+
withCommonAddOptions(legacyAdd, true).action(
|
|
2599
|
+
async (repo, options) => {
|
|
2600
|
+
const code = await runAddCommand(repo, options);
|
|
2601
|
+
process.exitCode = code;
|
|
2602
|
+
}
|
|
2603
|
+
);
|
|
2604
|
+
const skills = program.command("skills").description("Guarded wrapper for skills.sh install commands");
|
|
2605
|
+
withCommonAddOptions(
|
|
2606
|
+
skills.command("add").description("Scan a skill source and conditionally install it via skills CLI").argument("<repo>", "GitHub repository URL or owner/repo"),
|
|
2607
|
+
false
|
|
2608
|
+
).action(async (repo, options) => {
|
|
2609
|
+
const resolvedSkill = typeof options.skill === "string" ? options.skill : void 0;
|
|
2610
|
+
const code = resolvedSkill ? await runAddCommand(repo, options, {
|
|
2611
|
+
provider: "skills",
|
|
2612
|
+
commandName: "guardskills skills add"
|
|
2613
|
+
}) : await runInteractiveInstallCommand("skills", repo, void 0, options);
|
|
2614
|
+
process.exitCode = code;
|
|
2615
|
+
});
|
|
2616
|
+
const playbooks = program.command("playbooks").description("Guarded wrapper for Playbooks install commands");
|
|
2617
|
+
withCommonAddOptions(
|
|
2618
|
+
playbooks.command("add").description("Scan a skill source and conditionally install it via Playbooks CLI").argument("<resource>", "Playbooks resource type (must be 'skill')").argument("<repo>", "GitHub repository URL or owner/repo"),
|
|
2619
|
+
true
|
|
2620
|
+
).action(async (resource, repo, options) => {
|
|
2621
|
+
if (resource !== "skill") {
|
|
2622
|
+
throw new GuardSkillsError(
|
|
2623
|
+
"INVALID_OPTIONS",
|
|
2624
|
+
`Unsupported playbooks resource '${resource}'. Use: guardskills playbooks add skill <repo> --skill <name>`
|
|
2625
|
+
);
|
|
2626
|
+
}
|
|
2627
|
+
const code = await runAddCommand(repo, options, {
|
|
2628
|
+
provider: "playbooks",
|
|
2629
|
+
commandName: "guardskills playbooks add skill"
|
|
2630
|
+
});
|
|
2631
|
+
process.exitCode = code;
|
|
2632
|
+
});
|
|
2633
|
+
const openskills = program.command("openskills").description("Guarded wrapper for OpenSkills install commands");
|
|
2634
|
+
withCommonAddOptions(
|
|
2635
|
+
openskills.command("install").description("Scan a skill source and conditionally install it via OpenSkills CLI").argument("<repo>", "GitHub repository URL or owner/repo").argument("[skill]", "Skill name to install"),
|
|
2636
|
+
false
|
|
2637
|
+
).action(async (repo, skill, options) => {
|
|
2638
|
+
const resolvedSkill = skill ?? (typeof options.skill === "string" ? options.skill : void 0);
|
|
2639
|
+
const code = await runInteractiveInstallCommand("openskills", repo, resolvedSkill, options);
|
|
2640
|
+
process.exitCode = code;
|
|
2641
|
+
});
|
|
2642
|
+
const skillkit = program.command("skillkit").description("Guarded wrapper for skillkit install commands");
|
|
2643
|
+
withCommonAddOptions(
|
|
2644
|
+
skillkit.command("install").description("Scan a skill source and conditionally install it via skillkit CLI").argument("<repo>", "GitHub repository URL or owner/repo").argument("[skill]", "Skill name to install"),
|
|
2645
|
+
false
|
|
2646
|
+
).action(async (repo, skill, options) => {
|
|
2647
|
+
const resolvedSkill = skill ?? (typeof options.skill === "string" ? options.skill : void 0);
|
|
2648
|
+
const code = await runInteractiveInstallCommand("skillkit", repo, resolvedSkill, options);
|
|
2649
|
+
process.exitCode = code;
|
|
2650
|
+
});
|
|
2651
|
+
program.command("scan-local").description("Scan a local skill folder and print a risk decision").argument("<path>", "Local folder path (or SKILL.md file path)").option("--skill <name>", "Skill directory name when path contains multiple skills").option("--config <path>", "Path to guardskills.config.json").option("--strict", "Use stricter risk thresholds").option("--json", "Output machine-readable JSON").option("--max-file-bytes <bytes>", "Max file size to scan").option("--max-total-files <count>", "Max total files to scan").action(async (repo, options) => {
|
|
2652
|
+
const code = await runScanLocalCommand(repo, options);
|
|
1475
2653
|
process.exitCode = code;
|
|
1476
2654
|
});
|
|
1477
|
-
program.command("scan-
|
|
1478
|
-
const code = await
|
|
2655
|
+
program.command("scan-clawhub").description("Scan a ClawHub skill package and print a risk decision").argument("<identifier>", "ClawHub package identifier").option("--skill <name>", "Override skill folder name to resolve in source repository").option("--version <version>", "Preferred package version/tag").option("--clawhub-registry <url>", "ClawHub registry base URL").option("--config <path>", "Path to guardskills.config.json").option("--strict", "Use stricter risk thresholds").option("--json", "Output machine-readable JSON").option("--github-timeout-ms <ms>", "Upstream resolver request timeout in milliseconds").option("--github-retries <count>", "Retry count for retryable upstream errors").option("--github-retry-base-ms <ms>", "Base backoff delay for upstream retries").option("--max-file-bytes <bytes>", "Max file size to scan").option("--max-aux-files <count>", "Max auxiliary files from scripts/src folders").option("--max-total-files <count>", "Max total resolved files to scan").action(async (identifier, options) => {
|
|
2656
|
+
const code = await runScanClawHubCommand(identifier, options);
|
|
1479
2657
|
process.exitCode = code;
|
|
1480
2658
|
});
|
|
1481
2659
|
await program.parseAsync(process.argv);
|