mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
Ships the fail-closed skills.sh catalog control plane validated by the bounded 500-row permanent Test gate. No production ingestion, schedule, visibility, or bulk scanning is enabled.
679 lines
21 KiB
TypeScript
679 lines
21 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { getVercelOidcToken, verifyVercelOidcToken, type VercelOidcPayload } from "@vercel/oidc";
|
|
|
|
const SKILLS_SH_API_BASE = "https://skills.sh/api/v1";
|
|
const MAX_SOURCE_PAGE_SIZE = 500;
|
|
const MAX_TEST_SCAN_ADMISSIONS = 10;
|
|
const DETAIL_CONCURRENCY = 8;
|
|
const CLAWHUB_VERCEL_OWNER_ID = "team_pLdjXbfy0XvPRiNmAygTjTSH";
|
|
const CLAWHUB_VERCEL_PROJECT_ID = "prj_UVAJPNPYrBwTEkPJwkpEySsge8Mc";
|
|
const CLAWHUB_TEST_CONVEX_URL = "https://academic-chihuahua-392.convex.cloud";
|
|
|
|
export type SkillsShCatalogSourceEnv = {
|
|
CLAWHUB_SKILLS_SH_TEST_LIVE_FETCH_ENABLED?: string;
|
|
VERCEL_ENV?: string;
|
|
VERCEL_OIDC_TOKEN?: string;
|
|
VERCEL_TARGET_ENV?: string;
|
|
VITE_CLAWHUB_DEPLOY_ENV?: string;
|
|
VITE_CONVEX_URL?: string;
|
|
};
|
|
|
|
export type SkillsShCatalogListRow = {
|
|
id: string;
|
|
installUrl: string | null;
|
|
installs: number;
|
|
name: string;
|
|
slug: string;
|
|
source: string;
|
|
sourceType: string;
|
|
url: string;
|
|
};
|
|
|
|
export type SkillsShCatalogDetail = {
|
|
id: string;
|
|
source: string;
|
|
slug: string;
|
|
installs: number;
|
|
hash: string | null;
|
|
files: Array<{
|
|
name?: unknown;
|
|
content?: unknown;
|
|
}> | null;
|
|
};
|
|
|
|
type SkillsShCatalogPage = {
|
|
data: SkillsShCatalogListRow[];
|
|
pagination: {
|
|
page: number;
|
|
perPage: number;
|
|
total: number;
|
|
hasMore: boolean;
|
|
};
|
|
};
|
|
|
|
type SkillsShCatalogSearch = {
|
|
data: SkillsShCatalogListRow[];
|
|
};
|
|
|
|
type HashQualifiedSkillsShCatalogDetail = SkillsShCatalogDetail & {
|
|
hash: string;
|
|
};
|
|
|
|
function assertIntegerInRange(name: string, value: number, min: number, max: number) {
|
|
if (!Number.isInteger(value) || value < min || value > max) {
|
|
throw new Error(`${name} must be an integer between ${min} and ${max}`);
|
|
}
|
|
}
|
|
|
|
function requireOidcToken(env: SkillsShCatalogSourceEnv, requestOidcToken?: string) {
|
|
const token = requestOidcToken?.trim() || env.VERCEL_OIDC_TOKEN?.trim();
|
|
if (!token) {
|
|
throw new Error("skills.sh catalog source requires VERCEL_OIDC_TOKEN");
|
|
}
|
|
return token;
|
|
}
|
|
|
|
async function fetchSkillsShJson<T>(
|
|
path: string,
|
|
options: {
|
|
env?: SkillsShCatalogSourceEnv;
|
|
fetchImpl?: typeof fetch;
|
|
oidcToken?: string;
|
|
} = {},
|
|
): Promise<T> {
|
|
const env = options.env ?? process.env;
|
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
const response = await fetchImpl(`${SKILLS_SH_API_BASE}${path}`, {
|
|
headers: {
|
|
Accept: "application/json",
|
|
Authorization: `Bearer ${requireOidcToken(env, options.oidcToken)}`,
|
|
},
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`skills.sh catalog source returned HTTP ${response.status}`);
|
|
}
|
|
return (await response.json()) as T;
|
|
}
|
|
|
|
export async function fetchSkillsShCatalogPage(
|
|
args: {
|
|
page: number;
|
|
perPage: number;
|
|
},
|
|
options: {
|
|
env?: SkillsShCatalogSourceEnv;
|
|
fetchImpl?: typeof fetch;
|
|
oidcToken?: string;
|
|
} = {},
|
|
) {
|
|
assertIntegerInRange("page", args.page, 0, 100_000);
|
|
assertIntegerInRange("perPage", args.perPage, 1, MAX_SOURCE_PAGE_SIZE);
|
|
return await fetchSkillsShJson<SkillsShCatalogPage>(
|
|
`/skills?page=${args.page}&per_page=${args.perPage}`,
|
|
options,
|
|
);
|
|
}
|
|
|
|
export async function searchSkillsShCatalog(
|
|
args: {
|
|
query: string;
|
|
owner?: string;
|
|
limit: number;
|
|
},
|
|
options: {
|
|
env?: SkillsShCatalogSourceEnv;
|
|
fetchImpl?: typeof fetch;
|
|
oidcToken?: string;
|
|
} = {},
|
|
) {
|
|
assertIntegerInRange("limit", args.limit, 1, MAX_SOURCE_PAGE_SIZE);
|
|
const params = new URLSearchParams({
|
|
q: args.query,
|
|
limit: String(args.limit),
|
|
});
|
|
if (args.owner) params.set("owner", args.owner);
|
|
return await fetchSkillsShJson<SkillsShCatalogSearch>(
|
|
`/skills/search?${params.toString()}`,
|
|
options,
|
|
);
|
|
}
|
|
|
|
export async function fetchSkillsShCatalogDetail(
|
|
id: string,
|
|
options: {
|
|
env?: SkillsShCatalogSourceEnv;
|
|
fetchImpl?: typeof fetch;
|
|
oidcToken?: string;
|
|
} = {},
|
|
) {
|
|
const normalizedId = id
|
|
.split("/")
|
|
.map((part) => encodeURIComponent(part))
|
|
.join("/");
|
|
if (normalizedId.split("/").length !== 3) {
|
|
throw new Error("skills.sh catalog detail id must be owner/repo/skill");
|
|
}
|
|
return await fetchSkillsShJson<SkillsShCatalogDetail>(`/skills/${normalizedId}`, options);
|
|
}
|
|
|
|
export function getSkillsShCatalogTestSourcePolicy(env: SkillsShCatalogSourceEnv = process.env) {
|
|
if (env.VITE_CLAWHUB_DEPLOY_ENV !== "test") {
|
|
return {
|
|
allowed: false as const,
|
|
environment: env.VITE_CLAWHUB_DEPLOY_ENV?.trim() || "unknown",
|
|
reason: "skills.sh live Test discovery requires the Test build marker",
|
|
};
|
|
}
|
|
if (env.VERCEL_ENV !== "preview") {
|
|
return {
|
|
allowed: false as const,
|
|
environment: env.VERCEL_ENV?.trim() || "unknown",
|
|
reason: "skills.sh live Test discovery requires the Vercel Preview runtime",
|
|
};
|
|
}
|
|
if (env.VITE_CONVEX_URL !== CLAWHUB_TEST_CONVEX_URL) {
|
|
return {
|
|
allowed: false as const,
|
|
environment: "test",
|
|
reason: "skills.sh live Test discovery requires the baked Test Convex backend",
|
|
};
|
|
}
|
|
if (env.CLAWHUB_SKILLS_SH_TEST_LIVE_FETCH_ENABLED !== "1") {
|
|
return {
|
|
allowed: false as const,
|
|
environment: "test",
|
|
reason: "skills.sh live Test discovery is disabled",
|
|
};
|
|
}
|
|
return {
|
|
allowed: true as const,
|
|
environment: "test",
|
|
maxDiscoveryRows: MAX_SOURCE_PAGE_SIZE,
|
|
maxRealScanAdmissions: MAX_TEST_SCAN_ADMISSIONS,
|
|
};
|
|
}
|
|
|
|
type VerifyVercelOidc = (
|
|
token: string,
|
|
options: {
|
|
projectId: string;
|
|
ownerId: string;
|
|
environment: string;
|
|
},
|
|
) => Promise<{ payload: VercelOidcPayload }>;
|
|
|
|
type SkillsShCatalogTestControl = {
|
|
mode: "off" | "fixture" | "staging-live";
|
|
discoveryEnabled: boolean;
|
|
writesEnabled: boolean;
|
|
scanPlanningEnabled: boolean;
|
|
maxEntriesPerRun: number;
|
|
publicVisibilityEnabled: boolean;
|
|
};
|
|
|
|
export type SkillsShCatalogGitHubOwnerProof = {
|
|
authentication: "clawhub-github-authenticated";
|
|
provenance:
|
|
| "live-github"
|
|
| "stored-authenticated-staging-live"
|
|
| "stored-authenticated-staging-live+live-github";
|
|
fetches: number;
|
|
reused: number;
|
|
owners: Array<{ owner: string; id: number; login: string }>;
|
|
};
|
|
|
|
export class SkillsShCatalogOwnerProofRequiredError extends Error {
|
|
constructor(
|
|
readonly owners: string[],
|
|
readonly sourcePreflight: {
|
|
skillsShFetches: number;
|
|
listFetches: number;
|
|
searchFetches: number;
|
|
detailFetches: number;
|
|
selection: {
|
|
rows: number;
|
|
nvidiaRows: number;
|
|
requiredCollisionIds: readonly string[];
|
|
skippedIncompleteDetails: number;
|
|
};
|
|
},
|
|
) {
|
|
super("skills.sh live Test source requires authenticated GitHub owner proofs");
|
|
this.name = "SkillsShCatalogOwnerProofRequiredError";
|
|
}
|
|
}
|
|
|
|
export function validateSkillsShCatalogGitHubOwnerProof(
|
|
selectedOwners: readonly string[],
|
|
proof: SkillsShCatalogGitHubOwnerProof,
|
|
) {
|
|
const expectedProvenance =
|
|
proof.reused === 0
|
|
? "live-github"
|
|
: proof.fetches === 0
|
|
? "stored-authenticated-staging-live"
|
|
: "stored-authenticated-staging-live+live-github";
|
|
if (
|
|
proof.authentication !== "clawhub-github-authenticated" ||
|
|
!Number.isInteger(proof.fetches) ||
|
|
proof.fetches < 0 ||
|
|
!Number.isInteger(proof.reused) ||
|
|
proof.reused < 0 ||
|
|
proof.fetches + proof.reused !== selectedOwners.length ||
|
|
proof.provenance !== expectedProvenance ||
|
|
proof.owners.length !== selectedOwners.length
|
|
) {
|
|
throw new Error("skills.sh live Test source lacks complete authenticated GitHub owner proof");
|
|
}
|
|
const selected = new Set(selectedOwners);
|
|
const ids = new Map<string, number>();
|
|
for (const resolvedOwner of proof.owners) {
|
|
const owner = resolvedOwner.owner.trim().toLowerCase();
|
|
const login = resolvedOwner.login.trim().toLowerCase();
|
|
if (
|
|
!selected.has(owner) ||
|
|
login !== owner ||
|
|
ids.has(owner) ||
|
|
!Number.isSafeInteger(resolvedOwner.id) ||
|
|
resolvedOwner.id <= 0
|
|
) {
|
|
throw new Error(
|
|
"skills.sh live Test source returned invalid authenticated GitHub owner proof",
|
|
);
|
|
}
|
|
ids.set(owner, resolvedOwner.id);
|
|
}
|
|
for (const owner of selectedOwners) {
|
|
if (!ids.has(owner)) {
|
|
throw new Error(
|
|
`skills.sh live Test source lacks an authenticated immutable owner id for ${owner}`,
|
|
);
|
|
}
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
async function authorizeSkillsShCatalogTestRequest(
|
|
options: {
|
|
env?: SkillsShCatalogSourceEnv;
|
|
getOidcToken?: () => Promise<string>;
|
|
verifyOidcToken?: VerifyVercelOidc;
|
|
} = {},
|
|
) {
|
|
const env = options.env ?? process.env;
|
|
const policy = getSkillsShCatalogTestSourcePolicy(env);
|
|
if (!policy.allowed) throw new Error(policy.reason);
|
|
|
|
const getOidcToken = options.getOidcToken ?? getVercelOidcToken;
|
|
const verifyOidcToken = options.verifyOidcToken ?? verifyVercelOidcToken;
|
|
const token = await getOidcToken();
|
|
const verified = await verifyOidcToken(token, {
|
|
projectId: CLAWHUB_VERCEL_PROJECT_ID,
|
|
ownerId: CLAWHUB_VERCEL_OWNER_ID,
|
|
environment: "test",
|
|
});
|
|
if (
|
|
verified.payload.project_id !== CLAWHUB_VERCEL_PROJECT_ID ||
|
|
verified.payload.owner_id !== CLAWHUB_VERCEL_OWNER_ID ||
|
|
verified.payload.environment !== "test"
|
|
) {
|
|
throw new Error("skills.sh live Test discovery requires verified ClawHub Vercel identity");
|
|
}
|
|
return {
|
|
...policy,
|
|
oidcToken: token,
|
|
verifiedIdentity: {
|
|
ownerId: verified.payload.owner_id,
|
|
projectId: verified.payload.project_id,
|
|
environment: verified.payload.environment,
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function fetchSkillsShCatalogTestPage(options: {
|
|
env?: SkillsShCatalogSourceEnv;
|
|
fetchImpl?: typeof fetch;
|
|
getOidcToken?: () => Promise<string>;
|
|
verifyOidcToken?: VerifyVercelOidc;
|
|
readConvexControl: () => Promise<SkillsShCatalogTestControl>;
|
|
}) {
|
|
const authorization = await authorizeSkillsShCatalogTestRequest(options);
|
|
const control = await options.readConvexControl();
|
|
if (
|
|
control.mode !== "staging-live" ||
|
|
!control.discoveryEnabled ||
|
|
!control.writesEnabled ||
|
|
!control.scanPlanningEnabled ||
|
|
control.maxEntriesPerRun < 1 ||
|
|
control.maxEntriesPerRun > authorization.maxDiscoveryRows ||
|
|
control.publicVisibilityEnabled
|
|
) {
|
|
throw new Error("skills.sh live Test discovery requires the dark Convex staging control");
|
|
}
|
|
const page = await fetchSkillsShCatalogPage(
|
|
{ page: 0, perPage: authorization.maxDiscoveryRows },
|
|
{
|
|
env: options.env,
|
|
fetchImpl: options.fetchImpl,
|
|
oidcToken: authorization.oidcToken,
|
|
},
|
|
);
|
|
if (page.data.length > control.maxEntriesPerRun) {
|
|
throw new Error("skills.sh live Test discovery exceeded the Convex run budget");
|
|
}
|
|
return {
|
|
page,
|
|
verifiedIdentity: authorization.verifiedIdentity,
|
|
controls: {
|
|
maxDiscoveryRows: control.maxEntriesPerRun,
|
|
maxRealScanAdmissions: authorization.maxRealScanAdmissions,
|
|
publicVisibilityEnabled: false,
|
|
},
|
|
};
|
|
}
|
|
|
|
const REQUIRED_COLLISION_IDS = [
|
|
"anthropics/skills/frontend-design",
|
|
"anthropics/claude-code/frontend-design",
|
|
] as const;
|
|
|
|
function isGitHubCatalogRow(row: SkillsShCatalogListRow) {
|
|
const source = row.source.trim().toLowerCase();
|
|
const slug = row.slug.trim().toLowerCase();
|
|
return (
|
|
row.sourceType === "github" &&
|
|
source.split("/").length === 2 &&
|
|
/^[a-z0-9][a-z0-9-]*$/.test(slug) &&
|
|
row.id.trim().toLowerCase() === `${source}/${slug}`
|
|
);
|
|
}
|
|
|
|
function normalizeListRow(row: SkillsShCatalogListRow) {
|
|
const [owner = "", repo = ""] = row.source.split("/");
|
|
const slug = row.slug.trim().toLowerCase();
|
|
return {
|
|
...row,
|
|
owner: owner.trim().toLowerCase(),
|
|
repo: repo.trim().toLowerCase(),
|
|
slug,
|
|
externalId: `${owner.trim().toLowerCase()}/${repo.trim().toLowerCase()}/${slug}`,
|
|
};
|
|
}
|
|
|
|
function sha256Hex(bytes: Uint8Array | string) {
|
|
return createHash("sha256").update(bytes).digest("hex");
|
|
}
|
|
|
|
type CompleteArtifactFile = { name: string; content: string };
|
|
|
|
function hasCompleteArtifactFiles(
|
|
files: SkillsShCatalogDetail["files"],
|
|
): files is CompleteArtifactFile[] {
|
|
return (
|
|
Array.isArray(files) &&
|
|
files.length > 0 &&
|
|
files.every(
|
|
(file) =>
|
|
typeof file.name === "string" &&
|
|
file.name.trim().length > 0 &&
|
|
typeof file.content === "string",
|
|
)
|
|
);
|
|
}
|
|
|
|
function buildArtifact(detail: SkillsShCatalogDetail) {
|
|
if (!hasCompleteArtifactFiles(detail.files)) {
|
|
throw new Error(`skills.sh live admission has incomplete artifact files: ${detail.id}`);
|
|
}
|
|
const files = detail.files
|
|
.map((file) => {
|
|
const bytes = Buffer.from(file.content, "utf8");
|
|
return {
|
|
path: file.name,
|
|
size: bytes.byteLength,
|
|
sha256: sha256Hex(bytes),
|
|
contentType: file.name.toLowerCase().endsWith(".md")
|
|
? "text/markdown; charset=utf-8"
|
|
: "text/plain; charset=utf-8",
|
|
contentBase64: bytes.toString("base64"),
|
|
};
|
|
})
|
|
.sort((left, right) => left.path.localeCompare(right.path));
|
|
const manifest = files.map((file) => `${file.path}\0${file.sha256}\n`).join("");
|
|
return {
|
|
artifactContentHash: sha256Hex(manifest),
|
|
files,
|
|
};
|
|
}
|
|
|
|
function createSkillsShFetchOptions(
|
|
env: SkillsShCatalogSourceEnv | undefined,
|
|
fetchImpl: typeof fetch | undefined,
|
|
value: string,
|
|
) {
|
|
return {
|
|
env,
|
|
fetchImpl,
|
|
oidcToken: value,
|
|
};
|
|
}
|
|
|
|
async function selectSkillsShCatalogTestRows(
|
|
fetchOptions: {
|
|
env?: SkillsShCatalogSourceEnv;
|
|
fetchImpl?: typeof fetch;
|
|
oidcToken?: string;
|
|
},
|
|
requiredArtifactIds: ReadonlySet<string>,
|
|
) {
|
|
const candidates = new Map<string, ReturnType<typeof normalizeListRow>>();
|
|
let listFetches = 0;
|
|
for (let page = 0; candidates.size < MAX_SOURCE_PAGE_SIZE + 100; page += 1) {
|
|
const response = await fetchSkillsShCatalogPage(
|
|
{ page, perPage: MAX_SOURCE_PAGE_SIZE },
|
|
fetchOptions,
|
|
);
|
|
listFetches += 1;
|
|
for (const row of response.data) {
|
|
if (!isGitHubCatalogRow(row)) continue;
|
|
const normalized = normalizeListRow(row);
|
|
candidates.set(normalized.externalId, normalized);
|
|
}
|
|
if (!response.pagination.hasMore) break;
|
|
}
|
|
const nvidia = await searchSkillsShCatalog(
|
|
{ query: "nvidia", owner: "nvidia", limit: 200 },
|
|
fetchOptions,
|
|
);
|
|
const nvidiaRows = nvidia.data
|
|
.filter(isGitHubCatalogRow)
|
|
.map(normalizeListRow)
|
|
.filter((row) => row.owner === "nvidia")
|
|
.slice(0, 10);
|
|
const requiredIds = new Set([
|
|
...REQUIRED_COLLISION_IDS,
|
|
...nvidiaRows.map((row) => row.externalId),
|
|
...requiredArtifactIds,
|
|
]);
|
|
for (const row of nvidiaRows) candidates.set(row.externalId, row);
|
|
for (const id of requiredIds) {
|
|
if (!candidates.has(id)) throw new Error(`Required skills.sh live row is missing: ${id}`);
|
|
}
|
|
const candidateRows = [
|
|
...Array.from(requiredIds, (id) => candidates.get(id)!),
|
|
...Array.from(candidates.values()).filter((row) => !requiredIds.has(row.externalId)),
|
|
];
|
|
const selected: Array<{
|
|
row: ReturnType<typeof normalizeListRow>;
|
|
detail: HashQualifiedSkillsShCatalogDetail;
|
|
}> = [];
|
|
let detailFetches = 0;
|
|
let skippedIncompleteDetails = 0;
|
|
for (
|
|
let offset = 0;
|
|
offset < candidateRows.length && selected.length < MAX_SOURCE_PAGE_SIZE;
|
|
offset += DETAIL_CONCURRENCY
|
|
) {
|
|
const batch = candidateRows.slice(offset, offset + DETAIL_CONCURRENCY);
|
|
const details = await Promise.all(
|
|
batch.map(async (row) => await fetchSkillsShCatalogDetail(row.externalId, fetchOptions)),
|
|
);
|
|
detailFetches += details.length;
|
|
for (let index = 0; index < batch.length; index += 1) {
|
|
const row = batch[index]!;
|
|
const detail = details[index]!;
|
|
const hasExactHash =
|
|
detail.id.trim().toLowerCase() === row.externalId &&
|
|
typeof detail.hash === "string" &&
|
|
/^[a-f0-9]{64}$/i.test(detail.hash);
|
|
if (!hasExactHash) {
|
|
if (requiredIds.has(row.externalId) || requiredArtifactIds.has(row.externalId)) {
|
|
throw new Error(
|
|
`Required skills.sh live row lacks an exact detail hash: ${row.externalId}`,
|
|
);
|
|
}
|
|
skippedIncompleteDetails += 1;
|
|
continue;
|
|
}
|
|
if (
|
|
requiredArtifactIds.has(row.externalId) &&
|
|
(!Array.isArray(detail.files) || detail.files.length < 1)
|
|
) {
|
|
throw new Error(`skills.sh live admission lacks artifact files: ${row.externalId}`);
|
|
}
|
|
if (requiredArtifactIds.has(row.externalId) && !hasCompleteArtifactFiles(detail.files)) {
|
|
throw new Error(
|
|
`skills.sh live admission has incomplete artifact files: ${row.externalId}`,
|
|
);
|
|
}
|
|
selected.push({ row, detail: detail as HashQualifiedSkillsShCatalogDetail });
|
|
if (selected.length === MAX_SOURCE_PAGE_SIZE) break;
|
|
}
|
|
}
|
|
if (selected.length !== MAX_SOURCE_PAGE_SIZE) {
|
|
throw new Error(`Expected ${MAX_SOURCE_PAGE_SIZE} hash-qualified live rows`);
|
|
}
|
|
return {
|
|
selected,
|
|
selectedOwners: Array.from(new Set(selected.map(({ row }) => row.owner))).sort(),
|
|
listFetches,
|
|
detailFetches,
|
|
skippedIncompleteDetails,
|
|
selection: {
|
|
rows: selected.length,
|
|
nvidiaRows: selected.filter(({ row }) => row.owner === "nvidia").length,
|
|
requiredCollisionIds: REQUIRED_COLLISION_IDS,
|
|
skippedIncompleteDetails,
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function captureSkillsShCatalogTestSnapshot(options: {
|
|
env?: SkillsShCatalogSourceEnv;
|
|
fetchImpl?: typeof fetch;
|
|
getOidcToken?: () => Promise<string>;
|
|
verifyOidcToken?: VerifyVercelOidc;
|
|
readConvexControl: () => Promise<SkillsShCatalogTestControl>;
|
|
admitExternalIds?: string[];
|
|
githubOwnerProof?: SkillsShCatalogGitHubOwnerProof;
|
|
resolveGitHubOwners?: (owners: string[]) => Promise<SkillsShCatalogGitHubOwnerProof>;
|
|
}) {
|
|
const startedAt = Date.now();
|
|
const authorization = await authorizeSkillsShCatalogTestRequest(options);
|
|
const control = await options.readConvexControl();
|
|
if (
|
|
control.mode !== "staging-live" ||
|
|
!control.discoveryEnabled ||
|
|
!control.writesEnabled ||
|
|
!control.scanPlanningEnabled ||
|
|
control.maxEntriesPerRun !== MAX_SOURCE_PAGE_SIZE ||
|
|
control.publicVisibilityEnabled
|
|
) {
|
|
throw new Error("skills.sh live Test capture requires the exact dark 500-row control");
|
|
}
|
|
const fetchOptions = createSkillsShFetchOptions(
|
|
options.env,
|
|
options.fetchImpl,
|
|
authorization.oidcToken,
|
|
);
|
|
const admitted = new Set(
|
|
(options.admitExternalIds ?? []).map((externalId) => externalId.trim().toLowerCase()),
|
|
);
|
|
if (admitted.size > authorization.maxRealScanAdmissions) {
|
|
throw new Error(
|
|
`skills.sh live Test admission cannot exceed ${authorization.maxRealScanAdmissions}`,
|
|
);
|
|
}
|
|
const selection = await selectSkillsShCatalogTestRows(fetchOptions, admitted);
|
|
const sourcePreflight = {
|
|
skillsShFetches: selection.listFetches + 1 + selection.detailFetches,
|
|
listFetches: selection.listFetches,
|
|
searchFetches: 1,
|
|
detailFetches: selection.detailFetches,
|
|
selection: selection.selection,
|
|
};
|
|
const githubOwnerProof =
|
|
options.githubOwnerProof ??
|
|
(options.resolveGitHubOwners
|
|
? await options.resolveGitHubOwners(selection.selectedOwners)
|
|
: null);
|
|
if (!githubOwnerProof) {
|
|
throw new SkillsShCatalogOwnerProofRequiredError(selection.selectedOwners, sourcePreflight);
|
|
}
|
|
const githubOwnerIds = validateSkillsShCatalogGitHubOwnerProof(
|
|
selection.selectedOwners,
|
|
githubOwnerProof,
|
|
);
|
|
const selected = selection.selected;
|
|
const rows = selected.map(({ row, detail }) => {
|
|
const githubOwnerId = githubOwnerIds.get(row.owner)!;
|
|
return {
|
|
externalId: row.externalId,
|
|
githubOwnerId,
|
|
owner: row.owner,
|
|
repo: row.repo,
|
|
slug: row.slug,
|
|
displayName: row.name.trim() || row.slug,
|
|
sourceUrl: row.url,
|
|
githubRepoUrl: row.installUrl ?? `https://github.com/${row.owner}/${row.repo}`,
|
|
sourceContentHash: detail.hash.toLowerCase(),
|
|
installs: row.installs,
|
|
};
|
|
});
|
|
const artifacts = selected
|
|
.filter(({ row }) => admitted.has(row.externalId))
|
|
.map(({ row, detail }) => ({
|
|
externalId: row.externalId,
|
|
...buildArtifact(detail),
|
|
}));
|
|
if (artifacts.length !== admitted.size) {
|
|
throw new Error("skills.sh live Test admission artifact is not present in the selected 500");
|
|
}
|
|
return {
|
|
snapshotId: `skills-sh-test-live-500:${sha256Hex(
|
|
rows.map((row) => `${row.externalId}:${row.sourceContentHash}\n`).join(""),
|
|
).slice(0, 16)}`,
|
|
capturedAt: new Date().toISOString(),
|
|
rows,
|
|
artifacts,
|
|
verifiedIdentity: authorization.verifiedIdentity,
|
|
selection: {
|
|
rows: rows.length,
|
|
nvidiaRows: rows.filter((row) => row.owner === "nvidia").length,
|
|
requiredCollisionIds: REQUIRED_COLLISION_IDS,
|
|
skippedIncompleteDetails: selection.skippedIncompleteDetails,
|
|
},
|
|
metrics: {
|
|
runtimeMs: Date.now() - startedAt,
|
|
skillsShFetches: selection.listFetches + 1 + selection.detailFetches,
|
|
listFetches: selection.listFetches,
|
|
searchFetches: 1,
|
|
detailFetches: selection.detailFetches,
|
|
githubOwnerFetches: githubOwnerProof.fetches,
|
|
githubOwnerIdsReused: githubOwnerProof.reused,
|
|
githubOwnerProofProvenance: githubOwnerProof.provenance,
|
|
skippedIncompleteDetails: selection.skippedIncompleteDetails,
|
|
},
|
|
};
|
|
}
|