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

@@ -502,6 +502,64 @@ name: Nested Archive CEO
expect(pkg.company?.name).toBe("Flat Zip Co");
});
it("uses subPath to select the correct company from a monorepo archive", async () => {
const root = createTempDir();
const archivePath = join(root, "mono.zip");
createZipFromEntries(archivePath, [
{ path: "companies-main/aeon/COMPANY.md", content: `---\nname: Aeon\n---` },
{ path: "companies-main/gstack/COMPANY.md", content: `---\nname: GStack\n---` },
{ path: "companies-main/gstack/agents/ceo/AGENTS.md", content: `---\nname: GStack CEO\n---` },
]);
const pkg = await parseCompanyArchive(archivePath, { subPath: "gstack" });
expect(pkg.company?.name).toBe("GStack");
expect(pkg.company?.name).not.toBe("Aeon");
expect(pkg.agents[0]?.name).toBe("GStack CEO");
});
it("throws when subPath does not contain COMPANY.md", async () => {
const root = createTempDir();
const archivePath = join(root, "missing-subpath.zip");
createZipFromEntries(archivePath, [
{ path: "companies-main/aeon/COMPANY.md", content: `---\nname: Aeon\n---` },
]);
await expect(parseCompanyArchive(archivePath, { subPath: "gstack" })).rejects.toThrow(
AgentCompaniesParseError,
);
});
it("rejects invalid subPath values", async () => {
const root = createTempDir();
const archivePath = join(root, "invalid-subpath.zip");
createZipFromEntries(archivePath, [
{ path: "companies-main/gstack/COMPANY.md", content: `---\nname: GStack\n---` },
]);
await expect(parseCompanyArchive(archivePath, { subPath: "../etc" })).rejects.toThrow(
AgentCompaniesParseError,
);
await expect(parseCompanyArchive(archivePath, { subPath: "/abs" })).rejects.toThrow(
AgentCompaniesParseError,
);
});
it("preserves legacy extraction behavior when subPath is omitted", async () => {
const root = createTempDir();
const archivePath = join(root, "legacy-behavior.zip");
createZipFromEntries(archivePath, [
{ path: "companies-main/aeon/COMPANY.md", content: `---\nname: Aeon\n---` },
{ path: "companies-main/gstack/COMPANY.md", content: `---\nname: GStack\n---` },
]);
const pkg = await parseCompanyArchive(archivePath);
expect(pkg.company?.name).toBe("Aeon");
});
it("throws for unsupported archive extension", async () => {
const root = createTempDir();
const archivePath = join(root, "company.rar");

View File

@@ -6,7 +6,7 @@
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { isAbsolute, join, normalize, resolve } from "node:path";
import extractZip from "extract-zip";
import { parse as parseYaml } from "yaml";
@@ -488,7 +488,42 @@ async function extractTarArchive(archivePath: string, outputDir: string): Promis
await execFileAsync("tar", ["xzf", archivePath, "-C", outputDir]);
}
export async function parseCompanyArchive(archivePath: string): Promise<AgentCompaniesPackage> {
function sanitizeCompanySubPath(subPath: string): string {
const trimmed = subPath.trim();
if (trimmed.length === 0) {
throw new AgentCompaniesParseError("subPath must not be empty");
}
if (trimmed.includes("\\")) {
throw new AgentCompaniesParseError(`Invalid subPath "${subPath}": backslashes are not allowed`);
}
if (isAbsolute(trimmed)) {
throw new AgentCompaniesParseError(`Invalid subPath "${subPath}": absolute paths are not allowed`);
}
const normalized = normalize(trimmed).replace(/^\/+/, "");
if (normalized === "" || normalized === "." || normalized.split("/").some((segment) => segment === "..")) {
throw new AgentCompaniesParseError(`Invalid subPath "${subPath}": path traversal is not allowed`);
}
return normalized;
}
function findArchiveWrapperRoot(tempDir: string): string {
const directories = readdirSync(tempDir, { withFileTypes: true }).filter((entry) =>
entry.isDirectory(),
);
if (directories.length === 1) {
return join(tempDir, directories[0].name);
}
return tempDir;
}
export async function parseCompanyArchive(
archivePath: string,
options?: { subPath?: string },
): Promise<AgentCompaniesPackage> {
const resolvedArchivePath = resolve(archivePath);
const tempDir = mkdtempSync(join(tmpdir(), "agent-companies-"));
@@ -503,6 +538,19 @@ export async function parseCompanyArchive(archivePath: string): Promise<AgentCom
);
}
if (typeof options?.subPath === "string") {
const wrapperRoot = findArchiveWrapperRoot(tempDir);
const sanitizedSubPath = sanitizeCompanySubPath(options.subPath);
const candidateRoot = join(wrapperRoot, sanitizedSubPath);
const companyManifestPath = join(candidateRoot, "COMPANY.md");
if (!existsSync(companyManifestPath)) {
throw new AgentCompaniesParseError(
`Company manifest not found at archive subPath "${sanitizedSubPath}" (expected ${companyManifestPath})`,
);
}
return parseCompanyDirectory(candidateRoot);
}
return parseCompanyDirectory(resolveExtractionRoot(tempDir));
} catch (error) {
if (error instanceof AgentCompaniesParseError) {