341 lines
23 KiB
TypeScript
341 lines
23 KiB
TypeScript
import { createHash, randomUUID } from "node:crypto";
|
|
import type { BackendProfile, CreateCommandInput, CreateQueueTaskInput, CreateRunInput, ExecutionPolicy, JsonRecord, JsonValue, QueueTaskState, ResourceBundleRef, SecretRef, SessionListState, SessionRef } from "./types.js";
|
|
import { AgentRunError } from "./errors.js";
|
|
import { backendProfileSpec, backendProfiles, isBackendProfile } from "./backend-profiles.js";
|
|
|
|
const allowedTenants = new Set(["unidesk", "hwlab"]);
|
|
const allowedToolCredentials = ["github", "unidesk-ssh"] as const;
|
|
|
|
export function nowIso(): string {
|
|
return new Date().toISOString();
|
|
}
|
|
|
|
export function newId(prefix: string): string {
|
|
return `${prefix}_${randomUUID().replace(/-/gu, "")}`;
|
|
}
|
|
|
|
export function stableHash(value: JsonValue): string {
|
|
return createHash("sha256").update(JSON.stringify(sortJson(value))).digest("hex");
|
|
}
|
|
|
|
function sortJson(value: JsonValue): JsonValue {
|
|
if (Array.isArray(value)) return value.map(sortJson);
|
|
if (typeof value !== "object" || value === null) return value;
|
|
return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, sortJson(entry)]));
|
|
}
|
|
|
|
export function asRecord(value: unknown, fieldName: string): JsonRecord {
|
|
if (typeof value === "object" && value !== null && !Array.isArray(value)) return value as JsonRecord;
|
|
throw new AgentRunError("schema-invalid", `${fieldName} must be an object`, { httpStatus: 400 });
|
|
}
|
|
|
|
function requiredString(record: JsonRecord, key: string): string {
|
|
const value = record[key];
|
|
if (typeof value !== "string" || value.trim().length === 0) throw new AgentRunError("schema-invalid", `${key} is required`, { httpStatus: 400 });
|
|
return value.trim();
|
|
}
|
|
|
|
function requiredRecord(record: JsonRecord, key: string): JsonRecord {
|
|
return asRecord(record[key], key);
|
|
}
|
|
|
|
export function validateCreateRun(input: unknown): CreateRunInput {
|
|
const record = asRecord(input, "run");
|
|
const tenantId = requiredString(record, "tenantId");
|
|
if (!allowedTenants.has(tenantId)) throw new AgentRunError("tenant-policy-denied", `tenantId ${tenantId} is not allowed`, { httpStatus: 403 });
|
|
const backendProfileValue = requiredString(record, "backendProfile");
|
|
if (!isBackendProfile(backendProfileValue)) throw new AgentRunError("schema-invalid", `backendProfile ${backendProfileValue} is not supported in v0.1`, { httpStatus: 400, details: { allowedBackends: [...backendProfiles] } });
|
|
const backendProfile = backendProfileValue as BackendProfile;
|
|
const executionPolicy = validateExecutionPolicy(requiredRecord(record, "executionPolicy"));
|
|
validateBackendSecretScope(backendProfile, executionPolicy);
|
|
return {
|
|
tenantId,
|
|
projectId: requiredString(record, "projectId"),
|
|
workspaceRef: requiredRecord(record, "workspaceRef") as CreateRunInput["workspaceRef"],
|
|
sessionRef: validateSessionRef(record.sessionRef),
|
|
resourceBundleRef: validateResourceBundleRef(record.resourceBundleRef),
|
|
providerId: requiredString(record, "providerId"),
|
|
backendProfile,
|
|
executionPolicy,
|
|
traceSink: record.traceSink ?? null,
|
|
};
|
|
}
|
|
|
|
export function validateSessionRef(value: unknown): SessionRef | null {
|
|
if (value === undefined || value === null) return null;
|
|
const record = asRecord(value, "sessionRef");
|
|
const sessionId = requiredString(record, "sessionId");
|
|
const result: SessionRef = { sessionId };
|
|
const conversationId = optionalString(record.conversationId);
|
|
const threadId = optionalString(record.threadId);
|
|
const expiresAt = optionalString(record.expiresAt);
|
|
const metadata = record.metadata === undefined ? undefined : asRecord(record.metadata, "sessionRef.metadata");
|
|
if (conversationId) result.conversationId = conversationId;
|
|
if (threadId) result.threadId = threadId;
|
|
if (expiresAt) result.expiresAt = expiresAt;
|
|
if (metadata) result.metadata = metadata;
|
|
return result;
|
|
}
|
|
|
|
export function validateResourceBundleRef(value: unknown): ResourceBundleRef | null {
|
|
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 });
|
|
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);
|
|
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;
|
|
if (record.lfs === false) result.lfs = false;
|
|
if (record.credentialRef !== undefined) result.credentialRef = validateSecretRef(asRecord(record.credentialRef, "resourceBundleRef.credentialRef"));
|
|
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 });
|
|
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 };
|
|
});
|
|
}
|
|
|
|
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 });
|
|
const secretScope = asRecord(record.secretScope ?? {}, "executionPolicy.secretScope");
|
|
if (secretScope.allowCredentialEcho !== undefined && secretScope.allowCredentialEcho !== false) throw new AgentRunError("tenant-policy-denied", "allowCredentialEcho must be false", { httpStatus: 403 });
|
|
const providerCredentials = Array.isArray(secretScope.providerCredentials) ? secretScope.providerCredentials : [];
|
|
for (const credential of providerCredentials) {
|
|
const item = asRecord(credential, "providerCredential");
|
|
const profile = typeof item.profile === "string" ? item.profile.trim() : "";
|
|
if (profile.length === 0) throw new AgentRunError("schema-invalid", "provider credential profile is required", { httpStatus: 400 });
|
|
if (!isBackendProfile(profile)) throw new AgentRunError("schema-invalid", `provider credential profile ${profile} is not supported in v0.1`, { httpStatus: 400, details: { allowedBackends: [...backendProfiles] } });
|
|
const secretRef = asRecord(item.secretRef, "providerCredential.secretRef");
|
|
if (typeof secretRef.name !== "string" || secretRef.name.length === 0) throw new AgentRunError("schema-invalid", "provider credential secretRef.name is required", { httpStatus: 400 });
|
|
const keys = Array.isArray(secretRef.keys) ? secretRef.keys : [];
|
|
for (const requiredKey of backendProfileSpec(profile)?.requiredSecretKeys ?? []) {
|
|
if (!keys.includes(requiredKey)) throw new AgentRunError("schema-invalid", `provider credential ${profile} secretRef.keys must include ${requiredKey}`, { httpStatus: 400 });
|
|
}
|
|
}
|
|
const toolCredentials = validateToolCredentials(secretScope.toolCredentials);
|
|
const secretScopeResult: ExecutionPolicy["secretScope"] = { allowCredentialEcho: false };
|
|
if (providerCredentials.length > 0) secretScopeResult.providerCredentials = providerCredentials as NonNullable<ExecutionPolicy["secretScope"]["providerCredentials"]>;
|
|
if (toolCredentials.length > 0) secretScopeResult.toolCredentials = toolCredentials;
|
|
return {
|
|
sandbox: requiredString(record, "sandbox"),
|
|
approval: requiredString(record, "approval"),
|
|
timeoutMs: timeout,
|
|
network: requiredString(record, "network"),
|
|
secretScope: secretScopeResult,
|
|
};
|
|
}
|
|
|
|
function validateToolCredentials(value: unknown): NonNullable<ExecutionPolicy["secretScope"]["toolCredentials"]> {
|
|
if (value === undefined) return [];
|
|
if (!Array.isArray(value)) throw new AgentRunError("schema-invalid", "toolCredentials must be an array", { httpStatus: 400 });
|
|
if (value.length > 8) throw new AgentRunError("schema-invalid", "toolCredentials must contain at most 8 entries", { httpStatus: 400 });
|
|
const seen = new Set<string>();
|
|
return value.map((credential, index) => {
|
|
const item = asRecord(credential, `toolCredentials[${index}]`);
|
|
const tool = requiredString(item, "tool");
|
|
if (!allowedToolCredentials.includes(tool as (typeof allowedToolCredentials)[number])) throw new AgentRunError("schema-invalid", `tool credential ${tool} is not supported in v0.1`, { httpStatus: 400, details: { allowedTools: [...allowedToolCredentials] } });
|
|
const purpose = optionalString(item.purpose);
|
|
const secretRef = validateSecretRef(asRecord(item.secretRef, `toolCredentials[${index}].secretRef`));
|
|
const keys = secretRef.keys ?? [];
|
|
if (keys.length === 0) throw new AgentRunError("schema-invalid", `tool credential ${tool} secretRef.keys must not be empty`, { httpStatus: 400 });
|
|
const projection = asRecord(item.projection, `toolCredentials[${index}].projection`);
|
|
const kind = requiredString(projection, "kind");
|
|
if (kind !== "env") throw new AgentRunError("schema-invalid", "toolCredentials[].projection.kind must be env in v0.1", { httpStatus: 400 });
|
|
const envName = requiredString(projection, "envName");
|
|
validateEnvName(envName, `toolCredentials[${index}].projection.envName`);
|
|
const secretKey = optionalString(projection.secretKey) ?? keys[0];
|
|
if (!secretKey || !keys.includes(secretKey)) throw new AgentRunError("schema-invalid", `tool credential ${tool} projection.secretKey must be included in secretRef.keys`, { httpStatus: 400 });
|
|
validateToolCredentialProjection(tool, envName, secretKey, keys, index);
|
|
const identity = `${tool}:${purpose ?? ""}:${envName}`;
|
|
if (seen.has(identity)) throw new AgentRunError("schema-invalid", `tool credential projection ${identity} is duplicated`, { httpStatus: 400 });
|
|
seen.add(identity);
|
|
return { tool, ...(purpose ? { purpose } : {}), secretRef, projection: { kind: "env", envName, secretKey } };
|
|
});
|
|
}
|
|
|
|
function validateToolCredentialProjection(tool: string, envName: string, secretKey: string, keys: string[], index: number): void {
|
|
if (tool !== "unidesk-ssh") return;
|
|
if (!keys.includes("UNIDESK_SSH_CLIENT_TOKEN")) {
|
|
throw new AgentRunError("schema-invalid", `toolCredentials[${index}] unidesk-ssh secretRef.keys must include UNIDESK_SSH_CLIENT_TOKEN`, { httpStatus: 400 });
|
|
}
|
|
if (envName !== "UNIDESK_SSH_CLIENT_TOKEN" || secretKey !== "UNIDESK_SSH_CLIENT_TOKEN") {
|
|
throw new AgentRunError("schema-invalid", `toolCredentials[${index}] unidesk-ssh must project UNIDESK_SSH_CLIENT_TOKEN from the same Secret key`, { httpStatus: 400 });
|
|
}
|
|
}
|
|
|
|
export function validateEnvName(name: string, fieldName: string): void {
|
|
if (!/^[A-Z_][A-Z0-9_]{0,63}$/u.test(name)) throw new AgentRunError("schema-invalid", `${fieldName} must be an uppercase env name`, { httpStatus: 400 });
|
|
}
|
|
|
|
function validateSecretRef(record: JsonRecord): SecretRef {
|
|
const name = requiredString(record, "name");
|
|
const result: SecretRef = { name };
|
|
const namespace = optionalString(record.namespace);
|
|
const mountPath = optionalString(record.mountPath);
|
|
if (namespace) result.namespace = namespace;
|
|
if (mountPath) result.mountPath = mountPath;
|
|
if (record.keys !== undefined) {
|
|
if (!Array.isArray(record.keys) || !record.keys.every((item) => typeof item === "string" && item.length > 0)) throw new AgentRunError("schema-invalid", "secretRef.keys must be non-empty strings", { httpStatus: 400 });
|
|
result.keys = record.keys as string[];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function optionalString(value: unknown): string | undefined {
|
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
}
|
|
|
|
function validateBackendSecretScope(backendProfile: BackendProfile, executionPolicy: ExecutionPolicy): void {
|
|
const credentials = executionPolicy.secretScope.providerCredentials ?? [];
|
|
const matching = credentials.filter((item) => item.profile === backendProfile);
|
|
if (matching.length === 0) {
|
|
throw new AgentRunError("secret-unavailable", `backendProfile ${backendProfile} requires a matching provider credential SecretRef`, { httpStatus: 400, details: { backendProfile, requiredSecretName: backendProfileSpec(backendProfile)?.defaultSecretName ?? null } });
|
|
}
|
|
if (matching.length > 1) throw new AgentRunError("schema-invalid", `backendProfile ${backendProfile} has multiple matching provider credentials`, { httpStatus: 400 });
|
|
}
|
|
|
|
export function validateCreateCommand(input: unknown): CreateCommandInput {
|
|
const record = asRecord(input, "command");
|
|
const type = requiredString(record, "type");
|
|
if (type !== "turn" && type !== "steer" && type !== "interrupt") throw new AgentRunError("schema-invalid", `command type ${type} is not supported`, { httpStatus: 400 });
|
|
const payload = asRecord(record.payload ?? {}, "payload");
|
|
if (type === "steer" && !steerPrompt(payload)) throw new AgentRunError("schema-invalid", "steer command payload requires a non-empty prompt, message, or text", { httpStatus: 400 });
|
|
const idempotencyKey = typeof record.idempotencyKey === "string" && record.idempotencyKey.trim().length > 0 ? record.idempotencyKey.trim() : undefined;
|
|
return { type, payload, ...(idempotencyKey ? { idempotencyKey } : {}) };
|
|
}
|
|
|
|
function steerPrompt(payload: JsonRecord): string | null {
|
|
for (const key of ["prompt", "message", "text"]) {
|
|
const value = payload[key];
|
|
if (typeof value === "string" && value.trim().length > 0) return value.trim();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function validateCreateQueueTask(input: unknown): CreateQueueTaskInput {
|
|
const record = asRecord(input, "queueTask");
|
|
const tenantId = requiredString(record, "tenantId");
|
|
if (!allowedTenants.has(tenantId)) throw new AgentRunError("tenant-policy-denied", `tenantId ${tenantId} is not allowed`, { httpStatus: 403 });
|
|
const backendProfileValue = optionalString(record.backendProfile) ?? "codex";
|
|
if (!isBackendProfile(backendProfileValue)) throw new AgentRunError("schema-invalid", `backendProfile ${backendProfileValue} is not supported in v0.1`, { httpStatus: 400, details: { allowedBackends: [...backendProfiles] } });
|
|
const queue = optionalString(record.queue) ?? "default";
|
|
const lane = optionalString(record.lane) ?? "default";
|
|
const priorityValue = record.priority ?? 0;
|
|
if (typeof priorityValue !== "number" || !Number.isFinite(priorityValue)) throw new AgentRunError("schema-invalid", "priority must be a finite number", { httpStatus: 400 });
|
|
const referencesValue = record.references ?? [];
|
|
if (!Array.isArray(referencesValue)) throw new AgentRunError("schema-invalid", "references must be an array", { httpStatus: 400 });
|
|
const result: CreateQueueTaskInput = {
|
|
tenantId,
|
|
projectId: requiredString(record, "projectId"),
|
|
queue,
|
|
lane,
|
|
title: requiredString(record, "title"),
|
|
priority: priorityValue,
|
|
backendProfile: backendProfileValue,
|
|
providerId: optionalString(record.providerId) ?? null,
|
|
workspaceRef: record.workspaceRef === undefined || record.workspaceRef === null ? null : requiredRecord(record, "workspaceRef") as CreateQueueTaskInput["workspaceRef"],
|
|
executionPolicy: record.executionPolicy === undefined || record.executionPolicy === null ? null : validateExecutionPolicy(requiredRecord(record, "executionPolicy")),
|
|
resourceBundleRef: validateResourceBundleRef(record.resourceBundleRef),
|
|
payload: record.payload === undefined ? {} : asRecord(record.payload, "payload"),
|
|
references: referencesValue.map((item, index) => asRecord(item, `references[${index}]`)),
|
|
metadata: record.metadata === undefined ? {} : asRecord(record.metadata, "metadata"),
|
|
};
|
|
const idempotencyKey = optionalString(record.idempotencyKey);
|
|
if (idempotencyKey) result.idempotencyKey = idempotencyKey;
|
|
return result;
|
|
}
|
|
|
|
export function validateQueueTaskState(value: string): QueueTaskState {
|
|
if (value === "pending" || value === "running" || value === "completed" || value === "failed" || value === "blocked" || value === "cancelled") return value;
|
|
throw new AgentRunError("schema-invalid", `queue task state ${value} is not supported`, { httpStatus: 400 });
|
|
}
|
|
|
|
export function validateSessionListState(value: string): SessionListState {
|
|
if (value === "default" || value === "running" || value === "unread" || value === "terminal" || value === "idle" || value === "all") return value;
|
|
throw new AgentRunError("schema-invalid", `session state ${value} is not supported`, { httpStatus: 400 });
|
|
}
|
|
|
|
export function validateBackendProfile(value: string): BackendProfile {
|
|
if (isBackendProfile(value)) return value;
|
|
throw new AgentRunError("schema-invalid", `backendProfile ${value} is not supported in v0.1`, { httpStatus: 400, details: { allowedBackends: [...backendProfiles] } });
|
|
}
|