支持 gitbundle 资源装配

This commit is contained in:
Codex
2026-06-08 11:21:34 +08:00
parent 47b02b5101
commit 52376fb71e
13 changed files with 287 additions and 364 deletions
+39 -76
View File
@@ -83,26 +83,13 @@ export function validateResourceBundleRef(value: unknown): ResourceBundleRef | n
if (value === undefined || value === null) return null;
const record = asRecord(value, "resourceBundleRef");
const kind = requiredString(record, "kind");
if (kind !== "git") throw new AgentRunError("schema-invalid", "resourceBundleRef.kind must be git in v0.1", { httpStatus: 400 });
if (kind !== "gitbundle") throw new AgentRunError("schema-invalid", "resourceBundleRef.kind must be gitbundle", { httpStatus: 400 });
const repoUrl = requiredString(record, "repoUrl");
const commitId = requiredString(record, "commitId");
if (!/^[0-9a-f]{40}$/u.test(commitId)) throw new AgentRunError("schema-invalid", "resourceBundleRef.commitId must be a full 40-character git commit sha", { httpStatus: 400 });
const result: ResourceBundleRef = { kind: "git", repoUrl, commitId };
const subdir = optionalString(record.subdir);
if (subdir) {
if (subdir.startsWith("/") || subdir.includes("..")) throw new AgentRunError("schema-invalid", "resourceBundleRef.subdir must stay within the checkout", { httpStatus: 400 });
result.subdir = subdir;
}
if (record.sparsePaths !== undefined) {
if (!Array.isArray(record.sparsePaths) || !record.sparsePaths.every((item) => typeof item === "string" && item.length > 0 && !item.startsWith("/") && !item.includes(".."))) {
throw new AgentRunError("schema-invalid", "resourceBundleRef.sparsePaths must be relative path strings", { httpStatus: 400 });
}
result.sparsePaths = record.sparsePaths as string[];
}
if (record.toolAliases !== undefined) result.toolAliases = validateResourceToolAliases(record.toolAliases);
validateCommitId(commitId, "resourceBundleRef.commitId");
rejectLegacyResourceBundleFields(record);
const result: ResourceBundleRef = { kind: "gitbundle", repoUrl, commitId, bundles: validateResourceGitBundles(record.bundles, repoUrl, commitId) };
if (record.promptRefs !== undefined) result.promptRefs = validateResourcePromptRefs(record.promptRefs);
if (record.skillRefs !== undefined) result.skillRefs = validateResourceSkillRefs(record.skillRefs);
if (record.workspaceFiles !== undefined) result.workspaceFiles = validateResourceWorkspaceFiles(record.workspaceFiles);
if (record.submodules !== undefined && record.submodules !== false) throw new AgentRunError("schema-invalid", "resourceBundleRef.submodules can only be false in v0.1", { httpStatus: 400 });
if (record.lfs !== undefined && record.lfs !== false) throw new AgentRunError("schema-invalid", "resourceBundleRef.lfs can only be false in v0.1", { httpStatus: 400 });
if (record.submodules === false) result.submodules = false;
@@ -111,24 +98,39 @@ export function validateResourceBundleRef(value: unknown): ResourceBundleRef | n
return result;
}
function validateResourceToolAliases(value: unknown): NonNullable<ResourceBundleRef["toolAliases"]> {
if (!Array.isArray(value)) throw new AgentRunError("schema-invalid", "resourceBundleRef.toolAliases must be an array", { httpStatus: 400 });
if (value.length > 16) throw new AgentRunError("schema-invalid", "resourceBundleRef.toolAliases must contain at most 16 entries", { httpStatus: 400 });
function rejectLegacyResourceBundleFields(record: JsonRecord): void {
for (const field of ["toolAliases", "skillRefs", "workspaceFiles", "subdir", "sparsePaths"] as const) {
if (record[field] !== undefined) throw new AgentRunError("schema-invalid", `resourceBundleRef.${field} is removed; use resourceBundleRef.bundles[] with kind=gitbundle`, { httpStatus: 400 });
}
}
function validateResourceGitBundles(value: unknown, defaultRepoUrl: string, defaultCommitId: string): ResourceBundleRef["bundles"] {
if (!Array.isArray(value)) throw new AgentRunError("schema-invalid", "resourceBundleRef.bundles must be an array", { httpStatus: 400 });
if (value.length === 0) throw new AgentRunError("schema-invalid", "resourceBundleRef.bundles must contain at least one entry", { httpStatus: 400 });
if (value.length > 64) throw new AgentRunError("schema-invalid", "resourceBundleRef.bundles must contain at most 64 entries", { httpStatus: 400 });
const seen = new Set<string>();
return value.map((entry, index) => {
const record = asRecord(entry, `resourceBundleRef.toolAliases[${index}]`);
const name = requiredString(record, "name");
if (!/^[a-z][a-z0-9._-]{0,62}$/u.test(name)) throw new AgentRunError("schema-invalid", `resourceBundleRef.toolAliases[${index}].name must be a lowercase command name`, { httpStatus: 400 });
if (seen.has(name)) throw new AgentRunError("schema-invalid", `resourceBundleRef.toolAliases name ${name} is duplicated`, { httpStatus: 400 });
seen.add(name);
const aliasPath = requiredString(record, "path");
if (aliasPath.startsWith("/") || aliasPath.includes("..")) throw new AgentRunError("schema-invalid", `resourceBundleRef.toolAliases[${index}].path must stay within the checkout`, { httpStatus: 400 });
const kind = requiredString(record, "kind");
if (kind !== "node-script" && kind !== "bun-script" && kind !== "sh-script" && kind !== "executable") throw new AgentRunError("schema-invalid", `resourceBundleRef.toolAliases[${index}].kind is not supported in v0.1`, { httpStatus: 400, details: { allowedKinds: ["node-script", "bun-script", "sh-script", "executable"] } });
return { name, path: aliasPath, kind };
const record = asRecord(entry, `resourceBundleRef.bundles[${index}]`);
const name = optionalString(record.name);
if (name) validateResourceName(name, `resourceBundleRef.bundles[${index}].name`);
const repoUrl = optionalString(record.repoUrl) ?? defaultRepoUrl;
const commitId = optionalString(record.commitId) ?? defaultCommitId;
validateCommitId(commitId, `resourceBundleRef.bundles[${index}].commitId`);
const subpath = validateBundleSubpath(requiredString(record, "subpath"), `resourceBundleRef.bundles[${index}].subpath`);
const rawTargetPath = typeof record.targetPath === "string" ? record.targetPath : record.target_path;
if (typeof rawTargetPath !== "string" || rawTargetPath.trim().length === 0) throw new AgentRunError("schema-invalid", `resourceBundleRef.bundles[${index}].target_path is required`, { httpStatus: 400 });
const targetPath = validateWorkspaceRelativePath(rawTargetPath.trim(), `resourceBundleRef.bundles[${index}].target_path`);
const identity = `${repoUrl}\0${commitId}\0${subpath}\0${targetPath}`;
if (seen.has(identity)) throw new AgentRunError("schema-invalid", `resourceBundleRef.bundles[${index}] duplicates an earlier bundle target`, { httpStatus: 400 });
seen.add(identity);
return { ...(name ? { name } : {}), ...(repoUrl === defaultRepoUrl ? {} : { repoUrl }), ...(commitId === defaultCommitId ? {} : { commitId }), subpath, targetPath };
});
}
function validateCommitId(commitId: string, fieldName: string): void {
if (!/^[0-9a-f]{40}$/u.test(commitId)) throw new AgentRunError("schema-invalid", `${fieldName} must be a full 40-character git commit sha`, { httpStatus: 400 });
}
function validateResourcePromptRefs(value: unknown): NonNullable<ResourceBundleRef["promptRefs"]> {
if (!Array.isArray(value)) throw new AgentRunError("schema-invalid", "resourceBundleRef.promptRefs must be an array", { httpStatus: 400 });
if (value.length > 16) throw new AgentRunError("schema-invalid", "resourceBundleRef.promptRefs must contain at most 16 entries", { httpStatus: 400 });
@@ -147,62 +149,23 @@ function validateResourcePromptRefs(value: unknown): NonNullable<ResourceBundleR
});
}
function validateResourceSkillRefs(value: unknown): NonNullable<ResourceBundleRef["skillRefs"]> {
if (!Array.isArray(value)) throw new AgentRunError("schema-invalid", "resourceBundleRef.skillRefs must be an array", { httpStatus: 400 });
if (value.length > 32) throw new AgentRunError("schema-invalid", "resourceBundleRef.skillRefs must contain at most 32 entries", { httpStatus: 400 });
const seen = new Set<string>();
return value.map((entry, index) => {
const record = asRecord(entry, `resourceBundleRef.skillRefs[${index}]`);
const name = validateResourceName(requiredString(record, "name"), `resourceBundleRef.skillRefs[${index}].name`);
if (seen.has(name)) throw new AgentRunError("schema-invalid", `resourceBundleRef.skillRefs name ${name} is duplicated`, { httpStatus: 400 });
seen.add(name);
const skillPath = validateBundleRelativePath(requiredString(record, "path"), `resourceBundleRef.skillRefs[${index}].path`);
if (!skillPath.endsWith("SKILL.md")) throw new AgentRunError("schema-invalid", `resourceBundleRef.skillRefs[${index}].path must point to SKILL.md in v0.1`, { httpStatus: 400 });
const required = record.required === undefined ? false : record.required;
if (typeof required !== "boolean") throw new AgentRunError("schema-invalid", `resourceBundleRef.skillRefs[${index}].required must be boolean`, { httpStatus: 400 });
const aggregateAs = optionalString(record.aggregateAs);
if (aggregateAs) validateResourceName(aggregateAs, `resourceBundleRef.skillRefs[${index}].aggregateAs`);
return { name, path: skillPath, required, ...(aggregateAs ? { aggregateAs } : {}) };
});
}
const maxWorkspaceFiles = Number.MAX_SAFE_INTEGER;
const maxWorkspaceFileBytes = 128 * 1024;
const maxWorkspaceFilesTotalBytes = 4 * 1024 * 1024;
function validateResourceWorkspaceFiles(value: unknown): NonNullable<ResourceBundleRef["workspaceFiles"]> {
if (!Array.isArray(value)) throw new AgentRunError("schema-invalid", "resourceBundleRef.workspaceFiles must be an array", { httpStatus: 400 });
if (value.length > maxWorkspaceFiles) throw new AgentRunError("schema-invalid", `resourceBundleRef.workspaceFiles must contain at most ${maxWorkspaceFiles} entries`, { httpStatus: 400 });
const seen = new Set<string>();
let totalBytes = 0;
return value.map((entry, index) => {
const record = asRecord(entry, `resourceBundleRef.workspaceFiles[${index}]`);
const filePath = validateWorkspaceRelativePath(requiredString(record, "path"), `resourceBundleRef.workspaceFiles[${index}].path`);
if (seen.has(filePath)) throw new AgentRunError("schema-invalid", `resourceBundleRef.workspaceFiles path ${filePath} is duplicated`, { httpStatus: 400 });
seen.add(filePath);
if (typeof record.content !== "string") throw new AgentRunError("schema-invalid", `resourceBundleRef.workspaceFiles[${index}].content must be a utf8 string`, { httpStatus: 400 });
const encoding = optionalString(record.encoding) ?? "utf8";
if (encoding !== "utf8") throw new AgentRunError("schema-invalid", `resourceBundleRef.workspaceFiles[${index}].encoding must be utf8`, { httpStatus: 400 });
const bytes = Buffer.byteLength(record.content, "utf8");
if (bytes > maxWorkspaceFileBytes) throw new AgentRunError("schema-invalid", `resourceBundleRef.workspaceFiles[${index}] exceeds the per-file size limit`, { httpStatus: 400, details: { path: filePath, bytes, maxWorkspaceFileBytes, valuesPrinted: false } });
totalBytes += bytes;
if (totalBytes > maxWorkspaceFilesTotalBytes) throw new AgentRunError("schema-invalid", "resourceBundleRef.workspaceFiles exceeds the total size limit", { httpStatus: 400, details: { totalBytes, maxWorkspaceFilesTotalBytes, valuesPrinted: false } });
return { path: filePath, content: record.content, encoding: "utf8" as const };
});
}
function validateResourceName(name: string, fieldName: string): string {
if (!/^[a-z][a-z0-9._-]{0,62}$/u.test(name)) throw new AgentRunError("schema-invalid", `${fieldName} must be a lowercase resource name`, { httpStatus: 400 });
return name;
}
function validateBundleSubpath(relativePath: string, fieldName: string): string {
if (relativePath.startsWith("/") || relativePath.includes("..") || relativePath.includes("\\")) throw new AgentRunError("schema-invalid", `${fieldName} must stay within the checkout`, { httpStatus: 400 });
return relativePath;
}
function validateBundleRelativePath(relativePath: string, fieldName: string): string {
if (relativePath.startsWith("/") || relativePath.includes("..")) throw new AgentRunError("schema-invalid", `${fieldName} must stay within the checkout`, { httpStatus: 400 });
if (relativePath === "." || relativePath.startsWith("/") || relativePath.includes("..") || relativePath.includes("\\")) throw new AgentRunError("schema-invalid", `${fieldName} must stay within the checkout`, { httpStatus: 400 });
return relativePath;
}
function validateWorkspaceRelativePath(relativePath: string, fieldName: string): string {
if (relativePath === "." || relativePath.startsWith("/") || relativePath.includes("..")) throw new AgentRunError("schema-invalid", `${fieldName} must stay within the workspace`, { httpStatus: 400 });
if (relativePath === "." || relativePath.startsWith("/") || relativePath.includes("..") || relativePath.includes("\\")) throw new AgentRunError("schema-invalid", `${fieldName} must stay within the workspace`, { httpStatus: 400 });
return relativePath;
}