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