feat: add v0.1 runtime skeleton

This commit is contained in:
Codex
2026-05-29 10:52:41 +08:00
parent 4b33e67484
commit 5deb9fa7fd
20 changed files with 1238 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
import type { FailureKind, JsonRecord } from "./types.js";
export class AgentRunError extends Error {
readonly failureKind: FailureKind;
readonly details: JsonRecord | null;
readonly httpStatus: number;
constructor(failureKind: FailureKind, message: string, options: { httpStatus?: number; details?: JsonRecord } = {}) {
super(message);
this.name = "AgentRunError";
this.failureKind = failureKind;
this.httpStatus = options.httpStatus ?? 400;
this.details = options.details ?? null;
}
}
export function errorToJson(error: unknown): JsonRecord {
if (error instanceof AgentRunError) {
return {
name: error.name,
failureKind: error.failureKind,
message: error.message,
details: error.details,
};
}
if (error instanceof Error) return { name: error.name, message: error.message, stack: error.stack ?? null };
return { name: "UnknownError", message: String(error) };
}
+22
View File
@@ -0,0 +1,22 @@
export function redactText(value: string): string {
return value
.replace(/(authorization\s*[:=]\s*)(bearer\s+)?[A-Za-z0-9._~+/=-]+/giu, "$1$2REDACTED")
.replace(/((?:api[_-]?key|token|password|secret)\s*[:=]\s*)[A-Za-z0-9._~+/=-]+/giu, "$1REDACTED")
.replace(/(postgres(?:ql)?:\/\/[^:\s/@]+:)[^@\s]+(@)/giu, "$1REDACTED$2")
.replace(/(https?:\/\/[^:\s/@]+:)[^@\s]+(@)/giu, "$1REDACTED$2");
}
export function redactJson<T>(value: T): T {
if (typeof value === "string") return redactText(value) as T;
if (Array.isArray(value)) return value.map((item) => redactJson(item)) as T;
if (typeof value !== "object" || value === null) return value;
const result: Record<string, unknown> = {};
for (const [key, entry] of Object.entries(value)) {
if (/auth|authorization|api[_-]?key|token|password|secret|credential|dsn/iu.test(key)) {
result[key] = typeof entry === "boolean" ? entry : "REDACTED";
continue;
}
result[key] = redactJson(entry);
}
return result as T;
}
+137
View File
@@ -0,0 +1,137 @@
export type JsonPrimitive = string | number | boolean | null;
export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
export type JsonRecord = { [key: string]: JsonValue };
export type FailureKind =
| "schema-invalid"
| "tenant-policy-denied"
| "secret-unavailable"
| "runner-lease-conflict"
| "backend-failed"
| "backend-protocol-error"
| "backend-timeout"
| "provider-auth-failed"
| "provider-rate-limited"
| "infra-failed"
| "cancelled";
export type RunStatus = "pending" | "claimed" | "running" | "completed" | "failed" | "blocked" | "cancelled";
export type CommandState = "pending" | "acknowledged" | "completed" | "failed" | "cancelled";
export type TerminalStatus = "completed" | "failed" | "blocked" | "cancelled";
export type BackendProfile = "codex";
export interface WorkspaceRef extends JsonRecord {
kind: "git-worktree" | "host-path" | "kubernetes-pvc" | "opaque";
path?: string;
repo?: string;
branch?: string;
}
export interface SecretRef extends JsonRecord {
namespace?: string;
name: string;
keys?: string[];
mountPath?: string;
}
export interface ExecutionPolicy extends JsonRecord {
sandbox: string;
approval: string;
timeoutMs: number;
network: string;
secretScope: {
providerCredentials?: Array<{
profile: BackendProfile | string;
secretRef: SecretRef;
}>;
allowCredentialEcho?: false;
};
}
export interface CreateRunInput extends JsonRecord {
tenantId: string;
projectId: string;
workspaceRef: WorkspaceRef;
providerId: string;
backendProfile: BackendProfile;
executionPolicy: ExecutionPolicy;
traceSink: JsonValue;
}
export interface RunRecord extends CreateRunInput {
id: string;
status: RunStatus;
terminalStatus: TerminalStatus | null;
failureKind: FailureKind | null;
failureMessage: string | null;
createdAt: string;
updatedAt: string;
claimedBy: string | null;
leaseExpiresAt: string | null;
}
export interface CreateCommandInput extends JsonRecord {
type: "turn" | "interrupt";
payload: JsonRecord;
idempotencyKey?: string;
}
export interface CommandRecord extends CreateCommandInput {
id: string;
runId: string;
seq: number;
state: CommandState;
payloadHash: string;
createdAt: string;
updatedAt: string;
acknowledgedAt: string | null;
}
export type EventType = "backend_status" | "assistant_message" | "tool_call" | "command_output" | "diff" | "error" | "terminal_status";
export interface RunEvent extends JsonRecord {
id: string;
runId: string;
seq: number;
type: EventType;
payload: JsonRecord;
createdAt: string;
}
export interface RunnerRecord extends JsonRecord {
id: string;
runId?: string;
attemptId?: string;
backendProfile?: BackendProfile;
placement?: string;
sourceCommit?: string;
registeredAt: string;
heartbeatAt: string;
}
export interface BackendEvent {
type: EventType;
payload: JsonRecord;
}
export interface BackendTurnResult {
terminalStatus: TerminalStatus;
failureKind: FailureKind | null;
failureMessage: string | null;
events: BackendEvent[];
threadId?: string;
turnId?: string;
}
export interface ApiErrorBody extends JsonRecord {
ok: false;
failureKind: FailureKind;
message: string;
traceId: string;
}
export interface ApiOkBody<T extends JsonValue = JsonValue> extends JsonRecord {
ok: true;
data: T;
traceId: string;
}
+88
View File
@@ -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 } : {}) };
}