feat(FN-3896): support companies.sh repo subpaths in company imports

- Add secure archive subPath handling to company package parsing, including traversal/absolute-path validation
- Parse GitHub website tree URLs from companies.sh metadata to derive preferred branch and nested subPath
- Wire dashboard company import flow to download the correct branch archive and parse nested manifests with clear API errors
- Add parser and route tests for subPath success/failure cases and include changeset for @runfusion/fusion

Fusion-Task-Id: FN-3896
This commit is contained in:
Fusion
2026-05-09 15:56:04 -07:00
committed by gsxdsm
parent 00c580d388
commit 89acfd0466
5 changed files with 276 additions and 10 deletions

View File

@@ -112,6 +112,48 @@ export function registerAgentImportExportRoutes(ctx: ApiRoutesContext): void {
return /^[a-z0-9][a-z0-9-]{0,48}[a-z0-9]$/.test(slug) || /^[a-z0-9]$/.test(slug);
}
function parseCompanyWebsiteSubPath(
website: string | undefined,
expectedRepo: string,
): { branch?: string; subPath?: string } | null {
if (typeof website !== "string" || website.trim().length === 0) {
return null;
}
const repoMatch = expectedRepo.match(/^([a-zA-Z0-9_-]+)\/([a-zA-Z0-9_.-]+)$/);
if (!repoMatch) {
return null;
}
let url: URL;
try {
url = new URL(website);
} catch {
return null;
}
if (url.hostname.toLowerCase() !== "github.com") {
return null;
}
const segments = url.pathname.split("/").filter(Boolean);
if (segments.length < 5 || segments[2] !== "tree") {
return null;
}
const [, expectedOwner, expectedRepoName] = repoMatch;
const [owner, repoName, , branch, ...subPathParts] = segments;
if (owner !== expectedOwner || repoName !== expectedRepoName) {
return null;
}
const subPath = subPathParts.join("/").trim();
return {
branch: typeof branch === "string" && branch.length > 0 ? branch : undefined,
subPath: subPath.length > 0 ? subPath : undefined,
};
}
/**
* GET /api/agents/companies
* Browse companies from companies.sh catalog.
@@ -496,7 +538,7 @@ async function persistImportedSkills(
// Note: The per-company endpoint (/api/companies/:slug) returns HTML (SPA),
// so we fetch the full list and filter by slug.
const companyApiUrl = "https://companies.sh/api/companies";
let companyInfo: { name: string; repo?: string; tagline?: string } | null = null;
let companyInfo: { name: string; repo?: string; tagline?: string; website?: string } | null = null;
try {
const controller = new AbortController();
@@ -538,8 +580,9 @@ async function persistImportedSkills(
const name = typeof match.name === "string" ? match.name : importCompanySlug;
const repo = typeof match.repo === "string" ? match.repo : undefined;
const tagline = typeof match.tagline === "string" ? match.tagline : undefined;
const website = typeof match.website === "string" ? match.website : undefined;
companyInfo = { name, repo, tagline };
companyInfo = { name, repo, tagline, website };
} catch (fetchErr) {
const message = fetchErr instanceof Error ? fetchErr.message : String(fetchErr);
if (fetchErr instanceof ApiError) throw fetchErr;
@@ -563,8 +606,10 @@ async function persistImportedSkills(
}
const [, repoOwner, repoName] = repoMatch;
// Use GitHub's archive API to get the default branch archive
const archiveUrl = `https://github.com/${repoOwner}/${repoName}/archive/refs/heads/main.tar.gz`;
const websiteInfo = parseCompanyWebsiteSubPath(companyInfo.website, `${repoOwner}/${repoName}`);
const preferredBranch = websiteInfo?.branch?.trim() || "main";
const subPath = websiteInfo?.subPath;
const archiveUrl = `https://github.com/${repoOwner}/${repoName}/archive/refs/heads/${preferredBranch}.tar.gz`;
// Download and extract to temp directory
let tempDir: string | null = null;
@@ -616,7 +661,14 @@ async function persistImportedSkills(
);
// Parse the downloaded archive directly to avoid requiring shell tar tools.
pkg = await parseCompanyArchive(archivePath);
try {
pkg = await parseCompanyArchive(archivePath, subPath ? { subPath } : undefined);
} catch (parseErr) {
if (parseErr instanceof Error && parseErr.name === "AgentCompaniesParseError" && subPath) {
throw badRequest(`Failed to import companies.sh slug "${importCompanySlug}" from subpath "${subPath}": ${parseErr.message}`);
}
throw parseErr;
}
// Override company info if available from API
if (companyInfo) {