支持 gitbundle 资源装配
This commit is contained in:
+39
-76
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user