feat: add v0.1 runtime skeleton
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import type { BackendProfile, CreateCommandInput, CreateRunInput, ExecutionPolicy, JsonRecord, JsonValue } from "./types.js";
|
||||
import { AgentRunError } from "./errors.js";
|
||||
|
||||
const allowedTenants = new Set(["unidesk", "hwlab"]);
|
||||
const allowedBackends = new Set<BackendProfile>(["codex"]);
|
||||
|
||||
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 backendProfile = requiredString(record, "backendProfile") as BackendProfile;
|
||||
if (!allowedBackends.has(backendProfile)) throw new AgentRunError("schema-invalid", `backendProfile ${backendProfile} is not supported in v0.1`, { httpStatus: 400 });
|
||||
const executionPolicy = validateExecutionPolicy(requiredRecord(record, "executionPolicy"));
|
||||
return {
|
||||
tenantId,
|
||||
projectId: requiredString(record, "projectId"),
|
||||
workspaceRef: requiredRecord(record, "workspaceRef") as CreateRunInput["workspaceRef"],
|
||||
providerId: requiredString(record, "providerId"),
|
||||
backendProfile,
|
||||
executionPolicy,
|
||||
traceSink: record.traceSink ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
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 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 secretScopeResult: ExecutionPolicy["secretScope"] = { allowCredentialEcho: false };
|
||||
if (providerCredentials.length > 0) secretScopeResult.providerCredentials = providerCredentials as NonNullable<ExecutionPolicy["secretScope"]["providerCredentials"]>;
|
||||
return {
|
||||
sandbox: requiredString(record, "sandbox"),
|
||||
approval: requiredString(record, "approval"),
|
||||
timeoutMs: timeout,
|
||||
network: requiredString(record, "network"),
|
||||
secretScope: secretScopeResult,
|
||||
};
|
||||
}
|
||||
|
||||
export function validateCreateCommand(input: unknown): CreateCommandInput {
|
||||
const record = asRecord(input, "command");
|
||||
const type = requiredString(record, "type");
|
||||
if (type !== "turn" && type !== "interrupt") throw new AgentRunError("schema-invalid", `command type ${type} is not supported`, { httpStatus: 400 });
|
||||
const payload = asRecord(record.payload ?? {}, "payload");
|
||||
const idempotencyKey = typeof record.idempotencyKey === "string" && record.idempotencyKey.trim().length > 0 ? record.idempotencyKey.trim() : undefined;
|
||||
return { type, payload, ...(idempotencyKey ? { idempotencyKey } : {}) };
|
||||
}
|
||||
Reference in New Issue
Block a user