guardskills 0.1.0-alpha.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +184 -28
- package/dist/cli.cjs +791 -28
- package/dist/cli.js +791 -28
- package/package.json +2 -1
package/dist/cli.cjs
CHANGED
|
@@ -308,6 +308,73 @@ function printHumanLocalReport(report) {
|
|
|
308
308
|
function printJsonLocalReport(report) {
|
|
309
309
|
console.log(JSON.stringify(report, null, 2));
|
|
310
310
|
}
|
|
311
|
+
function printHumanClawHubReport(report) {
|
|
312
|
+
console.log(`Command: ${report.command}`);
|
|
313
|
+
console.log(`Identifier: ${report.identifier}`);
|
|
314
|
+
console.log(`Registry: ${report.registry}`);
|
|
315
|
+
console.log(`Mapped Repo: ${report.repo}`);
|
|
316
|
+
console.log(`Skill: ${report.skill}`);
|
|
317
|
+
if (report.version) {
|
|
318
|
+
console.log(`Version: ${report.version}`);
|
|
319
|
+
}
|
|
320
|
+
console.log(`Mode: ${report.strict ? "strict" : "standard"}`);
|
|
321
|
+
if (report.configPath) {
|
|
322
|
+
console.log(`Config: ${report.configPath}`);
|
|
323
|
+
}
|
|
324
|
+
if (report.skillDir) {
|
|
325
|
+
console.log(`Skill Dir: ${report.skillDir}`);
|
|
326
|
+
}
|
|
327
|
+
if (report.commitSha) {
|
|
328
|
+
console.log(`Commit: ${report.commitSha}`);
|
|
329
|
+
}
|
|
330
|
+
if (report.moderation) {
|
|
331
|
+
const parts = [];
|
|
332
|
+
if (report.moderation.isSuspicious) {
|
|
333
|
+
parts.push("suspicious");
|
|
334
|
+
}
|
|
335
|
+
if (report.moderation.isMalwareBlocked) {
|
|
336
|
+
parts.push("malware-blocked");
|
|
337
|
+
}
|
|
338
|
+
if (report.moderation.isRemoved) {
|
|
339
|
+
parts.push("removed");
|
|
340
|
+
}
|
|
341
|
+
if (parts.length > 0) {
|
|
342
|
+
console.log(`ClawHub Moderation: ${parts.join(", ")}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
console.log(`Files Scanned: ${report.scanFiles.length}`);
|
|
346
|
+
if (report.decision.riskScore === null) {
|
|
347
|
+
console.log("Result: UNVERIFIABLE");
|
|
348
|
+
} else {
|
|
349
|
+
console.log(`Risk Score: ${report.decision.riskScore.toFixed(1)}/100`);
|
|
350
|
+
console.log(`Decision: ${report.decision.level}`);
|
|
351
|
+
}
|
|
352
|
+
if (report.unverifiableReasons && report.unverifiableReasons.length > 0) {
|
|
353
|
+
console.log("Unverifiable Reasons:");
|
|
354
|
+
for (const reason of report.unverifiableReasons) {
|
|
355
|
+
console.log(`- ${reason}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (report.decision.chainMatches.length > 0) {
|
|
359
|
+
console.log("Attack Chains:");
|
|
360
|
+
for (const chain of report.decision.chainMatches) {
|
|
361
|
+
console.log(`- ${chain.id} (+${chain.bonus}): ${chain.description}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (report.decision.findings.length > 0) {
|
|
365
|
+
console.log("Findings:");
|
|
366
|
+
for (const finding of report.decision.findings.slice(0, 10)) {
|
|
367
|
+
const fileText = finding.file ? ` (${finding.file})` : "";
|
|
368
|
+
console.log(`- [${finding.severity}/${finding.confidence}] ${finding.title}${fileText}`);
|
|
369
|
+
}
|
|
370
|
+
} else {
|
|
371
|
+
console.log("Findings: none");
|
|
372
|
+
}
|
|
373
|
+
console.log(`Note: ${report.note}`);
|
|
374
|
+
}
|
|
375
|
+
function printJsonClawHubReport(report) {
|
|
376
|
+
console.log(JSON.stringify(report, null, 2));
|
|
377
|
+
}
|
|
311
378
|
|
|
312
379
|
// src/resolver/github.ts
|
|
313
380
|
var import_node_path2 = __toESM(require("path"), 1);
|
|
@@ -1170,10 +1237,18 @@ async function runAddCommand(repo, rawOptions) {
|
|
|
1170
1237
|
return runSkillsInstall(repo, options.skill);
|
|
1171
1238
|
}
|
|
1172
1239
|
|
|
1173
|
-
// src/commands/scan-
|
|
1174
|
-
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
1175
|
-
var import_node_path4 = __toESM(require("path"), 1);
|
|
1240
|
+
// src/commands/scan-clawhub.ts
|
|
1176
1241
|
var import_zod3 = require("zod");
|
|
1242
|
+
|
|
1243
|
+
// src/resolver/clawhub.ts
|
|
1244
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
1245
|
+
var import_jszip = __toESM(require("jszip"), 1);
|
|
1246
|
+
var DEFAULT_REGISTRY_BASE_URL = "https://clawhub.ai";
|
|
1247
|
+
var DEFAULT_ARCHIVE_BASE_URL = "https://auth.clawdhub.com";
|
|
1248
|
+
var DEFAULT_REQUEST_TIMEOUT_MS2 = 15e3;
|
|
1249
|
+
var DEFAULT_MAX_FILE_SIZE_BYTES = 25e4;
|
|
1250
|
+
var DEFAULT_MAX_TOTAL_FILES = 120;
|
|
1251
|
+
var RETRYABLE_STATUS2 = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
1177
1252
|
var ALLOWED_TEXT_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
1178
1253
|
".md",
|
|
1179
1254
|
".txt",
|
|
@@ -1193,23 +1268,707 @@ var ALLOWED_TEXT_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
|
1193
1268
|
".ini",
|
|
1194
1269
|
".cfg"
|
|
1195
1270
|
]);
|
|
1196
|
-
|
|
1197
|
-
|
|
1271
|
+
function normalizeRegistryBaseUrl(input) {
|
|
1272
|
+
const raw = input?.trim() || DEFAULT_REGISTRY_BASE_URL;
|
|
1273
|
+
const withProtocol = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
|
|
1274
|
+
const url = new URL(withProtocol);
|
|
1275
|
+
const pathname = url.pathname.endsWith("/") ? url.pathname.slice(0, -1) : url.pathname;
|
|
1276
|
+
return `${url.origin}${pathname}`;
|
|
1277
|
+
}
|
|
1278
|
+
function toApiCandidates(registryBaseUrl, identifier) {
|
|
1279
|
+
const encodedIdentifier = encodeURIComponent(identifier);
|
|
1280
|
+
const slugOnly = identifier.split("/").filter(Boolean).at(-1) ?? identifier;
|
|
1281
|
+
const encodedSlug = encodeURIComponent(slugOnly);
|
|
1282
|
+
return [
|
|
1283
|
+
`${registryBaseUrl}/api/v1/package/${encodedIdentifier}`,
|
|
1284
|
+
`${registryBaseUrl}/api/package/${encodedIdentifier}`,
|
|
1285
|
+
`${registryBaseUrl}/api/v1/skills/${encodedSlug}`
|
|
1286
|
+
];
|
|
1287
|
+
}
|
|
1288
|
+
function tryParseClawHubUrl(input) {
|
|
1289
|
+
try {
|
|
1290
|
+
const parsed = new URL(input.trim());
|
|
1291
|
+
const host = parsed.hostname.toLowerCase();
|
|
1292
|
+
if (host !== "clawhub.ai" && host !== "www.clawhub.ai") {
|
|
1293
|
+
return null;
|
|
1294
|
+
}
|
|
1295
|
+
return parsed;
|
|
1296
|
+
} catch {
|
|
1297
|
+
return null;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
function extractIdentifierCandidates(input) {
|
|
1301
|
+
const trimmed = input.trim();
|
|
1302
|
+
if (!trimmed) {
|
|
1303
|
+
return [];
|
|
1304
|
+
}
|
|
1305
|
+
const candidates = [];
|
|
1306
|
+
const pushUnique = (value) => {
|
|
1307
|
+
if (!value) {
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
const normalized = value.trim();
|
|
1311
|
+
if (!normalized) {
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
if (!candidates.includes(normalized)) {
|
|
1315
|
+
candidates.push(normalized);
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
try {
|
|
1319
|
+
const parsed = new URL(trimmed);
|
|
1320
|
+
const host = parsed.hostname.toLowerCase();
|
|
1321
|
+
if (host !== "clawhub.ai" && host !== "www.clawhub.ai") {
|
|
1322
|
+
return [trimmed];
|
|
1323
|
+
}
|
|
1324
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
1325
|
+
if (segments.length >= 2 && segments[0] && segments[1]) {
|
|
1326
|
+
pushUnique(`${segments[0]}/${segments[1]}`);
|
|
1327
|
+
}
|
|
1328
|
+
if (segments.length >= 3 && segments[0]?.toLowerCase() === "skills" && segments[1] && segments[2]) {
|
|
1329
|
+
pushUnique(`${segments[1]}/${segments[2]}`);
|
|
1330
|
+
}
|
|
1331
|
+
if (segments.length > 0) {
|
|
1332
|
+
pushUnique(segments.join("/"));
|
|
1333
|
+
const last = segments.at(-1);
|
|
1334
|
+
pushUnique(last);
|
|
1335
|
+
}
|
|
1336
|
+
} catch {
|
|
1337
|
+
pushUnique(trimmed);
|
|
1338
|
+
}
|
|
1339
|
+
return candidates;
|
|
1340
|
+
}
|
|
1341
|
+
function mapClawHubError(error, operation) {
|
|
1342
|
+
if (error instanceof GuardSkillsError) {
|
|
1343
|
+
return error;
|
|
1344
|
+
}
|
|
1345
|
+
const status = typeof error === "object" && error !== null && "status" in error ? error.status : void 0;
|
|
1346
|
+
const message = typeof error === "object" && error !== null && "message" in error ? String(error.message) : String(error);
|
|
1347
|
+
const lowerMessage = message.toLowerCase();
|
|
1348
|
+
if (status === 401 || status === 403) {
|
|
1349
|
+
return new GuardSkillsError(
|
|
1350
|
+
"CLAWHUB_AUTH",
|
|
1351
|
+
`${operation} failed: authentication/authorization error from ClawHub.`,
|
|
1352
|
+
{ status, cause: error }
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
if (status === 404) {
|
|
1356
|
+
return new GuardSkillsError("CLAWHUB_NOT_FOUND", `${operation} failed: resource not found.`, {
|
|
1357
|
+
status,
|
|
1358
|
+
cause: error
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
if (status !== void 0 && RETRYABLE_STATUS2.has(status)) {
|
|
1362
|
+
return new GuardSkillsError(
|
|
1363
|
+
status === 429 ? "CLAWHUB_RATE_LIMIT" : "CLAWHUB_TRANSIENT",
|
|
1364
|
+
`${operation} failed with retryable ClawHub status ${status}.`,
|
|
1365
|
+
{ status, retryable: true, cause: error }
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
if (lowerMessage.includes("timeout") || lowerMessage.includes("timed out") || lowerMessage.includes("abort")) {
|
|
1369
|
+
return new GuardSkillsError("CLAWHUB_TIMEOUT", `${operation} timed out while calling ClawHub API.`, {
|
|
1370
|
+
retryable: true,
|
|
1371
|
+
cause: error
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
return new GuardSkillsError("CLAWHUB_UNKNOWN", `${operation} failed: ${message}`, {
|
|
1375
|
+
status,
|
|
1376
|
+
cause: error
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
function unwrapResponsePayload(payload) {
|
|
1380
|
+
if (!payload || typeof payload !== "object") {
|
|
1381
|
+
throw new GuardSkillsError("CLAWHUB_UNKNOWN", "ClawHub returned a non-object JSON payload.");
|
|
1382
|
+
}
|
|
1383
|
+
const objectPayload = payload;
|
|
1384
|
+
if (objectPayload.data && typeof objectPayload.data === "object") {
|
|
1385
|
+
return objectPayload.data;
|
|
1386
|
+
}
|
|
1387
|
+
return objectPayload;
|
|
1388
|
+
}
|
|
1389
|
+
async function fetchJson(url, timeoutMs) {
|
|
1390
|
+
const controller = new AbortController();
|
|
1391
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1392
|
+
try {
|
|
1393
|
+
const response = await fetch(url, {
|
|
1394
|
+
method: "GET",
|
|
1395
|
+
headers: { accept: "application/json" },
|
|
1396
|
+
signal: controller.signal
|
|
1397
|
+
});
|
|
1398
|
+
if (!response.ok) {
|
|
1399
|
+
throw { status: response.status, message: `${response.status} ${response.statusText}` };
|
|
1400
|
+
}
|
|
1401
|
+
const payload = await response.json();
|
|
1402
|
+
return unwrapResponsePayload(payload);
|
|
1403
|
+
} catch (error) {
|
|
1404
|
+
throw mapClawHubError(error, `ClawHub metadata fetch (${url})`);
|
|
1405
|
+
} finally {
|
|
1406
|
+
clearTimeout(timeout);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
async function fetchText(url, timeoutMs) {
|
|
1410
|
+
const controller = new AbortController();
|
|
1411
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1412
|
+
try {
|
|
1413
|
+
const response = await fetch(url, {
|
|
1414
|
+
method: "GET",
|
|
1415
|
+
headers: { accept: "text/html,application/xhtml+xml" },
|
|
1416
|
+
signal: controller.signal
|
|
1417
|
+
});
|
|
1418
|
+
if (!response.ok) {
|
|
1419
|
+
throw { status: response.status, message: `${response.status} ${response.statusText}` };
|
|
1420
|
+
}
|
|
1421
|
+
return await response.text();
|
|
1422
|
+
} catch (error) {
|
|
1423
|
+
throw mapClawHubError(error, `ClawHub page fetch (${url})`);
|
|
1424
|
+
} finally {
|
|
1425
|
+
clearTimeout(timeout);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
async function fetchArrayBuffer(url, timeoutMs) {
|
|
1429
|
+
const controller = new AbortController();
|
|
1430
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1431
|
+
try {
|
|
1432
|
+
const response = await fetch(url, {
|
|
1433
|
+
method: "GET",
|
|
1434
|
+
signal: controller.signal
|
|
1435
|
+
});
|
|
1436
|
+
if (!response.ok) {
|
|
1437
|
+
throw { status: response.status, message: `${response.status} ${response.statusText}` };
|
|
1438
|
+
}
|
|
1439
|
+
return await response.arrayBuffer();
|
|
1440
|
+
} catch (error) {
|
|
1441
|
+
throw mapClawHubError(error, `ClawHub archive fetch (${url})`);
|
|
1442
|
+
} finally {
|
|
1443
|
+
clearTimeout(timeout);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
function normalizeRepoRef(candidate) {
|
|
1447
|
+
const trimmed = candidate.trim();
|
|
1448
|
+
if (!trimmed) {
|
|
1449
|
+
return null;
|
|
1450
|
+
}
|
|
1451
|
+
const shorthand = trimmed.match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/);
|
|
1452
|
+
if (shorthand && shorthand[1] && shorthand[2]) {
|
|
1453
|
+
return `${shorthand[1]}/${shorthand[2].replace(/\.git$/i, "")}`;
|
|
1454
|
+
}
|
|
1455
|
+
try {
|
|
1456
|
+
const parsed = new URL(trimmed);
|
|
1457
|
+
if (!(parsed.hostname === "github.com" || parsed.hostname === "www.github.com")) {
|
|
1458
|
+
return null;
|
|
1459
|
+
}
|
|
1460
|
+
const parts = parsed.pathname.split("/").filter(Boolean);
|
|
1461
|
+
if (parts.length < 2 || !parts[0] || !parts[1]) {
|
|
1462
|
+
return null;
|
|
1463
|
+
}
|
|
1464
|
+
return `${parts[0]}/${parts[1].replace(/\.git$/i, "")}`;
|
|
1465
|
+
} catch {
|
|
1466
|
+
return null;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
function getNestedString(obj, ...keys) {
|
|
1470
|
+
let cursor = obj;
|
|
1471
|
+
for (const key of keys) {
|
|
1472
|
+
if (!cursor || typeof cursor !== "object" || !(key in cursor)) {
|
|
1473
|
+
return void 0;
|
|
1474
|
+
}
|
|
1475
|
+
cursor = cursor[key];
|
|
1476
|
+
}
|
|
1477
|
+
return typeof cursor === "string" && cursor.trim() ? cursor.trim() : void 0;
|
|
1478
|
+
}
|
|
1479
|
+
function getNestedBoolean(obj, ...keys) {
|
|
1480
|
+
let cursor = obj;
|
|
1481
|
+
for (const key of keys) {
|
|
1482
|
+
if (!cursor || typeof cursor !== "object" || !(key in cursor)) {
|
|
1483
|
+
return void 0;
|
|
1484
|
+
}
|
|
1485
|
+
cursor = cursor[key];
|
|
1486
|
+
}
|
|
1487
|
+
return typeof cursor === "boolean" ? cursor : void 0;
|
|
1488
|
+
}
|
|
1489
|
+
function isLikelyTextFile2(filePath) {
|
|
1490
|
+
const lower = filePath.toLowerCase();
|
|
1491
|
+
if (lower.endsWith("/skill.md") || lower === "skill.md") {
|
|
1492
|
+
return true;
|
|
1493
|
+
}
|
|
1494
|
+
const ext = import_node_path4.default.posix.extname(lower);
|
|
1495
|
+
return ALLOWED_TEXT_EXTENSIONS2.has(ext);
|
|
1496
|
+
}
|
|
1497
|
+
function isBinaryContent2(content) {
|
|
1498
|
+
return content.includes("\0");
|
|
1499
|
+
}
|
|
1500
|
+
function collectGitHubRepos(value, collected, seen) {
|
|
1501
|
+
if (value === null || value === void 0) {
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
if (typeof value === "string") {
|
|
1505
|
+
const repo = normalizeRepoRef(value);
|
|
1506
|
+
if (repo) {
|
|
1507
|
+
collected.add(repo);
|
|
1508
|
+
}
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
if (typeof value !== "object") {
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
if (seen.has(value)) {
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
seen.add(value);
|
|
1518
|
+
if (Array.isArray(value)) {
|
|
1519
|
+
for (const item of value) {
|
|
1520
|
+
collectGitHubRepos(item, collected, seen);
|
|
1521
|
+
}
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
for (const nested of Object.values(value)) {
|
|
1525
|
+
collectGitHubRepos(nested, collected, seen);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
function collectPossibleSkillNames(identifier, metadata, override) {
|
|
1529
|
+
const names = /* @__PURE__ */ new Set();
|
|
1530
|
+
if (override?.trim()) {
|
|
1531
|
+
names.add(override.trim());
|
|
1532
|
+
}
|
|
1533
|
+
const lastSegment = identifier.split(/[/:@]/).filter(Boolean).at(-1);
|
|
1534
|
+
if (lastSegment) {
|
|
1535
|
+
names.add(lastSegment);
|
|
1536
|
+
}
|
|
1537
|
+
const maybePush = (value) => {
|
|
1538
|
+
if (typeof value === "string" && value.trim()) {
|
|
1539
|
+
names.add(value.trim());
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1542
|
+
maybePush(metadata.skill);
|
|
1543
|
+
maybePush(metadata.slug);
|
|
1544
|
+
maybePush(metadata.name);
|
|
1545
|
+
maybePush(metadata.id);
|
|
1546
|
+
return [...names];
|
|
1547
|
+
}
|
|
1548
|
+
function parseArchiveMetadata(metadata, identifier, requestedVersion) {
|
|
1549
|
+
const ownerFromMetadata = getNestedString(metadata, "owner", "handle") ?? getNestedString(metadata, "owner") ?? getNestedString(metadata, "author") ?? getNestedString(metadata, "publisher");
|
|
1550
|
+
const slugFromMetadata = getNestedString(metadata, "skill", "slug") ?? getNestedString(metadata, "slug") ?? getNestedString(metadata, "name");
|
|
1551
|
+
const versionFromMetadata = requestedVersion ?? getNestedString(metadata, "latestVersion", "version") ?? getNestedString(metadata, "version");
|
|
1552
|
+
const identifierParts = identifier.split("/").filter(Boolean);
|
|
1553
|
+
const ownerFromIdentifier = identifierParts.length >= 2 ? identifierParts[0] : void 0;
|
|
1554
|
+
const slugFromIdentifier = identifierParts.length > 0 ? identifierParts.at(-1) : void 0;
|
|
1555
|
+
const owner = ownerFromMetadata ?? ownerFromIdentifier;
|
|
1556
|
+
const slug = slugFromMetadata ?? slugFromIdentifier;
|
|
1557
|
+
if (!owner || !slug) {
|
|
1558
|
+
return null;
|
|
1559
|
+
}
|
|
1560
|
+
return { owner, slug, version: versionFromMetadata };
|
|
1561
|
+
}
|
|
1562
|
+
function parseClawHubModeration(metadata) {
|
|
1563
|
+
const isSuspicious = getNestedBoolean(metadata, "moderation", "isSuspicious") ?? getNestedBoolean(metadata, "isSuspicious");
|
|
1564
|
+
const isMalwareBlocked = getNestedBoolean(metadata, "moderation", "isMalwareBlocked") ?? getNestedBoolean(metadata, "isMalwareBlocked");
|
|
1565
|
+
const isRemoved = getNestedBoolean(metadata, "moderation", "isRemoved") ?? getNestedBoolean(metadata, "isRemoved");
|
|
1566
|
+
if (isSuspicious === void 0 && isMalwareBlocked === void 0 && isRemoved === void 0) {
|
|
1567
|
+
return void 0;
|
|
1568
|
+
}
|
|
1569
|
+
return { isSuspicious, isMalwareBlocked, isRemoved };
|
|
1570
|
+
}
|
|
1571
|
+
async function resolveSkillFromArchive(metadata, identifier, options) {
|
|
1572
|
+
const archiveMeta = parseArchiveMetadata(metadata, identifier, options.version);
|
|
1573
|
+
if (!archiveMeta) {
|
|
1574
|
+
return null;
|
|
1575
|
+
}
|
|
1576
|
+
const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS2;
|
|
1577
|
+
const maxFileSizeBytes = options.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE_BYTES;
|
|
1578
|
+
const maxTotalFiles = options.maxTotalFiles ?? DEFAULT_MAX_TOTAL_FILES;
|
|
1579
|
+
const params = new URLSearchParams({ slug: archiveMeta.slug });
|
|
1580
|
+
if (archiveMeta.version) {
|
|
1581
|
+
params.set("version", archiveMeta.version);
|
|
1582
|
+
}
|
|
1583
|
+
const downloadUrl = `${DEFAULT_ARCHIVE_BASE_URL}/api/v1/download?${params.toString()}`;
|
|
1584
|
+
const archiveBuffer = await fetchArrayBuffer(downloadUrl, timeoutMs);
|
|
1585
|
+
const zip = await import_jszip.default.loadAsync(Buffer.from(archiveBuffer));
|
|
1586
|
+
const files = new Array();
|
|
1587
|
+
const unverifiableReasons = [];
|
|
1588
|
+
let skillFilePath = null;
|
|
1589
|
+
const entries = Object.values(zip.files);
|
|
1590
|
+
for (const entry of entries) {
|
|
1591
|
+
if (entry.dir) {
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1594
|
+
const normalizedPath = entry.name.replace(/\\/g, "/");
|
|
1595
|
+
if (!isLikelyTextFile2(normalizedPath)) {
|
|
1596
|
+
continue;
|
|
1597
|
+
}
|
|
1598
|
+
if (files.length >= maxTotalFiles) {
|
|
1599
|
+
unverifiableReasons.push(
|
|
1600
|
+
`Resolved archive file count exceeds maxTotalFiles=${maxTotalFiles}; scan truncated.`
|
|
1601
|
+
);
|
|
1602
|
+
break;
|
|
1603
|
+
}
|
|
1604
|
+
const contentBuffer = await entry.async("nodebuffer");
|
|
1605
|
+
if (contentBuffer.length > maxFileSizeBytes) {
|
|
1606
|
+
unverifiableReasons.push(
|
|
1607
|
+
`File too large to scan safely: ${normalizedPath}`
|
|
1608
|
+
);
|
|
1609
|
+
continue;
|
|
1610
|
+
}
|
|
1611
|
+
const content = contentBuffer.toString("utf8");
|
|
1612
|
+
if (isBinaryContent2(content)) {
|
|
1613
|
+
unverifiableReasons.push(`Binary content detected: ${normalizedPath}`);
|
|
1614
|
+
continue;
|
|
1615
|
+
}
|
|
1616
|
+
files.push({ path: normalizedPath, content });
|
|
1617
|
+
if (!skillFilePath && normalizedPath.toLowerCase().endsWith("/skill.md")) {
|
|
1618
|
+
skillFilePath = normalizedPath;
|
|
1619
|
+
}
|
|
1620
|
+
if (!skillFilePath && normalizedPath.toLowerCase() === "skill.md") {
|
|
1621
|
+
skillFilePath = normalizedPath;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
if (!skillFilePath) {
|
|
1625
|
+
skillFilePath = files.find((file) => file.path.toLowerCase().endsWith("skill.md"))?.path ?? null;
|
|
1626
|
+
}
|
|
1627
|
+
if (!skillFilePath || files.length === 0) {
|
|
1628
|
+
return null;
|
|
1629
|
+
}
|
|
1630
|
+
const moderation = parseClawHubModeration(metadata);
|
|
1631
|
+
return {
|
|
1632
|
+
source: `clawhub:${archiveMeta.owner}/${archiveMeta.slug}${archiveMeta.version ? `@${archiveMeta.version}` : ""}`,
|
|
1633
|
+
owner: archiveMeta.owner,
|
|
1634
|
+
repo: "clawhub-archive",
|
|
1635
|
+
defaultBranch: "archive",
|
|
1636
|
+
commitSha: archiveMeta.version ?? "archive",
|
|
1637
|
+
skillName: options.skillNameOverride ?? archiveMeta.slug,
|
|
1638
|
+
skillDir: import_node_path4.default.posix.dirname(skillFilePath),
|
|
1639
|
+
skillFilePath,
|
|
1640
|
+
files,
|
|
1641
|
+
unverifiableReasons: [...unverifiableReasons],
|
|
1642
|
+
sourceMetadata: moderation ? {
|
|
1643
|
+
clawhubModeration: moderation
|
|
1644
|
+
} : void 0
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
function extractMetadataFromClawHubPage(html, pageUrl) {
|
|
1648
|
+
const metadata = {};
|
|
1649
|
+
const githubUrlMatch = html.match(/https?:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)/i);
|
|
1650
|
+
if (githubUrlMatch?.[1] && githubUrlMatch[2]) {
|
|
1651
|
+
metadata.repository = `https://github.com/${githubUrlMatch[1]}/${githubUrlMatch[2]}`;
|
|
1652
|
+
}
|
|
1653
|
+
const directRepoMatch = html.match(/"repository"\s*:\s*"([^"]+)"/i);
|
|
1654
|
+
if (directRepoMatch?.[1]) {
|
|
1655
|
+
metadata.repository = directRepoMatch[1];
|
|
1656
|
+
}
|
|
1657
|
+
const pathnameParts = pageUrl.pathname.split("/").filter(Boolean);
|
|
1658
|
+
if (pathnameParts.length >= 2 && pathnameParts[1]) {
|
|
1659
|
+
metadata.skill = pathnameParts[1];
|
|
1660
|
+
}
|
|
1661
|
+
return Object.keys(metadata).length > 0 ? metadata : null;
|
|
1662
|
+
}
|
|
1663
|
+
async function resolveFromClawHubPageUrl(input, timeoutMs) {
|
|
1664
|
+
const parsed = tryParseClawHubUrl(input);
|
|
1665
|
+
if (!parsed) {
|
|
1666
|
+
return null;
|
|
1667
|
+
}
|
|
1668
|
+
const html = await fetchText(parsed.toString(), timeoutMs);
|
|
1669
|
+
return extractMetadataFromClawHubPage(html, parsed);
|
|
1670
|
+
}
|
|
1671
|
+
async function resolveSkillFromClawHub(identifier, options = {}) {
|
|
1672
|
+
const identifierCandidates = extractIdentifierCandidates(identifier);
|
|
1673
|
+
if (identifierCandidates.length === 0) {
|
|
1674
|
+
throw new GuardSkillsError("CLAWHUB_UNKNOWN", "ClawHub identifier is required.");
|
|
1675
|
+
}
|
|
1676
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS2;
|
|
1677
|
+
const registryBaseUrl = normalizeRegistryBaseUrl(options.registryBaseUrl);
|
|
1678
|
+
let metadata = null;
|
|
1679
|
+
let resolvedIdentifier = null;
|
|
1680
|
+
let lastError = null;
|
|
1681
|
+
for (const identifierCandidate of identifierCandidates) {
|
|
1682
|
+
const candidateUrls = toApiCandidates(registryBaseUrl, identifierCandidate);
|
|
1683
|
+
for (const url of candidateUrls) {
|
|
1684
|
+
try {
|
|
1685
|
+
metadata = await fetchJson(url, requestTimeoutMs);
|
|
1686
|
+
resolvedIdentifier = identifierCandidate;
|
|
1687
|
+
break;
|
|
1688
|
+
} catch (error) {
|
|
1689
|
+
const mapped = mapClawHubError(error, "ClawHub package lookup");
|
|
1690
|
+
lastError = mapped;
|
|
1691
|
+
if (mapped.code !== "CLAWHUB_NOT_FOUND") {
|
|
1692
|
+
break;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
if (metadata) {
|
|
1697
|
+
break;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
if (!metadata) {
|
|
1701
|
+
try {
|
|
1702
|
+
metadata = await resolveFromClawHubPageUrl(identifier, requestTimeoutMs);
|
|
1703
|
+
if (metadata) {
|
|
1704
|
+
resolvedIdentifier = identifierCandidates[0] ?? identifier;
|
|
1705
|
+
}
|
|
1706
|
+
} catch (error) {
|
|
1707
|
+
lastError = mapClawHubError(error, "ClawHub skill page fallback");
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
if (!metadata) {
|
|
1711
|
+
if (lastError) {
|
|
1712
|
+
throw lastError;
|
|
1713
|
+
}
|
|
1714
|
+
throw new GuardSkillsError(
|
|
1715
|
+
"CLAWHUB_NOT_FOUND",
|
|
1716
|
+
`Unable to resolve '${identifier}' from ClawHub. Candidates tried: ${identifierCandidates.join(", ")}.`
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
const canonicalIdentifier = resolvedIdentifier ?? identifierCandidates[0] ?? identifier;
|
|
1720
|
+
const repos = /* @__PURE__ */ new Set();
|
|
1721
|
+
collectGitHubRepos(metadata, repos, /* @__PURE__ */ new Set());
|
|
1722
|
+
const skillCandidates = collectPossibleSkillNames(canonicalIdentifier, metadata, options.skillNameOverride);
|
|
1723
|
+
if (skillCandidates.length === 0) {
|
|
1724
|
+
throw new GuardSkillsError(
|
|
1725
|
+
"CLAWHUB_UNKNOWN",
|
|
1726
|
+
`Could not infer skill name for ClawHub package '${canonicalIdentifier}'. Use --skill.`
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
let resolved = null;
|
|
1730
|
+
let resolveError;
|
|
1731
|
+
const githubOptions = {
|
|
1732
|
+
requestTimeoutMs: options.requestTimeoutMs,
|
|
1733
|
+
maxFileSizeBytes: options.maxFileSizeBytes,
|
|
1734
|
+
maxAuxFiles: options.maxAuxFiles,
|
|
1735
|
+
maxTotalFiles: options.maxTotalFiles,
|
|
1736
|
+
retries: options.retries,
|
|
1737
|
+
retryBaseDelayMs: options.retryBaseDelayMs
|
|
1738
|
+
};
|
|
1739
|
+
if (repos.size > 0) {
|
|
1740
|
+
for (const repo of repos) {
|
|
1741
|
+
for (const skillName of skillCandidates) {
|
|
1742
|
+
try {
|
|
1743
|
+
resolved = await resolveSkillFromGitHub(repo, skillName, githubOptions);
|
|
1744
|
+
break;
|
|
1745
|
+
} catch (error) {
|
|
1746
|
+
resolveError = error;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
if (resolved) {
|
|
1750
|
+
break;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
if (!resolved) {
|
|
1755
|
+
const archiveResolved = await resolveSkillFromArchive(metadata, canonicalIdentifier, options);
|
|
1756
|
+
if (archiveResolved) {
|
|
1757
|
+
return archiveResolved;
|
|
1758
|
+
}
|
|
1759
|
+
if (repos.size === 0) {
|
|
1760
|
+
throw new GuardSkillsError(
|
|
1761
|
+
"CLAWHUB_UNKNOWN",
|
|
1762
|
+
`ClawHub package '${canonicalIdentifier}' did not expose a resolvable GitHub source or archive payload.`
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
throw new GuardSkillsError(
|
|
1766
|
+
"SKILL_NOT_FOUND",
|
|
1767
|
+
`Unable to map ClawHub package '${canonicalIdentifier}' to a GitHub skill. Repos tried: ${[...repos].join(", ")}. Skill names tried: ${skillCandidates.join(", ")}.`,
|
|
1768
|
+
{ cause: resolveError }
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
const packageVersion = options.version ?? (typeof metadata.version === "string" ? metadata.version : void 0);
|
|
1772
|
+
const sourceSuffix = packageVersion ? `${canonicalIdentifier}@${packageVersion}` : canonicalIdentifier;
|
|
1773
|
+
const moderation = parseClawHubModeration(metadata);
|
|
1774
|
+
return {
|
|
1775
|
+
...resolved,
|
|
1776
|
+
source: `clawhub:${sourceSuffix}`,
|
|
1777
|
+
unverifiableReasons: [...resolved.unverifiableReasons],
|
|
1778
|
+
sourceMetadata: moderation ? {
|
|
1779
|
+
...resolved.sourceMetadata ?? {},
|
|
1780
|
+
clawhubModeration: moderation
|
|
1781
|
+
} : resolved.sourceMetadata
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
// src/commands/scan-clawhub.ts
|
|
1786
|
+
var cliScanClawHubOptionsSchema = import_zod3.z.object({
|
|
1198
1787
|
config: import_zod3.z.string().optional(),
|
|
1199
1788
|
strict: import_zod3.z.boolean().optional(),
|
|
1200
1789
|
json: import_zod3.z.boolean().optional(),
|
|
1201
1790
|
skill: import_zod3.z.string().min(1).optional(),
|
|
1791
|
+
version: import_zod3.z.string().min(1).optional(),
|
|
1792
|
+
clawhubRegistry: import_zod3.z.string().min(1).optional(),
|
|
1793
|
+
githubTimeoutMs: import_zod3.z.coerce.number().int().min(1e3).max(12e4).optional(),
|
|
1794
|
+
githubRetries: import_zod3.z.coerce.number().int().min(0).max(6).optional(),
|
|
1795
|
+
githubRetryBaseMs: import_zod3.z.coerce.number().int().min(50).max(5e3).optional(),
|
|
1202
1796
|
maxFileBytes: import_zod3.z.coerce.number().int().min(4096).max(5e6).optional(),
|
|
1797
|
+
maxAuxFiles: import_zod3.z.coerce.number().int().min(1).max(200).optional(),
|
|
1203
1798
|
maxTotalFiles: import_zod3.z.coerce.number().int().min(1).max(400).optional()
|
|
1204
1799
|
});
|
|
1205
|
-
var
|
|
1800
|
+
var effectiveScanClawHubOptionsSchema = import_zod3.z.object({
|
|
1206
1801
|
strict: import_zod3.z.boolean(),
|
|
1207
1802
|
json: import_zod3.z.boolean(),
|
|
1208
1803
|
skill: import_zod3.z.string().min(1).optional(),
|
|
1804
|
+
version: import_zod3.z.string().min(1).optional(),
|
|
1805
|
+
clawhubRegistry: import_zod3.z.string().min(1),
|
|
1806
|
+
githubTimeoutMs: import_zod3.z.number().int().min(1e3).max(12e4),
|
|
1807
|
+
githubRetries: import_zod3.z.number().int().min(0).max(6),
|
|
1808
|
+
githubRetryBaseMs: import_zod3.z.number().int().min(50).max(5e3),
|
|
1209
1809
|
maxFileBytes: import_zod3.z.number().int().min(4096).max(5e6),
|
|
1810
|
+
maxAuxFiles: import_zod3.z.number().int().min(1).max(200),
|
|
1210
1811
|
maxTotalFiles: import_zod3.z.number().int().min(1).max(400)
|
|
1211
1812
|
});
|
|
1212
1813
|
var DEFAULT_OPTIONS2 = {
|
|
1814
|
+
strict: false,
|
|
1815
|
+
json: false,
|
|
1816
|
+
skill: void 0,
|
|
1817
|
+
version: void 0,
|
|
1818
|
+
clawhubRegistry: "https://clawhub.ai",
|
|
1819
|
+
githubTimeoutMs: 15e3,
|
|
1820
|
+
githubRetries: 2,
|
|
1821
|
+
githubRetryBaseMs: 300,
|
|
1822
|
+
maxFileBytes: 25e4,
|
|
1823
|
+
maxAuxFiles: 40,
|
|
1824
|
+
maxTotalFiles: 120
|
|
1825
|
+
};
|
|
1826
|
+
function alignWithClawHubModeration(baseDecision, moderation) {
|
|
1827
|
+
if (!moderation) {
|
|
1828
|
+
return { decision: baseDecision };
|
|
1829
|
+
}
|
|
1830
|
+
if (moderation.isMalwareBlocked) {
|
|
1831
|
+
return {
|
|
1832
|
+
decision: {
|
|
1833
|
+
...baseDecision,
|
|
1834
|
+
riskScore: 100,
|
|
1835
|
+
safetyScore: 0,
|
|
1836
|
+
level: "CRITICAL",
|
|
1837
|
+
reason: "ClawHub moderation marked this skill as malware-blocked."
|
|
1838
|
+
},
|
|
1839
|
+
moderationNote: "Aligned with ClawHub moderation: malware-blocked."
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
if (moderation.isSuspicious && baseDecision.level === "SAFE") {
|
|
1843
|
+
const adjustedRisk = Math.max(baseDecision.riskScore ?? 0, 30);
|
|
1844
|
+
return {
|
|
1845
|
+
decision: {
|
|
1846
|
+
...baseDecision,
|
|
1847
|
+
riskScore: adjustedRisk,
|
|
1848
|
+
safetyScore: 100 - adjustedRisk,
|
|
1849
|
+
level: "WARNING",
|
|
1850
|
+
reason: "ClawHub moderation marked this skill as suspicious."
|
|
1851
|
+
},
|
|
1852
|
+
moderationNote: "Aligned with ClawHub moderation: suspicious."
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
return { decision: baseDecision };
|
|
1856
|
+
}
|
|
1857
|
+
function resolveEffectiveScanClawHubOptions(cliOptions, config) {
|
|
1858
|
+
const defaults = config.defaults ?? {};
|
|
1859
|
+
const resolver = config.resolver ?? {};
|
|
1860
|
+
return effectiveScanClawHubOptionsSchema.parse({
|
|
1861
|
+
strict: cliOptions.strict ?? defaults.strict ?? DEFAULT_OPTIONS2.strict,
|
|
1862
|
+
json: cliOptions.json ?? defaults.json ?? DEFAULT_OPTIONS2.json,
|
|
1863
|
+
skill: cliOptions.skill ?? DEFAULT_OPTIONS2.skill,
|
|
1864
|
+
version: cliOptions.version ?? DEFAULT_OPTIONS2.version,
|
|
1865
|
+
clawhubRegistry: cliOptions.clawhubRegistry ?? DEFAULT_OPTIONS2.clawhubRegistry,
|
|
1866
|
+
githubTimeoutMs: cliOptions.githubTimeoutMs ?? resolver.githubTimeoutMs ?? DEFAULT_OPTIONS2.githubTimeoutMs,
|
|
1867
|
+
githubRetries: cliOptions.githubRetries ?? resolver.githubRetries ?? DEFAULT_OPTIONS2.githubRetries,
|
|
1868
|
+
githubRetryBaseMs: cliOptions.githubRetryBaseMs ?? resolver.githubRetryBaseMs ?? DEFAULT_OPTIONS2.githubRetryBaseMs,
|
|
1869
|
+
maxFileBytes: cliOptions.maxFileBytes ?? resolver.maxFileBytes ?? DEFAULT_OPTIONS2.maxFileBytes,
|
|
1870
|
+
maxAuxFiles: cliOptions.maxAuxFiles ?? resolver.maxAuxFiles ?? DEFAULT_OPTIONS2.maxAuxFiles,
|
|
1871
|
+
maxTotalFiles: cliOptions.maxTotalFiles ?? resolver.maxTotalFiles ?? DEFAULT_OPTIONS2.maxTotalFiles
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
async function runScanClawHubCommand(identifier, rawOptions) {
|
|
1875
|
+
const cliOptions = cliScanClawHubOptionsSchema.parse(rawOptions);
|
|
1876
|
+
const loadedConfig = loadGuardSkillsConfig(cliOptions.config);
|
|
1877
|
+
const options = resolveEffectiveScanClawHubOptions(cliOptions, loadedConfig.config);
|
|
1878
|
+
const resolved = await resolveSkillFromClawHub(identifier, {
|
|
1879
|
+
registryBaseUrl: options.clawhubRegistry,
|
|
1880
|
+
version: options.version,
|
|
1881
|
+
skillNameOverride: options.skill,
|
|
1882
|
+
requestTimeoutMs: options.githubTimeoutMs,
|
|
1883
|
+
retries: options.githubRetries,
|
|
1884
|
+
retryBaseDelayMs: options.githubRetryBaseMs,
|
|
1885
|
+
maxFileSizeBytes: options.maxFileBytes,
|
|
1886
|
+
maxAuxFiles: options.maxAuxFiles,
|
|
1887
|
+
maxTotalFiles: options.maxTotalFiles
|
|
1888
|
+
});
|
|
1889
|
+
const scan = scanResolvedSkill(resolved);
|
|
1890
|
+
const baseDecision = calculateRiskScore(scan.findings, {
|
|
1891
|
+
strict: options.strict,
|
|
1892
|
+
trustCredits: 0,
|
|
1893
|
+
hasUnverifiableContent: scan.hasUnverifiableContent
|
|
1894
|
+
});
|
|
1895
|
+
const moderation = resolved.sourceMetadata?.clawhubModeration;
|
|
1896
|
+
const { decision, moderationNote } = alignWithClawHubModeration(baseDecision, moderation);
|
|
1897
|
+
const noteParts = ["ClawHub scan complete."];
|
|
1898
|
+
if (moderationNote) {
|
|
1899
|
+
noteParts.push(moderationNote);
|
|
1900
|
+
}
|
|
1901
|
+
if (decision.level === "UNSAFE" || decision.level === "CRITICAL" || decision.level === "UNVERIFIABLE") {
|
|
1902
|
+
noteParts.push("Blocked-level risk detected.");
|
|
1903
|
+
}
|
|
1904
|
+
if (loadedConfig.path) {
|
|
1905
|
+
noteParts.push(`Config: ${loadedConfig.path}`);
|
|
1906
|
+
}
|
|
1907
|
+
const report = {
|
|
1908
|
+
command: "guardskills scan-clawhub",
|
|
1909
|
+
identifier,
|
|
1910
|
+
registry: options.clawhubRegistry,
|
|
1911
|
+
strict: options.strict,
|
|
1912
|
+
configPath: loadedConfig.path ?? void 0,
|
|
1913
|
+
decision,
|
|
1914
|
+
scanFiles: resolved.files.map((file) => file.path),
|
|
1915
|
+
skillDir: resolved.skillDir,
|
|
1916
|
+
repo: `${resolved.owner}/${resolved.repo}`,
|
|
1917
|
+
skill: resolved.skillName,
|
|
1918
|
+
version: options.version,
|
|
1919
|
+
commitSha: resolved.commitSha,
|
|
1920
|
+
moderation,
|
|
1921
|
+
unverifiableReasons: scan.unverifiableReasons,
|
|
1922
|
+
note: noteParts.join(" ")
|
|
1923
|
+
};
|
|
1924
|
+
if (options.json) {
|
|
1925
|
+
printJsonClawHubReport(report);
|
|
1926
|
+
} else {
|
|
1927
|
+
printHumanClawHubReport(report);
|
|
1928
|
+
}
|
|
1929
|
+
return decision.level === "UNSAFE" || decision.level === "CRITICAL" || decision.level === "UNVERIFIABLE" ? 20 : 0;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
// src/commands/scan-local.ts
|
|
1933
|
+
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
1934
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
1935
|
+
var import_zod4 = require("zod");
|
|
1936
|
+
var ALLOWED_TEXT_EXTENSIONS3 = /* @__PURE__ */ new Set([
|
|
1937
|
+
".md",
|
|
1938
|
+
".txt",
|
|
1939
|
+
".sh",
|
|
1940
|
+
".bash",
|
|
1941
|
+
".zsh",
|
|
1942
|
+
".ps1",
|
|
1943
|
+
".py",
|
|
1944
|
+
".js",
|
|
1945
|
+
".ts",
|
|
1946
|
+
".mjs",
|
|
1947
|
+
".cjs",
|
|
1948
|
+
".json",
|
|
1949
|
+
".yaml",
|
|
1950
|
+
".yml",
|
|
1951
|
+
".toml",
|
|
1952
|
+
".ini",
|
|
1953
|
+
".cfg"
|
|
1954
|
+
]);
|
|
1955
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".turbo"]);
|
|
1956
|
+
var cliScanLocalOptionsSchema = import_zod4.z.object({
|
|
1957
|
+
config: import_zod4.z.string().optional(),
|
|
1958
|
+
strict: import_zod4.z.boolean().optional(),
|
|
1959
|
+
json: import_zod4.z.boolean().optional(),
|
|
1960
|
+
skill: import_zod4.z.string().min(1).optional(),
|
|
1961
|
+
maxFileBytes: import_zod4.z.coerce.number().int().min(4096).max(5e6).optional(),
|
|
1962
|
+
maxTotalFiles: import_zod4.z.coerce.number().int().min(1).max(400).optional()
|
|
1963
|
+
});
|
|
1964
|
+
var effectiveScanLocalOptionsSchema = import_zod4.z.object({
|
|
1965
|
+
strict: import_zod4.z.boolean(),
|
|
1966
|
+
json: import_zod4.z.boolean(),
|
|
1967
|
+
skill: import_zod4.z.string().min(1).optional(),
|
|
1968
|
+
maxFileBytes: import_zod4.z.number().int().min(4096).max(5e6),
|
|
1969
|
+
maxTotalFiles: import_zod4.z.number().int().min(1).max(400)
|
|
1970
|
+
});
|
|
1971
|
+
var DEFAULT_OPTIONS3 = {
|
|
1213
1972
|
strict: false,
|
|
1214
1973
|
json: false,
|
|
1215
1974
|
skill: void 0,
|
|
@@ -1220,28 +1979,28 @@ function toPosixPath(filePath) {
|
|
|
1220
1979
|
return filePath.replace(/\\/g, "/");
|
|
1221
1980
|
}
|
|
1222
1981
|
function getNearbyPathSuggestions(targetPath) {
|
|
1223
|
-
const parent =
|
|
1982
|
+
const parent = import_node_path5.default.dirname(targetPath);
|
|
1224
1983
|
if (!import_node_fs2.default.existsSync(parent)) {
|
|
1225
1984
|
return [];
|
|
1226
1985
|
}
|
|
1227
|
-
const needle =
|
|
1986
|
+
const needle = import_node_path5.default.basename(targetPath).toLowerCase();
|
|
1228
1987
|
const suggestions = [];
|
|
1229
1988
|
for (const entry of import_node_fs2.default.readdirSync(parent, { withFileTypes: true })) {
|
|
1230
1989
|
if (entry.name.toLowerCase().includes(needle)) {
|
|
1231
|
-
suggestions.push(
|
|
1990
|
+
suggestions.push(import_node_path5.default.join(parent, entry.name));
|
|
1232
1991
|
}
|
|
1233
1992
|
}
|
|
1234
1993
|
return suggestions.slice(0, 5);
|
|
1235
1994
|
}
|
|
1236
1995
|
function isSkillFile(filePath) {
|
|
1237
|
-
return
|
|
1996
|
+
return import_node_path5.default.basename(filePath).toLowerCase() === "skill.md";
|
|
1238
1997
|
}
|
|
1239
1998
|
function isScannableTextFile(filePath) {
|
|
1240
1999
|
if (isSkillFile(filePath)) {
|
|
1241
2000
|
return true;
|
|
1242
2001
|
}
|
|
1243
|
-
const ext =
|
|
1244
|
-
return
|
|
2002
|
+
const ext = import_node_path5.default.extname(filePath).toLowerCase();
|
|
2003
|
+
return ALLOWED_TEXT_EXTENSIONS3.has(ext);
|
|
1245
2004
|
}
|
|
1246
2005
|
function findSkillDirs(rootDir) {
|
|
1247
2006
|
const found = /* @__PURE__ */ new Set();
|
|
@@ -1256,7 +2015,7 @@ function findSkillDirs(rootDir) {
|
|
|
1256
2015
|
if (seen > 5e3) {
|
|
1257
2016
|
break;
|
|
1258
2017
|
}
|
|
1259
|
-
const skillFile =
|
|
2018
|
+
const skillFile = import_node_path5.default.join(current.dir, "SKILL.md");
|
|
1260
2019
|
if (import_node_fs2.default.existsSync(skillFile) && import_node_fs2.default.statSync(skillFile).isFile()) {
|
|
1261
2020
|
found.add(current.dir);
|
|
1262
2021
|
continue;
|
|
@@ -1277,7 +2036,7 @@ function findSkillDirs(rootDir) {
|
|
|
1277
2036
|
if (SKIP_DIRS.has(entry.name)) {
|
|
1278
2037
|
continue;
|
|
1279
2038
|
}
|
|
1280
|
-
stack.push({ dir:
|
|
2039
|
+
stack.push({ dir: import_node_path5.default.join(current.dir, entry.name), depth: current.depth + 1 });
|
|
1281
2040
|
}
|
|
1282
2041
|
}
|
|
1283
2042
|
return [...found].sort();
|
|
@@ -1286,7 +2045,7 @@ function formatCandidates(candidates) {
|
|
|
1286
2045
|
return candidates.map((candidate) => `- ${toPosixPath(candidate)}`).join("\n");
|
|
1287
2046
|
}
|
|
1288
2047
|
function resolveSkillDirectory(inputPath, preferredSkillName) {
|
|
1289
|
-
const absoluteInput =
|
|
2048
|
+
const absoluteInput = import_node_path5.default.resolve(inputPath);
|
|
1290
2049
|
if (!import_node_fs2.default.existsSync(absoluteInput)) {
|
|
1291
2050
|
const suggestions = getNearbyPathSuggestions(absoluteInput);
|
|
1292
2051
|
const suggestionText = suggestions.length > 0 ? `
|
|
@@ -1306,11 +2065,11 @@ ${formatCandidates(suggestions)}` : "";
|
|
|
1306
2065
|
);
|
|
1307
2066
|
}
|
|
1308
2067
|
return {
|
|
1309
|
-
skillDir:
|
|
2068
|
+
skillDir: import_node_path5.default.dirname(absoluteInput),
|
|
1310
2069
|
note: "Using parent directory of provided SKILL.md file."
|
|
1311
2070
|
};
|
|
1312
2071
|
}
|
|
1313
|
-
const directSkillFile =
|
|
2072
|
+
const directSkillFile = import_node_path5.default.join(absoluteInput, "SKILL.md");
|
|
1314
2073
|
if (import_node_fs2.default.existsSync(directSkillFile) && import_node_fs2.default.statSync(directSkillFile).isFile()) {
|
|
1315
2074
|
return { skillDir: absoluteInput };
|
|
1316
2075
|
}
|
|
@@ -1323,7 +2082,7 @@ ${formatCandidates(suggestions)}` : "";
|
|
|
1323
2082
|
}
|
|
1324
2083
|
if (preferredSkillName) {
|
|
1325
2084
|
const matches = discovered.filter(
|
|
1326
|
-
(directory) =>
|
|
2085
|
+
(directory) => import_node_path5.default.basename(directory).toLowerCase() === preferredSkillName.toLowerCase()
|
|
1327
2086
|
);
|
|
1328
2087
|
if (matches.length === 1) {
|
|
1329
2088
|
const selected = matches[0];
|
|
@@ -1335,7 +2094,7 @@ ${formatCandidates(suggestions)}` : "";
|
|
|
1335
2094
|
note: `Auto-selected skill '${preferredSkillName}' under the provided path.`
|
|
1336
2095
|
};
|
|
1337
2096
|
}
|
|
1338
|
-
const available = discovered.map((directory) =>
|
|
2097
|
+
const available = discovered.map((directory) => import_node_path5.default.basename(directory));
|
|
1339
2098
|
throw new GuardSkillsError(
|
|
1340
2099
|
"INVALID_LOCAL_PATH",
|
|
1341
2100
|
`Requested --skill '${preferredSkillName}' was not found.
|
|
@@ -1375,7 +2134,7 @@ function collectLocalFiles(skillDir, options) {
|
|
|
1375
2134
|
continue;
|
|
1376
2135
|
}
|
|
1377
2136
|
for (const entry of entries) {
|
|
1378
|
-
const fullPath =
|
|
2137
|
+
const fullPath = import_node_path5.default.join(currentDir, entry.name);
|
|
1379
2138
|
if (entry.isDirectory()) {
|
|
1380
2139
|
if (!SKIP_DIRS.has(entry.name)) {
|
|
1381
2140
|
stack.push(fullPath);
|
|
@@ -1385,7 +2144,7 @@ function collectLocalFiles(skillDir, options) {
|
|
|
1385
2144
|
if (!entry.isFile()) {
|
|
1386
2145
|
continue;
|
|
1387
2146
|
}
|
|
1388
|
-
const relativePath = toPosixPath(
|
|
2147
|
+
const relativePath = toPosixPath(import_node_path5.default.relative(skillDir, fullPath));
|
|
1389
2148
|
if (!isScannableTextFile(relativePath)) {
|
|
1390
2149
|
continue;
|
|
1391
2150
|
}
|
|
@@ -1424,11 +2183,11 @@ function resolveEffectiveScanLocalOptions(cliOptions, config) {
|
|
|
1424
2183
|
const defaults = config.defaults ?? {};
|
|
1425
2184
|
const resolver = config.resolver ?? {};
|
|
1426
2185
|
return effectiveScanLocalOptionsSchema.parse({
|
|
1427
|
-
strict: cliOptions.strict ?? defaults.strict ??
|
|
1428
|
-
json: cliOptions.json ?? defaults.json ??
|
|
1429
|
-
skill: cliOptions.skill ??
|
|
1430
|
-
maxFileBytes: cliOptions.maxFileBytes ?? resolver.maxFileBytes ??
|
|
1431
|
-
maxTotalFiles: cliOptions.maxTotalFiles ?? resolver.maxTotalFiles ??
|
|
2186
|
+
strict: cliOptions.strict ?? defaults.strict ?? DEFAULT_OPTIONS3.strict,
|
|
2187
|
+
json: cliOptions.json ?? defaults.json ?? DEFAULT_OPTIONS3.json,
|
|
2188
|
+
skill: cliOptions.skill ?? DEFAULT_OPTIONS3.skill,
|
|
2189
|
+
maxFileBytes: cliOptions.maxFileBytes ?? resolver.maxFileBytes ?? DEFAULT_OPTIONS3.maxFileBytes,
|
|
2190
|
+
maxTotalFiles: cliOptions.maxTotalFiles ?? resolver.maxTotalFiles ?? DEFAULT_OPTIONS3.maxTotalFiles
|
|
1432
2191
|
});
|
|
1433
2192
|
}
|
|
1434
2193
|
async function runScanLocalCommand(inputPath, rawOptions) {
|
|
@@ -1449,7 +2208,7 @@ async function runScanLocalCommand(inputPath, rawOptions) {
|
|
|
1449
2208
|
repo: "local",
|
|
1450
2209
|
defaultBranch: "local",
|
|
1451
2210
|
commitSha: "local",
|
|
1452
|
-
skillName: options.skill ??
|
|
2211
|
+
skillName: options.skill ?? import_node_path5.default.basename(target.skillDir),
|
|
1453
2212
|
skillDir: toPosixPath(target.skillDir),
|
|
1454
2213
|
skillFilePath: "SKILL.md",
|
|
1455
2214
|
files,
|
|
@@ -1493,7 +2252,7 @@ async function runScanLocalCommand(inputPath, rawOptions) {
|
|
|
1493
2252
|
// src/cli.ts
|
|
1494
2253
|
async function main() {
|
|
1495
2254
|
const program = new import_commander.Command();
|
|
1496
|
-
program.name("guardskills").description("Security wrapper around skills add").version("
|
|
2255
|
+
program.name("guardskills").description("Security wrapper around skills add").version("1.1.0");
|
|
1497
2256
|
program.command("add").description("Scan a skill source and conditionally install it via skills CLI").argument("<repo>", "GitHub repository URL or owner/repo").requiredOption("--skill <name>", "Skill name to install").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").action(async (repo, options) => {
|
|
1498
2257
|
const code = await runAddCommand(repo, options);
|
|
1499
2258
|
process.exitCode = code;
|
|
@@ -1502,6 +2261,10 @@ async function main() {
|
|
|
1502
2261
|
const code = await runScanLocalCommand(inputPath, options);
|
|
1503
2262
|
process.exitCode = code;
|
|
1504
2263
|
});
|
|
2264
|
+
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) => {
|
|
2265
|
+
const code = await runScanClawHubCommand(identifier, options);
|
|
2266
|
+
process.exitCode = code;
|
|
2267
|
+
});
|
|
1505
2268
|
await program.parseAsync(process.argv);
|
|
1506
2269
|
}
|
|
1507
2270
|
main().catch((error) => {
|