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