feat: 为 gitbundle 装配 required skills (#138)

* Add gitbundle required skills validation

* fix: 限定 required skill blocked result 覆盖

---------

Co-authored-by: AgentRun Codex <agentrun@example.invalid>
Co-authored-by: Codex <codex@pikas.tech>
This commit is contained in:
Lyon
2026-06-10 10:36:26 +08:00
committed by GitHub
parent 2e95276db8
commit 74b83b43c2
12 changed files with 247 additions and 45 deletions
+4
View File
@@ -8,6 +8,7 @@ export type FailureKind =
| "secret-unavailable"
| "prompt-unavailable"
| "prompt-too-large"
| "required-skill-unavailable"
| "skill-unavailable"
| "runner-lease-conflict"
| "backend-failed"
@@ -81,6 +82,9 @@ export interface ResourceBundleRef extends JsonRecord {
inject?: "thread-start";
required?: boolean;
}>;
requiredSkills?: Array<{
name: string;
}>;
submodules?: false;
lfs?: false;
credentialRef?: SecretRef;
+17
View File
@@ -91,6 +91,7 @@ export function validateResourceBundleRef(value: unknown): ResourceBundleRef | n
rejectLegacyResourceBundleFields(record);
const result: ResourceBundleRef = { kind: "gitbundle", repoUrl, ...(commitId ? { commitId } : {}), ...(ref ? { ref } : {}), bundles: validateResourceGitBundles(record.bundles, repoUrl, commitId, ref) };
if (record.promptRefs !== undefined) result.promptRefs = validateResourcePromptRefs(record.promptRefs);
if (record.requiredSkills !== undefined) result.requiredSkills = validateResourceRequiredSkills(record.requiredSkills);
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;
@@ -160,6 +161,22 @@ function validateResourcePromptRefs(value: unknown): NonNullable<ResourceBundleR
});
}
function validateResourceRequiredSkills(value: unknown): NonNullable<ResourceBundleRef["requiredSkills"]> {
if (!Array.isArray(value)) throw new AgentRunError("schema-invalid", "resourceBundleRef.requiredSkills must be an array", { httpStatus: 400 });
if (value.length > 32) throw new AgentRunError("schema-invalid", "resourceBundleRef.requiredSkills must contain at most 32 entries", { httpStatus: 400 });
const seen = new Set<string>();
return value.map((entry, index) => {
const record = asRecord(entry, `resourceBundleRef.requiredSkills[${index}]`);
const allowedKeys = new Set(["name"]);
const extraKeys = Object.keys(record).filter((key) => !allowedKeys.has(key));
if (extraKeys.length > 0) throw new AgentRunError("schema-invalid", `resourceBundleRef.requiredSkills[${index}] only supports name`, { httpStatus: 400, details: { rejectedKeys: extraKeys.sort(), valuesPrinted: false } });
const name = validateResourceName(requiredString(record, "name"), `resourceBundleRef.requiredSkills[${index}].name`);
if (seen.has(name)) throw new AgentRunError("schema-invalid", `resourceBundleRef.requiredSkills name ${name} is duplicated`, { httpStatus: 400 });
seen.add(name);
return { name };
});
}
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;