feat: assemble resource prompts and skills
This commit is contained in:
@@ -6,6 +6,9 @@ export type FailureKind =
|
||||
| "schema-invalid"
|
||||
| "tenant-policy-denied"
|
||||
| "secret-unavailable"
|
||||
| "prompt-unavailable"
|
||||
| "prompt-too-large"
|
||||
| "skill-unavailable"
|
||||
| "runner-lease-conflict"
|
||||
| "backend-failed"
|
||||
| "backend-protocol-error"
|
||||
@@ -60,6 +63,18 @@ export interface ResourceBundleRef extends JsonRecord {
|
||||
path: string;
|
||||
kind: "node-script" | "bun-script" | "sh-script" | "executable";
|
||||
}>;
|
||||
promptRefs?: Array<{
|
||||
name: string;
|
||||
path: string;
|
||||
inject?: "thread-start";
|
||||
required?: boolean;
|
||||
}>;
|
||||
skillRefs?: Array<{
|
||||
name: string;
|
||||
path: string;
|
||||
required?: boolean;
|
||||
aggregateAs?: string;
|
||||
}>;
|
||||
submodules?: false;
|
||||
lfs?: false;
|
||||
credentialRef?: SecretRef;
|
||||
@@ -283,6 +298,11 @@ export interface BackendTurnResult {
|
||||
turnId?: string;
|
||||
}
|
||||
|
||||
export interface InitialPromptAssembly extends JsonRecord {
|
||||
text: string;
|
||||
summary: JsonRecord;
|
||||
}
|
||||
|
||||
export interface ApiErrorBody extends JsonRecord {
|
||||
ok: false;
|
||||
failureKind: FailureKind;
|
||||
|
||||
@@ -98,6 +98,8 @@ export function validateResourceBundleRef(value: unknown): ResourceBundleRef | n
|
||||
result.sparsePaths = record.sparsePaths as string[];
|
||||
}
|
||||
if (record.toolAliases !== undefined) result.toolAliases = validateResourceToolAliases(record.toolAliases);
|
||||
if (record.promptRefs !== undefined) result.promptRefs = validateResourcePromptRefs(record.promptRefs);
|
||||
if (record.skillRefs !== undefined) result.skillRefs = validateResourceSkillRefs(record.skillRefs);
|
||||
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;
|
||||
@@ -124,6 +126,53 @@ function validateResourceToolAliases(value: unknown): NonNullable<ResourceBundle
|
||||
});
|
||||
}
|
||||
|
||||
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 });
|
||||
const seen = new Set<string>();
|
||||
return value.map((entry, index) => {
|
||||
const record = asRecord(entry, `resourceBundleRef.promptRefs[${index}]`);
|
||||
const name = validateResourceName(requiredString(record, "name"), `resourceBundleRef.promptRefs[${index}].name`);
|
||||
if (seen.has(name)) throw new AgentRunError("schema-invalid", `resourceBundleRef.promptRefs name ${name} is duplicated`, { httpStatus: 400 });
|
||||
seen.add(name);
|
||||
const promptPath = validateBundleRelativePath(requiredString(record, "path"), `resourceBundleRef.promptRefs[${index}].path`);
|
||||
const inject = optionalString(record.inject) ?? "thread-start";
|
||||
if (inject !== "thread-start") throw new AgentRunError("schema-invalid", `resourceBundleRef.promptRefs[${index}].inject must be thread-start in v0.1`, { httpStatus: 400 });
|
||||
const required = record.required === undefined ? false : record.required;
|
||||
if (typeof required !== "boolean") throw new AgentRunError("schema-invalid", `resourceBundleRef.promptRefs[${index}].required must be boolean`, { httpStatus: 400 });
|
||||
return { name, path: promptPath, inject: "thread-start" as const, required };
|
||||
});
|
||||
}
|
||||
|
||||
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 } : {}) };
|
||||
});
|
||||
}
|
||||
|
||||
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 validateBundleRelativePath(relativePath: string, fieldName: string): string {
|
||||
if (relativePath.startsWith("/") || relativePath.includes("..")) throw new AgentRunError("schema-invalid", `${fieldName} must stay within the checkout`, { httpStatus: 400 });
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
export function validateExecutionPolicy(record: JsonRecord): ExecutionPolicy {
|
||||
const timeout = record.timeoutMs;
|
||||
if (typeof timeout !== "number" || !Number.isFinite(timeout) || timeout <= 0) throw new AgentRunError("schema-invalid", "executionPolicy.timeoutMs must be a positive number", { httpStatus: 400 });
|
||||
|
||||
Reference in New Issue
Block a user