feat: 补齐 HWLAB 手动调度能力
This commit is contained in:
@@ -31,6 +31,14 @@ export interface WorkspaceRef extends JsonRecord {
|
||||
branch?: string;
|
||||
}
|
||||
|
||||
export interface SessionRef extends JsonRecord {
|
||||
sessionId: string;
|
||||
conversationId?: string;
|
||||
threadId?: string;
|
||||
expiresAt?: string;
|
||||
metadata?: JsonRecord;
|
||||
}
|
||||
|
||||
export interface SecretRef extends JsonRecord {
|
||||
namespace?: string;
|
||||
name: string;
|
||||
@@ -38,6 +46,17 @@ export interface SecretRef extends JsonRecord {
|
||||
mountPath?: string;
|
||||
}
|
||||
|
||||
export interface ResourceBundleRef extends JsonRecord {
|
||||
kind: "git";
|
||||
repoUrl: string;
|
||||
commitId: string;
|
||||
subdir?: string;
|
||||
sparsePaths?: string[];
|
||||
submodules?: false;
|
||||
lfs?: false;
|
||||
credentialRef?: SecretRef;
|
||||
}
|
||||
|
||||
export interface ExecutionPolicy extends JsonRecord {
|
||||
sandbox: string;
|
||||
approval: string;
|
||||
@@ -56,6 +75,8 @@ export interface CreateRunInput extends JsonRecord {
|
||||
tenantId: string;
|
||||
projectId: string;
|
||||
workspaceRef: WorkspaceRef;
|
||||
sessionRef?: SessionRef | null;
|
||||
resourceBundleRef?: ResourceBundleRef | null;
|
||||
providerId: string;
|
||||
backendProfile: BackendProfile;
|
||||
executionPolicy: ExecutionPolicy;
|
||||
@@ -64,6 +85,8 @@ export interface CreateRunInput extends JsonRecord {
|
||||
|
||||
export interface RunRecord extends CreateRunInput {
|
||||
id: string;
|
||||
sessionRef: SessionRef | null;
|
||||
resourceBundleRef: ResourceBundleRef | null;
|
||||
status: RunStatus;
|
||||
terminalStatus: TerminalStatus | null;
|
||||
failureKind: FailureKind | null;
|
||||
@@ -113,6 +136,38 @@ export interface RunnerRecord extends JsonRecord {
|
||||
heartbeatAt: string;
|
||||
}
|
||||
|
||||
export interface SessionRecord extends JsonRecord {
|
||||
sessionId: string;
|
||||
tenantId: string;
|
||||
projectId: string;
|
||||
backendProfile: BackendProfile;
|
||||
conversationId: string | null;
|
||||
threadId: string | null;
|
||||
metadata: JsonRecord;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
expiresAt: string | null;
|
||||
}
|
||||
|
||||
export interface RunnerJobRecord extends JsonRecord {
|
||||
id: string;
|
||||
runId: string;
|
||||
commandId: string;
|
||||
idempotencyKey: string | null;
|
||||
payloadHash: string;
|
||||
attemptId: string;
|
||||
runnerId: string;
|
||||
namespace: string;
|
||||
jobName: string;
|
||||
managerUrl: string;
|
||||
image: string;
|
||||
sourceCommit: string;
|
||||
serviceAccountName: string | null;
|
||||
result: JsonRecord;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BackendEvent {
|
||||
type: EventType;
|
||||
payload: JsonRecord;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import type { BackendProfile, CreateCommandInput, CreateRunInput, ExecutionPolicy, JsonRecord, JsonValue } from "./types.js";
|
||||
import type { BackendProfile, CreateCommandInput, CreateRunInput, ExecutionPolicy, JsonRecord, JsonValue, ResourceBundleRef, SecretRef, SessionRef } from "./types.js";
|
||||
import { AgentRunError } from "./errors.js";
|
||||
import { backendProfileSpec, backendProfiles, isBackendProfile } from "./backend-profiles.js";
|
||||
|
||||
@@ -51,6 +51,8 @@ export function validateCreateRun(input: unknown): CreateRunInput {
|
||||
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,
|
||||
@@ -58,6 +60,50 @@ export function validateCreateRun(input: unknown): CreateRunInput {
|
||||
};
|
||||
}
|
||||
|
||||
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.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;
|
||||
}
|
||||
|
||||
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 });
|
||||
@@ -87,6 +133,24 @@ export function validateExecutionPolicy(record: JsonRecord): ExecutionPolicy {
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user