feat(ql3): establish 3.0 incubation baseline

This commit is contained in:
whyour
2026-08-12 00:25:26 +08:00
parent 4bf92dcfeb
commit c699c32461
2817 changed files with 779642 additions and 653 deletions
+524
View File
@@ -0,0 +1,524 @@
import {
MAX_PROJECT_ROLE_BINDING_VERSION,
assertProjectPolicyProjectId,
normalizePolicySubject,
normalizeProjectPermission,
type PolicySubject,
type ProjectPermission,
type ProjectPolicyFence,
} from './projectPolicy';
export const APPROVAL_RISKS = ['low', 'medium', 'high', 'critical'] as const;
export const APPROVAL_REQUEST_STATES = [
'pending',
'approved',
'rejected',
'consumed',
] as const;
export const APPROVAL_DECISIONS = ['approved', 'rejected'] as const;
export const APPROVED_ACTION_DISPATCH_STATES = ['pending'] as const;
export type ApprovalRisk = (typeof APPROVAL_RISKS)[number];
export type ApprovalRequestState = (typeof APPROVAL_REQUEST_STATES)[number];
export type ApprovalDecision = (typeof APPROVAL_DECISIONS)[number];
export type ApprovalRequestEffectiveStatus = ApprovalRequestState | 'expired';
export type ApprovedActionDispatchState =
(typeof APPROVED_ACTION_DISPATCH_STATES)[number];
export interface ApprovalActionBinding {
permission: ProjectPermission;
actionType: string;
actionRef: string;
actionDigest: string;
previewDigest: string;
}
export interface ApprovalRequestRecord {
id: string;
projectId: string;
version: number;
state: ApprovalRequestState;
action: ApprovalActionBinding;
risk: ApprovalRisk;
requestedBy: PolicySubject;
requestedAtMs: number;
expiresAtMs: number;
decisionId: string | null;
decision: ApprovalDecision | null;
decisionReasonCode: string | null;
decidedBy: PolicySubject | null;
decidedAtMs: number | null;
consumptionId: string | null;
dispatchId: string | null;
consumedBy: PolicySubject | null;
consumedAtMs: number | null;
}
export interface ApprovedActionDispatchRecord {
id: string;
approvalRequestId: string;
approvalRequestVersion: number;
projectId: string;
state: ApprovedActionDispatchState;
action: ApprovalActionBinding;
requestedBy: PolicySubject;
consumedBy: PolicySubject;
createdAtMs: number;
}
export const MAX_APPROVAL_REQUEST_ID_LENGTH = 64;
export const MAX_APPROVAL_MUTATION_ID_LENGTH = 64;
export const MAX_APPROVAL_ACTION_TYPE_LENGTH = 64;
export const MAX_APPROVAL_ACTION_REF_LENGTH = 255;
export const MAX_APPROVAL_REASON_CODE_LENGTH = 64;
export const MAX_APPROVAL_LIFETIME_MS = 24 * 60 * 60 * 1000;
export const MAX_APPROVAL_REQUEST_VERSION = 2_147_483_647;
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
export class InvalidApprovalValueError extends TypeError {
constructor(message: string) {
super(`Approval value is invalid: ${message}`);
this.name = 'InvalidApprovalValueError';
}
}
export class ApprovalRequestNotFoundError extends Error {
readonly code = 'APPROVAL_REQUEST_NOT_FOUND';
constructor() {
super('Approval request does not exist');
this.name = 'ApprovalRequestNotFoundError';
}
}
export class ApprovalPolicyDeniedError extends Error {
readonly code = 'APPROVAL_POLICY_DENIED';
constructor() {
super('Approval operation is denied by project policy');
this.name = 'ApprovalPolicyDeniedError';
}
}
export class ApprovalPolicyFenceConflictError extends Error {
readonly code = 'APPROVAL_POLICY_FENCE_CONFLICT';
constructor() {
super('Approval policy snapshot changed before the mutation committed');
this.name = 'ApprovalPolicyFenceConflictError';
}
}
export class ApprovalRequestVersionConflictError extends Error {
readonly code = 'APPROVAL_REQUEST_VERSION_CONFLICT';
constructor() {
super('Approval request version changed');
this.name = 'ApprovalRequestVersionConflictError';
}
}
export class ApprovalMutationConflictError extends Error {
readonly code = 'APPROVAL_MUTATION_CONFLICT';
constructor() {
super('Approval mutation does not match its previous request');
this.name = 'ApprovalMutationConflictError';
}
}
export class ApprovalRequestStateConflictError extends Error {
readonly code = 'APPROVAL_REQUEST_STATE_CONFLICT';
constructor() {
super('Approval request is not in the required state');
this.name = 'ApprovalRequestStateConflictError';
}
}
export class ApprovalRequestExpiredError extends Error {
readonly code = 'APPROVAL_REQUEST_EXPIRED';
constructor() {
super('Approval request expired');
this.name = 'ApprovalRequestExpiredError';
}
}
export class ApprovalHumanDecisionRequiredError extends Error {
readonly code = 'APPROVAL_HUMAN_DECISION_REQUIRED';
constructor() {
super('Approval decisions require an authenticated user subject');
this.name = 'ApprovalHumanDecisionRequiredError';
}
}
export class ApprovalSelfDecisionError extends Error {
readonly code = 'APPROVAL_SELF_DECISION_REJECTED';
constructor() {
super('An approval requester cannot decide its own request');
this.name = 'ApprovalSelfDecisionError';
}
}
export class ApprovalUnavailableError extends Error {
readonly code = 'APPROVAL_UNAVAILABLE';
constructor() {
super('Approval storage is unavailable');
this.name = 'ApprovalUnavailableError';
}
}
function assertExactKeys(
name: string,
value: object,
expected: readonly string[],
): void {
const keys = Object.keys(value).sort();
const canonical = [...expected].sort();
if (
keys.length !== canonical.length ||
keys.some((key, index) => key !== canonical[index])
) {
throw new InvalidApprovalValueError(`${name} shape is invalid`);
}
}
function assertIdentifier(name: string, value: string, maximum: number): void {
if (
typeof value !== 'string' ||
value.length < 1 ||
value.length > maximum ||
!IDENTIFIER_PATTERN.test(value)
) {
throw new InvalidApprovalValueError(`${name} is invalid`);
}
}
function assertTimestamp(name: string, value: number): void {
if (!Number.isSafeInteger(value) || value < 0) {
throw new InvalidApprovalValueError(`${name} is invalid`);
}
}
function sameSubject(
left: Readonly<PolicySubject>,
right: Readonly<PolicySubject>,
): boolean {
return left.type === right.type && left.id === right.id;
}
export function assertApprovalRequestId(value: string): void {
assertIdentifier('request id', value, MAX_APPROVAL_REQUEST_ID_LENGTH);
}
export function assertApprovalMutationId(value: string): void {
assertIdentifier('mutation id', value, MAX_APPROVAL_MUTATION_ID_LENGTH);
}
export function assertApprovalReasonCode(value: string): void {
assertIdentifier(
'decision reason code',
value,
MAX_APPROVAL_REASON_CODE_LENGTH,
);
}
export function assertApprovalTimestamp(name: string, value: number): void {
assertTimestamp(name, value);
}
export function assertApprovalRequestVersion(value: number): void {
if (
!Number.isSafeInteger(value) ||
value < 1 ||
value > MAX_APPROVAL_REQUEST_VERSION
) {
throw new InvalidApprovalValueError('request version is invalid');
}
}
export function normalizeApprovalActionBinding(
value: ApprovalActionBinding,
): Readonly<ApprovalActionBinding> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidApprovalValueError('action must be an object');
}
assertExactKeys('action', value, [
'permission',
'actionType',
'actionRef',
'actionDigest',
'previewDigest',
]);
const permission = normalizeProjectPermission(value.permission);
assertIdentifier(
'action type',
value.actionType,
MAX_APPROVAL_ACTION_TYPE_LENGTH,
);
assertIdentifier(
'action ref',
value.actionRef,
MAX_APPROVAL_ACTION_REF_LENGTH,
);
if (!DIGEST_PATTERN.test(value.actionDigest)) {
throw new InvalidApprovalValueError('action digest is invalid');
}
if (!DIGEST_PATTERN.test(value.previewDigest)) {
throw new InvalidApprovalValueError('preview digest is invalid');
}
return Object.freeze({
permission,
actionType: value.actionType,
actionRef: value.actionRef,
actionDigest: value.actionDigest,
previewDigest: value.previewDigest,
});
}
export function normalizeApprovalPolicyFence(
value: ProjectPolicyFence,
): Readonly<ProjectPolicyFence> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidApprovalValueError('policy fence must be an object');
}
assertExactKeys('policy fence', value, ['projectVersion', 'bindingVersion']);
if (
!Number.isSafeInteger(value.projectVersion) ||
value.projectVersion < 1 ||
value.projectVersion > MAX_PROJECT_ROLE_BINDING_VERSION
) {
throw new InvalidApprovalValueError('project policy version is invalid');
}
if (
value.bindingVersion !== null &&
(!Number.isSafeInteger(value.bindingVersion) ||
value.bindingVersion < 1 ||
value.bindingVersion > MAX_PROJECT_ROLE_BINDING_VERSION)
) {
throw new InvalidApprovalValueError('binding policy version is invalid');
}
return Object.freeze({ ...value });
}
export function normalizeApprovalRequestRecord(
value: ApprovalRequestRecord,
): Readonly<ApprovalRequestRecord> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidApprovalValueError('request must be an object');
}
assertExactKeys('request', value, [
'id',
'projectId',
'version',
'state',
'action',
'risk',
'requestedBy',
'requestedAtMs',
'expiresAtMs',
'decisionId',
'decision',
'decisionReasonCode',
'decidedBy',
'decidedAtMs',
'consumptionId',
'dispatchId',
'consumedBy',
'consumedAtMs',
]);
assertApprovalRequestId(value.id);
assertProjectPolicyProjectId(value.projectId);
assertApprovalRequestVersion(value.version);
if (!APPROVAL_REQUEST_STATES.includes(value.state)) {
throw new InvalidApprovalValueError('request state is invalid');
}
const action = normalizeApprovalActionBinding(value.action);
if (!APPROVAL_RISKS.includes(value.risk)) {
throw new InvalidApprovalValueError('risk is invalid');
}
const requestedBy = normalizePolicySubject(value.requestedBy);
assertTimestamp('requestedAtMs', value.requestedAtMs);
assertTimestamp('expiresAtMs', value.expiresAtMs);
if (
value.expiresAtMs <= value.requestedAtMs ||
value.expiresAtMs - value.requestedAtMs > MAX_APPROVAL_LIFETIME_MS
) {
throw new InvalidApprovalValueError('request lifetime is invalid');
}
const decisionValues = [
value.decisionId,
value.decision,
value.decisionReasonCode,
value.decidedBy,
value.decidedAtMs,
];
const hasDecision = decisionValues.every((candidate) => candidate !== null);
if (!hasDecision && decisionValues.some((candidate) => candidate !== null)) {
throw new InvalidApprovalValueError('decision tuple is incomplete');
}
let decidedBy: Readonly<PolicySubject> | null = null;
if (hasDecision) {
assertApprovalMutationId(value.decisionId!);
if (!APPROVAL_DECISIONS.includes(value.decision!)) {
throw new InvalidApprovalValueError('decision is invalid');
}
assertApprovalReasonCode(value.decisionReasonCode!);
decidedBy = normalizePolicySubject(value.decidedBy!);
assertTimestamp('decidedAtMs', value.decidedAtMs!);
if (
value.decidedAtMs! < value.requestedAtMs ||
value.decidedAtMs! >= value.expiresAtMs
) {
throw new InvalidApprovalValueError('decision timestamp is invalid');
}
}
const consumptionValues = [
value.consumptionId,
value.dispatchId,
value.consumedBy,
value.consumedAtMs,
];
const hasConsumption = consumptionValues.every(
(candidate) => candidate !== null,
);
if (
!hasConsumption &&
consumptionValues.some((candidate) => candidate !== null)
) {
throw new InvalidApprovalValueError('consumption tuple is incomplete');
}
let consumedBy: Readonly<PolicySubject> | null = null;
if (hasConsumption) {
assertApprovalMutationId(value.consumptionId!);
assertApprovalMutationId(value.dispatchId!);
consumedBy = normalizePolicySubject(value.consumedBy!);
assertTimestamp('consumedAtMs', value.consumedAtMs!);
if (
!hasDecision ||
value.decision !== 'approved' ||
value.consumedAtMs! < value.decidedAtMs! ||
value.consumedAtMs! >= value.expiresAtMs
) {
throw new InvalidApprovalValueError('consumption tuple is invalid');
}
}
if (
(value.state === 'pending' && (hasDecision || hasConsumption)) ||
(value.state === 'approved' &&
(!hasDecision || value.decision !== 'approved' || hasConsumption)) ||
(value.state === 'rejected' &&
(!hasDecision || value.decision !== 'rejected' || hasConsumption)) ||
(value.state === 'consumed' && !hasConsumption)
) {
throw new InvalidApprovalValueError('state tuple is inconsistent');
}
if (value.state === 'pending' && value.version !== 1) {
throw new InvalidApprovalValueError('pending request version is invalid');
}
if (
(value.state === 'approved' || value.state === 'rejected') &&
value.version !== 2
) {
throw new InvalidApprovalValueError('decided request version is invalid');
}
if (value.state === 'consumed' && value.version !== 3) {
throw new InvalidApprovalValueError('consumed request version is invalid');
}
return Object.freeze({
...value,
action,
requestedBy,
decidedBy,
consumedBy,
});
}
export function normalizeApprovedActionDispatchRecord(
value: ApprovedActionDispatchRecord,
): Readonly<ApprovedActionDispatchRecord> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidApprovalValueError('dispatch must be an object');
}
assertExactKeys('dispatch', value, [
'id',
'approvalRequestId',
'approvalRequestVersion',
'projectId',
'state',
'action',
'requestedBy',
'consumedBy',
'createdAtMs',
]);
assertApprovalMutationId(value.id);
assertApprovalRequestId(value.approvalRequestId);
assertApprovalRequestVersion(value.approvalRequestVersion);
if (value.approvalRequestVersion !== 3) {
throw new InvalidApprovalValueError('dispatch approval version is invalid');
}
assertProjectPolicyProjectId(value.projectId);
if (!APPROVED_ACTION_DISPATCH_STATES.includes(value.state)) {
throw new InvalidApprovalValueError('dispatch state is invalid');
}
const action = normalizeApprovalActionBinding(value.action);
const requestedBy = normalizePolicySubject(value.requestedBy);
const consumedBy = normalizePolicySubject(value.consumedBy);
assertTimestamp('dispatch createdAtMs', value.createdAtMs);
return Object.freeze({
...value,
action,
requestedBy,
consumedBy,
});
}
export function approvalRequestEffectiveStatus(
request: Readonly<ApprovalRequestRecord>,
nowMs: number,
): ApprovalRequestEffectiveStatus {
const normalized = normalizeApprovalRequestRecord(request);
assertTimestamp('nowMs', nowMs);
if (
nowMs >= normalized.expiresAtMs &&
(normalized.state === 'pending' || normalized.state === 'approved')
) {
return 'expired';
}
return normalized.state;
}
export function sameApprovalAction(
left: Readonly<ApprovalActionBinding>,
right: Readonly<ApprovalActionBinding>,
): boolean {
const normalizedLeft = normalizeApprovalActionBinding(left);
const normalizedRight = normalizeApprovalActionBinding(right);
return (
normalizedLeft.permission === normalizedRight.permission &&
normalizedLeft.actionType === normalizedRight.actionType &&
normalizedLeft.actionRef === normalizedRight.actionRef &&
normalizedLeft.actionDigest === normalizedRight.actionDigest &&
normalizedLeft.previewDigest === normalizedRight.previewDigest
);
}
export function sameApprovalSubject(
left: Readonly<PolicySubject>,
right: Readonly<PolicySubject>,
): boolean {
return sameSubject(
normalizePolicySubject(left),
normalizePolicySubject(right),
);
}
@@ -0,0 +1,430 @@
import {
assertApprovalMutationId,
assertApprovalTimestamp,
type ApprovedActionDispatchRecord,
} from './approvalRequest';
import { assertProjectPolicyProjectId } from './projectPolicy';
export const APPROVED_ACTION_EXECUTION_STATUSES = [
'pending',
'leased',
'executing',
'retry_wait',
'succeeded',
'failed',
'blocked',
] as const;
export type ApprovedActionExecutionStatus =
(typeof APPROVED_ACTION_EXECUTION_STATUSES)[number];
export type ApprovedActionExecutionEffectiveStatus =
| ApprovedActionExecutionStatus
| 'recovery_required';
export interface ApprovedActionDispatchExecutionRecord {
dispatchId: string;
projectId: string;
status: ApprovedActionExecutionStatus;
version: number;
attemptCount: number;
maxAttempts: number;
eligibleAtMs: number | null;
nextAttemptAtMs: number | null;
leaseOwner: string | null;
leaseToken: string | null;
leaseExpiresAtMs: number | null;
startedAtMs: number | null;
resultMutationId: string | null;
lastResultCode: string | null;
completedAtMs: number | null;
createdAtMs: number;
updatedAtMs: number;
}
export interface ApprovedActionDispatchExecutionSnapshot {
dispatch: Readonly<ApprovedActionDispatchRecord>;
execution: Readonly<ApprovedActionDispatchExecutionRecord>;
}
export interface ApprovedActionDispatchCursor {
eligibleAtMs: number;
dispatchId: string;
}
export const DEFAULT_APPROVED_ACTION_MAX_ATTEMPTS = 5;
export const MAX_APPROVED_ACTION_ATTEMPTS = 16;
export const MAX_APPROVED_ACTION_EXECUTION_VERSION = 2_147_483_647;
export const MAX_APPROVED_ACTION_LEASE_ID_LENGTH = 128;
export const MAX_APPROVED_ACTION_RESULT_CODE_LENGTH = 64;
export const MAX_APPROVED_ACTION_DISPATCH_PAGE_SIZE = 64;
export const MAX_APPROVED_ACTION_LEASE_DURATION_MS = 10 * 60 * 1000;
const EXECUTION_IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
export class InvalidApprovedActionExecutionError extends TypeError {
constructor(message: string) {
super(`Approved action execution is invalid: ${message}`);
this.name = 'InvalidApprovedActionExecutionError';
}
}
export class ApprovedActionDispatchNotFoundError extends Error {
readonly code = 'APPROVED_ACTION_DISPATCH_NOT_FOUND';
constructor() {
super('Approved action dispatch does not exist');
this.name = 'ApprovedActionDispatchNotFoundError';
}
}
export class ApprovedActionDispatchFenceRejectedError extends Error {
readonly code = 'APPROVED_ACTION_DISPATCH_FENCE_REJECTED';
constructor() {
super('Approved action dispatch execution fence was rejected');
this.name = 'ApprovedActionDispatchFenceRejectedError';
}
}
export class ApprovedActionDispatchStateConflictError extends Error {
readonly code = 'APPROVED_ACTION_DISPATCH_STATE_CONFLICT';
constructor() {
super('Approved action dispatch is not in the required execution state');
this.name = 'ApprovedActionDispatchStateConflictError';
}
}
export class ApprovedActionDispatchBindingConflictError extends Error {
readonly code = 'APPROVED_ACTION_DISPATCH_BINDING_CONFLICT';
constructor() {
super('Approved action dispatch identity does not match its execution');
this.name = 'ApprovedActionDispatchBindingConflictError';
}
}
export class ApprovedActionDispatchRepositoryError extends Error {
readonly code = 'APPROVED_ACTION_DISPATCH_REPOSITORY_ERROR';
constructor() {
super('Approved action dispatch repository is unavailable');
this.name = 'ApprovedActionDispatchRepositoryError';
}
}
function assertExactKeys(
name: string,
value: object,
expected: readonly string[],
): void {
const keys = Object.keys(value).sort();
const canonical = [...expected].sort();
if (
keys.length !== canonical.length ||
keys.some((key, index) => key !== canonical[index])
) {
throw new InvalidApprovedActionExecutionError(`${name} shape is invalid`);
}
}
function assertInteger(
name: string,
value: number,
minimum: number,
maximum: number,
): void {
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
throw new InvalidApprovedActionExecutionError(`${name} is invalid`);
}
}
function assertNullableTimestamp(name: string, value: number | null): void {
if (value !== null) assertApprovalTimestamp(name, value);
}
function assertNullableIdentifier(
name: string,
value: string | null,
maximum: number,
): void {
if (
value !== null &&
(typeof value !== 'string' ||
value.length < 1 ||
value.length > maximum ||
!EXECUTION_IDENTIFIER_PATTERN.test(value))
) {
throw new InvalidApprovedActionExecutionError(`${name} is invalid`);
}
}
function allNull(values: readonly unknown[]): boolean {
return values.every((value) => value === null);
}
function allPresent(values: readonly unknown[]): boolean {
return values.every((value) => value !== null);
}
export function assertApprovedActionLeaseIdentity(value: string): void {
if (typeof value !== 'string') {
throw new InvalidApprovedActionExecutionError('lease identity is invalid');
}
assertNullableIdentifier(
'lease identity',
value,
MAX_APPROVED_ACTION_LEASE_ID_LENGTH,
);
}
export function assertApprovedActionResultCode(value: string): void {
if (typeof value !== 'string') {
throw new InvalidApprovedActionExecutionError('result code is invalid');
}
assertNullableIdentifier(
'result code',
value,
MAX_APPROVED_ACTION_RESULT_CODE_LENGTH,
);
}
export function assertApprovedActionLeaseDuration(value: number): void {
assertInteger(
'lease duration',
value,
1,
MAX_APPROVED_ACTION_LEASE_DURATION_MS,
);
}
export function assertApprovedActionPageSize(value: number): void {
assertInteger('page size', value, 1, MAX_APPROVED_ACTION_DISPATCH_PAGE_SIZE);
}
export function normalizeApprovedActionDispatchCursor(
value: ApprovedActionDispatchCursor,
): Readonly<ApprovedActionDispatchCursor> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidApprovedActionExecutionError('cursor must be an object');
}
assertExactKeys('cursor', value, ['eligibleAtMs', 'dispatchId']);
assertApprovalTimestamp('cursor eligibleAtMs', value.eligibleAtMs);
assertApprovalMutationId(value.dispatchId);
return Object.freeze({ ...value });
}
export function normalizeApprovedActionDispatchExecutionRecord(
value: ApprovedActionDispatchExecutionRecord,
): Readonly<ApprovedActionDispatchExecutionRecord> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidApprovedActionExecutionError('record must be an object');
}
assertExactKeys('record', value, [
'dispatchId',
'projectId',
'status',
'version',
'attemptCount',
'maxAttempts',
'eligibleAtMs',
'nextAttemptAtMs',
'leaseOwner',
'leaseToken',
'leaseExpiresAtMs',
'startedAtMs',
'resultMutationId',
'lastResultCode',
'completedAtMs',
'createdAtMs',
'updatedAtMs',
]);
assertApprovalMutationId(value.dispatchId);
assertProjectPolicyProjectId(value.projectId);
if (!APPROVED_ACTION_EXECUTION_STATUSES.includes(value.status)) {
throw new InvalidApprovedActionExecutionError('status is invalid');
}
assertInteger(
'version',
value.version,
0,
MAX_APPROVED_ACTION_EXECUTION_VERSION,
);
assertInteger(
'attempt count',
value.attemptCount,
0,
MAX_APPROVED_ACTION_ATTEMPTS,
);
assertInteger(
'max attempts',
value.maxAttempts,
1,
MAX_APPROVED_ACTION_ATTEMPTS,
);
if (value.attemptCount > value.maxAttempts) {
throw new InvalidApprovedActionExecutionError(
'attempt count exceeds its maximum',
);
}
for (const [name, timestamp] of [
['eligibleAtMs', value.eligibleAtMs],
['nextAttemptAtMs', value.nextAttemptAtMs],
['leaseExpiresAtMs', value.leaseExpiresAtMs],
['startedAtMs', value.startedAtMs],
['completedAtMs', value.completedAtMs],
] as const) {
assertNullableTimestamp(name, timestamp);
}
assertApprovalTimestamp('createdAtMs', value.createdAtMs);
assertApprovalTimestamp('updatedAtMs', value.updatedAtMs);
if (value.updatedAtMs < value.createdAtMs) {
throw new InvalidApprovedActionExecutionError('timestamps are invalid');
}
assertNullableIdentifier(
'lease owner',
value.leaseOwner,
MAX_APPROVED_ACTION_LEASE_ID_LENGTH,
);
assertNullableIdentifier(
'lease token',
value.leaseToken,
MAX_APPROVED_ACTION_LEASE_ID_LENGTH,
);
assertNullableIdentifier('result mutation id', value.resultMutationId, 64);
assertNullableIdentifier(
'last result code',
value.lastResultCode,
MAX_APPROVED_ACTION_RESULT_CODE_LENGTH,
);
const leaseTuple = [
value.leaseOwner,
value.leaseToken,
value.leaseExpiresAtMs,
];
if (!allNull(leaseTuple) && !allPresent(leaseTuple)) {
throw new InvalidApprovedActionExecutionError('lease tuple is incomplete');
}
const hasLease = allPresent(leaseTuple);
if (
hasLease &&
(value.leaseExpiresAtMs! <= value.updatedAtMs ||
value.leaseExpiresAtMs! - value.updatedAtMs >
MAX_APPROVED_ACTION_LEASE_DURATION_MS)
) {
throw new InvalidApprovedActionExecutionError('lease lifetime is invalid');
}
const terminal = ['succeeded', 'failed', 'blocked'].includes(value.status);
if (
value.status === 'pending' &&
(value.version !== 0 ||
value.attemptCount !== 0 ||
value.eligibleAtMs === null ||
value.nextAttemptAtMs !== null ||
hasLease ||
value.startedAtMs !== null ||
value.resultMutationId !== null ||
value.lastResultCode !== null ||
value.completedAtMs !== null)
) {
throw new InvalidApprovedActionExecutionError('pending tuple is invalid');
}
if (
value.status === 'leased' &&
(!hasLease ||
value.attemptCount < 1 ||
value.eligibleAtMs !== value.leaseExpiresAtMs ||
value.nextAttemptAtMs !== null ||
value.startedAtMs !== null ||
value.resultMutationId !== null ||
value.lastResultCode !== null ||
value.completedAtMs !== null)
) {
throw new InvalidApprovedActionExecutionError('leased tuple is invalid');
}
if (
value.status === 'executing' &&
(!hasLease ||
value.attemptCount < 1 ||
value.eligibleAtMs !== null ||
value.nextAttemptAtMs !== null ||
value.startedAtMs === null ||
value.resultMutationId !== null ||
value.lastResultCode !== null ||
value.completedAtMs !== null)
) {
throw new InvalidApprovedActionExecutionError('executing tuple is invalid');
}
if (
value.status === 'retry_wait' &&
(hasLease ||
value.attemptCount < 1 ||
value.attemptCount >= value.maxAttempts ||
value.eligibleAtMs === null ||
value.eligibleAtMs !== value.nextAttemptAtMs ||
value.startedAtMs !== null ||
value.resultMutationId === null ||
value.lastResultCode === null ||
value.completedAtMs !== null)
) {
throw new InvalidApprovedActionExecutionError(
'retry wait tuple is invalid',
);
}
if (
terminal &&
(hasLease ||
value.eligibleAtMs !== null ||
value.nextAttemptAtMs !== null ||
value.resultMutationId === null ||
value.lastResultCode === null ||
value.completedAtMs === null)
) {
throw new InvalidApprovedActionExecutionError('terminal tuple is invalid');
}
if (
(value.status === 'succeeded' || value.status === 'failed') &&
value.startedAtMs === null
) {
throw new InvalidApprovedActionExecutionError(
'completed execution has no start barrier',
);
}
if (
value.startedAtMs !== null &&
(value.startedAtMs < value.createdAtMs ||
value.startedAtMs > value.updatedAtMs)
) {
throw new InvalidApprovedActionExecutionError(
'execution start timestamp is invalid',
);
}
if (
value.completedAtMs !== null &&
(value.completedAtMs < (value.startedAtMs ?? value.createdAtMs) ||
value.completedAtMs !== value.updatedAtMs)
) {
throw new InvalidApprovedActionExecutionError(
'completion timestamp is invalid',
);
}
return Object.freeze({ ...value });
}
export function approvedActionExecutionEffectiveStatus(
record: Readonly<ApprovedActionDispatchExecutionRecord>,
nowMs: number,
): ApprovedActionExecutionEffectiveStatus {
const normalized = normalizeApprovedActionDispatchExecutionRecord(record);
assertApprovalTimestamp('nowMs', nowMs);
if (
normalized.status === 'executing' &&
normalized.leaseExpiresAtMs !== null &&
nowMs >= normalized.leaseExpiresAtMs
) {
return 'recovery_required';
}
return normalized.status;
}
@@ -0,0 +1,402 @@
import {
assertApprovedActionLeaseDuration,
assertApprovedActionLeaseIdentity,
assertApprovedActionPageSize,
assertApprovedActionResultCode,
type ApprovedActionDispatchExecutionSnapshot,
} from './approvedActionDispatchExecution';
import {
assertApprovalMutationId,
assertApprovalTimestamp,
} from './approvalRequest';
import {
assertProjectPolicyProjectId,
normalizePolicySubject,
type PolicySubject,
} from './projectPolicy';
export const APPROVED_ACTION_RECOVERY_CONTROL_STATUSES = [
'armed',
'leased',
'manual_required',
'resolved',
] as const;
export const APPROVED_ACTION_RECOVERY_FINDINGS = [
'verified_succeeded',
'verified_failed',
'still_running',
'missing',
'conflict',
'unsupported',
'unavailable',
] as const;
export const APPROVED_ACTION_RECOVERY_SOURCES = [
'automatic_evidence',
'human',
] as const;
export const APPROVED_ACTION_RECOVERY_DECISIONS = [
'confirm_succeeded',
'confirm_failed',
'abandon_unknown',
] as const;
export type ApprovedActionRecoveryControlStatus =
(typeof APPROVED_ACTION_RECOVERY_CONTROL_STATUSES)[number];
export type ApprovedActionRecoveryFinding =
(typeof APPROVED_ACTION_RECOVERY_FINDINGS)[number];
export type ApprovedActionRecoverySource =
(typeof APPROVED_ACTION_RECOVERY_SOURCES)[number];
export type ApprovedActionRecoveryDecision =
(typeof APPROVED_ACTION_RECOVERY_DECISIONS)[number];
export interface ApprovedActionRecoveryControlRecord {
dispatchId: string;
projectId: string;
executionVersion: number;
status: ApprovedActionRecoveryControlStatus;
version: number;
nextScanAtMs: number | null;
leaseOwner: string | null;
leaseToken: string | null;
leaseExpiresAtMs: number | null;
findingCount: number;
lastFindingMutationId: string | null;
lastFinding: ApprovedActionRecoveryFinding | null;
lastResultCode: string | null;
lastEvidenceDigest: string | null;
resolutionMutationId: string | null;
createdAtMs: number;
updatedAtMs: number;
}
export interface ApprovedActionRecoveryResolutionRecord {
dispatchId: string;
projectId: string;
executionVersion: number;
mutationId: string;
source: ApprovedActionRecoverySource;
decision: ApprovedActionRecoveryDecision;
evidenceDigest: string | null;
reasonCode: string;
resolvedBy: Readonly<PolicySubject> | null;
resolvedAtMs: number;
}
export interface ApprovedActionRecoverySnapshot {
action: Readonly<ApprovedActionDispatchExecutionSnapshot>;
recovery: Readonly<ApprovedActionRecoveryControlRecord>;
resolution: Readonly<ApprovedActionRecoveryResolutionRecord> | null;
}
export interface ApprovedActionRecoveryCursor {
nextScanAtMs: number;
dispatchId: string;
}
export const MAX_APPROVED_ACTION_RECOVERY_VERSION = 2_147_483_647;
export const MAX_APPROVED_ACTION_RECOVERY_FINDINGS = 2_147_483_647;
const EVIDENCE_DIGEST_PATTERN = /^[0-9a-f]{64}$/;
export class InvalidApprovedActionRecoveryError extends TypeError {
constructor(message: string) {
super(`Approved action recovery is invalid: ${message}`);
this.name = 'InvalidApprovedActionRecoveryError';
}
}
export class ApprovedActionRecoveryFenceRejectedError extends Error {
readonly code = 'APPROVED_ACTION_RECOVERY_FENCE_REJECTED';
constructor() {
super('Approved action recovery fence was rejected');
this.name = 'ApprovedActionRecoveryFenceRejectedError';
}
}
export class ApprovedActionRecoveryBindingConflictError extends Error {
readonly code = 'APPROVED_ACTION_RECOVERY_BINDING_CONFLICT';
constructor() {
super('Approved action recovery identity does not match its execution');
this.name = 'ApprovedActionRecoveryBindingConflictError';
}
}
export class ApprovedActionRecoveryRepositoryError extends Error {
readonly code = 'APPROVED_ACTION_RECOVERY_REPOSITORY_ERROR';
constructor() {
super('Approved action recovery repository is unavailable');
this.name = 'ApprovedActionRecoveryRepositoryError';
}
}
function assertExactKeys(
name: string,
value: object,
expected: readonly string[],
): void {
const keys = Object.keys(value).sort();
const canonical = [...expected].sort();
if (
keys.length !== canonical.length ||
keys.some((key, index) => key !== canonical[index])
) {
throw new InvalidApprovedActionRecoveryError(`${name} shape is invalid`);
}
}
function assertInteger(
name: string,
value: number,
minimum: number,
maximum: number,
): void {
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
throw new InvalidApprovedActionRecoveryError(`${name} is invalid`);
}
}
function allNull(values: readonly unknown[]): boolean {
return values.every((value) => value === null);
}
function allPresent(values: readonly unknown[]): boolean {
return values.every((value) => value !== null);
}
export function assertApprovedActionEvidenceDigest(value: string): void {
if (typeof value !== 'string' || !EVIDENCE_DIGEST_PATTERN.test(value)) {
throw new InvalidApprovedActionRecoveryError(
'evidence digest must be a lowercase SHA-256 digest',
);
}
}
export function normalizeApprovedActionRecoveryCursor(
value: ApprovedActionRecoveryCursor,
): Readonly<ApprovedActionRecoveryCursor> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidApprovedActionRecoveryError('cursor must be an object');
}
assertExactKeys('cursor', value, ['nextScanAtMs', 'dispatchId']);
assertApprovalTimestamp('nextScanAtMs', value.nextScanAtMs);
assertApprovalMutationId(value.dispatchId);
return Object.freeze({ ...value });
}
export function normalizeApprovedActionRecoveryControlRecord(
value: ApprovedActionRecoveryControlRecord,
): Readonly<ApprovedActionRecoveryControlRecord> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidApprovedActionRecoveryError(
'recovery control must be an object',
);
}
assertExactKeys('recovery control', value, [
'dispatchId',
'projectId',
'executionVersion',
'status',
'version',
'nextScanAtMs',
'leaseOwner',
'leaseToken',
'leaseExpiresAtMs',
'findingCount',
'lastFindingMutationId',
'lastFinding',
'lastResultCode',
'lastEvidenceDigest',
'resolutionMutationId',
'createdAtMs',
'updatedAtMs',
]);
assertApprovalMutationId(value.dispatchId);
assertProjectPolicyProjectId(value.projectId);
if (!APPROVED_ACTION_RECOVERY_CONTROL_STATUSES.includes(value.status)) {
throw new InvalidApprovedActionRecoveryError('status is invalid');
}
assertInteger(
'execution version',
value.executionVersion,
1,
MAX_APPROVED_ACTION_RECOVERY_VERSION,
);
assertInteger(
'version',
value.version,
0,
MAX_APPROVED_ACTION_RECOVERY_VERSION,
);
assertInteger(
'finding count',
value.findingCount,
0,
MAX_APPROVED_ACTION_RECOVERY_FINDINGS,
);
assertApprovalTimestamp('createdAtMs', value.createdAtMs);
assertApprovalTimestamp('updatedAtMs', value.updatedAtMs);
if (value.updatedAtMs < value.createdAtMs) {
throw new InvalidApprovedActionRecoveryError('timestamps are invalid');
}
if (value.nextScanAtMs !== null) {
assertApprovalTimestamp('nextScanAtMs', value.nextScanAtMs);
if (value.nextScanAtMs <= value.updatedAtMs) {
throw new InvalidApprovedActionRecoveryError(
'next scan must be after the latest update',
);
}
}
const lease = [value.leaseOwner, value.leaseToken, value.leaseExpiresAtMs];
if (!allNull(lease) && !allPresent(lease)) {
throw new InvalidApprovedActionRecoveryError('lease tuple is incomplete');
}
const hasLease = allPresent(lease);
if (hasLease) {
assertApprovedActionLeaseIdentity(value.leaseOwner!);
assertApprovedActionLeaseIdentity(value.leaseToken!);
assertApprovalTimestamp('leaseExpiresAtMs', value.leaseExpiresAtMs!);
assertApprovedActionLeaseDuration(
value.leaseExpiresAtMs! - value.updatedAtMs,
);
}
const finding = [
value.lastFindingMutationId,
value.lastFinding,
value.lastResultCode,
];
if (!allNull(finding) && !allPresent(finding)) {
throw new InvalidApprovedActionRecoveryError('finding tuple is incomplete');
}
if (value.lastFinding !== null) {
assertApprovalMutationId(value.lastFindingMutationId!);
if (!APPROVED_ACTION_RECOVERY_FINDINGS.includes(value.lastFinding)) {
throw new InvalidApprovedActionRecoveryError('finding is invalid');
}
assertApprovedActionResultCode(value.lastResultCode!);
}
if (
(value.findingCount === 0) !==
(value.lastFindingMutationId === null &&
value.lastFinding === null &&
value.lastResultCode === null)
) {
throw new InvalidApprovedActionRecoveryError('finding count is invalid');
}
if (value.lastEvidenceDigest !== null) {
assertApprovedActionEvidenceDigest(value.lastEvidenceDigest);
}
if (value.resolutionMutationId !== null) {
assertApprovalMutationId(value.resolutionMutationId);
}
if (
value.status === 'armed' &&
(value.nextScanAtMs === null ||
hasLease ||
value.resolutionMutationId !== null)
) {
throw new InvalidApprovedActionRecoveryError('armed tuple is invalid');
}
if (
value.status === 'leased' &&
(!hasLease ||
value.nextScanAtMs !== value.leaseExpiresAtMs ||
value.resolutionMutationId !== null)
) {
throw new InvalidApprovedActionRecoveryError('leased tuple is invalid');
}
if (
value.status === 'manual_required' &&
(value.nextScanAtMs !== null ||
hasLease ||
value.findingCount < 1 ||
value.resolutionMutationId !== null)
) {
throw new InvalidApprovedActionRecoveryError(
'manual-required tuple is invalid',
);
}
if (
value.status === 'resolved' &&
(value.nextScanAtMs !== null ||
hasLease ||
value.resolutionMutationId === null)
) {
throw new InvalidApprovedActionRecoveryError('resolved tuple is invalid');
}
return Object.freeze({ ...value });
}
export function normalizeApprovedActionRecoveryResolutionRecord(
value: ApprovedActionRecoveryResolutionRecord,
): Readonly<ApprovedActionRecoveryResolutionRecord> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidApprovedActionRecoveryError(
'recovery resolution must be an object',
);
}
assertExactKeys('recovery resolution', value, [
'dispatchId',
'projectId',
'executionVersion',
'mutationId',
'source',
'decision',
'evidenceDigest',
'reasonCode',
'resolvedBy',
'resolvedAtMs',
]);
assertApprovalMutationId(value.dispatchId);
assertProjectPolicyProjectId(value.projectId);
assertInteger(
'execution version',
value.executionVersion,
1,
MAX_APPROVED_ACTION_RECOVERY_VERSION,
);
assertApprovalMutationId(value.mutationId);
if (!APPROVED_ACTION_RECOVERY_SOURCES.includes(value.source)) {
throw new InvalidApprovedActionRecoveryError('source is invalid');
}
if (!APPROVED_ACTION_RECOVERY_DECISIONS.includes(value.decision)) {
throw new InvalidApprovedActionRecoveryError('decision is invalid');
}
assertApprovedActionResultCode(value.reasonCode);
assertApprovalTimestamp('resolvedAtMs', value.resolvedAtMs);
if (value.evidenceDigest !== null) {
assertApprovedActionEvidenceDigest(value.evidenceDigest);
}
const resolvedBy = value.resolvedBy
? normalizePolicySubject(value.resolvedBy)
: null;
if (
value.source === 'automatic_evidence' &&
(value.evidenceDigest === null ||
resolvedBy !== null ||
value.decision === 'abandon_unknown')
) {
throw new InvalidApprovedActionRecoveryError(
'automatic resolution tuple is invalid',
);
}
if (value.source === 'human' && (!resolvedBy || resolvedBy.type !== 'user')) {
throw new InvalidApprovedActionRecoveryError(
'human resolution requires a User',
);
}
return Object.freeze({ ...value, resolvedBy });
}
export function assertApprovedActionRecoveryPageSize(value: number): void {
assertApprovedActionPageSize(value);
}
export function assertApprovedActionRecoveryLeaseDuration(value: number): void {
assertApprovedActionLeaseDuration(value);
}
@@ -0,0 +1,214 @@
import { createHash } from 'crypto';
import type { AuthenticationAssurance } from './authenticatedPrincipal';
import {
assertApprovalMutationId,
assertApprovalTimestamp,
} from './approvalRequest';
import {
assertProjectPolicyProjectId,
normalizePolicySubject,
type PolicySubject,
} from './projectPolicy';
export const APPROVED_ACTION_RECOVERY_STRONG_ASSURANCES = [
'multi_factor',
'hardware',
'local_console',
] as const satisfies readonly AuthenticationAssurance[];
export type ApprovedActionRecoveryStrongAssurance =
(typeof APPROVED_ACTION_RECOVERY_STRONG_ASSURANCES)[number];
export const MAX_APPROVED_ACTION_RECOVERY_AUTH_AGE_MS = 5 * 60 * 1000;
export const MAX_APPROVED_ACTION_RECOVERY_AUTHENTICATION_ID_LENGTH = 128;
export interface ApprovedActionRecoveryAuthorizationFact {
dispatchId: string;
projectId: string;
mutationId: string;
resolvedBy: Readonly<PolicySubject>;
authenticationId: string;
assurance: ApprovedActionRecoveryStrongAssurance;
authenticatedAtMs: number;
projectVersion: number;
bindingVersion: number;
authorizedAtMs: number;
factDigest: string;
}
const AUTHENTICATION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
const MAX_VERSION = 2_147_483_647;
export class InvalidApprovedActionRecoveryAuthorizationError extends TypeError {
constructor(message: string) {
super(`Approved action recovery authorization is invalid: ${message}`);
this.name = 'InvalidApprovedActionRecoveryAuthorizationError';
}
}
export class ApprovedActionRecoveryHumanRequiredError extends Error {
readonly code = 'APPROVED_ACTION_RECOVERY_HUMAN_REQUIRED';
constructor() {
super('Manual recovery requires a stable authenticated User');
this.name = 'ApprovedActionRecoveryHumanRequiredError';
}
}
export class ApprovedActionRecoveryStrongAuthenticationRequiredError extends Error {
readonly code = 'APPROVED_ACTION_RECOVERY_STRONG_AUTHENTICATION_REQUIRED';
constructor() {
super('Manual recovery requires recent strong authentication');
this.name = 'ApprovedActionRecoveryStrongAuthenticationRequiredError';
}
}
export class ApprovedActionRecoveryAuthorizationDeniedError extends Error {
readonly code = 'APPROVED_ACTION_RECOVERY_AUTHORIZATION_DENIED';
constructor() {
super('Manual recovery is denied by Project policy');
this.name = 'ApprovedActionRecoveryAuthorizationDeniedError';
}
}
export class ApprovedActionRecoveryNotFoundError extends Error {
readonly code = 'APPROVED_ACTION_RECOVERY_NOT_FOUND';
constructor() {
super('Approved action recovery does not exist');
this.name = 'ApprovedActionRecoveryNotFoundError';
}
}
function exactKeys(value: object, expected: readonly string[]): void {
const keys = Object.keys(value).sort();
const canonical = [...expected].sort();
if (
keys.length !== canonical.length ||
keys.some((key, index) => key !== canonical[index])
) {
throw new InvalidApprovedActionRecoveryAuthorizationError(
'fact shape is invalid',
);
}
}
function assertVersion(name: string, value: number): void {
if (!Number.isSafeInteger(value) || value < 1 || value > MAX_VERSION) {
throw new InvalidApprovedActionRecoveryAuthorizationError(
`${name} is invalid`,
);
}
}
function canonicalFact(
fact: Omit<ApprovedActionRecoveryAuthorizationFact, 'factDigest'>,
): string {
return JSON.stringify({
dispatchId: fact.dispatchId,
projectId: fact.projectId,
mutationId: fact.mutationId,
resolvedByType: fact.resolvedBy.type,
resolvedById: fact.resolvedBy.id,
authenticationId: fact.authenticationId,
assurance: fact.assurance,
authenticatedAtMs: fact.authenticatedAtMs,
projectVersion: fact.projectVersion,
bindingVersion: fact.bindingVersion,
authorizedAtMs: fact.authorizedAtMs,
});
}
export function digestApprovedActionRecoveryAuthorizationFact(
fact: Omit<ApprovedActionRecoveryAuthorizationFact, 'factDigest'>,
): string {
return createHash('sha256').update(canonicalFact(fact), 'utf8').digest('hex');
}
export function normalizeApprovedActionRecoveryAuthorizationFact(
value: ApprovedActionRecoveryAuthorizationFact,
): Readonly<ApprovedActionRecoveryAuthorizationFact> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidApprovedActionRecoveryAuthorizationError(
'fact must be an object',
);
}
exactKeys(value, [
'dispatchId',
'projectId',
'mutationId',
'resolvedBy',
'authenticationId',
'assurance',
'authenticatedAtMs',
'projectVersion',
'bindingVersion',
'authorizedAtMs',
'factDigest',
]);
assertApprovalMutationId(value.dispatchId);
assertProjectPolicyProjectId(value.projectId);
assertApprovalMutationId(value.mutationId);
const resolvedBy = normalizePolicySubject(value.resolvedBy);
if (resolvedBy.type !== 'user') {
throw new InvalidApprovedActionRecoveryAuthorizationError(
'resolvedBy must be a User',
);
}
if (
typeof value.authenticationId !== 'string' ||
value.authenticationId.length < 1 ||
value.authenticationId.length >
MAX_APPROVED_ACTION_RECOVERY_AUTHENTICATION_ID_LENGTH ||
!AUTHENTICATION_ID_PATTERN.test(value.authenticationId)
) {
throw new InvalidApprovedActionRecoveryAuthorizationError(
'authenticationId is invalid',
);
}
if (!APPROVED_ACTION_RECOVERY_STRONG_ASSURANCES.includes(value.assurance)) {
throw new InvalidApprovedActionRecoveryAuthorizationError(
'assurance is not strong',
);
}
assertApprovalTimestamp('authenticatedAtMs', value.authenticatedAtMs);
assertApprovalTimestamp('authorizedAtMs', value.authorizedAtMs);
if (
value.authenticatedAtMs > value.authorizedAtMs ||
value.authorizedAtMs - value.authenticatedAtMs >
MAX_APPROVED_ACTION_RECOVERY_AUTH_AGE_MS
) {
throw new InvalidApprovedActionRecoveryAuthorizationError(
'authentication is not recent',
);
}
assertVersion('projectVersion', value.projectVersion);
assertVersion('bindingVersion', value.bindingVersion);
if (
typeof value.factDigest !== 'string' ||
!DIGEST_PATTERN.test(value.factDigest)
) {
throw new InvalidApprovedActionRecoveryAuthorizationError(
'factDigest is invalid',
);
}
const { factDigest, ...unsigned } = value;
if (digestApprovedActionRecoveryAuthorizationFact(unsigned) !== factDigest) {
throw new InvalidApprovedActionRecoveryAuthorizationError(
'factDigest does not match',
);
}
return Object.freeze({ ...value, resolvedBy });
}
export function createApprovedActionRecoveryAuthorizationFact(
value: Omit<ApprovedActionRecoveryAuthorizationFact, 'factDigest'>,
): Readonly<ApprovedActionRecoveryAuthorizationFact> {
return normalizeApprovedActionRecoveryAuthorizationFact({
...value,
factDigest: digestApprovedActionRecoveryAuthorizationFact(value),
});
}
+286
View File
@@ -0,0 +1,286 @@
import { createHash } from 'crypto';
import { EXECUTOR_TYPES, type ExecutorType } from './execution';
import {
assertApprovalMutationId,
assertApprovalRequestId,
assertApprovalTimestamp,
} from './approvalRequest';
import { assertProjectPolicyProjectId } from './projectPolicy';
export const APPROVED_RUN_ACTION_TYPE = 'run.create';
export const APPROVED_RUN_RECEIPT_SCHEMA_VERSION = 1;
export const APPROVED_RUN_RECEIPT_RESULT_CODE = 'approved_run_created';
export interface ApprovedRunCreationPlan {
schemaVersion: 1;
actionRef: string;
projectId: string;
taskId: string;
taskRevision: string;
executorType: ExecutorType;
priority: number;
taskName?: string;
taskSnapshotRef?: string;
inputRef?: string;
}
export interface ApprovedRunCreationReceipt {
schemaVersion: 1;
dispatchId: string;
approvalRequestId: string;
projectId: string;
actionType: typeof APPROVED_RUN_ACTION_TYPE;
actionDigest: string;
executionAttempt: number;
executionVersion: number;
startedAtMs: number;
idempotencyKey: string;
outcome: 'succeeded';
resultCode: typeof APPROVED_RUN_RECEIPT_RESULT_CODE;
resourceType: 'run';
resourceId: string;
finishedAtMs: number;
evidenceDigest: string;
createdAtMs: number;
}
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
const MAX_EXECUTION_VERSION = 2_147_483_647;
export class InvalidApprovedRunActionError extends TypeError {
constructor(message: string) {
super(`Approved Run action is invalid: ${message}`);
this.name = 'InvalidApprovedRunActionError';
}
}
export class ApprovedRunActionBindingConflictError extends Error {
readonly code = 'APPROVED_RUN_ACTION_BINDING_CONFLICT';
constructor() {
super('Approved Run action identity does not match its durable receipt');
this.name = 'ApprovedRunActionBindingConflictError';
}
}
export class ApprovedRunActionRepositoryError extends Error {
readonly code = 'APPROVED_RUN_ACTION_REPOSITORY_ERROR';
constructor() {
super('Approved Run action repository is unavailable');
this.name = 'ApprovedRunActionRepositoryError';
}
}
function exactKeys(value: object, expected: readonly string[]): void {
const keys = Object.keys(value).sort();
const canonical = [...expected].sort();
if (
keys.length !== canonical.length ||
keys.some((key, index) => key !== canonical[index])
) {
throw new InvalidApprovedRunActionError('object shape is invalid');
}
}
function assertBoundedText(name: string, value: string, maximum: number): void {
if (
typeof value !== 'string' ||
value.length < 1 ||
value.length > maximum ||
/[\u0000-\u001f\u007f]/.test(value)
) {
throw new InvalidApprovedRunActionError(`${name} is invalid`);
}
}
function assertIdentifier(name: string, value: string, maximum: number): void {
if (
typeof value !== 'string' ||
value.length < 1 ||
value.length > maximum ||
!IDENTIFIER_PATTERN.test(value)
) {
throw new InvalidApprovedRunActionError(`${name} is invalid`);
}
}
function assertPositiveInteger(
name: string,
value: number,
maximum = MAX_EXECUTION_VERSION,
): void {
if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
throw new InvalidApprovedRunActionError(`${name} is invalid`);
}
}
export function normalizeApprovedRunCreationPlan(
value: ApprovedRunCreationPlan,
): Readonly<ApprovedRunCreationPlan> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidApprovedRunActionError('plan must be an object');
}
exactKeys(value, [
'schemaVersion',
'actionRef',
'projectId',
'taskId',
'taskRevision',
'executorType',
'priority',
...(value.taskName === undefined ? [] : ['taskName']),
...(value.taskSnapshotRef === undefined ? [] : ['taskSnapshotRef']),
...(value.inputRef === undefined ? [] : ['inputRef']),
]);
if (value.schemaVersion !== 1) {
throw new InvalidApprovedRunActionError('schema version is unsupported');
}
assertIdentifier('actionRef', value.actionRef, 255);
assertProjectPolicyProjectId(value.projectId);
assertBoundedText('taskId', value.taskId, 255);
assertBoundedText('taskRevision', value.taskRevision, 128);
if (!EXECUTOR_TYPES.includes(value.executorType)) {
throw new InvalidApprovedRunActionError('executorType is unsupported');
}
if (
!Number.isSafeInteger(value.priority) ||
value.priority < -2_147_483_648 ||
value.priority > 2_147_483_647
) {
throw new InvalidApprovedRunActionError('priority is invalid');
}
if (value.taskName !== undefined) {
assertBoundedText('taskName', value.taskName, 255);
}
if (value.taskSnapshotRef !== undefined) {
assertBoundedText('taskSnapshotRef', value.taskSnapshotRef, 512);
}
if (value.inputRef !== undefined) {
assertBoundedText('inputRef', value.inputRef, 512);
}
return Object.freeze({ ...value });
}
export function digestApprovedRunCreationPlan(
value: ApprovedRunCreationPlan,
): string {
const plan = normalizeApprovedRunCreationPlan(value);
const canonical = JSON.stringify({
schemaVersion: plan.schemaVersion,
actionType: APPROVED_RUN_ACTION_TYPE,
actionRef: plan.actionRef,
projectId: plan.projectId,
taskId: plan.taskId,
taskRevision: plan.taskRevision,
executorType: plan.executorType,
priority: plan.priority,
taskName: plan.taskName ?? null,
taskSnapshotRef: plan.taskSnapshotRef ?? null,
inputRef: plan.inputRef ?? null,
});
return createHash('sha256').update(canonical, 'utf8').digest('hex');
}
function canonicalApprovedRunReceipt(
receipt: Omit<ApprovedRunCreationReceipt, 'evidenceDigest'>,
): string {
return JSON.stringify({
schemaVersion: receipt.schemaVersion,
dispatchId: receipt.dispatchId,
approvalRequestId: receipt.approvalRequestId,
projectId: receipt.projectId,
actionType: receipt.actionType,
actionDigest: receipt.actionDigest,
executionAttempt: receipt.executionAttempt,
executionVersion: receipt.executionVersion,
startedAtMs: receipt.startedAtMs,
idempotencyKey: receipt.idempotencyKey,
outcome: receipt.outcome,
resultCode: receipt.resultCode,
resourceType: receipt.resourceType,
resourceId: receipt.resourceId,
finishedAtMs: receipt.finishedAtMs,
createdAtMs: receipt.createdAtMs,
});
}
export function digestApprovedRunCreationReceipt(
receipt: Omit<ApprovedRunCreationReceipt, 'evidenceDigest'>,
): string {
return createHash('sha256')
.update(canonicalApprovedRunReceipt(receipt), 'utf8')
.digest('hex');
}
export function normalizeApprovedRunCreationReceipt(
value: ApprovedRunCreationReceipt,
): Readonly<ApprovedRunCreationReceipt> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidApprovedRunActionError('receipt must be an object');
}
exactKeys(value, [
'schemaVersion',
'dispatchId',
'approvalRequestId',
'projectId',
'actionType',
'actionDigest',
'executionAttempt',
'executionVersion',
'startedAtMs',
'idempotencyKey',
'outcome',
'resultCode',
'resourceType',
'resourceId',
'finishedAtMs',
'evidenceDigest',
'createdAtMs',
]);
if (value.schemaVersion !== APPROVED_RUN_RECEIPT_SCHEMA_VERSION) {
throw new InvalidApprovedRunActionError(
'receipt schema version is unsupported',
);
}
assertApprovalMutationId(value.dispatchId);
assertApprovalRequestId(value.approvalRequestId);
assertProjectPolicyProjectId(value.projectId);
if (value.actionType !== APPROVED_RUN_ACTION_TYPE) {
throw new InvalidApprovedRunActionError('actionType is invalid');
}
if (!DIGEST_PATTERN.test(value.actionDigest)) {
throw new InvalidApprovedRunActionError('actionDigest is invalid');
}
assertPositiveInteger('executionAttempt', value.executionAttempt, 16);
assertPositiveInteger('executionVersion', value.executionVersion);
assertApprovalTimestamp('startedAtMs', value.startedAtMs);
if (value.idempotencyKey !== value.dispatchId) {
throw new InvalidApprovedRunActionError('idempotency binding is invalid');
}
if (
value.outcome !== 'succeeded' ||
value.resultCode !== APPROVED_RUN_RECEIPT_RESULT_CODE ||
value.resourceType !== 'run'
) {
throw new InvalidApprovedRunActionError('result tuple is invalid');
}
assertIdentifier('resourceId', value.resourceId, 64);
assertApprovalTimestamp('finishedAtMs', value.finishedAtMs);
assertApprovalTimestamp('createdAtMs', value.createdAtMs);
if (
value.finishedAtMs < value.startedAtMs ||
value.createdAtMs !== value.finishedAtMs
) {
throw new InvalidApprovedRunActionError('receipt timestamps are invalid');
}
if (!DIGEST_PATTERN.test(value.evidenceDigest)) {
throw new InvalidApprovedRunActionError('evidenceDigest is invalid');
}
const { evidenceDigest, ...unsigned } = value;
if (digestApprovedRunCreationReceipt(unsigned) !== evidenceDigest) {
throw new InvalidApprovedRunActionError('evidenceDigest does not match');
}
return Object.freeze({ ...value });
}
+138
View File
@@ -0,0 +1,138 @@
import { assertCompletionReceiptId } from './completionReceipt';
import {
LOCAL_ARTIFACT_RETENTION_DISPOSITIONS,
assertLocalArtifactRetentionTimestamp,
type LocalArtifactRetentionDisposition,
} from './localArtifactRetention';
import { assertLocalExecutionArtifactId } from './localExecutionArtifact';
import {
MAX_POLICY_SUBJECT_ID_LENGTH,
POLICY_SUBJECT_TYPES,
normalizePolicySubject,
type PolicySubject,
type PolicySubjectType,
} from './projectPolicy';
export const MAX_LOCAL_ARTIFACT_READ_BYTES = 256 * 1024;
export const MAX_ARTIFACT_READ_SUBJECT_ID_LENGTH = MAX_POLICY_SUBJECT_ID_LENGTH;
export const ARTIFACT_READ_SUBJECT_TYPES = POLICY_SUBJECT_TYPES;
export type ArtifactReadSubjectType = PolicySubjectType;
export type ArtifactReadSubject = PolicySubject;
export interface LocalArtifactReadRange {
offset: number;
length: number;
}
export interface LocalArtifactReadRetentionEvidence {
disposition: LocalArtifactRetentionDisposition;
finishedAtMs: number;
eligibleAtMs: number;
bytesReclaimed: number;
recordedAtMs: number;
}
export interface LocalArtifactReadMetadata {
projectId: string;
runId: string;
attemptId: string;
logArtifactId: string;
retention?: LocalArtifactReadRetentionEvidence;
}
export function assertArtifactReadProjectId(value: string): void {
if (
typeof value !== 'string' ||
value.length < 1 ||
value.length > 128 ||
/[\u0000-\u001f\u007f]/.test(value)
) {
throw new TypeError('Artifact read projectId is invalid');
}
}
export function normalizeArtifactReadSubject(
subject: ArtifactReadSubject,
): Readonly<ArtifactReadSubject> {
return normalizePolicySubject(subject);
}
export function normalizeLocalArtifactReadRange(
range: LocalArtifactReadRange,
): Readonly<LocalArtifactReadRange> {
if (!range || typeof range !== 'object' || Array.isArray(range)) {
throw new TypeError('Local Artifact read range must be an object');
}
if (!Number.isSafeInteger(range.offset) || range.offset < 0) {
throw new RangeError('Local Artifact read offset is invalid');
}
if (
!Number.isSafeInteger(range.length) ||
range.length < 1 ||
range.length > MAX_LOCAL_ARTIFACT_READ_BYTES
) {
throw new RangeError('Local Artifact read length is invalid');
}
return Object.freeze({ offset: range.offset, length: range.length });
}
export function normalizeLocalArtifactReadMetadata(
value: LocalArtifactReadMetadata,
): Readonly<LocalArtifactReadMetadata> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new TypeError('Local Artifact read metadata must be an object');
}
assertArtifactReadProjectId(value.projectId);
assertCompletionReceiptId(value.runId, 'runId');
assertCompletionReceiptId(value.attemptId, 'attemptId');
assertLocalExecutionArtifactId(value.logArtifactId);
let retention: Readonly<LocalArtifactReadRetentionEvidence> | undefined;
if (value.retention) {
if (
!LOCAL_ARTIFACT_RETENTION_DISPOSITIONS.includes(
value.retention.disposition,
)
) {
throw new TypeError('Local Artifact retention disposition is invalid');
}
assertLocalArtifactRetentionTimestamp(
'finishedAtMs',
value.retention.finishedAtMs,
);
assertLocalArtifactRetentionTimestamp(
'eligibleAtMs',
value.retention.eligibleAtMs,
);
assertLocalArtifactRetentionTimestamp(
'bytesReclaimed',
value.retention.bytesReclaimed,
);
assertLocalArtifactRetentionTimestamp(
'recordedAtMs',
value.retention.recordedAtMs,
);
if (value.retention.eligibleAtMs < value.retention.finishedAtMs) {
throw new TypeError('Artifact retention eligibility is invalid');
}
if (value.retention.recordedAtMs < value.retention.eligibleAtMs) {
throw new TypeError('Artifact retention recording time is invalid');
}
if (
value.retention.disposition === 'already_absent' &&
value.retention.bytesReclaimed !== 0
) {
throw new TypeError(
'Absent Artifact retention reclaimed bytes is invalid',
);
}
retention = Object.freeze({ ...value.retention });
}
return Object.freeze({
projectId: value.projectId,
runId: value.runId,
attemptId: value.attemptId,
logArtifactId: value.logArtifactId,
...(retention ? { retention } : {}),
});
}
@@ -0,0 +1,109 @@
import {
InvalidProjectPolicyValueError,
normalizePolicySubject,
type PolicySubject,
} from './projectPolicy';
export const AUTHENTICATION_ASSURANCE_LEVELS = [
'single_factor',
'multi_factor',
'service',
'hardware',
'local_console',
] as const;
export type AuthenticationAssurance =
(typeof AUTHENTICATION_ASSURANCE_LEVELS)[number];
export interface AuthenticatedPrincipal {
subject: PolicySubject;
authenticationId: string;
authenticatedAtMs: number;
expiresAtMs: number;
assurance: AuthenticationAssurance;
}
export const MAX_AUTHENTICATION_ID_LENGTH = 128;
const AUTHENTICATION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
function assertTimestamp(name: string, value: number): void {
if (!Number.isSafeInteger(value) || value < 0) {
throw new InvalidProjectPolicyValueError(`${name} is invalid`);
}
}
export function normalizeAuthenticatedPrincipal(
value: AuthenticatedPrincipal,
): Readonly<AuthenticatedPrincipal> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidProjectPolicyValueError(
'authenticated principal must be an object',
);
}
const keys = Object.keys(value).sort();
const expected = [
'assurance',
'authenticatedAtMs',
'authenticationId',
'expiresAtMs',
'subject',
];
if (
keys.length !== expected.length ||
keys.some((key, index) => key !== expected[index])
) {
throw new InvalidProjectPolicyValueError(
'authenticated principal shape is invalid',
);
}
const subject = normalizePolicySubject(value.subject);
if (
typeof value.authenticationId !== 'string' ||
value.authenticationId.length < 1 ||
value.authenticationId.length > MAX_AUTHENTICATION_ID_LENGTH ||
!AUTHENTICATION_ID_PATTERN.test(value.authenticationId)
) {
throw new InvalidProjectPolicyValueError('authenticationId is invalid');
}
assertTimestamp('authenticatedAtMs', value.authenticatedAtMs);
assertTimestamp('expiresAtMs', value.expiresAtMs);
if (value.expiresAtMs <= value.authenticatedAtMs) {
throw new InvalidProjectPolicyValueError(
'authenticated principal lifetime is invalid',
);
}
if (!AUTHENTICATION_ASSURANCE_LEVELS.includes(value.assurance)) {
throw new InvalidProjectPolicyValueError(
'authentication assurance is invalid',
);
}
return Object.freeze({
subject,
authenticationId: value.authenticationId,
authenticatedAtMs: value.authenticatedAtMs,
expiresAtMs: value.expiresAtMs,
assurance: value.assurance,
});
}
export function assertAuthenticatedPrincipalActive(
principal: Readonly<AuthenticatedPrincipal>,
nowMs: number,
): void {
if (!Number.isSafeInteger(nowMs) || nowMs < 0) {
throw new InvalidProjectPolicyValueError('current time is invalid');
}
if (principal.authenticatedAtMs > nowMs || principal.expiresAtMs <= nowMs) {
throw new AuthenticatedPrincipalExpiredError();
}
}
export class AuthenticatedPrincipalExpiredError extends Error {
readonly code = 'AUTHENTICATED_PRINCIPAL_EXPIRED';
constructor() {
super('Authenticated principal is not active');
this.name = 'AuthenticatedPrincipalExpiredError';
}
}
@@ -0,0 +1,41 @@
export const CANCELLATION_DISPATCH_STATUSES = [
'pending',
'leased',
'retry_wait',
'dispatched',
'blocked',
] as const;
export type CancellationDispatchStatus =
(typeof CANCELLATION_DISPATCH_STATUSES)[number];
export const CANCELLATION_DISPATCH_RESULTS = [
'termination_requested',
'already_exited',
'identity_mismatch',
'pid_mismatch',
'unsupported',
'invalid',
'controller_missing',
'handle_missing',
'dispatch_error',
] as const;
export type CancellationDispatchResult =
(typeof CANCELLATION_DISPATCH_RESULTS)[number];
export interface CancellationDispatchRecord {
runId: string;
attemptId: string;
status: CancellationDispatchStatus;
version: number;
dispatchCount: number;
nextAttemptAtMs?: number;
leaseOwner?: string;
leaseToken?: string;
leaseExpiresAtMs?: number;
lastResult?: CancellationDispatchResult;
lastDispatchedAtMs?: number;
createdAtMs: number;
updatedAtMs: number;
}
@@ -0,0 +1,46 @@
export class CancellationDispatchError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly retryable = false,
public readonly cause?: unknown,
) {
super(message);
this.name = new.target.name;
}
}
export class InvalidCancellationDispatchCommandError extends CancellationDispatchError {
constructor(message: string) {
super(message, 'INVALID_CANCELLATION_DISPATCH_COMMAND');
}
}
export class CancellationDispatchBindingConflictError extends CancellationDispatchError {
constructor(runId: string, attemptId: string) {
super(
`Cancellation dispatch for Run ${runId} is already bound to another Attempt than ${attemptId}`,
'CANCELLATION_DISPATCH_BINDING_CONFLICT',
);
}
}
export class CancellationDispatchFenceRejectedError extends CancellationDispatchError {
constructor(runId: string) {
super(
`Cancellation dispatch lease for Run ${runId} is stale or no longer owned by this worker`,
'CANCELLATION_DISPATCH_FENCE_REJECTED',
);
}
}
export class CancellationDispatchRepositoryError extends CancellationDispatchError {
constructor(cause?: unknown) {
super(
'Cancellation dispatch repository operation failed',
'CANCELLATION_DISPATCH_REPOSITORY_FAILED',
false,
cause,
);
}
}
+218
View File
@@ -0,0 +1,218 @@
export const COMPLETION_RECEIPT_SCHEMA_VERSION = 1;
export const MAX_COMPLETION_RECEIPT_BYTES = 4 * 1024;
const RECEIPT_KEYS = [
'schemaVersion',
'runId',
'attemptId',
'callbackSequence',
'token',
'startedAtMs',
'finishedAtMs',
'exitCode',
] as const;
const RECEIPT_KEY_SET = new Set<string>(RECEIPT_KEYS);
const UUID_V7_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const TOKEN_PATTERN = /^[A-Za-z0-9_-]{32,128}$/;
export interface CompletionReceipt {
schemaVersion: typeof COMPLETION_RECEIPT_SCHEMA_VERSION;
runId: string;
attemptId: string;
callbackSequence: number;
token: string;
startedAtMs: number;
finishedAtMs: number;
exitCode: number;
}
export class InvalidCompletionReceiptError extends Error {
constructor(message: string) {
super(message);
this.name = 'InvalidCompletionReceiptError';
}
}
function invalid(message: string): never {
throw new InvalidCompletionReceiptError(message);
}
function skipWhitespace(value: string, from: number): number {
let index = from;
while (/\s/.test(value[index] ?? '')) index += 1;
return index;
}
function stringEnd(value: string, from: number): number {
if (value[from] !== '"') invalid('Completion receipt key is not a string');
for (let index = from + 1; index < value.length; index += 1) {
if (value[index] === '\\') {
index += 1;
continue;
}
if (value[index] === '"') return index + 1;
}
return invalid('Completion receipt contains an unterminated string');
}
/** JSON.parse silently accepts duplicate keys, so reject them before parsing. */
function assertUniqueFlatObjectKeys(value: string): void {
let index = skipWhitespace(value, 0);
if (value[index] !== '{') invalid('Completion receipt must be a JSON object');
index = skipWhitespace(value, index + 1);
const keys = new Set<string>();
if (value[index] === '}') return;
while (index < value.length) {
const keyStart = index;
const keyEnd = stringEnd(value, keyStart);
let key: string;
try {
key = JSON.parse(value.slice(keyStart, keyEnd));
} catch {
return invalid('Completion receipt contains an invalid key');
}
if (keys.has(key)) invalid('Completion receipt contains a duplicate key');
keys.add(key);
index = skipWhitespace(value, keyEnd);
if (value[index] !== ':') invalid('Completion receipt key has no value');
index = skipWhitespace(value, index + 1);
if (value[index] === '"') {
index = stringEnd(value, index);
} else {
if (value[index] === '{' || value[index] === '[') {
invalid('Completion receipt values must be scalar');
}
const valueStart = index;
while (
index < value.length &&
value[index] !== ',' &&
value[index] !== '}'
) {
index += 1;
}
if (value.slice(valueStart, index).trim().length === 0) {
invalid('Completion receipt contains an empty value');
}
}
index = skipWhitespace(value, index);
if (value[index] === '}') return;
if (value[index] !== ',') invalid('Completion receipt is not a flat object');
index = skipWhitespace(value, index + 1);
}
invalid('Completion receipt object is incomplete');
}
export function assertCompletionReceiptId(
value: string,
field: 'runId' | 'attemptId',
): void {
if (!UUID_V7_PATTERN.test(value)) {
invalid(`${field} must be a lowercase UUIDv7`);
}
}
function assertSafeTimestamp(value: unknown, field: string): asserts value is number {
if (!Number.isSafeInteger(value) || (value as number) < 0) {
invalid(`${field} must be a non-negative safe integer`);
}
}
export function validateCompletionReceipt(
candidate: unknown,
): CompletionReceipt {
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
invalid('Completion receipt must be an object');
}
const record = candidate as Record<string, unknown>;
const keys = Object.keys(record);
if (
keys.length !== RECEIPT_KEYS.length ||
keys.some((key) => !RECEIPT_KEY_SET.has(key)) ||
RECEIPT_KEYS.some((key) => !Object.hasOwn(record, key))
) {
invalid('Completion receipt fields do not match schema version 1');
}
if (record.schemaVersion !== COMPLETION_RECEIPT_SCHEMA_VERSION) {
invalid('Completion receipt schema version is unsupported');
}
if (typeof record.runId !== 'string') invalid('runId must be a string');
if (typeof record.attemptId !== 'string') {
invalid('attemptId must be a string');
}
assertCompletionReceiptId(record.runId, 'runId');
assertCompletionReceiptId(record.attemptId, 'attemptId');
if (
!Number.isSafeInteger(record.callbackSequence) ||
(record.callbackSequence as number) < 1
) {
invalid('callbackSequence must be a positive safe integer');
}
if (typeof record.token !== 'string' || !TOKEN_PATTERN.test(record.token)) {
invalid('token must be a bounded base64url value');
}
assertSafeTimestamp(record.startedAtMs, 'startedAtMs');
assertSafeTimestamp(record.finishedAtMs, 'finishedAtMs');
if (record.finishedAtMs < record.startedAtMs) {
invalid('finishedAtMs must not be before startedAtMs');
}
if (
!Number.isInteger(record.exitCode) ||
(record.exitCode as number) < 0 ||
(record.exitCode as number) > 255
) {
invalid('exitCode must be an integer between 0 and 255');
}
return {
schemaVersion: COMPLETION_RECEIPT_SCHEMA_VERSION,
runId: record.runId,
attemptId: record.attemptId,
callbackSequence: record.callbackSequence as number,
token: record.token,
startedAtMs: record.startedAtMs,
finishedAtMs: record.finishedAtMs,
exitCode: record.exitCode as number,
};
}
export function serializeCompletionReceipt(receipt: CompletionReceipt): string {
const value = validateCompletionReceipt(receipt);
const serialized = JSON.stringify({
schemaVersion: value.schemaVersion,
runId: value.runId,
attemptId: value.attemptId,
callbackSequence: value.callbackSequence,
token: value.token,
startedAtMs: value.startedAtMs,
finishedAtMs: value.finishedAtMs,
exitCode: value.exitCode,
});
if (Buffer.byteLength(serialized, 'utf8') > MAX_COMPLETION_RECEIPT_BYTES) {
invalid('Completion receipt exceeds the byte limit');
}
return serialized;
}
export function parseCompletionReceipt(
input: string | Uint8Array,
): CompletionReceipt {
const bytes = typeof input === 'string' ? Buffer.from(input) : Buffer.from(input);
if (bytes.length === 0 || bytes.length > MAX_COMPLETION_RECEIPT_BYTES) {
invalid('Completion receipt size is outside the allowed range');
}
const value = bytes.toString('utf8');
if (!Buffer.from(value, 'utf8').equals(bytes)) {
invalid('Completion receipt must be valid UTF-8');
}
assertUniqueFlatObjectKeys(value);
let parsed: unknown;
try {
parsed = JSON.parse(value);
} catch {
return invalid('Completion receipt is not valid JSON');
}
return validateCompletionReceipt(parsed);
}
@@ -0,0 +1,37 @@
import type { RunAttemptStatus } from './run';
export const COMPLETION_RECEIPT_JOURNAL_STATES = [
'pending',
'quarantined',
] as const;
export type CompletionReceiptJournalState =
(typeof COMPLETION_RECEIPT_JOURNAL_STATES)[number];
export interface CompletionReceiptJournalRecord {
attemptId: string;
runId: string;
state: CompletionReceiptJournalState;
quarantineRef?: string;
purgeAfterMs?: number;
registeredAtMs: number;
updatedAtMs: number;
}
export interface CompletionReceiptJournalCandidate
extends CompletionReceiptJournalRecord {
attemptStatus: RunAttemptStatus;
executorType: string;
finishedAtMs?: number;
}
export interface CompletionReceiptJournalCursor {
updatedAtMs: number;
attemptId: string;
}
export interface CompletionReceiptJournalPage {
candidates: readonly CompletionReceiptJournalCandidate[];
truncated: boolean;
nextCursor?: CompletionReceiptJournalCursor;
}
+224
View File
@@ -0,0 +1,224 @@
export const DEPLOYMENT_PROFILES = [
'edge',
'standalone',
'cluster-control',
'worker',
] as const;
export type DeploymentProfile = (typeof DEPLOYMENT_PROFILES)[number];
export interface BoundedRecoveryResourcePolicy {
intervalMs: number;
initialDelayMs: number;
stopTimeoutMs: number;
pageSize: number;
maxPages: number;
}
export interface LocalArtifactRetentionResourcePolicy {
intervalMs: number;
initialDelayMs: number;
stopTimeoutMs: number;
pageSize: number;
maximumDeletions: number;
normalRetentionMs: number;
pressureRetentionMs: number;
}
export interface ApprovedActionResourcePolicy {
intervalMs: number;
initialDelayMs: number;
stopTimeoutMs: number;
dispatch: {
pageSize: number;
maxPages: number;
};
recovery: {
pageSize: number;
maxPages: number;
};
}
export interface LocalPrimaryResourcePolicy {
profile: 'edge' | 'standalone';
receiptPublishGraceMs: number;
receiptTerminalMissingRetentionMs: number;
receiptQuarantineRetentionMs: number;
completion: BoundedRecoveryResourcePolicy;
cancellation: BoundedRecoveryResourcePolicy;
timeout: BoundedRecoveryResourcePolicy;
retry: BoundedRecoveryResourcePolicy;
approvedAction: ApprovedActionResourcePolicy;
artifactRetention: LocalArtifactRetentionResourcePolicy;
}
const VALID_PROFILES = new Set<DeploymentProfile>(DEPLOYMENT_PROFILES);
const LOCAL_PRIMARY_POLICIES: Record<
LocalPrimaryResourcePolicy['profile'],
LocalPrimaryResourcePolicy
> = {
edge: {
profile: 'edge',
receiptPublishGraceMs: 50,
receiptTerminalMissingRetentionMs: 2 * 60_000,
receiptQuarantineRetentionMs: 5 * 60_000,
completion: {
intervalMs: 30_000,
initialDelayMs: 0,
stopTimeoutMs: 5_000,
pageSize: 8,
maxPages: 2,
},
cancellation: {
intervalMs: 5_000,
initialDelayMs: 0,
stopTimeoutMs: 5_000,
pageSize: 8,
maxPages: 2,
},
timeout: {
intervalMs: 30_000,
initialDelayMs: 0,
stopTimeoutMs: 5_000,
pageSize: 8,
maxPages: 2,
},
retry: {
intervalMs: 30_000,
initialDelayMs: 0,
stopTimeoutMs: 5_000,
pageSize: 8,
maxPages: 1,
},
approvedAction: {
intervalMs: 30_000,
initialDelayMs: 0,
stopTimeoutMs: 5_000,
dispatch: { pageSize: 8, maxPages: 1 },
recovery: { pageSize: 8, maxPages: 1 },
},
artifactRetention: {
intervalMs: 5 * 60_000,
initialDelayMs: 0,
stopTimeoutMs: 5_000,
pageSize: 8,
maximumDeletions: 4,
normalRetentionMs: 7 * 24 * 60 * 60_000,
pressureRetentionMs: 24 * 60 * 60_000,
},
},
standalone: {
profile: 'standalone',
receiptPublishGraceMs: 100,
receiptTerminalMissingRetentionMs: 60_000,
receiptQuarantineRetentionMs: 60 * 60_000,
completion: {
intervalMs: 2_000,
initialDelayMs: 0,
stopTimeoutMs: 5_000,
pageSize: 32,
maxPages: 4,
},
cancellation: {
intervalMs: 1_000,
initialDelayMs: 0,
stopTimeoutMs: 5_000,
pageSize: 32,
maxPages: 4,
},
timeout: {
intervalMs: 5_000,
initialDelayMs: 0,
stopTimeoutMs: 5_000,
pageSize: 32,
maxPages: 4,
},
retry: {
intervalMs: 5_000,
initialDelayMs: 0,
stopTimeoutMs: 5_000,
pageSize: 32,
maxPages: 1,
},
approvedAction: {
intervalMs: 2_000,
initialDelayMs: 0,
stopTimeoutMs: 5_000,
dispatch: { pageSize: 32, maxPages: 4 },
recovery: { pageSize: 16, maxPages: 2 },
},
artifactRetention: {
intervalMs: 60_000,
initialDelayMs: 0,
stopTimeoutMs: 5_000,
pageSize: 32,
maximumDeletions: 16,
normalRetentionMs: 30 * 24 * 60 * 60_000,
pressureRetentionMs: 7 * 24 * 60 * 60_000,
},
},
};
export function parseDeploymentProfile(
value: string | undefined,
): DeploymentProfile {
if (value === undefined || value === '') return 'standalone';
if (
value.trim() !== value ||
!VALID_PROFILES.has(value as DeploymentProfile)
) {
throw new TypeError('QL_DEPLOYMENT_PROFILE is invalid');
}
return value as DeploymentProfile;
}
function cloneRecoveryPolicy(
policy: BoundedRecoveryResourcePolicy,
): BoundedRecoveryResourcePolicy {
return { ...policy };
}
function cloneArtifactRetentionPolicy(
policy: LocalArtifactRetentionResourcePolicy,
): LocalArtifactRetentionResourcePolicy {
return { ...policy };
}
function cloneApprovedActionPolicy(
policy: ApprovedActionResourcePolicy,
): ApprovedActionResourcePolicy {
return {
...policy,
dispatch: { ...policy.dispatch },
recovery: { ...policy.recovery },
};
}
/**
* The incubating SQLite + LocalProcess Primary stack is intentionally local.
* Cluster control needs PostgreSQL/shared artifacts; worker has a separate boot
* topology. Refusing those profiles prevents accidental shared-SQLite clusters.
*/
export function localPrimaryResourcePolicy(
profile: DeploymentProfile,
): LocalPrimaryResourcePolicy {
if (profile !== 'edge' && profile !== 'standalone') {
throw new TypeError(
`Deployment profile ${profile} cannot host the local SQLite Primary stack`,
);
}
const policy = LOCAL_PRIMARY_POLICIES[profile];
return {
profile,
receiptPublishGraceMs: policy.receiptPublishGraceMs,
receiptTerminalMissingRetentionMs: policy.receiptTerminalMissingRetentionMs,
receiptQuarantineRetentionMs: policy.receiptQuarantineRetentionMs,
completion: cloneRecoveryPolicy(policy.completion),
cancellation: cloneRecoveryPolicy(policy.cancellation),
timeout: cloneRecoveryPolicy(policy.timeout),
retry: cloneRecoveryPolicy(policy.retry),
approvedAction: cloneApprovedActionPolicy(policy.approvedAction),
artifactRetention: cloneArtifactRetentionPolicy(policy.artifactRetention),
};
}
+154
View File
@@ -0,0 +1,154 @@
export const EXECUTOR_TYPES = [
'local_process',
'docker',
'kubernetes',
'remote_worker',
] as const;
export type ExecutorType = (typeof EXECUTOR_TYPES)[number];
export type ExecutionCommand =
| {
kind: 'argv';
file: string;
args: readonly string[];
}
| {
kind: 'shell';
command: string;
shell?: string;
};
export type ExecutionLimitEnforcement = 'required' | 'best_effort';
export interface ExecutionNumericLimit {
value: number;
enforcement: ExecutionLimitEnforcement;
}
export interface ExecutionResourcePolicy {
memoryBytes?: ExecutionNumericLimit;
cpuMillisPerSecond?: ExecutionNumericLimit;
filesystemIsolation?: ExecutionLimitEnforcement;
networkIsolation?: ExecutionLimitEnforcement;
}
export interface ExecutionSpec {
runId: string;
attemptId: string;
projectId: string;
taskId: string;
taskRevision: string;
command: ExecutionCommand;
workingDirectory?: string;
environmentPolicy: 'inherit' | 'isolated';
timeoutMs?: number;
terminationGraceMs: number;
resourcePolicy?: ExecutionResourcePolicy;
}
export type ExecutionOutputStream = 'stdout' | 'stderr';
export interface ExecutionOutputChunk {
stream: ExecutionOutputStream;
chunk: Uint8Array;
observedAtMs: number;
}
export interface ExecutionOutputSink {
write(output: ExecutionOutputChunk): Promise<void>;
}
export interface ExecutionAbortSignal {
readonly aborted: boolean;
addEventListener?: (
type: 'abort',
listener: () => void,
options?: { once?: boolean },
) => void;
removeEventListener?: (type: 'abort', listener: () => void) => void;
}
export interface ExecutionContext {
environment: Readonly<Record<string, string>>;
signal?: ExecutionAbortSignal;
output: ExecutionOutputSink;
/** Ephemeral capability for the durable completion launcher; never persist raw. */
completionCallback?: {
token: string;
callbackSequence: number;
};
}
export type ExecutionOutcome =
| 'succeeded'
| 'failed'
| 'cancelled'
| 'timed_out'
| 'lost';
export interface ExecutionDiagnostic {
code: string;
summary: string;
}
export interface ExecutionResult {
outcome: ExecutionOutcome;
startedAtMs: number;
finishedAtMs: number;
exitCode?: number;
signal?: NodeJS.Signals;
errorCode?: string;
errorSummary?: string;
diagnostics?: readonly ExecutionDiagnostic[];
}
export interface ExecutionHandle {
id: string;
/** Opaque, bounded identity used to verify ownership after a process restart. */
durableHandle?: string;
executorType: ExecutorType;
runId: string;
attemptId: string;
startedAtMs: number;
pid?: number;
completion: Promise<ExecutionResult>;
}
export type ExecutionStopKind =
| 'user'
| 'policy'
| 'shutdown'
| 'reconcile'
| 'timeout';
export interface ExecutionStopReason {
kind: ExecutionStopKind;
requestedAtMs: number;
}
export interface ExecutionStopResult {
status: 'already_exited' | 'termination_requested';
termSignalSent: boolean;
killSignalSent: boolean;
}
export type ExecutionInspectionStatus = 'running' | 'stopping' | 'exited';
export interface ExecutionInspection {
status: ExecutionInspectionStatus;
result?: ExecutionResult;
}
export type ExecutorCapabilityLevel = 'none' | 'best_effort' | 'enforced';
export interface ExecutorCapabilities {
timeout: boolean;
processGroupTermination: boolean;
workingDirectory: boolean;
isolatedEnvironment: boolean;
memoryLimit: ExecutorCapabilityLevel;
cpuLimit: ExecutorCapabilityLevel;
filesystemIsolation: ExecutorCapabilityLevel;
networkIsolation: ExecutorCapabilityLevel;
}
+72
View File
@@ -0,0 +1,72 @@
import type { ExecutionContext } from './execution';
export const MAX_EXECUTION_ENVIRONMENT_ENTRIES = 256;
export const MAX_EXECUTION_ENVIRONMENT_BYTES = 256 * 1024;
export const MAX_EXECUTION_ENVIRONMENT_VALUE_BYTES = 64 * 1024;
export function normalizeExecutionContext(
context: ExecutionContext,
): ExecutionContext {
if (!context || typeof context !== 'object' || Array.isArray(context)) {
throw new TypeError('ExecutionContext must be an object');
}
const environment = context.environment;
if (
!environment ||
typeof environment !== 'object' ||
Array.isArray(environment)
) {
throw new TypeError('ExecutionContext environment must be an object');
}
const entries = Object.entries(environment);
if (entries.length > MAX_EXECUTION_ENVIRONMENT_ENTRIES) {
throw new RangeError('ExecutionContext environment has too many entries');
}
const cloned: Record<string, string> = Object.create(null);
let totalBytes = 0;
for (const [name, value] of entries) {
if (
name.length < 1 ||
name.length > 255 ||
name.includes('=') ||
name.includes('\0') ||
typeof value !== 'string' ||
value.includes('\0')
) {
throw new TypeError('ExecutionContext environment entry is invalid');
}
const valueBytes = Buffer.byteLength(value, 'utf8');
if (valueBytes > MAX_EXECUTION_ENVIRONMENT_VALUE_BYTES) {
throw new RangeError('ExecutionContext environment value is too large');
}
totalBytes += Buffer.byteLength(name, 'utf8') + valueBytes;
if (totalBytes > MAX_EXECUTION_ENVIRONMENT_BYTES) {
throw new RangeError('ExecutionContext environment is too large');
}
cloned[name] = value;
}
if (
!context.output ||
typeof context.output !== 'object' ||
typeof context.output.write !== 'function'
) {
throw new TypeError('ExecutionContext output sink is invalid');
}
if (
context.signal !== undefined &&
(!context.signal ||
typeof context.signal !== 'object' ||
typeof context.signal.aborted !== 'boolean' ||
(context.signal.addEventListener !== undefined &&
typeof context.signal.addEventListener !== 'function') ||
(context.signal.removeEventListener !== undefined &&
typeof context.signal.removeEventListener !== 'function'))
) {
throw new TypeError('ExecutionContext signal is invalid');
}
return {
environment: Object.freeze(cloned),
...(context.signal === undefined ? {} : { signal: context.signal }),
output: context.output,
};
}
+242
View File
@@ -0,0 +1,242 @@
import path from 'path';
import type {
ExecutionNumericLimit,
ExecutionResourcePolicy,
ExecutionSpec,
} from './execution';
import { InvalidExecutionSpecError } from './executorErrors';
export const MAX_EXECUTION_TIMEOUT_MS = 7 * 24 * 60 * 60 * 1000;
export const MAX_TERMINATION_GRACE_MS = 5 * 60 * 1000;
export const MAX_EXECUTION_ARGUMENTS = 4096;
export const MAX_EXECUTION_COMMAND_BYTES = 128 * 1024;
function invalid(message: string): never {
throw new InvalidExecutionSpecError(message);
}
function assertNonEmptyIdentifier(value: string, name: string): void {
if (
typeof value !== 'string' ||
value.length < 1 ||
value.length > 255 ||
/[\u0000-\u001f\u007f]/.test(value)
) {
invalid(
`${name} must be between 1 and 255 characters and contain no control characters`,
);
}
}
function assertSafeDuration(
value: number,
name: string,
maximum: number,
allowZero: boolean,
): void {
if (
!Number.isSafeInteger(value) ||
value < (allowZero ? 0 : 1) ||
value > maximum
) {
invalid(
`${name} must be a safe integer between ${
allowZero ? 0 : 1
} and ${maximum}`,
);
}
}
function assertEnforcement(
value: unknown,
name: string,
): asserts value is 'required' | 'best_effort' {
if (value !== 'required' && value !== 'best_effort') {
invalid(`${name} enforcement is invalid`);
}
}
function assertNumericLimit(
value: ExecutionNumericLimit | undefined,
name: string,
): void {
if (value === undefined) return;
if (!value || typeof value !== 'object' || Array.isArray(value)) {
invalid(`${name} must be an object`);
}
if (!Number.isSafeInteger(value.value) || value.value < 1) {
invalid(`${name} must be a positive safe integer`);
}
assertEnforcement(value.enforcement, name);
}
function assertResourcePolicy(
policy: ExecutionResourcePolicy | undefined,
): void {
if (policy === undefined) return;
if (!policy || typeof policy !== 'object' || Array.isArray(policy)) {
invalid('resourcePolicy must be an object');
}
assertNumericLimit(policy.memoryBytes, 'memoryBytes');
assertNumericLimit(policy.cpuMillisPerSecond, 'cpuMillisPerSecond');
if (policy.filesystemIsolation !== undefined) {
assertEnforcement(policy.filesystemIsolation, 'filesystemIsolation');
}
if (policy.networkIsolation !== undefined) {
assertEnforcement(policy.networkIsolation, 'networkIsolation');
}
}
function commandSize(spec: ExecutionSpec): number {
if (spec.command.kind === 'shell') {
return Buffer.byteLength(spec.command.command, 'utf8');
}
return (
Buffer.byteLength(spec.command.file, 'utf8') +
spec.command.args.reduce(
(total, argument) => total + Buffer.byteLength(argument, 'utf8'),
0,
)
);
}
export function assertExecutionSpec(spec: ExecutionSpec): void {
if (!spec || typeof spec !== 'object' || Array.isArray(spec)) {
invalid('ExecutionSpec must be an object');
}
assertNonEmptyIdentifier(spec.runId, 'runId');
assertNonEmptyIdentifier(spec.attemptId, 'attemptId');
assertNonEmptyIdentifier(spec.projectId, 'projectId');
assertNonEmptyIdentifier(spec.taskId, 'taskId');
assertNonEmptyIdentifier(spec.taskRevision, 'taskRevision');
assertSafeDuration(
spec.terminationGraceMs,
'terminationGraceMs',
MAX_TERMINATION_GRACE_MS,
true,
);
if (spec.timeoutMs !== undefined) {
assertSafeDuration(
spec.timeoutMs,
'timeoutMs',
MAX_EXECUTION_TIMEOUT_MS,
false,
);
}
if (
spec.environmentPolicy !== 'inherit' &&
spec.environmentPolicy !== 'isolated'
) {
invalid('environmentPolicy is invalid');
}
if (
spec.workingDirectory !== undefined &&
(typeof spec.workingDirectory !== 'string' ||
spec.workingDirectory.includes('\0') ||
!path.isAbsolute(spec.workingDirectory))
) {
invalid('workingDirectory must be an absolute path containing no NUL');
}
if (!spec.command || typeof spec.command !== 'object') {
invalid('command must be an object');
}
if (spec.command.kind === 'argv') {
if (
typeof spec.command.file !== 'string' ||
!spec.command.file ||
spec.command.file.includes('\0')
) {
invalid('argv command file must be non-empty and contain no NUL');
}
if (!Array.isArray(spec.command.args)) {
invalid('argv command args must be an array');
}
if (spec.command.args.length > MAX_EXECUTION_ARGUMENTS) {
invalid('argv command has too many arguments');
}
if (
spec.command.args.some(
(argument) => typeof argument !== 'string' || argument.includes('\0'),
)
) {
invalid('argv command arguments must be strings containing no NUL');
}
} else if (spec.command.kind === 'shell') {
if (
typeof spec.command.command !== 'string' ||
!spec.command.command ||
spec.command.command.includes('\0')
) {
invalid('shell command must be non-empty and contain no NUL');
}
if (
spec.command.shell !== undefined &&
(typeof spec.command.shell !== 'string' ||
!path.isAbsolute(spec.command.shell) ||
spec.command.shell.includes('\0'))
) {
invalid('shell must be an absolute path containing no NUL');
}
} else {
invalid('command kind is invalid');
}
if (commandSize(spec) > MAX_EXECUTION_COMMAND_BYTES) {
invalid('execution command is too large');
}
assertResourcePolicy(spec.resourcePolicy);
}
export function cloneExecutionSpec(spec: ExecutionSpec): ExecutionSpec {
assertExecutionSpec(spec);
return {
runId: spec.runId,
attemptId: spec.attemptId,
projectId: spec.projectId,
taskId: spec.taskId,
taskRevision: spec.taskRevision,
command:
spec.command.kind === 'argv'
? {
kind: 'argv',
file: spec.command.file,
args: [...spec.command.args],
}
: {
kind: 'shell',
command: spec.command.command,
...(spec.command.shell === undefined
? {}
: { shell: spec.command.shell }),
},
...(spec.workingDirectory === undefined
? {}
: { workingDirectory: spec.workingDirectory }),
environmentPolicy: spec.environmentPolicy,
...(spec.timeoutMs === undefined ? {} : { timeoutMs: spec.timeoutMs }),
terminationGraceMs: spec.terminationGraceMs,
...(spec.resourcePolicy === undefined
? {}
: {
resourcePolicy: {
...(spec.resourcePolicy.memoryBytes === undefined
? {}
: { memoryBytes: { ...spec.resourcePolicy.memoryBytes } }),
...(spec.resourcePolicy.cpuMillisPerSecond === undefined
? {}
: {
cpuMillisPerSecond: {
...spec.resourcePolicy.cpuMillisPerSecond,
},
}),
...(spec.resourcePolicy.filesystemIsolation === undefined
? {}
: {
filesystemIsolation: spec.resourcePolicy.filesystemIsolation,
}),
...(spec.resourcePolicy.networkIsolation === undefined
? {}
: { networkIsolation: spec.resourcePolicy.networkIsolation }),
},
}),
};
}
+50
View File
@@ -0,0 +1,50 @@
export class ExecutorError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly cause?: unknown,
) {
super(message);
this.name = new.target.name;
}
}
export class InvalidExecutionSpecError extends ExecutorError {
constructor(message: string) {
super(message, 'INVALID_EXECUTION_SPEC');
}
}
export class ExecutorCapabilityUnavailableError extends ExecutorError {
constructor(public readonly capability: string) {
super(
`Required executor capability is unavailable: ${capability}`,
'EXECUTOR_CAPABILITY_UNAVAILABLE',
);
}
}
export class ExecutorStartError extends ExecutorError {
constructor(cause?: unknown) {
super(
'Executor failed to start the process',
'EXECUTOR_START_FAILED',
cause,
);
}
}
export class ExecutorHandleNotFoundError extends ExecutorError {
constructor(public readonly handleId: string) {
super(
'Execution handle is not owned by this executor instance',
'EXECUTOR_HANDLE_NOT_FOUND',
);
}
}
export class ExecutorStopError extends ExecutorError {
constructor(cause?: unknown) {
super('Executor failed to stop the process', 'EXECUTOR_STOP_FAILED', cause);
}
}
+192
View File
@@ -0,0 +1,192 @@
import {
InvalidProjectPolicyValueError,
normalizePolicySubject,
type PolicySubject,
} from './projectPolicy';
export const IDENTITY_SUBJECT_STATUSES = ['active', 'disabled'] as const;
export const IDENTITY_AUTHENTICATION_BINDING_STATES = [
'active',
'revoked',
] as const;
export const LEGACY_PANEL_IDENTITY_PROVIDER = 'legacy_panel';
export const LEGACY_PANEL_PROVIDER_SUBJECT = 'singleton';
export const LEGACY_PRIMARY_USER_SUBJECT_ID = 'usr_legacy_primary';
export type IdentitySubjectStatus = (typeof IDENTITY_SUBJECT_STATUSES)[number];
export type IdentityAuthenticationBindingState =
(typeof IDENTITY_AUTHENTICATION_BINDING_STATES)[number];
export interface IdentitySubjectRecord {
subject: PolicySubject;
status: IdentitySubjectStatus;
version: number;
createdAtMs: number;
updatedAtMs: number;
}
export interface IdentityAuthenticationBindingRecord {
provider: string;
providerSubject: string;
version: number;
state: IdentityAuthenticationBindingState;
subjectId: string;
createdAtMs: number;
}
export const MAX_IDENTITY_DIRECTORY_VERSION = 2_147_483_647;
export const MAX_IDENTITY_PROVIDER_LENGTH = 64;
export const MAX_IDENTITY_PROVIDER_SUBJECT_LENGTH = 128;
const IDENTITY_PROVIDER_PATTERN = /^[a-z][a-z0-9_:-]*$/;
const IDENTITY_PROVIDER_SUBJECT_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
export class InvalidIdentityDirectoryValueError extends TypeError {
constructor(message: string) {
super(`Identity directory value is invalid: ${message}`);
this.name = 'InvalidIdentityDirectoryValueError';
}
}
export class IdentityDirectoryUnavailableError extends Error {
readonly code = 'IDENTITY_DIRECTORY_UNAVAILABLE';
constructor() {
super('Identity directory is unavailable');
this.name = 'IdentityDirectoryUnavailableError';
}
}
function assertExactKeys(
name: string,
value: object,
expected: readonly string[],
): void {
const keys = Object.keys(value).sort();
const canonical = [...expected].sort();
if (
keys.length !== canonical.length ||
keys.some((key, index) => key !== canonical[index])
) {
throw new InvalidIdentityDirectoryValueError(`${name} shape is invalid`);
}
}
function assertTimestamp(name: string, value: number): void {
if (!Number.isSafeInteger(value) || value < 0) {
throw new InvalidIdentityDirectoryValueError(`${name} is invalid`);
}
}
function assertVersion(name: string, value: number): void {
if (
!Number.isSafeInteger(value) ||
value < 1 ||
value > MAX_IDENTITY_DIRECTORY_VERSION
) {
throw new InvalidIdentityDirectoryValueError(`${name} is invalid`);
}
}
export function assertIdentityProvider(value: string): void {
if (
typeof value !== 'string' ||
value.length < 1 ||
value.length > MAX_IDENTITY_PROVIDER_LENGTH ||
!IDENTITY_PROVIDER_PATTERN.test(value)
) {
throw new InvalidIdentityDirectoryValueError('provider is invalid');
}
}
export function assertIdentityProviderSubject(value: string): void {
if (
typeof value !== 'string' ||
value.length < 1 ||
value.length > MAX_IDENTITY_PROVIDER_SUBJECT_LENGTH ||
!IDENTITY_PROVIDER_SUBJECT_PATTERN.test(value)
) {
throw new InvalidIdentityDirectoryValueError('providerSubject is invalid');
}
}
export function normalizeIdentitySubjectRecord(
value: IdentitySubjectRecord,
): Readonly<IdentitySubjectRecord> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidIdentityDirectoryValueError(
'identity subject must be an object',
);
}
assertExactKeys('identity subject', value, [
'subject',
'status',
'version',
'createdAtMs',
'updatedAtMs',
]);
let subject: Readonly<PolicySubject>;
try {
subject = normalizePolicySubject(value.subject);
} catch (error) {
if (error instanceof InvalidProjectPolicyValueError) {
throw new InvalidIdentityDirectoryValueError('subject is invalid');
}
throw error;
}
if (!IDENTITY_SUBJECT_STATUSES.includes(value.status)) {
throw new InvalidIdentityDirectoryValueError('subject status is invalid');
}
assertVersion('subject version', value.version);
assertTimestamp('subject createdAtMs', value.createdAtMs);
assertTimestamp('subject updatedAtMs', value.updatedAtMs);
if (value.updatedAtMs < value.createdAtMs) {
throw new InvalidIdentityDirectoryValueError(
'subject timestamps are invalid',
);
}
return Object.freeze({
subject,
status: value.status,
version: value.version,
createdAtMs: value.createdAtMs,
updatedAtMs: value.updatedAtMs,
});
}
export function normalizeIdentityAuthenticationBindingRecord(
value: IdentityAuthenticationBindingRecord,
): Readonly<IdentityAuthenticationBindingRecord> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidIdentityDirectoryValueError(
'authentication binding must be an object',
);
}
assertExactKeys('authentication binding', value, [
'provider',
'providerSubject',
'version',
'state',
'subjectId',
'createdAtMs',
]);
assertIdentityProvider(value.provider);
assertIdentityProviderSubject(value.providerSubject);
assertVersion('authentication binding version', value.version);
if (!IDENTITY_AUTHENTICATION_BINDING_STATES.includes(value.state)) {
throw new InvalidIdentityDirectoryValueError(
'authentication binding state is invalid',
);
}
try {
normalizePolicySubject({ type: 'user', id: value.subjectId });
} catch (error) {
if (error instanceof InvalidProjectPolicyValueError) {
throw new InvalidIdentityDirectoryValueError('subjectId is invalid');
}
throw error;
}
assertTimestamp('authentication binding createdAtMs', value.createdAtMs);
return Object.freeze({ ...value });
}
@@ -0,0 +1,42 @@
export interface LegacyExecutionIdentity {
pid?: number;
logArtifactId?: string;
}
export function selectOneLegacyExecution<T extends LegacyExecutionIdentity>(
candidates: readonly T[],
selector: LegacyExecutionIdentity,
): T[] {
const byLog =
selector.logArtifactId === undefined
? []
: candidates.filter(
(candidate) => candidate.logArtifactId === selector.logArtifactId,
);
const byPid =
selector.pid === undefined
? []
: candidates.filter((candidate) => candidate.pid === selector.pid);
if (selector.logArtifactId !== undefined && selector.pid !== undefined) {
const byPidSet = new Set(byPid);
const intersection = byLog.filter((candidate) => byPidSet.has(candidate));
if (intersection.length === 1) return intersection;
if (intersection.length > 1) return [];
if (byLog.length === 0 && byPid.length === 1) return byPid;
if (byPid.length === 0 && byLog.length === 1) return byLog;
if (byLog.length === 0 && byPid.length === 0) {
return candidates.length === 1 ? [...candidates] : [];
}
return [];
}
if (selector.logArtifactId !== undefined) {
if (byLog.length === 1) return byLog;
return byLog.length === 0 && candidates.length === 1 ? [...candidates] : [];
}
if (selector.pid !== undefined) {
if (byPid.length === 1) return byPid;
return byPid.length === 0 && candidates.length === 1 ? [...candidates] : [];
}
return candidates.length === 1 ? [...candidates] : [];
}
@@ -0,0 +1,79 @@
export const MIN_LOCAL_ARTIFACT_MAXIMUM_BYTES = 64 * 1024;
export const MAX_LOCAL_ARTIFACT_MAXIMUM_BYTES = 1024 * 1024 * 1024;
export const MAX_LOCAL_ARTIFACT_MINIMUM_FREE_BYTES = 1024 * 1024 * 1024 * 1024;
export interface LocalArtifactCapacityPolicy {
maximumAttemptBytes: number;
minimumFreeBytes: number;
}
export class LocalArtifactCapacityUnavailableError extends Error {
readonly code = 'LOCAL_ARTIFACT_CAPACITY_UNAVAILABLE';
constructor() {
super('Local Artifact capacity is unavailable');
this.name = 'LocalArtifactCapacityUnavailableError';
}
}
export class LocalArtifactQuotaExceededError extends Error {
readonly code = 'LOCAL_ARTIFACT_QUOTA_EXCEEDED';
constructor() {
super('Local Artifact reached its byte quota');
this.name = 'LocalArtifactQuotaExceededError';
}
}
function assertIntegerBetween(
name: string,
value: number,
minimum: number,
maximum: number,
): void {
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
throw new RangeError(`${name} must be between ${minimum} and ${maximum}`);
}
}
export function normalizeLocalArtifactCapacityPolicy(
policy: LocalArtifactCapacityPolicy,
): Readonly<LocalArtifactCapacityPolicy> {
if (!policy || typeof policy !== 'object' || Array.isArray(policy)) {
throw new TypeError('Local Artifact capacity policy must be an object');
}
assertIntegerBetween(
'maximumAttemptBytes',
policy.maximumAttemptBytes,
MIN_LOCAL_ARTIFACT_MAXIMUM_BYTES,
MAX_LOCAL_ARTIFACT_MAXIMUM_BYTES,
);
assertIntegerBetween(
'minimumFreeBytes',
policy.minimumFreeBytes,
0,
MAX_LOCAL_ARTIFACT_MINIMUM_FREE_BYTES,
);
return Object.freeze({
maximumAttemptBytes: policy.maximumAttemptBytes,
minimumFreeBytes: policy.minimumFreeBytes,
});
}
export function localArtifactCapacityPolicyForProfile(
profile: 'edge' | 'standalone',
): Readonly<LocalArtifactCapacityPolicy> {
if (profile === 'edge') {
return Object.freeze({
maximumAttemptBytes: 4 * 1024 * 1024,
minimumFreeBytes: 32 * 1024 * 1024,
});
}
if (profile === 'standalone') {
return Object.freeze({
maximumAttemptBytes: 64 * 1024 * 1024,
minimumFreeBytes: 256 * 1024 * 1024,
});
}
throw new TypeError('Local Artifact capacity profile is invalid');
}
@@ -0,0 +1,93 @@
import { assertCompletionReceiptId } from './completionReceipt';
import { assertLocalExecutionArtifactId } from './localExecutionArtifact';
export const LOCAL_ARTIFACT_RETENTION_DISPOSITIONS = [
'deleted',
'already_absent',
] as const;
export type LocalArtifactRetentionDisposition =
(typeof LOCAL_ARTIFACT_RETENTION_DISPOSITIONS)[number];
export interface LocalArtifactRetentionCursor {
finishedAtMs: number;
attemptId: string;
}
export interface LocalArtifactRetentionCandidate
extends LocalArtifactRetentionCursor {
logArtifactId: string;
}
export interface LocalArtifactRetentionRecord
extends LocalArtifactRetentionCandidate {
eligibleAtMs: number;
disposition: LocalArtifactRetentionDisposition;
bytesReclaimed: number;
recordedAtMs: number;
}
export function assertLocalArtifactRetentionTimestamp(
name: string,
value: number,
): void {
if (!Number.isSafeInteger(value) || value < 0) {
throw new TypeError(`${name} must be a non-negative safe integer`);
}
}
export function normalizeLocalArtifactRetentionCursor(
cursor: LocalArtifactRetentionCursor,
): Readonly<LocalArtifactRetentionCursor> {
if (!cursor || typeof cursor !== 'object' || Array.isArray(cursor)) {
throw new TypeError('Local Artifact retention cursor must be an object');
}
assertLocalArtifactRetentionTimestamp('finishedAtMs', cursor.finishedAtMs);
assertCompletionReceiptId(cursor.attemptId, 'attemptId');
return Object.freeze({
finishedAtMs: cursor.finishedAtMs,
attemptId: cursor.attemptId,
});
}
export function normalizeLocalArtifactRetentionCandidate(
candidate: LocalArtifactRetentionCandidate,
): Readonly<LocalArtifactRetentionCandidate> {
const cursor = normalizeLocalArtifactRetentionCursor(candidate);
assertLocalExecutionArtifactId(candidate.logArtifactId);
return Object.freeze({
...cursor,
logArtifactId: candidate.logArtifactId,
});
}
export function normalizeLocalArtifactRetentionRecord(
record: LocalArtifactRetentionRecord,
): Readonly<LocalArtifactRetentionRecord> {
const candidate = normalizeLocalArtifactRetentionCandidate(record);
assertLocalArtifactRetentionTimestamp('eligibleAtMs', record.eligibleAtMs);
assertLocalArtifactRetentionTimestamp('recordedAtMs', record.recordedAtMs);
assertLocalArtifactRetentionTimestamp(
'bytesReclaimed',
record.bytesReclaimed,
);
if (record.eligibleAtMs < record.finishedAtMs) {
throw new TypeError('eligibleAtMs must not precede finishedAtMs');
}
if (record.recordedAtMs < record.eligibleAtMs) {
throw new TypeError('recordedAtMs must not precede eligibleAtMs');
}
if (!LOCAL_ARTIFACT_RETENTION_DISPOSITIONS.includes(record.disposition)) {
throw new TypeError('Local Artifact retention disposition is invalid');
}
if (record.disposition === 'already_absent' && record.bytesReclaimed !== 0) {
throw new TypeError('An absent Artifact cannot reclaim bytes');
}
return Object.freeze({
...candidate,
eligibleAtMs: record.eligibleAtMs,
disposition: record.disposition,
bytesReclaimed: record.bytesReclaimed,
recordedAtMs: record.recordedAtMs,
});
}
@@ -0,0 +1,35 @@
import {
normalizeLocalArtifactRetentionCursor,
type LocalArtifactRetentionCursor,
} from './localArtifactRetention';
export interface LocalArtifactRetentionCheckpoint {
version: number;
cursor?: LocalArtifactRetentionCursor;
}
export function normalizeLocalArtifactRetentionCheckpoint(
value: LocalArtifactRetentionCheckpoint,
): Readonly<LocalArtifactRetentionCheckpoint> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new TypeError(
'Local Artifact retention checkpoint must be an object',
);
}
if (
!Number.isSafeInteger(value.version) ||
value.version < 0 ||
value.version >= Number.MAX_SAFE_INTEGER
) {
throw new TypeError(
'Local Artifact retention checkpoint version is invalid',
);
}
const cursor = value.cursor
? normalizeLocalArtifactRetentionCursor(value.cursor)
: undefined;
return Object.freeze({
version: value.version,
...(cursor ? { cursor } : {}),
});
}
@@ -0,0 +1,112 @@
import { assertCompletionReceiptId } from './completionReceipt';
import {
MAX_LOCAL_ARTIFACT_MAXIMUM_BYTES,
MIN_LOCAL_ARTIFACT_MAXIMUM_BYTES,
} from './localArtifactCapacity';
import { assertLocalExecutionArtifactId } from './localExecutionArtifact';
export const LOCAL_ARTIFACT_TRUNCATION_SCHEMA_VERSION = 1;
export const MAX_LOCAL_ARTIFACT_TRUNCATION_FACT_BYTES = 1024;
export interface LocalArtifactTruncationFact {
schemaVersion: 1;
runId: string;
attemptId: string;
logArtifactId: string;
maximumBytes: number;
quotaReached: boolean;
observedAtMs: number;
}
const FACT_KEYS = [
'attemptId',
'logArtifactId',
'maximumBytes',
'observedAtMs',
'quotaReached',
'runId',
'schemaVersion',
] as const;
function assertExactKeys(value: Record<string, unknown>): void {
const keys = Object.keys(value).sort();
if (
keys.length !== FACT_KEYS.length ||
keys.some((key, index) => key !== FACT_KEYS[index])
) {
throw new TypeError('Local Artifact truncation fact shape is invalid');
}
}
export function normalizeLocalArtifactTruncationFact(
value: LocalArtifactTruncationFact,
): Readonly<LocalArtifactTruncationFact> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new TypeError('Local Artifact truncation fact must be an object');
}
assertExactKeys(value as unknown as Record<string, unknown>);
if (value.schemaVersion !== LOCAL_ARTIFACT_TRUNCATION_SCHEMA_VERSION) {
throw new TypeError('Local Artifact truncation schema version is invalid');
}
assertCompletionReceiptId(value.runId, 'runId');
assertCompletionReceiptId(value.attemptId, 'attemptId');
assertLocalExecutionArtifactId(value.logArtifactId);
if (
!Number.isSafeInteger(value.maximumBytes) ||
value.maximumBytes < MIN_LOCAL_ARTIFACT_MAXIMUM_BYTES ||
value.maximumBytes > MAX_LOCAL_ARTIFACT_MAXIMUM_BYTES
) {
throw new TypeError('Local Artifact truncation maximumBytes is invalid');
}
if (typeof value.quotaReached !== 'boolean') {
throw new TypeError('Local Artifact truncation quotaReached is invalid');
}
if (!Number.isSafeInteger(value.observedAtMs) || value.observedAtMs < 0) {
throw new TypeError('Local Artifact truncation observedAtMs is invalid');
}
return Object.freeze({
schemaVersion: LOCAL_ARTIFACT_TRUNCATION_SCHEMA_VERSION,
runId: value.runId,
attemptId: value.attemptId,
logArtifactId: value.logArtifactId,
maximumBytes: value.maximumBytes,
quotaReached: value.quotaReached,
observedAtMs: value.observedAtMs,
});
}
export function encodeLocalArtifactTruncationFact(
value: LocalArtifactTruncationFact,
): string {
const fact = normalizeLocalArtifactTruncationFact(value);
const encoded = JSON.stringify(fact);
if (Buffer.byteLength(encoded) > MAX_LOCAL_ARTIFACT_TRUNCATION_FACT_BYTES) {
throw new TypeError('Local Artifact truncation fact is too large');
}
return encoded;
}
export function decodeLocalArtifactTruncationFact(
value: Buffer | string,
): Readonly<LocalArtifactTruncationFact> {
const encoded = Buffer.isBuffer(value) ? value.toString('utf8') : value;
if (
Buffer.byteLength(encoded) < 1 ||
Buffer.byteLength(encoded) > MAX_LOCAL_ARTIFACT_TRUNCATION_FACT_BYTES
) {
throw new TypeError('Local Artifact truncation fact size is invalid');
}
let parsed: unknown;
try {
parsed = JSON.parse(encoded);
} catch {
throw new TypeError('Local Artifact truncation fact JSON is invalid');
}
const fact = normalizeLocalArtifactTruncationFact(
parsed as LocalArtifactTruncationFact,
);
if (encodeLocalArtifactTruncationFact(fact) !== encoded) {
throw new TypeError('Local Artifact truncation fact is not canonical');
}
return fact;
}
@@ -0,0 +1,28 @@
import { createHash } from 'crypto';
import { MAX_LOG_ARTIFACT_ID_LENGTH } from './runStateMachine';
import type { RunDispatchCandidate } from './runDispatchCandidate';
import { assertRunDispatchCandidate } from './runDispatchCandidate';
const LOCAL_ARTIFACT_ID_PATTERN = /^local-[0-9a-f]{30}$/;
export function localExecutionArtifactId(
candidate: Readonly<RunDispatchCandidate>,
): string {
assertRunDispatchCandidate(candidate);
return `local-${createHash('sha256')
.update(candidate.runId, 'utf8')
.update('\0', 'utf8')
.update(candidate.attemptId, 'utf8')
.digest('hex')
.slice(0, 30)}`;
}
export function assertLocalExecutionArtifactId(value: string): void {
if (
typeof value !== 'string' ||
value.length > MAX_LOG_ARTIFACT_ID_LENGTH ||
!LOCAL_ARTIFACT_ID_PATTERN.test(value)
) {
throw new TypeError('Local execution artifact id is invalid');
}
}
@@ -0,0 +1,174 @@
import { createHash } from 'crypto';
import { MAX_EXECUTION_ENVIRONMENT_ENTRIES } from './executionContext';
export const MAX_LOCAL_CONTEXT_REF_LENGTH = 512;
export const MAX_LOCAL_SECRET_REF_LENGTH = 512;
export type LocalExecutionEnvironmentBinding =
| {
name: string;
kind: 'public';
value: string;
}
| {
name: string;
kind: 'secret';
secretRef: string;
};
export interface LocalExecutionContextRecipe {
contextRef: string;
environment: readonly LocalExecutionEnvironmentBinding[];
}
export interface LocalExecutionContextRecipeRecord
extends LocalExecutionContextRecipe {
contentDigest: string;
createdAtMs: number;
}
function assertBoundedText(name: string, value: string, maximum: number): void {
if (
typeof value !== 'string' ||
value.length < 1 ||
value.length > maximum ||
/[\u0000-\u001f\u007f]/.test(value)
) {
throw new TypeError(`${name} is invalid`);
}
}
function assertEnvironmentName(value: string): void {
if (
typeof value !== 'string' ||
value.length < 1 ||
value.length > 255 ||
value.includes('=') ||
value.includes('\0')
) {
throw new TypeError('Local context environment name is invalid');
}
}
export function assertLocalExecutionContextRef(value: string): void {
assertBoundedText(
'Local execution contextRef',
value,
MAX_LOCAL_CONTEXT_REF_LENGTH,
);
}
export function normalizeLocalExecutionContextRecipe(
recipe: LocalExecutionContextRecipe,
): LocalExecutionContextRecipe {
if (!recipe || typeof recipe !== 'object' || Array.isArray(recipe)) {
throw new TypeError('Local execution context recipe must be an object');
}
assertLocalExecutionContextRef(recipe.contextRef);
if (!Array.isArray(recipe.environment)) {
throw new TypeError('Local context environment bindings must be an array');
}
if (recipe.environment.length > MAX_EXECUTION_ENVIRONMENT_ENTRIES) {
throw new RangeError('Local context has too many environment bindings');
}
const names = new Set<string>();
const environment = recipe.environment
.map((binding) => {
if (!binding || typeof binding !== 'object' || Array.isArray(binding)) {
throw new TypeError('Local context environment binding is invalid');
}
assertEnvironmentName(binding.name);
if (names.has(binding.name)) {
throw new TypeError('Local context environment binding is duplicated');
}
names.add(binding.name);
if (binding.kind === 'public') {
if (typeof binding.value !== 'string' || binding.value.includes('\0')) {
throw new TypeError('Local public environment value is invalid');
}
return Object.freeze({
name: binding.name,
kind: 'public' as const,
value: binding.value,
});
}
if (binding.kind === 'secret') {
assertBoundedText(
'Local environment secretRef',
binding.secretRef,
MAX_LOCAL_SECRET_REF_LENGTH,
);
return Object.freeze({
name: binding.name,
kind: 'secret' as const,
secretRef: binding.secretRef,
});
}
throw new TypeError('Local context environment binding kind is invalid');
})
.sort((left, right) =>
left.name < right.name ? -1 : left.name > right.name ? 1 : 0,
);
return Object.freeze({
contextRef: recipe.contextRef,
environment: Object.freeze(environment),
});
}
function environmentDigest(
environment: readonly LocalExecutionEnvironmentBinding[],
): string {
return createHash('sha256')
.update(JSON.stringify(environment), 'utf8')
.digest('hex');
}
export function createLocalExecutionContextRecipe(
environment: readonly LocalExecutionEnvironmentBinding[],
): LocalExecutionContextRecipe {
const placeholder = normalizeLocalExecutionContextRecipe({
contextRef: 'localctx:pending',
environment,
});
return normalizeLocalExecutionContextRecipe({
contextRef: `localctx:sha256:${environmentDigest(placeholder.environment)}`,
environment: placeholder.environment,
});
}
export function localExecutionContextRecipeDigest(
recipe: LocalExecutionContextRecipe,
): string {
const normalized = normalizeLocalExecutionContextRecipe(recipe);
return createHash('sha256')
.update(JSON.stringify(normalized), 'utf8')
.digest('hex');
}
export function assertContentAddressedLocalExecutionContextRecipe(
recipe: LocalExecutionContextRecipe,
): void {
const normalized = normalizeLocalExecutionContextRecipe(recipe);
const expected = createLocalExecutionContextRecipe(normalized.environment);
if (normalized.contextRef !== expected.contextRef) {
throw new TypeError(
'Local execution context recipe is not content-addressed',
);
}
}
export function createLocalExecutionContextRecipeRecord(
recipe: LocalExecutionContextRecipe,
createdAtMs: number,
): LocalExecutionContextRecipeRecord {
if (!Number.isSafeInteger(createdAtMs) || createdAtMs < 0) {
throw new RangeError('createdAtMs must be a non-negative safe integer');
}
const normalized = normalizeLocalExecutionContextRecipe(recipe);
assertContentAddressedLocalExecutionContextRecipe(normalized);
return Object.freeze({
...normalized,
contentDigest: localExecutionContextRecipeDigest(normalized),
createdAtMs,
});
}
+270
View File
@@ -0,0 +1,270 @@
import { MAX_EXECUTION_ENVIRONMENT_VALUE_BYTES } from './executionContext';
export const LOCAL_SECRET_ALGORITHM = 'aes-256-gcm';
export const MAX_LOCAL_SECRET_NAME_LENGTH = 128;
export const MAX_LOCAL_SECRET_VERSION = 2_147_483_647;
export const MAX_LOCAL_SECRET_REF_LENGTH = 512;
export const MAX_LOCAL_SECRET_MUTATION_ID_LENGTH = 64;
export const MAX_LOCAL_SECRET_KEY_ID_LENGTH = 128;
const SECRET_REF_PREFIX = 'qlsecret:v1:';
const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;
const KEY_ID_PATTERN = /^[A-Za-z0-9._-]+$/;
export interface LocalSecretReference {
projectId: string;
name: string;
version?: number;
}
export interface LocalSecretEnvelope {
projectId: string;
name: string;
version: number;
mutationId: string;
keyId: string;
algorithm: typeof LOCAL_SECRET_ALGORITHM;
nonce: string;
ciphertext: string;
authTag: string;
createdAtMs: number;
}
export class InvalidLocalSecretError extends TypeError {
constructor(message: string) {
super(`Local Secret value is invalid: ${message}`);
this.name = 'InvalidLocalSecretError';
}
}
export class LocalSecretUnavailableError extends Error {
readonly code = 'LOCAL_SECRET_UNAVAILABLE';
constructor() {
super('Local Secret is unavailable');
this.name = 'LocalSecretUnavailableError';
}
}
export class LocalSecretVersionConflictError extends Error {
readonly code = 'LOCAL_SECRET_VERSION_CONFLICT';
constructor() {
super('Local Secret current version changed');
this.name = 'LocalSecretVersionConflictError';
}
}
export class LocalSecretMutationConflictError extends Error {
readonly code = 'LOCAL_SECRET_MUTATION_CONFLICT';
constructor() {
super('Local Secret mutation does not match its previous request');
this.name = 'LocalSecretMutationConflictError';
}
}
function assertIdentifier(name: string, value: string, maximum: number): void {
if (
typeof value !== 'string' ||
value.length < 1 ||
value.length > maximum ||
/[\u0000-\u001f\u007f]/.test(value)
) {
throw new InvalidLocalSecretError(`${name} is invalid`);
}
}
export function assertLocalSecretProjectId(value: string): void {
assertIdentifier('projectId', value, 128);
}
export function assertLocalSecretName(value: string): void {
assertIdentifier('name', value, MAX_LOCAL_SECRET_NAME_LENGTH);
}
export function assertLocalSecretVersion(value: number): void {
if (
!Number.isSafeInteger(value) ||
value < 1 ||
value > MAX_LOCAL_SECRET_VERSION
) {
throw new InvalidLocalSecretError('version is invalid');
}
}
export function assertLocalSecretMutationId(value: string): void {
assertIdentifier('mutationId', value, MAX_LOCAL_SECRET_MUTATION_ID_LENGTH);
}
export function assertLocalSecretKeyId(value: string): void {
if (
typeof value !== 'string' ||
value.length < 1 ||
value.length > MAX_LOCAL_SECRET_KEY_ID_LENGTH ||
!KEY_ID_PATTERN.test(value)
) {
throw new InvalidLocalSecretError('keyId is invalid');
}
}
export function assertLocalSecretPlaintext(value: string): void {
if (
typeof value !== 'string' ||
value.includes('\0') ||
Buffer.byteLength(value, 'utf8') > MAX_EXECUTION_ENVIRONMENT_VALUE_BYTES
) {
throw new InvalidLocalSecretError('plaintext is invalid');
}
}
function decodeBase64Url(name: string, value: string, bytes?: number): Buffer {
if (
typeof value !== 'string' ||
(value.length > 0 && !BASE64URL_PATTERN.test(value))
) {
throw new InvalidLocalSecretError(`${name} is invalid`);
}
const decoded = Buffer.from(value, 'base64url');
if (
decoded.toString('base64url') !== value ||
(bytes && decoded.length !== bytes)
) {
throw new InvalidLocalSecretError(`${name} is invalid`);
}
return decoded;
}
export function localSecretBinary(
name: 'nonce' | 'ciphertext' | 'authTag',
value: string,
): Buffer {
const decoded = decodeBase64Url(
name,
value,
name === 'nonce' ? 12 : name === 'authTag' ? 16 : undefined,
);
if (
name === 'ciphertext' &&
decoded.length > MAX_EXECUTION_ENVIRONMENT_VALUE_BYTES
) {
throw new InvalidLocalSecretError('ciphertext is too large');
}
return decoded;
}
export function createLocalSecretRef(reference: LocalSecretReference): string {
assertLocalSecretProjectId(reference.projectId);
assertLocalSecretName(reference.name);
if (reference.version !== undefined) {
assertLocalSecretVersion(reference.version);
}
const payload = JSON.stringify({
projectId: reference.projectId,
name: reference.name,
...(reference.version === undefined ? {} : { version: reference.version }),
});
const value =
SECRET_REF_PREFIX + Buffer.from(payload, 'utf8').toString('base64url');
if (value.length > MAX_LOCAL_SECRET_REF_LENGTH) {
throw new InvalidLocalSecretError('reference is too large');
}
return value;
}
export function parseLocalSecretRef(value: string): LocalSecretReference {
if (
typeof value !== 'string' ||
value.length > MAX_LOCAL_SECRET_REF_LENGTH ||
!value.startsWith(SECRET_REF_PREFIX)
) {
throw new InvalidLocalSecretError('reference is invalid');
}
const encoded = value.slice(SECRET_REF_PREFIX.length);
let parsed: unknown;
try {
parsed = JSON.parse(decodeBase64Url('reference', encoded).toString('utf8'));
} catch (error) {
if (error instanceof InvalidLocalSecretError) throw error;
throw new InvalidLocalSecretError('reference is invalid');
}
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new InvalidLocalSecretError('reference is invalid');
}
const record = parsed as Record<string, unknown>;
const keys = Object.keys(record).sort();
const expectedKeys =
record.version === undefined
? ['name', 'projectId']
: ['name', 'projectId', 'version'];
if (
keys.length !== expectedKeys.length ||
keys.some((key, index) => key !== expectedKeys[index])
) {
throw new InvalidLocalSecretError('reference is invalid');
}
const reference: LocalSecretReference = {
projectId: record.projectId as string,
name: record.name as string,
...(record.version === undefined
? {}
: { version: record.version as number }),
};
if (createLocalSecretRef(reference) !== value) {
throw new InvalidLocalSecretError('reference is not canonical');
}
return Object.freeze(reference);
}
export function normalizeLocalSecretEnvelope(
envelope: LocalSecretEnvelope,
): LocalSecretEnvelope {
if (!envelope || typeof envelope !== 'object' || Array.isArray(envelope)) {
throw new InvalidLocalSecretError('envelope must be an object');
}
assertLocalSecretProjectId(envelope.projectId);
assertLocalSecretName(envelope.name);
assertLocalSecretVersion(envelope.version);
assertLocalSecretMutationId(envelope.mutationId);
assertLocalSecretKeyId(envelope.keyId);
if (envelope.algorithm !== LOCAL_SECRET_ALGORITHM) {
throw new InvalidLocalSecretError('algorithm is invalid');
}
localSecretBinary('nonce', envelope.nonce);
localSecretBinary('ciphertext', envelope.ciphertext);
localSecretBinary('authTag', envelope.authTag);
if (!Number.isSafeInteger(envelope.createdAtMs) || envelope.createdAtMs < 0) {
throw new InvalidLocalSecretError('createdAtMs is invalid');
}
return Object.freeze({
projectId: envelope.projectId,
name: envelope.name,
version: envelope.version,
mutationId: envelope.mutationId,
keyId: envelope.keyId,
algorithm: LOCAL_SECRET_ALGORITHM,
nonce: envelope.nonce,
ciphertext: envelope.ciphertext,
authTag: envelope.authTag,
createdAtMs: envelope.createdAtMs,
});
}
export function localSecretEnvelopeAad(
envelope: Pick<
LocalSecretEnvelope,
'projectId' | 'name' | 'version' | 'mutationId' | 'keyId' | 'algorithm'
>,
): Buffer {
return Buffer.from(
JSON.stringify({
projectId: envelope.projectId,
name: envelope.name,
version: envelope.version,
mutationId: envelope.mutationId,
keyId: envelope.keyId,
algorithm: envelope.algorithm,
}),
'utf8',
);
}
@@ -0,0 +1,231 @@
import { createHash } from 'crypto';
import {
InvalidProjectPolicyValueError,
assertProjectPolicyProjectId,
normalizePolicySubject,
type PolicySubject,
} from './projectPolicy';
export const OWNER_BOOTSTRAP_TOKEN_BYTES = 32;
export const OWNER_BOOTSTRAP_CHALLENGE_ID_BYTES = 16;
export const OWNER_BOOTSTRAP_DEFAULT_TTL_MS = 10 * 60 * 1000;
export const OWNER_BOOTSTRAP_MIN_TTL_MS = 60 * 1000;
export const OWNER_BOOTSTRAP_MAX_TTL_MS = 30 * 60 * 1000;
export const OWNER_BOOTSTRAP_MAX_VERSION = 2_147_483_647;
export const OWNER_BOOTSTRAP_SYSTEM_SUBJECT = Object.freeze({
type: 'system' as const,
id: 'owner-bootstrap',
});
const TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/;
const CHALLENGE_ID_PATTERN = /^[A-Za-z0-9_-]{22}$/;
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
const DIGEST_DOMAIN = 'qinglong3-owner-bootstrap-v1\0';
export interface ProjectOwnerBootstrapChallengeRecord {
projectId: string;
version: number;
challengeId: string;
tokenDigest: string;
issuedAtMs: number;
expiresAtMs: number;
consumedAtMs?: number;
claimedSubject?: PolicySubject;
}
export class InvalidProjectOwnerBootstrapValueError extends TypeError {
constructor(message: string) {
super(`Project owner bootstrap value is invalid: ${message}`);
this.name = 'InvalidProjectOwnerBootstrapValueError';
}
}
export class ProjectOwnerBootstrapUnauthorizedError extends Error {
readonly code = 'PROJECT_OWNER_BOOTSTRAP_UNAUTHORIZED';
constructor() {
super('Project owner bootstrap caller is not authorized');
this.name = 'ProjectOwnerBootstrapUnauthorizedError';
}
}
export class ProjectOwnerBootstrapProjectNotFoundError extends Error {
readonly code = 'PROJECT_OWNER_BOOTSTRAP_PROJECT_NOT_FOUND';
constructor() {
super('Project does not exist');
this.name = 'ProjectOwnerBootstrapProjectNotFoundError';
}
}
export class ProjectOwnerBootstrapProjectInactiveError extends Error {
readonly code = 'PROJECT_OWNER_BOOTSTRAP_PROJECT_INACTIVE';
constructor() {
super('Project is not active');
this.name = 'ProjectOwnerBootstrapProjectInactiveError';
}
}
export class ProjectOwnerBootstrapProjectNotPristineError extends Error {
readonly code = 'PROJECT_OWNER_BOOTSTRAP_PROJECT_NOT_PRISTINE';
constructor() {
super('Project owner bootstrap is no longer available');
this.name = 'ProjectOwnerBootstrapProjectNotPristineError';
}
}
export class ProjectOwnerBootstrapChallengeActiveError extends Error {
readonly code = 'PROJECT_OWNER_BOOTSTRAP_CHALLENGE_ACTIVE';
constructor() {
super('A Project owner bootstrap challenge is already active');
this.name = 'ProjectOwnerBootstrapChallengeActiveError';
}
}
export class ProjectOwnerBootstrapClaimRejectedError extends Error {
readonly code = 'PROJECT_OWNER_BOOTSTRAP_CLAIM_REJECTED';
constructor() {
super('Project owner bootstrap claim was rejected');
this.name = 'ProjectOwnerBootstrapClaimRejectedError';
}
}
export class ProjectOwnerBootstrapUnavailableError extends Error {
readonly code = 'PROJECT_OWNER_BOOTSTRAP_UNAVAILABLE';
constructor() {
super('Project owner bootstrap is unavailable');
this.name = 'ProjectOwnerBootstrapUnavailableError';
}
}
function assertTimestamp(name: string, value: number): void {
if (!Number.isSafeInteger(value) || value < 0) {
throw new InvalidProjectOwnerBootstrapValueError(`${name} is invalid`);
}
}
export function assertProjectOwnerBootstrapToken(value: string): void {
if (typeof value !== 'string' || !TOKEN_PATTERN.test(value)) {
throw new InvalidProjectOwnerBootstrapValueError('token is invalid');
}
}
export function assertProjectOwnerBootstrapChallengeId(value: string): void {
if (typeof value !== 'string' || !CHALLENGE_ID_PATTERN.test(value)) {
throw new InvalidProjectOwnerBootstrapValueError('challengeId is invalid');
}
}
export function assertProjectOwnerBootstrapTokenDigest(value: string): void {
if (typeof value !== 'string' || !DIGEST_PATTERN.test(value)) {
throw new InvalidProjectOwnerBootstrapValueError('tokenDigest is invalid');
}
}
export function assertProjectOwnerBootstrapTtl(value: number): void {
if (
!Number.isSafeInteger(value) ||
value < OWNER_BOOTSTRAP_MIN_TTL_MS ||
value > OWNER_BOOTSTRAP_MAX_TTL_MS
) {
throw new InvalidProjectOwnerBootstrapValueError('ttlMs is invalid');
}
}
export function digestProjectOwnerBootstrapToken(
projectId: string,
challengeId: string,
token: string,
): string {
assertProjectPolicyProjectId(projectId);
assertProjectOwnerBootstrapChallengeId(challengeId);
assertProjectOwnerBootstrapToken(token);
return createHash('sha256')
.update(DIGEST_DOMAIN, 'utf8')
.update(projectId, 'utf8')
.update('\0', 'utf8')
.update(challengeId, 'utf8')
.update('\0', 'utf8')
.update(token, 'utf8')
.digest('hex');
}
export function normalizeProjectOwnerBootstrapChallengeRecord(
value: ProjectOwnerBootstrapChallengeRecord,
): Readonly<ProjectOwnerBootstrapChallengeRecord> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidProjectOwnerBootstrapValueError(
'challenge must be an object',
);
}
const consumed = value.consumedAtMs !== undefined;
const expected = consumed
? [
'challengeId',
'claimedSubject',
'consumedAtMs',
'expiresAtMs',
'issuedAtMs',
'projectId',
'tokenDigest',
'version',
]
: [
'challengeId',
'expiresAtMs',
'issuedAtMs',
'projectId',
'tokenDigest',
'version',
];
const keys = Object.keys(value).sort();
if (
keys.length !== expected.length ||
keys.some((key, index) => key !== expected[index])
) {
throw new InvalidProjectOwnerBootstrapValueError(
'challenge shape is invalid',
);
}
try {
assertProjectPolicyProjectId(value.projectId);
} catch (error) {
if (error instanceof InvalidProjectPolicyValueError) {
throw new InvalidProjectOwnerBootstrapValueError('projectId is invalid');
}
throw error;
}
if (
!Number.isSafeInteger(value.version) ||
value.version < 1 ||
value.version > OWNER_BOOTSTRAP_MAX_VERSION
) {
throw new InvalidProjectOwnerBootstrapValueError('version is invalid');
}
assertProjectOwnerBootstrapChallengeId(value.challengeId);
assertProjectOwnerBootstrapTokenDigest(value.tokenDigest);
assertTimestamp('issuedAtMs', value.issuedAtMs);
assertTimestamp('expiresAtMs', value.expiresAtMs);
if (value.expiresAtMs <= value.issuedAtMs) {
throw new InvalidProjectOwnerBootstrapValueError('lifetime is invalid');
}
if (!consumed) return Object.freeze({ ...value });
assertTimestamp('consumedAtMs', value.consumedAtMs!);
if (value.consumedAtMs! < value.issuedAtMs) {
throw new InvalidProjectOwnerBootstrapValueError(
'consumption time is invalid',
);
}
if (!value.claimedSubject) {
throw new InvalidProjectOwnerBootstrapValueError(
'claimedSubject is required',
);
}
const claimedSubject = normalizePolicySubject(value.claimedSubject);
return Object.freeze({ ...value, claimedSubject });
}
+371
View File
@@ -0,0 +1,371 @@
export const POLICY_SUBJECT_TYPES = [
'user',
'api_app',
'mcp_client',
'agent',
'system',
'worker',
] as const;
export const PROJECT_STATUSES = ['active', 'archived'] as const;
export const PROJECT_ROLES = ['owner', 'admin', 'operator', 'viewer'] as const;
export const PROJECT_ROLE_BINDING_STATES = ['active', 'revoked'] as const;
export const STATIC_PROJECT_PERMISSIONS = [
'project.read',
'project.manage',
'task.read',
'task.create',
'task.update',
'task.delete',
'run.read',
'run.start',
'run.stop',
'run.retry',
'artifact.read',
'secret.use',
'secret.manage',
'worker.manage',
'policy.manage',
'approval.decide',
'approval.recover',
] as const;
export type PolicySubjectType = (typeof POLICY_SUBJECT_TYPES)[number];
export type ProjectStatus = (typeof PROJECT_STATUSES)[number];
export type ProjectRole = (typeof PROJECT_ROLES)[number];
export type ProjectRoleBindingState =
(typeof PROJECT_ROLE_BINDING_STATES)[number];
export type StaticProjectPermission =
(typeof STATIC_PROJECT_PERMISSIONS)[number];
export type ProjectPermission = StaticProjectPermission | `tool.call:${string}`;
export interface PolicySubject {
type: PolicySubjectType;
id: string;
}
export interface ProjectRecord {
id: string;
name: string;
slug: string;
status: ProjectStatus;
version: number;
createdAtMs: number;
updatedAtMs: number;
}
export interface ProjectRoleBindingRecord {
projectId: string;
subject: PolicySubject;
version: number;
state: ProjectRoleBindingState;
role?: ProjectRole;
mutationId: string;
changedBy: PolicySubject;
createdAtMs: number;
}
export interface ProjectPolicySnapshot {
project: Readonly<ProjectRecord>;
binding?: Readonly<ProjectRoleBindingRecord>;
}
export type ProjectPolicyEffect = 'allow' | 'deny' | 'require_approval';
export interface ProjectPolicyDecision {
effect: ProjectPolicyEffect;
reasons: readonly string[];
}
export interface ProjectPolicyFence {
projectVersion: number;
bindingVersion: number | null;
}
export interface ProjectPolicyDecisionWithFence {
decision: Readonly<ProjectPolicyDecision>;
fence: Readonly<ProjectPolicyFence> | null;
}
export interface ProjectPolicyRequest {
subject: PolicySubject;
projectId: string;
permission: ProjectPermission;
}
export const MAX_POLICY_SUBJECT_ID_LENGTH = 255;
export const MAX_PROJECT_ID_LENGTH = 128;
export const MAX_PROJECT_NAME_LENGTH = 255;
export const MAX_PROJECT_SLUG_LENGTH = 128;
export const MAX_PROJECT_ROLE_BINDING_VERSION = 2_147_483_647;
export const MAX_PROJECT_POLICY_MUTATION_ID_LENGTH = 64;
export const MAX_PROJECT_PERMISSION_LENGTH = 255;
const IDENTIFIER_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
const PROJECT_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,126}[a-z0-9])?$/;
const MUTATION_ID_PATTERN = /^[A-Za-z0-9._:-]+$/;
const TOOL_PERMISSION_PATTERN =
/^tool\.call:[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
export class InvalidProjectPolicyValueError extends TypeError {
constructor(message: string) {
super(`Project policy value is invalid: ${message}`);
this.name = 'InvalidProjectPolicyValueError';
}
}
export class ProjectRoleBindingVersionConflictError extends Error {
readonly code = 'PROJECT_ROLE_BINDING_VERSION_CONFLICT';
constructor() {
super('Project role binding current version changed');
this.name = 'ProjectRoleBindingVersionConflictError';
}
}
export class ProjectRoleBindingMutationConflictError extends Error {
readonly code = 'PROJECT_ROLE_BINDING_MUTATION_CONFLICT';
constructor() {
super('Project role binding mutation does not match its previous request');
this.name = 'ProjectRoleBindingMutationConflictError';
}
}
export class ProjectPolicyUnavailableError extends Error {
readonly code = 'PROJECT_POLICY_UNAVAILABLE';
constructor() {
super('Project policy is unavailable');
this.name = 'ProjectPolicyUnavailableError';
}
}
export class ProjectPolicyProjectNotFoundError extends Error {
readonly code = 'PROJECT_NOT_FOUND';
constructor() {
super('Project does not exist');
this.name = 'ProjectPolicyProjectNotFoundError';
}
}
function assertBoundedIdentifier(
name: string,
value: string,
maximum: number,
): void {
if (
typeof value !== 'string' ||
value.length < 1 ||
value.length > maximum ||
IDENTIFIER_CONTROL_PATTERN.test(value)
) {
throw new InvalidProjectPolicyValueError(`${name} is invalid`);
}
}
function assertTimestamp(name: string, value: number): void {
if (!Number.isSafeInteger(value) || value < 0) {
throw new InvalidProjectPolicyValueError(`${name} is invalid`);
}
}
function assertExactKeys(
name: string,
value: object,
expected: readonly string[],
): void {
const keys = Object.keys(value).sort();
const canonical = [...expected].sort();
if (
keys.length !== canonical.length ||
keys.some((key, index) => key !== canonical[index])
) {
throw new InvalidProjectPolicyValueError(`${name} shape is invalid`);
}
}
export function assertProjectPolicyProjectId(value: string): void {
assertBoundedIdentifier('projectId', value, MAX_PROJECT_ID_LENGTH);
}
export function normalizePolicySubject(
value: PolicySubject,
): Readonly<PolicySubject> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidProjectPolicyValueError('subject must be an object');
}
assertExactKeys('subject', value, ['type', 'id']);
if (!POLICY_SUBJECT_TYPES.includes(value.type)) {
throw new InvalidProjectPolicyValueError('subject type is invalid');
}
assertBoundedIdentifier('subject id', value.id, MAX_POLICY_SUBJECT_ID_LENGTH);
return Object.freeze({ type: value.type, id: value.id });
}
export function normalizeProjectPermission(value: string): ProjectPermission {
if (
typeof value !== 'string' ||
value.length < 1 ||
value.length > MAX_PROJECT_PERMISSION_LENGTH
) {
throw new InvalidProjectPolicyValueError('permission is invalid');
}
if (
STATIC_PROJECT_PERMISSIONS.includes(value as StaticProjectPermission) ||
TOOL_PERMISSION_PATTERN.test(value)
) {
return value as ProjectPermission;
}
throw new InvalidProjectPolicyValueError('permission is invalid');
}
export function normalizeProjectRecord(
value: ProjectRecord,
): Readonly<ProjectRecord> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidProjectPolicyValueError('Project must be an object');
}
assertExactKeys('Project', value, [
'id',
'name',
'slug',
'status',
'version',
'createdAtMs',
'updatedAtMs',
]);
assertProjectPolicyProjectId(value.id);
assertBoundedIdentifier('Project name', value.name, MAX_PROJECT_NAME_LENGTH);
if (
typeof value.slug !== 'string' ||
value.slug.length > MAX_PROJECT_SLUG_LENGTH ||
!PROJECT_SLUG_PATTERN.test(value.slug)
) {
throw new InvalidProjectPolicyValueError('Project slug is invalid');
}
if (!PROJECT_STATUSES.includes(value.status)) {
throw new InvalidProjectPolicyValueError('Project status is invalid');
}
if (
!Number.isSafeInteger(value.version) ||
value.version < 1 ||
value.version > MAX_PROJECT_ROLE_BINDING_VERSION
) {
throw new InvalidProjectPolicyValueError('Project version is invalid');
}
assertTimestamp('Project createdAtMs', value.createdAtMs);
assertTimestamp('Project updatedAtMs', value.updatedAtMs);
if (value.updatedAtMs < value.createdAtMs) {
throw new InvalidProjectPolicyValueError('Project timestamps are invalid');
}
return Object.freeze({ ...value });
}
export function normalizeProjectRoleBindingRecord(
value: ProjectRoleBindingRecord,
): Readonly<ProjectRoleBindingRecord> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidProjectPolicyValueError(
'Project role binding must be an object',
);
}
assertExactKeys(
'Project role binding',
value,
value.state === 'active'
? [
'projectId',
'subject',
'version',
'state',
'role',
'mutationId',
'changedBy',
'createdAtMs',
]
: [
'projectId',
'subject',
'version',
'state',
'mutationId',
'changedBy',
'createdAtMs',
],
);
assertProjectPolicyProjectId(value.projectId);
const subject = normalizePolicySubject(value.subject);
const changedBy = normalizePolicySubject(value.changedBy);
if (
!Number.isSafeInteger(value.version) ||
value.version < 1 ||
value.version > MAX_PROJECT_ROLE_BINDING_VERSION
) {
throw new InvalidProjectPolicyValueError(
'Project role binding version is invalid',
);
}
if (!PROJECT_ROLE_BINDING_STATES.includes(value.state)) {
throw new InvalidProjectPolicyValueError(
'Project role binding state is invalid',
);
}
if (
(value.state === 'active' &&
(!value.role || !PROJECT_ROLES.includes(value.role))) ||
(value.state === 'revoked' && value.role !== undefined)
) {
throw new InvalidProjectPolicyValueError(
'Project role binding role is invalid',
);
}
if (
typeof value.mutationId !== 'string' ||
value.mutationId.length < 1 ||
value.mutationId.length > MAX_PROJECT_POLICY_MUTATION_ID_LENGTH ||
!MUTATION_ID_PATTERN.test(value.mutationId)
) {
throw new InvalidProjectPolicyValueError(
'Project role binding mutationId is invalid',
);
}
assertTimestamp('Project role binding createdAtMs', value.createdAtMs);
return Object.freeze({
projectId: value.projectId,
subject,
version: value.version,
state: value.state,
...(value.role ? { role: value.role } : {}),
mutationId: value.mutationId,
changedBy,
createdAtMs: value.createdAtMs,
});
}
export function normalizeProjectPolicySnapshot(
value: ProjectPolicySnapshot,
): Readonly<ProjectPolicySnapshot> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidProjectPolicyValueError(
'Project policy snapshot must be an object',
);
}
assertExactKeys(
'Project policy snapshot',
value,
value.binding ? ['project', 'binding'] : ['project'],
);
const project = normalizeProjectRecord(value.project);
const binding = value.binding
? normalizeProjectRoleBindingRecord(value.binding)
: undefined;
if (binding && binding.projectId !== project.id) {
throw new InvalidProjectPolicyValueError(
'Project policy snapshot binding is misplaced',
);
}
return Object.freeze({ project, ...(binding ? { binding } : {}) });
}
+87
View File
@@ -0,0 +1,87 @@
export class RunRepositoryError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly retryable = false,
public readonly cause?: unknown,
) {
super(message);
this.name = new.target.name;
}
}
export class DuplicateIdempotencyKeyError extends RunRepositoryError {
constructor(
public readonly projectId: string,
public readonly idempotencyKey: string,
) {
super(
'A Run with the same project idempotency key already exists',
'DUPLICATE_RUN_IDEMPOTENCY_KEY',
);
}
}
export class DuplicateRunAttemptError extends RunRepositoryError {
constructor(public readonly runId: string, public readonly attempt: number) {
super(
'A RunAttempt with the same run and attempt number already exists',
'DUPLICATE_RUN_ATTEMPT',
);
}
}
export class DuplicateRunEventError extends RunRepositoryError {
constructor(
public readonly runId: string,
public readonly dedupeKey?: string,
) {
super(
'A RunEvent with the same sequence or dedupe key already exists',
'DUPLICATE_RUN_EVENT',
);
}
}
export class RunRepositoryConstraintError extends RunRepositoryError {
constructor(
message = 'Run repository constraint violation',
cause?: unknown,
) {
super(message, 'RUN_REPOSITORY_CONSTRAINT', false, cause);
}
}
export class RunRepositoryBusyError extends RunRepositoryError {
constructor(cause?: unknown) {
super(
'Run repository is temporarily busy',
'RUN_REPOSITORY_BUSY',
true,
cause,
);
}
}
export class RunRepositoryOperationError extends RunRepositoryError {
constructor(cause?: unknown) {
super(
'Run repository operation failed',
'RUN_REPOSITORY_OPERATION_FAILED',
false,
cause,
);
}
}
export class RunEventPayloadTooLargeError extends RunRepositoryError {
constructor(
public readonly actualBytes: number,
public readonly maxBytes: number,
) {
super(
'RunEvent payload exceeds the configured size limit',
'RUN_EVENT_PAYLOAD_TOO_LARGE',
);
}
}
+144
View File
@@ -0,0 +1,144 @@
export const RUN_STATUSES = [
'created',
'queued',
'dispatching',
'running',
'waiting_approval',
'retry_wait',
'lost',
'succeeded',
'failed',
'cancelled',
'timed_out',
] as const;
export type RunStatus = (typeof RUN_STATUSES)[number];
export const RUN_ATTEMPT_STATUSES = [
'claimed',
'starting',
'running',
'succeeded',
'failed',
'cancelled',
'timed_out',
'lost',
] as const;
export type RunAttemptStatus = (typeof RUN_ATTEMPT_STATUSES)[number];
export const EXECUTION_ORIGINS = [
'manual',
'scheduled_system',
'scheduled_node',
'once',
'boot',
'grpc',
'subscription',
'system',
'script',
'legacy_import',
] as const;
export type ExecutionOrigin = (typeof EXECUTION_ORIGINS)[number];
export type ExecutionOwner = 'legacy' | 'runtime';
export const RUN_CANCELLATION_REASONS = [
'user',
'policy',
'shutdown',
'reconcile',
'timeout',
] as const;
export type RunCancellationReason = (typeof RUN_CANCELLATION_REASONS)[number];
export const RUN_EVENT_ACTOR_TYPES = [
'user',
'api_app',
'trigger',
'agent',
'mcp_client',
'worker',
'executor',
'system',
'legacy_shell',
'scheduler',
'reconciler',
'compatibility',
] as const;
export type RunEventActorType = (typeof RUN_EVENT_ACTOR_TYPES)[number];
export interface RunRecord {
id: string;
projectId: string;
taskId: string;
taskRevision: string;
taskName?: string;
taskSnapshotRef?: string;
legacyCronId?: number;
parentRunId?: string;
retryOfRunId?: string;
triggerId?: string;
triggerType: string;
executionOrigin: ExecutionOrigin;
executionOwner: ExecutionOwner;
triggeredBy?: string;
requestId?: string;
scheduledForMs?: number;
status: RunStatus;
version: number;
eventSequence: number;
priority: number;
idempotencyKey?: string;
inputRef?: string;
outputRef?: string;
createdAtMs: number;
queuedAtMs?: number;
startedAtMs?: number;
finishedAtMs?: number;
cancelRequestedAtMs?: number;
cancelReason?: RunCancellationReason;
errorCode?: string;
errorSummary?: string;
}
export interface RunAttemptRecord {
id: string;
runId: string;
stepRunId?: string;
attempt: number;
status: RunAttemptStatus;
executorType: string;
workerId?: string;
executorHandle?: string;
pid?: number;
logArtifactId?: string;
leaseToken?: string;
leaseExpiresAtMs?: number;
deadlineAtMs?: number;
callbackTokenHash?: string;
callbackSequence: number;
createdAtMs: number;
startedAtMs?: number;
finishedAtMs?: number;
exitCode?: number;
errorCode?: string;
errorSummary?: string;
}
export interface RunEventRecord {
id: string;
runId: string;
sequence: number;
type: string;
dedupeKey?: string;
actorType: RunEventActorType;
actorId?: string;
attemptId?: string;
stepRunId?: string;
payload: Readonly<Record<string, unknown>>;
createdAtMs: number;
}
@@ -0,0 +1,75 @@
import {
assertRunDispatchId,
assertRunDispatchLeaseVersion,
} from './runDispatchLease';
export const MAX_RUN_DISPATCH_CANDIDATE_PAGE_SIZE = 64;
export interface RunDispatchCandidateCursor {
priority: number;
queuedAtMs: number;
attemptCreatedAtMs: number;
attemptId: string;
}
export interface RunDispatchCandidate extends RunDispatchCandidateCursor {
runId: string;
projectId: string;
taskId: string;
taskRevision: string;
executorType: string;
}
function assertPriority(value: number): void {
if (!Number.isSafeInteger(value)) {
throw new TypeError(
'Run dispatch candidate priority must be a safe integer',
);
}
}
function assertExecutorType(value: string): void {
if (
typeof value !== 'string' ||
value.length < 1 ||
value.length > 64 ||
/[\u0000-\u001f\u007f]/.test(value)
) {
throw new TypeError('Run dispatch candidate executorType is invalid');
}
}
export function assertRunDispatchCandidateCursor(
cursor: RunDispatchCandidateCursor,
): void {
assertPriority(cursor.priority);
assertRunDispatchLeaseVersion('queuedAtMs', cursor.queuedAtMs);
assertRunDispatchLeaseVersion(
'attemptCreatedAtMs',
cursor.attemptCreatedAtMs,
);
assertRunDispatchId('attemptId', cursor.attemptId);
}
export function assertRunDispatchCandidate(
candidate: RunDispatchCandidate,
): void {
assertRunDispatchCandidateCursor(candidate);
assertRunDispatchId('runId', candidate.runId);
assertRunDispatchId('projectId', candidate.projectId);
assertRunDispatchId('taskId', candidate.taskId);
assertRunDispatchId('taskRevision', candidate.taskRevision);
assertExecutorType(candidate.executorType);
}
export function assertRunDispatchCandidatePageSize(limit: number): void {
if (
!Number.isSafeInteger(limit) ||
limit < 1 ||
limit > MAX_RUN_DISPATCH_CANDIDATE_PAGE_SIZE
) {
throw new RangeError(
`Run dispatch candidate page size must be between 1 and ${MAX_RUN_DISPATCH_CANDIDATE_PAGE_SIZE}`,
);
}
}
+210
View File
@@ -0,0 +1,210 @@
import { assertWorkerId, assertWorkerSessionId } from './worker';
export const RUN_DISPATCH_LEASE_STATUSES = [
'leased',
'released',
'completed',
] as const;
export type RunDispatchLeaseStatus =
(typeof RUN_DISPATCH_LEASE_STATUSES)[number];
export const RUN_DISPATCH_RELEASE_REASONS = [
'declined',
'shutdown',
'start_failed',
'capacity_changed',
'lease_expired',
] as const;
export type RunDispatchReleaseReason =
(typeof RUN_DISPATCH_RELEASE_REASONS)[number];
export const MIN_RUN_DISPATCH_LEASE_DURATION_MS = 5_000;
export const MAX_RUN_DISPATCH_LEASE_DURATION_MS = 10 * 60_000;
export interface RunDispatchLeaseRecord {
attemptId: string;
runId: string;
status: RunDispatchLeaseStatus;
version: number;
leaseGeneration: number;
workerId: string;
workerSessionId: string;
workerGeneration: number;
leaseToken: string;
acquiredAtMs: number;
renewedAtMs: number;
expiresAtMs: number;
releasedAtMs?: number;
releaseReason?: RunDispatchReleaseReason;
completedAtMs?: number;
updatedAtMs: number;
}
export type RunDispatchLeaseFenceReason =
| 'missing'
| 'run_mismatch'
| 'not_leased'
| 'worker_mismatch'
| 'worker_session_mismatch'
| 'worker_generation_mismatch'
| 'lease_generation_mismatch'
| 'lease_token_mismatch'
| 'version_mismatch'
| 'lease_expired'
| 'worker_unavailable';
export class InvalidRunDispatchLeaseValueError extends TypeError {
constructor(message: string) {
super(`Run dispatch lease value is invalid: ${message}`);
this.name = 'InvalidRunDispatchLeaseValueError';
}
}
export class RunDispatchLeaseFenceRejectedError extends Error {
constructor(
readonly attemptId: string,
readonly reason: RunDispatchLeaseFenceReason,
) {
super(`Run dispatch lease for Attempt ${attemptId} was fenced: ${reason}`);
this.name = 'RunDispatchLeaseFenceRejectedError';
}
}
export function assertRunDispatchId(name: string, value: string): void {
if (
typeof value !== 'string' ||
value.length < 1 ||
value.length > 128 ||
/[\u0000-\u001f\u007f]/.test(value)
) {
throw new InvalidRunDispatchLeaseValueError(`${name} is invalid`);
}
}
export function assertRunDispatchLeaseToken(value: string): void {
if (
typeof value !== 'string' ||
value.length < 32 ||
value.length > 128 ||
!/^[A-Za-z0-9_-]+$/.test(value)
) {
throw new InvalidRunDispatchLeaseValueError('leaseToken is invalid');
}
}
export function assertRunDispatchLeaseVersion(
name: string,
value: number,
positive = false,
): void {
if (!Number.isSafeInteger(value) || value < (positive ? 1 : 0)) {
throw new InvalidRunDispatchLeaseValueError(
`${name} must be a ${
positive ? 'positive' : 'non-negative'
} safe integer`,
);
}
}
export function assertRunDispatchWorkerFence(value: {
workerId: string;
workerSessionId: string;
workerGeneration: number;
}): void {
assertWorkerId(value.workerId);
assertWorkerSessionId(value.workerSessionId);
assertRunDispatchLeaseVersion(
'workerGeneration',
value.workerGeneration,
true,
);
}
export function assertRunDispatchLeaseDuration(value: number): void {
if (
!Number.isSafeInteger(value) ||
value < MIN_RUN_DISPATCH_LEASE_DURATION_MS ||
value > MAX_RUN_DISPATCH_LEASE_DURATION_MS
) {
throw new InvalidRunDispatchLeaseValueError(
`leaseDurationMs must be between ${MIN_RUN_DISPATCH_LEASE_DURATION_MS} and ${MAX_RUN_DISPATCH_LEASE_DURATION_MS}`,
);
}
}
export function assertRunDispatchLeaseRecord(
value: RunDispatchLeaseRecord,
): void {
assertRunDispatchId('attemptId', value.attemptId);
assertRunDispatchId('runId', value.runId);
if (!RUN_DISPATCH_LEASE_STATUSES.includes(value.status)) {
throw new InvalidRunDispatchLeaseValueError('status is invalid');
}
assertRunDispatchLeaseVersion('version', value.version);
assertRunDispatchLeaseVersion('leaseGeneration', value.leaseGeneration, true);
assertRunDispatchWorkerFence(value);
assertRunDispatchLeaseToken(value.leaseToken);
assertRunDispatchLeaseVersion('acquiredAtMs', value.acquiredAtMs);
assertRunDispatchLeaseVersion('renewedAtMs', value.renewedAtMs);
assertRunDispatchLeaseVersion('expiresAtMs', value.expiresAtMs);
assertRunDispatchLeaseVersion('updatedAtMs', value.updatedAtMs);
if (
value.renewedAtMs < value.acquiredAtMs ||
value.expiresAtMs <= value.renewedAtMs ||
value.updatedAtMs < value.acquiredAtMs
) {
throw new InvalidRunDispatchLeaseValueError('timestamps are inconsistent');
}
if (value.status === 'leased') {
if (
value.releasedAtMs !== undefined ||
value.releaseReason !== undefined ||
value.completedAtMs !== undefined
) {
throw new InvalidRunDispatchLeaseValueError(
'active lease has terminal metadata',
);
}
return;
}
if (value.status === 'released') {
if (
value.releasedAtMs === undefined ||
value.completedAtMs !== undefined ||
value.releaseReason === undefined ||
!RUN_DISPATCH_RELEASE_REASONS.includes(value.releaseReason)
) {
throw new InvalidRunDispatchLeaseValueError(
'released lease metadata is inconsistent',
);
}
assertRunDispatchLeaseVersion('releasedAtMs', value.releasedAtMs);
return;
}
if (
value.completedAtMs === undefined ||
value.releasedAtMs !== undefined ||
value.releaseReason !== undefined
) {
throw new InvalidRunDispatchLeaseValueError(
'completed lease metadata is inconsistent',
);
}
assertRunDispatchLeaseVersion('completedAtMs', value.completedAtMs);
}
export function runDispatchLeaseExpiration(
nowMs: number,
durationMs: number,
): number {
assertRunDispatchLeaseVersion('nowMs', nowMs);
assertRunDispatchLeaseDuration(durationMs);
if (nowMs > Number.MAX_SAFE_INTEGER - durationMs) {
throw new InvalidRunDispatchLeaseValueError(
'lease expiration exceeds the safe integer range',
);
}
return nowMs + durationMs;
}
+108
View File
@@ -0,0 +1,108 @@
import { createHash } from 'crypto';
import type { ExecutionSpec } from './execution';
import { cloneExecutionSpec } from './executionSpec';
import type { RunDispatchCandidate } from './runDispatchCandidate';
import {
assertRunDispatchLeaseRecord,
type RunDispatchLeaseRecord,
} from './runDispatchLease';
export const RUN_DISPATCH_OFFER_ID_PATTERN = /^[0-9a-f]{64}$/;
export interface RunDispatchPlan {
/** Untrusted until normalized by the Dispatcher. */
placement: unknown;
executionSpec: ExecutionSpec;
}
export interface ClaimedExecutionOffer {
/** Stable for one Attempt lease generation, including crash recovery. */
offerId: string;
/** Worker dedupe must reject the same offerId with a different digest. */
executionSpecDigest: string;
deliveryKind: 'new_claim' | 'lease_recovery';
candidate: RunDispatchCandidate;
worker: {
id: string;
sessionId: string;
generation: number;
};
/** Contains the opaque lease capability; never log or persist outside its owner. */
lease: RunDispatchLeaseRecord;
executionSpec: ExecutionSpec;
placementScore?: number;
}
export interface RunDispatcherStats {
recoveryPages: number;
recoveriesScanned: number;
recoveryPlansUnavailable: number;
candidatePages: number;
candidatesScanned: number;
workerPages: number;
workersScanned: number;
plansUnavailable: number;
matchingWorkers: number;
claimAttempts: number;
claimRaces: number;
}
export type RunDispatcherIdleReason =
| 'recovery_plans_unavailable'
| 'recovery_scan_budget_exhausted'
| 'no_candidates'
| 'no_workers'
| 'plans_unavailable'
| 'no_match'
| 'claim_raced'
| 'claim_budget_exhausted'
| 'scan_budget_exhausted';
export type RunDispatcherResult =
| {
status: 'offered';
offer: ClaimedExecutionOffer;
stats: RunDispatcherStats;
truncated: boolean;
}
| {
status: 'idle';
reason: RunDispatcherIdleReason;
stats: RunDispatcherStats;
truncated: boolean;
};
export function createRunDispatchOfferId(
lease: RunDispatchLeaseRecord,
): string {
assertRunDispatchLeaseRecord(lease);
if (lease.status !== 'leased') {
throw new TypeError(
'Execution offer requires an active Run dispatch lease',
);
}
return createHash('sha256')
.update('qinglong-run-dispatch-offer-v1\0', 'utf8')
.update(lease.attemptId, 'utf8')
.update('\0', 'utf8')
.update(String(lease.leaseGeneration), 'utf8')
.update('\0', 'utf8')
.update(lease.workerId, 'utf8')
.update('\0', 'utf8')
.update(lease.workerSessionId, 'utf8')
.update('\0', 'utf8')
.update(String(lease.workerGeneration), 'utf8')
.digest('hex');
}
export function createExecutionSpecDigest(spec: ExecutionSpec): string {
return createHash('sha256')
.update(JSON.stringify(cloneExecutionSpec(spec)), 'utf8')
.digest('hex');
}
export function assertRunDispatchOfferId(value: string): void {
if (!RUN_DISPATCH_OFFER_ID_PATTERN.test(value)) {
throw new TypeError('Run dispatch offer ID is invalid');
}
}
+23
View File
@@ -0,0 +1,23 @@
import type { ExecutionSpec } from './execution';
import { cloneExecutionSpec } from './executionSpec';
import type { RunDispatchCandidate } from './runDispatchCandidate';
/** Normalizes a trusted plan and binds it to one persisted candidate identity. */
export function executionSpecForRunDispatchCandidate(
candidate: RunDispatchCandidate,
value: ExecutionSpec,
): ExecutionSpec {
const executionSpec = cloneExecutionSpec(value);
if (
executionSpec.runId !== candidate.runId ||
executionSpec.attemptId !== candidate.attemptId ||
executionSpec.projectId !== candidate.projectId ||
executionSpec.taskId !== candidate.taskId ||
executionSpec.taskRevision !== candidate.taskRevision
) {
throw new TypeError(
'ExecutionSpec identity does not match its dispatch candidate',
);
}
return executionSpec;
}
@@ -0,0 +1,58 @@
import {
assertRunDispatchCandidate,
assertRunDispatchCandidatePageSize,
type RunDispatchCandidate,
} from './runDispatchCandidate';
import {
assertRunDispatchId,
assertRunDispatchLeaseRecord,
assertRunDispatchLeaseVersion,
type RunDispatchLeaseRecord,
} from './runDispatchLease';
export const MAX_RUN_DISPATCH_RECOVERY_PAGE_SIZE = 64;
export interface RunDispatchRecoveryCursor {
expiresAtMs: number;
attemptId: string;
}
export interface RecoverableRunDispatch {
candidate: RunDispatchCandidate;
lease: RunDispatchLeaseRecord;
}
export function assertRunDispatchRecoveryCursor(
cursor: RunDispatchRecoveryCursor,
): void {
assertRunDispatchLeaseVersion('expiresAtMs', cursor.expiresAtMs);
assertRunDispatchId('attemptId', cursor.attemptId);
}
export function assertRecoverableRunDispatch(
recovery: RecoverableRunDispatch,
): void {
if (!recovery || typeof recovery !== 'object' || Array.isArray(recovery)) {
throw new TypeError('Recoverable Run dispatch must be an object');
}
assertRunDispatchCandidate(recovery.candidate);
assertRunDispatchLeaseRecord(recovery.lease);
if (
recovery.lease.status !== 'leased' ||
recovery.lease.runId !== recovery.candidate.runId ||
recovery.lease.attemptId !== recovery.candidate.attemptId
) {
throw new TypeError(
'Recoverable Run dispatch candidate and active lease do not match',
);
}
}
export function assertRunDispatchRecoveryPageSize(limit: number): void {
assertRunDispatchCandidatePageSize(limit);
if (limit > MAX_RUN_DISPATCH_RECOVERY_PAGE_SIZE) {
throw new RangeError(
`Run dispatch recovery page size must not exceed ${MAX_RUN_DISPATCH_RECOVERY_PAGE_SIZE}`,
);
}
}
+138
View File
@@ -0,0 +1,138 @@
export const RUN_RETRY_SAFETIES = [
'unknown',
'idempotent',
'deduplicated',
] as const;
export type RunRetrySafety = (typeof RUN_RETRY_SAFETIES)[number];
export const MAX_RUN_ATTEMPTS = 16;
export const MAX_RUN_RETRY_BACKOFF_MS = 24 * 60 * 60 * 1000;
export interface RunRetryPolicyDefinition {
maxAttempts: number;
retryOnLost: boolean;
safety: RunRetrySafety;
backoffBaseMs: number;
backoffMaxMs: number;
}
export interface RunRetryPolicyRecord extends RunRetryPolicyDefinition {
runId: string;
nextAttemptAtMs?: number;
version: number;
createdAtMs: number;
updatedAtMs: number;
}
export const NO_AUTOMATIC_RUN_RETRY: Readonly<RunRetryPolicyDefinition> = {
maxAttempts: 1,
retryOnLost: false,
safety: 'unknown',
backoffBaseMs: 0,
backoffMaxMs: 0,
};
export class InvalidRunRetryPolicyError extends TypeError {
readonly code = 'INVALID_RUN_RETRY_POLICY';
constructor(message: string) {
super(message);
this.name = 'InvalidRunRetryPolicyError';
}
}
function assertNonNegativeTime(name: string, value: number): void {
if (!Number.isSafeInteger(value) || value < 0) {
throw new InvalidRunRetryPolicyError(
`${name} must be a non-negative safe integer`,
);
}
}
export function assertRunRetryPolicyDefinition(
policy: RunRetryPolicyDefinition,
): void {
if (
!Number.isSafeInteger(policy.maxAttempts) ||
policy.maxAttempts < 1 ||
policy.maxAttempts > MAX_RUN_ATTEMPTS
) {
throw new InvalidRunRetryPolicyError(
`maxAttempts must be between 1 and ${MAX_RUN_ATTEMPTS}`,
);
}
if (typeof policy.retryOnLost !== 'boolean') {
throw new InvalidRunRetryPolicyError('retryOnLost must be boolean');
}
if (!RUN_RETRY_SAFETIES.includes(policy.safety)) {
throw new InvalidRunRetryPolicyError('safety is not supported');
}
assertNonNegativeTime('backoffBaseMs', policy.backoffBaseMs);
assertNonNegativeTime('backoffMaxMs', policy.backoffMaxMs);
if (
policy.backoffBaseMs > MAX_RUN_RETRY_BACKOFF_MS ||
policy.backoffMaxMs > MAX_RUN_RETRY_BACKOFF_MS
) {
throw new InvalidRunRetryPolicyError(
`retry backoff cannot exceed ${MAX_RUN_RETRY_BACKOFF_MS}ms`,
);
}
if (policy.backoffMaxMs < policy.backoffBaseMs) {
throw new InvalidRunRetryPolicyError(
'backoffMaxMs cannot be smaller than backoffBaseMs',
);
}
}
export function assertAdmittedRunRetryPolicy(
policy: RunRetryPolicyDefinition,
): void {
assertRunRetryPolicyDefinition(policy);
if (
policy.retryOnLost &&
policy.maxAttempts > 1 &&
policy.safety === 'unknown'
) {
throw new InvalidRunRetryPolicyError(
'automatic lost retry requires idempotent or deduplicated safety',
);
}
}
export function assertRunRetryPolicyRecord(policy: RunRetryPolicyRecord): void {
assertRunRetryPolicyDefinition(policy);
if (!policy.runId) {
throw new InvalidRunRetryPolicyError('runId is required');
}
if (!Number.isSafeInteger(policy.version) || policy.version < 0) {
throw new InvalidRunRetryPolicyError(
'version must be a non-negative safe integer',
);
}
assertNonNegativeTime('createdAtMs', policy.createdAtMs);
assertNonNegativeTime('updatedAtMs', policy.updatedAtMs);
if (policy.updatedAtMs < policy.createdAtMs) {
throw new InvalidRunRetryPolicyError(
'updatedAtMs cannot be earlier than createdAtMs',
);
}
if (policy.nextAttemptAtMs !== undefined) {
assertNonNegativeTime('nextAttemptAtMs', policy.nextAttemptAtMs);
}
}
export function runRetryDelayMs(
policy: RunRetryPolicyDefinition,
lostAttempt: number,
): number {
assertRunRetryPolicyDefinition(policy);
if (!Number.isSafeInteger(lostAttempt) || lostAttempt < 1) {
throw new InvalidRunRetryPolicyError(
'lostAttempt must be a positive safe integer',
);
}
if (policy.backoffBaseMs === 0) return 0;
const exponent = Math.min(lostAttempt - 1, MAX_RUN_ATTEMPTS - 1);
return Math.min(policy.backoffMaxMs, policy.backoffBaseMs * 2 ** exponent);
}
+559
View File
@@ -0,0 +1,559 @@
import type {
RunAttemptRecord,
RunAttemptStatus,
RunCancellationReason,
RunRecord,
RunStatus,
} from './run';
import { RUN_CANCELLATION_REASONS } from './run';
import {
InvalidRunAttemptTransitionError,
InvalidRunTransitionError,
InvalidTransitionMetadataError,
InvalidTransitionTimestampError,
RunVersionConflictError,
} from './stateMachineErrors';
export const MAX_RUN_ERROR_CODE_LENGTH = 128;
export const MAX_RUN_ERROR_SUMMARY_LENGTH = 1024;
export const MAX_EXECUTOR_HANDLE_LENGTH = 2048;
export const MAX_LOG_ARTIFACT_ID_LENGTH = 36;
export const RUN_TRANSITIONS: Readonly<
Record<RunStatus, readonly RunStatus[]>
> = {
created: ['queued', 'cancelled'],
queued: ['dispatching', 'cancelled', 'timed_out'],
dispatching: [
'running',
'retry_wait',
'failed',
'cancelled',
'timed_out',
'lost',
],
running: [
'waiting_approval',
'retry_wait',
'succeeded',
'failed',
'cancelled',
'timed_out',
'lost',
],
waiting_approval: ['running', 'cancelled', 'timed_out'],
retry_wait: ['queued', 'cancelled', 'timed_out'],
lost: ['retry_wait', 'queued', 'failed', 'cancelled'],
succeeded: [],
failed: [],
cancelled: [],
timed_out: [],
};
export const RUN_ATTEMPT_TRANSITIONS: Readonly<
Record<RunAttemptStatus, readonly RunAttemptStatus[]>
> = {
claimed: ['starting', 'cancelled', 'lost'],
starting: ['running', 'failed', 'cancelled', 'timed_out', 'lost'],
running: ['succeeded', 'failed', 'cancelled', 'timed_out', 'lost'],
succeeded: [],
failed: [],
cancelled: [],
timed_out: [],
lost: [],
};
const TERMINAL_RUN_STATUSES = new Set<RunStatus>([
'succeeded',
'failed',
'cancelled',
'timed_out',
]);
const TERMINAL_RUN_ATTEMPT_STATUSES = new Set<RunAttemptStatus>([
'succeeded',
'failed',
'cancelled',
'timed_out',
'lost',
]);
const ERROR_RUN_STATUSES = new Set<RunStatus>([
'retry_wait',
'failed',
'cancelled',
'timed_out',
'lost',
]);
const ERROR_RUN_ATTEMPT_STATUSES = new Set<RunAttemptStatus>([
'failed',
'cancelled',
'timed_out',
'lost',
]);
export interface RunDomainEventDraft {
sequence: number;
type: string;
payload: Readonly<Record<string, unknown>>;
}
export interface RunTransitionCommand {
to: RunStatus;
expectedVersion: number;
atMs: number;
errorCode?: string;
errorSummary?: string;
}
export interface RunAttemptTransitionCommand {
to: RunAttemptStatus;
expectedRunVersion: number;
atMs: number;
executorHandle?: string;
pid?: number;
logArtifactId?: string;
deadlineAtMs?: number;
callbackTokenHash?: string;
callbackSequence?: number;
exitCode?: number;
errorCode?: string;
errorSummary?: string;
}
export interface RunTransitionDecision {
run: RunRecord;
event: RunDomainEventDraft;
}
export interface RunAttemptTransitionDecision {
run: RunRecord;
attempt: RunAttemptRecord;
event: RunDomainEventDraft;
}
export interface RunCancellationRequestCommand {
expectedVersion: number;
atMs: number;
reason: RunCancellationReason;
}
export type RunCancellationRequestDecision =
| {
status: 'accepted';
run: RunRecord;
event: RunDomainEventDraft;
}
| {
status: 'already_requested' | 'already_terminal';
run: RunRecord;
};
export function isTerminalRunStatus(status: RunStatus): boolean {
return TERMINAL_RUN_STATUSES.has(status);
}
export function isTerminalRunAttemptStatus(status: RunAttemptStatus): boolean {
return TERMINAL_RUN_ATTEMPT_STATUSES.has(status);
}
function assertVersion(run: RunRecord, expectedVersion: number): void {
if (!Number.isInteger(expectedVersion) || expectedVersion < 0) {
throw new InvalidTransitionMetadataError(
'expectedVersion must be a non-negative integer',
);
}
if (run.version !== expectedVersion) {
throw new RunVersionConflictError(run.id, expectedVersion, run.version);
}
}
function assertTimestamp(
atMs: number,
createdAtMs: number,
startedAtMs?: number,
): void {
if (!Number.isSafeInteger(atMs) || atMs < 0) {
throw new InvalidTransitionTimestampError(
'Transition timestamp must be a non-negative safe integer',
);
}
if (atMs < createdAtMs) {
throw new InvalidTransitionTimestampError(
'Transition timestamp cannot be earlier than creation time',
);
}
if (startedAtMs !== undefined && atMs < startedAtMs) {
throw new InvalidTransitionTimestampError(
'Transition timestamp cannot be earlier than start time',
);
}
}
function assertErrorMetadata(
status: RunStatus | RunAttemptStatus,
errorStatuses: ReadonlySet<RunStatus | RunAttemptStatus>,
errorCode?: string,
errorSummary?: string,
): void {
if (
(errorCode !== undefined || errorSummary !== undefined) &&
!errorStatuses.has(status)
) {
throw new InvalidTransitionMetadataError(
'Error metadata is not allowed for the target status',
);
}
if (errorCode !== undefined && errorCode.length > MAX_RUN_ERROR_CODE_LENGTH) {
throw new InvalidTransitionMetadataError('errorCode is too long');
}
if (
errorSummary !== undefined &&
errorSummary.length > MAX_RUN_ERROR_SUMMARY_LENGTH
) {
throw new InvalidTransitionMetadataError('errorSummary is too long');
}
}
function assertAttemptExecutionMetadata(
attempt: RunAttemptRecord,
command: RunAttemptTransitionCommand,
): void {
const hasExecutionMetadata =
command.executorHandle !== undefined ||
command.pid !== undefined ||
command.logArtifactId !== undefined ||
command.deadlineAtMs !== undefined ||
command.callbackTokenHash !== undefined;
if (
hasExecutionMetadata &&
command.to !== 'starting' &&
command.to !== 'running'
) {
throw new InvalidTransitionMetadataError(
'Execution metadata is only allowed while an Attempt is starting or running',
);
}
if (
command.executorHandle !== undefined &&
(command.executorHandle.length < 1 ||
command.executorHandle.length > MAX_EXECUTOR_HANDLE_LENGTH)
) {
throw new InvalidTransitionMetadataError(
'executorHandle has an invalid length',
);
}
if (
command.pid !== undefined &&
(!Number.isSafeInteger(command.pid) || command.pid < 1)
) {
throw new InvalidTransitionMetadataError(
'pid must be a positive safe integer',
);
}
if (
command.logArtifactId !== undefined &&
(command.logArtifactId.length < 1 ||
command.logArtifactId.length > MAX_LOG_ARTIFACT_ID_LENGTH)
) {
throw new InvalidTransitionMetadataError(
'logArtifactId has an invalid length',
);
}
if (command.deadlineAtMs !== undefined) {
if (command.to !== 'starting') {
throw new InvalidTransitionMetadataError(
'deadlineAtMs is only allowed when an Attempt starts',
);
}
if (
!Number.isSafeInteger(command.deadlineAtMs) ||
command.deadlineAtMs <= command.atMs
) {
throw new InvalidTransitionMetadataError(
'deadlineAtMs must be a safe integer after the transition time',
);
}
if (
attempt.deadlineAtMs !== undefined &&
attempt.deadlineAtMs !== command.deadlineAtMs
) {
throw new InvalidTransitionMetadataError(
'deadlineAtMs cannot replace an existing Attempt deadline',
);
}
}
if (command.callbackTokenHash !== undefined) {
if (command.to !== 'starting') {
throw new InvalidTransitionMetadataError(
'callbackTokenHash is only allowed when an Attempt starts',
);
}
if (!/^[a-f0-9]{64}$/.test(command.callbackTokenHash)) {
throw new InvalidTransitionMetadataError(
'callbackTokenHash must be a lowercase SHA-256 digest',
);
}
if (
attempt.callbackTokenHash !== undefined &&
attempt.callbackTokenHash !== command.callbackTokenHash
) {
throw new InvalidTransitionMetadataError(
'callbackTokenHash cannot replace an existing Attempt token',
);
}
}
}
function assertAttemptCallbackSequence(
attempt: RunAttemptRecord,
command: RunAttemptTransitionCommand,
): void {
if (command.callbackSequence === undefined) return;
if (!isTerminalRunAttemptStatus(command.to)) {
throw new InvalidTransitionMetadataError(
'callbackSequence is only allowed for terminal Attempt states',
);
}
if (
!Number.isSafeInteger(command.callbackSequence) ||
command.callbackSequence !== attempt.callbackSequence + 1
) {
throw new InvalidTransitionMetadataError(
'callbackSequence must advance the Attempt sequence exactly once',
);
}
}
export function reserveRunEvent(
run: RunRecord,
expectedVersion: number,
): { run: RunRecord; sequence: number } {
assertVersion(run, expectedVersion);
const sequence = run.eventSequence + 1;
if (!Number.isSafeInteger(sequence) || sequence < 1) {
throw new InvalidTransitionMetadataError(
'Run event sequence exceeds the supported range',
);
}
return {
run: {
...run,
version: run.version + 1,
eventSequence: sequence,
},
sequence,
};
}
export function requestRunCancellation(
current: RunRecord,
command: RunCancellationRequestCommand,
): RunCancellationRequestDecision {
assertTimestamp(command.atMs, current.createdAtMs, current.startedAtMs);
if (!RUN_CANCELLATION_REASONS.includes(command.reason)) {
throw new InvalidTransitionMetadataError(
'Cancellation reason is not supported',
);
}
assertVersion(current, command.expectedVersion);
if (isTerminalRunStatus(current.status)) {
return { status: 'already_terminal', run: current };
}
if (current.cancelRequestedAtMs !== undefined) {
return { status: 'already_requested', run: current };
}
const reserved = reserveRunEvent(current, command.expectedVersion);
const run: RunRecord = {
...reserved.run,
cancelRequestedAtMs: command.atMs,
cancelReason: command.reason,
};
return {
status: 'accepted',
run,
event: {
sequence: reserved.sequence,
type: 'run.cancel_requested',
payload: {
status: current.status,
reason: command.reason,
requested_at_ms: command.atMs,
version: run.version,
},
},
};
}
export function transitionRun(
current: RunRecord,
command: RunTransitionCommand,
): RunTransitionDecision {
assertTimestamp(command.atMs, current.createdAtMs, current.startedAtMs);
assertErrorMetadata(
command.to,
ERROR_RUN_STATUSES,
command.errorCode,
command.errorSummary,
);
if (!RUN_TRANSITIONS[current.status].includes(command.to)) {
throw new InvalidRunTransitionError(current.id, current.status, command.to);
}
const reserved = reserveRunEvent(current, command.expectedVersion);
const next: RunRecord = {
...reserved.run,
status: command.to,
};
if (command.to === 'queued') {
next.queuedAtMs = command.atMs;
delete next.errorCode;
delete next.errorSummary;
}
if (command.to === 'running' && next.startedAtMs === undefined) {
next.startedAtMs = command.atMs;
}
if (isTerminalRunStatus(command.to)) {
next.finishedAtMs = command.atMs;
}
if (ERROR_RUN_STATUSES.has(command.to)) {
if (command.errorCode !== undefined) next.errorCode = command.errorCode;
if (command.errorSummary !== undefined)
next.errorSummary = command.errorSummary;
} else if (command.to === 'succeeded') {
delete next.errorCode;
delete next.errorSummary;
}
return {
run: next,
event: {
sequence: reserved.sequence,
type: `run.${command.to}`,
payload: {
from_status: current.status,
to_status: command.to,
version: next.version,
...(command.errorCode ? { error_code: command.errorCode } : {}),
},
},
};
}
export function transitionRunAttempt(
currentRun: RunRecord,
currentAttempt: RunAttemptRecord,
command: RunAttemptTransitionCommand,
): RunAttemptTransitionDecision {
if (currentAttempt.runId !== currentRun.id) {
throw new InvalidTransitionMetadataError(
'RunAttempt does not belong to the supplied Run',
);
}
assertTimestamp(
command.atMs,
currentAttempt.createdAtMs,
currentAttempt.startedAtMs,
);
assertErrorMetadata(
command.to,
ERROR_RUN_ATTEMPT_STATUSES,
command.errorCode,
command.errorSummary,
);
assertAttemptExecutionMetadata(currentAttempt, command);
assertAttemptCallbackSequence(currentAttempt, command);
if (!RUN_ATTEMPT_TRANSITIONS[currentAttempt.status].includes(command.to)) {
throw new InvalidRunAttemptTransitionError(
currentAttempt.id,
currentAttempt.status,
command.to,
);
}
if (isTerminalRunStatus(currentRun.status)) {
throw new InvalidTransitionMetadataError(
'Cannot transition an Attempt after its Run is terminal',
);
}
if (
command.exitCode !== undefined &&
!isTerminalRunAttemptStatus(command.to)
) {
throw new InvalidTransitionMetadataError(
'exitCode is only allowed for terminal Attempt states',
);
}
const reserved = reserveRunEvent(currentRun, command.expectedRunVersion);
const nextAttempt: RunAttemptRecord = {
...currentAttempt,
status: command.to,
};
if (command.to === 'running' && nextAttempt.startedAtMs === undefined) {
nextAttempt.startedAtMs = command.atMs;
}
if (isTerminalRunAttemptStatus(command.to)) {
nextAttempt.finishedAtMs = command.atMs;
}
if (command.exitCode !== undefined) {
nextAttempt.exitCode = command.exitCode;
}
if (command.executorHandle !== undefined) {
nextAttempt.executorHandle = command.executorHandle;
}
if (command.pid !== undefined) nextAttempt.pid = command.pid;
if (command.logArtifactId !== undefined) {
nextAttempt.logArtifactId = command.logArtifactId;
}
if (command.deadlineAtMs !== undefined) {
nextAttempt.deadlineAtMs = command.deadlineAtMs;
}
if (command.callbackTokenHash !== undefined) {
nextAttempt.callbackTokenHash = command.callbackTokenHash;
}
if (command.callbackSequence !== undefined) {
nextAttempt.callbackSequence = command.callbackSequence;
}
if (ERROR_RUN_ATTEMPT_STATUSES.has(command.to)) {
if (command.errorCode !== undefined)
nextAttempt.errorCode = command.errorCode;
if (command.errorSummary !== undefined)
nextAttempt.errorSummary = command.errorSummary;
} else if (command.to === 'succeeded') {
delete nextAttempt.errorCode;
delete nextAttempt.errorSummary;
}
return {
run: reserved.run,
attempt: nextAttempt,
event: {
sequence: reserved.sequence,
type: `attempt.${command.to}`,
payload: {
attempt_id: currentAttempt.id,
attempt: currentAttempt.attempt,
from_status: currentAttempt.status,
to_status: command.to,
version: reserved.run.version,
...(command.exitCode !== undefined
? { exit_code: command.exitCode }
: {}),
...(command.errorCode ? { error_code: command.errorCode } : {}),
...(command.deadlineAtMs === undefined
? {}
: { deadline_at_ms: command.deadlineAtMs }),
...(command.callbackSequence === undefined
? {}
: { callback_sequence: command.callbackSequence }),
},
},
};
}
+82
View File
@@ -0,0 +1,82 @@
import {
EXECUTION_ORIGINS,
type ExecutionOrigin,
type ExecutionOwner,
} from './run';
export const COMPATIBILITY_MODES = ['off', 'shadow', 'primary'] as const;
export type CompatibilityMode = (typeof COMPATIBILITY_MODES)[number];
export interface RuntimeRolloutConfig {
defaultMode: CompatibilityMode;
origins: Partial<Record<ExecutionOrigin, CompatibilityMode>>;
allowLegacyFallbackBeforeStart: boolean;
}
export interface ExecutionOwnershipDecision {
origin: ExecutionOrigin;
mode: CompatibilityMode;
owner: ExecutionOwner;
}
const VALID_ORIGINS = new Set<ExecutionOrigin>(EXECUTION_ORIGINS);
const VALID_MODES = new Set<CompatibilityMode>(COMPATIBILITY_MODES);
function assertConfig(config: RuntimeRolloutConfig): void {
if (!VALID_MODES.has(config.defaultMode)) {
throw new TypeError('Runtime rollout defaultMode is invalid');
}
for (const [origin, mode] of Object.entries(config.origins)) {
if (!VALID_ORIGINS.has(origin as ExecutionOrigin)) {
throw new TypeError(`Runtime rollout origin is invalid: ${origin}`);
}
if (!VALID_MODES.has(mode as CompatibilityMode)) {
throw new TypeError(`Runtime rollout mode is invalid: ${String(mode)}`);
}
}
}
export class RuntimeRolloutPolicy {
private readonly config: RuntimeRolloutConfig;
constructor(config: RuntimeRolloutConfig) {
assertConfig(config);
this.config = {
defaultMode: config.defaultMode,
origins: { ...config.origins },
allowLegacyFallbackBeforeStart: config.allowLegacyFallbackBeforeStart,
};
}
modeFor(origin: ExecutionOrigin): CompatibilityMode {
return this.config.origins[origin] ?? this.config.defaultMode;
}
decide(origin: ExecutionOrigin): ExecutionOwnershipDecision {
const mode = this.modeFor(origin);
return {
origin,
mode,
owner: mode === 'primary' ? 'runtime' : 'legacy',
};
}
snapshot(): RuntimeRolloutConfig {
return {
defaultMode: this.config.defaultMode,
origins: { ...this.config.origins },
allowLegacyFallbackBeforeStart:
this.config.allowLegacyFallbackBeforeStart,
};
}
}
export function shadowOnlyRollout(
origins: readonly ExecutionOrigin[],
): RuntimeRolloutPolicy {
return new RuntimeRolloutPolicy({
defaultMode: 'off',
origins: Object.fromEntries(origins.map((origin) => [origin, 'shadow'])),
allowLegacyFallbackBeforeStart: false,
});
}
@@ -0,0 +1,231 @@
import type { ExecutionOrigin } from './run';
import {
RuntimeRolloutPolicy,
type CompatibilityMode,
type RuntimeRolloutConfig,
} from './runtimeRollout';
export const RUNTIME_ROLLOUT_MANIFEST_VERSION = 1;
export const MAX_RUNTIME_ROLLOUT_APPROVAL_MS = 30 * 24 * 60 * 60 * 1000;
export const REQUIRED_RUNTIME_ROLLOUT_GATES = [
'durableCancellation',
'startupReconciliation',
'atomicLegacyProjection',
'rollbackDrill',
'edgeBudget',
] as const;
export type RuntimeRolloutGate =
(typeof REQUIRED_RUNTIME_ROLLOUT_GATES)[number];
export interface DisabledRuntimeRolloutManifest {
schemaVersion: typeof RUNTIME_ROLLOUT_MANIFEST_VERSION;
revision: string;
enabled: false;
}
export interface EnabledRuntimeRolloutManifest {
schemaVersion: typeof RUNTIME_ROLLOUT_MANIFEST_VERSION;
revision: string;
enabled: true;
approvedBy: string;
approvedAtMs: number;
expiresAtMs: number;
rollbackPlanRef: string;
rollout: RuntimeRolloutConfig;
gates: Record<RuntimeRolloutGate, 'passed'>;
}
export type RuntimeRolloutManifest =
| DisabledRuntimeRolloutManifest
| EnabledRuntimeRolloutManifest;
export interface RuntimeRolloutManifestDecision {
manifest: RuntimeRolloutManifest;
policy: RuntimeRolloutPolicy;
}
const MANUAL_ORIGIN: ExecutionOrigin = 'manual';
const ALLOWED_MANUAL_MODES = new Set<CompatibilityMode>([
'off',
'shadow',
'primary',
]);
function asObject(value: unknown, name: string): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new TypeError(`${name} must be a JSON object`);
}
return value as Record<string, unknown>;
}
function assertExactKeys(
value: Record<string, unknown>,
expected: readonly string[],
name: string,
): void {
const allowed = new Set(expected);
for (const key of Object.keys(value)) {
if (!allowed.has(key)) throw new TypeError(`${name}.${key} is not allowed`);
}
for (const key of expected) {
if (!(key in value)) throw new TypeError(`${name}.${key} is required`);
}
}
function boundedString(
value: unknown,
name: string,
maxLength: number,
): string {
if (
typeof value !== 'string' ||
value.trim() !== value ||
value.length < 1 ||
value.length > maxLength ||
/[\u0000-\u001f\u007f]/.test(value)
) {
throw new TypeError(`${name} is invalid`);
}
return value;
}
function safeTimestamp(value: unknown, name: string): number {
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
throw new TypeError(`${name} must be a non-negative safe integer`);
}
return value;
}
function disabledPolicy(): RuntimeRolloutPolicy {
return new RuntimeRolloutPolicy({
defaultMode: 'off',
origins: {},
allowLegacyFallbackBeforeStart: false,
});
}
export function parseRuntimeRolloutManifest(
value: unknown,
evaluatedAtMs: number,
): RuntimeRolloutManifestDecision {
const now = safeTimestamp(evaluatedAtMs, 'evaluatedAtMs');
const object = asObject(value, 'manifest');
if (object.enabled === false) {
assertExactKeys(
object,
['schemaVersion', 'revision', 'enabled'],
'manifest',
);
if (object.schemaVersion !== RUNTIME_ROLLOUT_MANIFEST_VERSION) {
throw new TypeError('manifest.schemaVersion is unsupported');
}
const manifest: DisabledRuntimeRolloutManifest = {
schemaVersion: RUNTIME_ROLLOUT_MANIFEST_VERSION,
revision: boundedString(object.revision, 'manifest.revision', 128),
enabled: false,
};
return { manifest, policy: disabledPolicy() };
}
assertExactKeys(
object,
[
'schemaVersion',
'revision',
'enabled',
'approvedBy',
'approvedAtMs',
'expiresAtMs',
'rollbackPlanRef',
'rollout',
'gates',
],
'manifest',
);
if (object.schemaVersion !== RUNTIME_ROLLOUT_MANIFEST_VERSION) {
throw new TypeError('manifest.schemaVersion is unsupported');
}
if (object.enabled !== true) {
throw new TypeError('manifest.enabled must be a boolean');
}
const approvedAtMs = safeTimestamp(
object.approvedAtMs,
'manifest.approvedAtMs',
);
const expiresAtMs = safeTimestamp(object.expiresAtMs, 'manifest.expiresAtMs');
if (approvedAtMs > now) {
throw new TypeError('manifest approval is not active yet');
}
if (expiresAtMs <= now) {
throw new TypeError('manifest approval has expired');
}
if (
expiresAtMs <= approvedAtMs ||
expiresAtMs - approvedAtMs > MAX_RUNTIME_ROLLOUT_APPROVAL_MS
) {
throw new TypeError('manifest approval window is invalid');
}
const rolloutObject = asObject(object.rollout, 'manifest.rollout');
assertExactKeys(
rolloutObject,
['defaultMode', 'origins', 'allowLegacyFallbackBeforeStart'],
'manifest.rollout',
);
if (rolloutObject.defaultMode !== 'off') {
throw new TypeError('manifest.rollout.defaultMode must remain off');
}
if (rolloutObject.allowLegacyFallbackBeforeStart !== false) {
throw new TypeError(
'manifest.rollout.allowLegacyFallbackBeforeStart must remain false',
);
}
const origins = asObject(rolloutObject.origins, 'manifest.rollout.origins');
assertExactKeys(origins, [MANUAL_ORIGIN], 'manifest.rollout.origins');
if (!ALLOWED_MANUAL_MODES.has(origins.manual as CompatibilityMode)) {
throw new TypeError('manifest.rollout.origins.manual is invalid');
}
const gatesObject = asObject(object.gates, 'manifest.gates');
assertExactKeys(
gatesObject,
REQUIRED_RUNTIME_ROLLOUT_GATES,
'manifest.gates',
);
for (const gate of REQUIRED_RUNTIME_ROLLOUT_GATES) {
if (gatesObject[gate] !== 'passed') {
throw new TypeError(`manifest.gates.${gate} must be passed`);
}
}
const rollout: RuntimeRolloutConfig = {
defaultMode: 'off',
origins: { manual: origins.manual as CompatibilityMode },
allowLegacyFallbackBeforeStart: false,
};
const manifest: EnabledRuntimeRolloutManifest = {
schemaVersion: RUNTIME_ROLLOUT_MANIFEST_VERSION,
revision: boundedString(object.revision, 'manifest.revision', 128),
enabled: true,
approvedBy: boundedString(object.approvedBy, 'manifest.approvedBy', 128),
approvedAtMs,
expiresAtMs,
rollbackPlanRef: boundedString(
object.rollbackPlanRef,
'manifest.rollbackPlanRef',
512,
),
rollout,
gates: Object.fromEntries(
REQUIRED_RUNTIME_ROLLOUT_GATES.map((gate) => [gate, 'passed']),
) as Record<RuntimeRolloutGate, 'passed'>,
};
return { manifest, policy: new RuntimeRolloutPolicy(rollout) };
}
export function defaultOffRuntimeRolloutPolicy(): RuntimeRolloutPolicy {
return disabledPolicy();
}
+59
View File
@@ -0,0 +1,59 @@
import type { RunAttemptStatus, RunStatus } from './run';
export class RunStateMachineError extends Error {
constructor(message: string, public readonly code: string) {
super(message);
this.name = new.target.name;
}
}
export class RunVersionConflictError extends RunStateMachineError {
constructor(
public readonly runId: string,
public readonly expectedVersion: number,
public readonly actualVersion: number,
) {
super(
'Run version does not match the expected version',
'RUN_VERSION_CONFLICT',
);
}
}
export class InvalidRunTransitionError extends RunStateMachineError {
constructor(
public readonly runId: string,
public readonly from: RunStatus,
public readonly to: RunStatus,
) {
super(
`Run cannot transition from ${from} to ${to}`,
'INVALID_RUN_TRANSITION',
);
}
}
export class InvalidRunAttemptTransitionError extends RunStateMachineError {
constructor(
public readonly attemptId: string,
public readonly from: RunAttemptStatus,
public readonly to: RunAttemptStatus,
) {
super(
`RunAttempt cannot transition from ${from} to ${to}`,
'INVALID_RUN_ATTEMPT_TRANSITION',
);
}
}
export class InvalidTransitionTimestampError extends RunStateMachineError {
constructor(message: string) {
super(message, 'INVALID_TRANSITION_TIMESTAMP');
}
}
export class InvalidTransitionMetadataError extends RunStateMachineError {
constructor(message: string) {
super(message, 'INVALID_TRANSITION_METADATA');
}
}
@@ -0,0 +1,118 @@
import type {
ExecutionCommand,
ExecutionResourcePolicy,
ExecutionSpec,
ExecutorType,
} from './execution';
import { EXECUTOR_TYPES } from './execution';
import { cloneExecutionSpec } from './executionSpec';
import type { RunDispatchCandidate } from './runDispatchCandidate';
export interface TaskExecutionSpecTemplate {
command: ExecutionCommand;
workingDirectory?: string;
environmentPolicy: ExecutionSpec['environmentPolicy'];
timeoutMs?: number;
terminationGraceMs: number;
resourcePolicy?: ExecutionResourcePolicy;
}
export interface PinnedTaskExecutionRevision {
projectId: string;
taskId: string;
taskRevision: string;
executorType: ExecutorType;
execution: TaskExecutionSpecTemplate;
/** Opaque immutable recipe for Secret, output and callback capabilities. */
contextRef: string;
}
function assertIdentifier(name: string, value: string, maximum: number): void {
if (
typeof value !== 'string' ||
value.length < 1 ||
value.length > maximum ||
/[\u0000-\u001f\u007f]/.test(value)
) {
throw new TypeError(`${name} is invalid`);
}
}
function templateSpec(
revision: PinnedTaskExecutionRevision,
identity: Pick<
ExecutionSpec,
'runId' | 'attemptId' | 'projectId' | 'taskId' | 'taskRevision'
>,
): ExecutionSpec {
const execution = revision.execution;
return {
runId: identity.runId,
attemptId: identity.attemptId,
projectId: identity.projectId,
taskId: identity.taskId,
taskRevision: identity.taskRevision,
command: execution.command,
...(execution.workingDirectory === undefined
? {}
: { workingDirectory: execution.workingDirectory }),
environmentPolicy: execution.environmentPolicy,
...(execution.timeoutMs === undefined
? {}
: { timeoutMs: execution.timeoutMs }),
terminationGraceMs: execution.terminationGraceMs,
...(execution.resourcePolicy === undefined
? {}
: { resourcePolicy: execution.resourcePolicy }),
};
}
export function assertPinnedTaskExecutionRevision(
revision: PinnedTaskExecutionRevision,
): void {
if (!revision || typeof revision !== 'object' || Array.isArray(revision)) {
throw new TypeError('Pinned Task execution revision must be an object');
}
assertIdentifier('projectId', revision.projectId, 128);
assertIdentifier('taskId', revision.taskId, 255);
assertIdentifier('taskRevision', revision.taskRevision, 128);
assertIdentifier('contextRef', revision.contextRef, 512);
if (!EXECUTOR_TYPES.includes(revision.executorType)) {
throw new TypeError('Pinned Task executorType is invalid');
}
cloneExecutionSpec(
templateSpec(revision, {
runId: 'revision-validation-run',
attemptId: 'revision-validation-attempt',
projectId: revision.projectId,
taskId: revision.taskId,
taskRevision: revision.taskRevision,
}),
);
}
export function executionSpecFromPinnedTaskRevision(
candidate: RunDispatchCandidate,
revision: PinnedTaskExecutionRevision,
): ExecutionSpec {
assertPinnedTaskExecutionRevision(revision);
if (
revision.projectId !== candidate.projectId ||
revision.taskId !== candidate.taskId ||
revision.taskRevision !== candidate.taskRevision ||
revision.executorType !== candidate.executorType
) {
throw new TypeError(
'Pinned Task execution revision does not match its dispatch candidate',
);
}
return cloneExecutionSpec(
templateSpec(revision, {
runId: candidate.runId,
attemptId: candidate.attemptId,
projectId: candidate.projectId,
taskId: candidate.taskId,
taskRevision: candidate.taskRevision,
}),
);
}
@@ -0,0 +1,137 @@
import { createHash } from 'crypto';
import type {
ExecutionCommand,
ExecutionResourcePolicy,
ExecutionSpec,
} from './execution';
import { cloneExecutionSpec } from './executionSpec';
import {
assertPinnedTaskExecutionRevision,
type PinnedTaskExecutionRevision,
type TaskExecutionSpecTemplate,
} from './taskExecutionRevision';
export interface PinnedTaskExecutionRevisionRecord
extends PinnedTaskExecutionRevision {
contentDigest: string;
createdAtMs: number;
}
export class TaskExecutionRevisionCorruptError extends Error {
constructor(message: string) {
super(message);
this.name = 'TaskExecutionRevisionCorruptError';
}
}
function frozenCommand(command: ExecutionCommand): ExecutionCommand {
return command.kind === 'argv'
? Object.freeze({
kind: 'argv' as const,
file: command.file,
args: Object.freeze([...command.args]),
})
: Object.freeze({
kind: 'shell' as const,
command: command.command,
...(command.shell === undefined ? {} : { shell: command.shell }),
});
}
function frozenResourcePolicy(
policy: ExecutionResourcePolicy | undefined,
): ExecutionResourcePolicy | undefined {
if (policy === undefined) return undefined;
return Object.freeze({
...(policy.memoryBytes === undefined
? {}
: { memoryBytes: Object.freeze({ ...policy.memoryBytes }) }),
...(policy.cpuMillisPerSecond === undefined
? {}
: {
cpuMillisPerSecond: Object.freeze({
...policy.cpuMillisPerSecond,
}),
}),
...(policy.filesystemIsolation === undefined
? {}
: { filesystemIsolation: policy.filesystemIsolation }),
...(policy.networkIsolation === undefined
? {}
: { networkIsolation: policy.networkIsolation }),
});
}
function normalizedTemplate(
revision: PinnedTaskExecutionRevision,
): TaskExecutionSpecTemplate {
const spec: ExecutionSpec = cloneExecutionSpec({
runId: 'task-revision-normalization-run',
attemptId: 'task-revision-normalization-attempt',
projectId: revision.projectId,
taskId: revision.taskId,
taskRevision: revision.taskRevision,
command: revision.execution.command,
...(revision.execution.workingDirectory === undefined
? {}
: { workingDirectory: revision.execution.workingDirectory }),
environmentPolicy: revision.execution.environmentPolicy,
...(revision.execution.timeoutMs === undefined
? {}
: { timeoutMs: revision.execution.timeoutMs }),
terminationGraceMs: revision.execution.terminationGraceMs,
...(revision.execution.resourcePolicy === undefined
? {}
: { resourcePolicy: revision.execution.resourcePolicy }),
});
const resourcePolicy = frozenResourcePolicy(spec.resourcePolicy);
return Object.freeze({
command: frozenCommand(spec.command),
...(spec.workingDirectory === undefined
? {}
: { workingDirectory: spec.workingDirectory }),
environmentPolicy: spec.environmentPolicy,
...(spec.timeoutMs === undefined ? {} : { timeoutMs: spec.timeoutMs }),
terminationGraceMs: spec.terminationGraceMs,
...(resourcePolicy === undefined ? {} : { resourcePolicy }),
});
}
/** Removes unknown fields, deep-clones mutable values and freezes the result. */
export function normalizePinnedTaskExecutionRevision(
revision: PinnedTaskExecutionRevision,
): PinnedTaskExecutionRevision {
assertPinnedTaskExecutionRevision(revision);
return Object.freeze({
projectId: revision.projectId,
taskId: revision.taskId,
taskRevision: revision.taskRevision,
executorType: revision.executorType,
execution: normalizedTemplate(revision),
contextRef: revision.contextRef,
});
}
export function taskExecutionRevisionDigest(
revision: PinnedTaskExecutionRevision,
): string {
const normalized = normalizePinnedTaskExecutionRevision(revision);
return createHash('sha256')
.update(JSON.stringify(normalized), 'utf8')
.digest('hex');
}
export function createPinnedTaskExecutionRevisionRecord(
revision: PinnedTaskExecutionRevision,
createdAtMs: number,
): PinnedTaskExecutionRevisionRecord {
if (!Number.isSafeInteger(createdAtMs) || createdAtMs < 0) {
throw new RangeError('createdAtMs must be a non-negative safe integer');
}
const normalized = normalizePinnedTaskExecutionRevision(revision);
return Object.freeze({
...normalized,
contentDigest: taskExecutionRevisionDigest(normalized),
createdAtMs,
});
}
+402
View File
@@ -0,0 +1,402 @@
import { createHash } from 'crypto';
export const WORKER_STATUSES = ['online', 'draining', 'offline'] as const;
export type WorkerStatus = (typeof WORKER_STATUSES)[number];
export const MAX_WORKER_ID_LENGTH = 128;
export const MAX_WORKER_CAPABILITIES_BYTES = 16 * 1024;
export const MAX_WORKER_EXECUTORS = 16;
export const MAX_WORKER_RUNTIMES = 32;
export const MAX_WORKER_LABELS = 32;
export const MAX_WORKER_FEATURES = 32;
export const MAX_WORKER_GPUS = 8;
export const MAX_WORKER_CONCURRENT_RUNS = 1024;
const UUID_V7_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const WORKER_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
const CAPABILITY_NAME_PATTERN = /^[a-z0-9][a-z0-9._+-]*$/;
const LABEL_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
export interface WorkerRuntimeCapability {
name: string;
version: string;
}
export interface WorkerGpuCapability {
vendor: string;
model?: string;
memoryBytes?: number;
}
export interface WorkerCapacity {
cpuCores?: number;
memoryBytes?: number;
diskBytes?: number;
gpu?: readonly WorkerGpuCapability[];
}
export interface WorkerCapabilities {
architecture: string;
operatingSystem: string;
executors: readonly string[];
runtimes: readonly WorkerRuntimeCapability[];
labels: Readonly<Record<string, string>>;
capacity: WorkerCapacity;
features: readonly string[];
}
export interface WorkerRecord {
id: string;
sessionId: string;
generation: number;
status: WorkerStatus;
version: number;
capabilities: WorkerCapabilities;
capabilitiesHash: string;
maxConcurrentRuns: number;
availableSlots: number;
registeredAtMs: number;
lastHeartbeatAtMs: number;
leaseExpiresAtMs: number;
updatedAtMs: number;
}
export class InvalidWorkerValueError extends Error {
constructor(message: string) {
super(message);
this.name = 'InvalidWorkerValueError';
}
}
export class WorkerFenceRejectedError extends Error {
constructor(
readonly workerId: string,
readonly reason:
| 'missing'
| 'session_mismatch'
| 'generation_mismatch'
| 'version_mismatch'
| 'lease_expired'
| 'offline',
) {
super(`Worker ${workerId} fence rejected: ${reason}`);
this.name = 'WorkerFenceRejectedError';
}
}
export class WorkerSessionConflictError extends Error {
constructor(readonly workerId: string) {
super(`Worker ${workerId} session replay conflicts with persisted data`);
this.name = 'WorkerSessionConflictError';
}
}
function invalid(message: string): never {
throw new InvalidWorkerValueError(message);
}
function record(value: unknown, name: string): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
invalid(`${name} must be an object`);
}
return value as Record<string, unknown>;
}
function assertKeys(
value: Record<string, unknown>,
name: string,
required: readonly string[],
optional: readonly string[] = [],
): void {
const allowed = new Set([...required, ...optional]);
if (
required.some((key) => !Object.hasOwn(value, key)) ||
Object.keys(value).some((key) => !allowed.has(key))
) {
invalid(`${name} fields do not match the supported schema`);
}
}
function boundedString(
value: unknown,
name: string,
maximum: number,
pattern?: RegExp,
): string {
if (
typeof value !== 'string' ||
value.length < 1 ||
value.length > maximum ||
value.includes('\0') ||
/[\u0000-\u001f\u007f]/.test(value) ||
(pattern && !pattern.test(value))
) {
invalid(`${name} is invalid`);
}
return value;
}
function positiveInteger(
value: unknown,
name: string,
maximum = Number.MAX_SAFE_INTEGER,
): number {
if (
!Number.isSafeInteger(value) ||
(value as number) < 1 ||
(value as number) > maximum
) {
invalid(`${name} must be a positive safe integer`);
}
return value as number;
}
function optionalPositiveInteger(
value: unknown,
name: string,
): number | undefined {
return value === undefined ? undefined : positiveInteger(value, name);
}
function uniqueSortedStrings(
value: unknown,
name: string,
maximumItems: number,
): string[] {
if (!Array.isArray(value) || value.length > maximumItems) {
invalid(`${name} must be an array with at most ${maximumItems} items`);
}
const normalized = value.map((item, index) =>
boundedString(item, `${name}[${index}]`, 64, CAPABILITY_NAME_PATTERN),
);
if (new Set(normalized).size !== normalized.length) {
invalid(`${name} must not contain duplicates`);
}
return normalized.sort();
}
function normalizeRuntimes(value: unknown): WorkerRuntimeCapability[] {
if (!Array.isArray(value) || value.length > MAX_WORKER_RUNTIMES) {
invalid(`runtimes must contain at most ${MAX_WORKER_RUNTIMES} items`);
}
const runtimes = value.map((candidate, index) => {
const item = record(candidate, `runtimes[${index}]`);
assertKeys(item, `runtimes[${index}]`, ['name', 'version']);
return {
name: boundedString(
item.name,
`runtimes[${index}].name`,
64,
CAPABILITY_NAME_PATTERN,
),
version: boundedString(item.version, `runtimes[${index}].version`, 64),
};
});
const keys = runtimes.map((item) => `${item.name}\0${item.version}`);
if (new Set(keys).size !== keys.length) {
invalid('runtimes must not contain duplicates');
}
return runtimes.sort(
(left, right) =>
left.name.localeCompare(right.name) ||
left.version.localeCompare(right.version),
);
}
function normalizeLabels(value: unknown): Record<string, string> {
const labels = record(value, 'labels');
const entries = Object.entries(labels);
if (entries.length > MAX_WORKER_LABELS) {
invalid(`labels must contain at most ${MAX_WORKER_LABELS} entries`);
}
return Object.fromEntries(
entries
.map(([key, candidate]) => [
boundedString(key, 'label key', 128, LABEL_KEY_PATTERN),
boundedString(candidate, `labels.${key}`, 256),
])
.sort(([left], [right]) => left.localeCompare(right)),
);
}
function normalizeGpu(value: unknown): WorkerGpuCapability[] | undefined {
if (value === undefined) return undefined;
if (!Array.isArray(value) || value.length > MAX_WORKER_GPUS) {
invalid(`capacity.gpu must contain at most ${MAX_WORKER_GPUS} items`);
}
return value.map((candidate, index) => {
const item = record(candidate, `capacity.gpu[${index}]`);
assertKeys(
item,
`capacity.gpu[${index}]`,
['vendor'],
['model', 'memoryBytes'],
);
return {
vendor: boundedString(
item.vendor,
`capacity.gpu[${index}].vendor`,
64,
CAPABILITY_NAME_PATTERN,
),
...(item.model === undefined
? {}
: {
model: boundedString(
item.model,
`capacity.gpu[${index}].model`,
128,
),
}),
...(item.memoryBytes === undefined
? {}
: {
memoryBytes: positiveInteger(
item.memoryBytes,
`capacity.gpu[${index}].memoryBytes`,
),
}),
};
});
}
function normalizeCapacity(value: unknown): WorkerCapacity {
const capacity = record(value, 'capacity');
assertKeys(
capacity,
'capacity',
[],
['cpuCores', 'memoryBytes', 'diskBytes', 'gpu'],
);
const cpuCores =
capacity.cpuCores === undefined
? undefined
: positiveInteger(capacity.cpuCores, 'capacity.cpuCores', 4096);
const memoryBytes = optionalPositiveInteger(
capacity.memoryBytes,
'capacity.memoryBytes',
);
const diskBytes = optionalPositiveInteger(
capacity.diskBytes,
'capacity.diskBytes',
);
const gpu = normalizeGpu(capacity.gpu);
return {
...(cpuCores === undefined ? {} : { cpuCores }),
...(memoryBytes === undefined ? {} : { memoryBytes }),
...(diskBytes === undefined ? {} : { diskBytes }),
...(gpu === undefined ? {} : { gpu }),
};
}
export function assertWorkerId(value: string): void {
boundedString(value, 'workerId', MAX_WORKER_ID_LENGTH, WORKER_ID_PATTERN);
}
export function assertWorkerSessionId(value: string): void {
if (!UUID_V7_PATTERN.test(value))
invalid('sessionId must be a lowercase UUIDv7');
}
export function normalizeWorkerCapabilities(
value: unknown,
): WorkerCapabilities {
const capabilities = record(value, 'capabilities');
assertKeys(capabilities, 'capabilities', [
'architecture',
'operatingSystem',
'executors',
'runtimes',
'labels',
'capacity',
'features',
]);
const normalized: WorkerCapabilities = {
architecture: boundedString(
capabilities.architecture,
'architecture',
32,
CAPABILITY_NAME_PATTERN,
),
operatingSystem: boundedString(
capabilities.operatingSystem,
'operatingSystem',
32,
CAPABILITY_NAME_PATTERN,
),
executors: uniqueSortedStrings(
capabilities.executors,
'executors',
MAX_WORKER_EXECUTORS,
),
runtimes: normalizeRuntimes(capabilities.runtimes),
labels: normalizeLabels(capabilities.labels),
capacity: normalizeCapacity(capabilities.capacity),
features: uniqueSortedStrings(
capabilities.features,
'features',
MAX_WORKER_FEATURES,
),
};
const serialized = JSON.stringify(normalized);
if (Buffer.byteLength(serialized, 'utf8') > MAX_WORKER_CAPABILITIES_BYTES) {
invalid('capabilities exceed the byte limit');
}
return normalized;
}
export function serializeWorkerCapabilities(value: unknown): string {
return JSON.stringify(normalizeWorkerCapabilities(value));
}
export function parseWorkerCapabilities(value: string): WorkerCapabilities {
if (
Buffer.byteLength(value, 'utf8') < 2 ||
Buffer.byteLength(value, 'utf8') > MAX_WORKER_CAPABILITIES_BYTES
) {
invalid('capabilities size is outside the allowed range');
}
let parsed: unknown;
try {
parsed = JSON.parse(value);
} catch {
return invalid('capabilities are not valid JSON');
}
return normalizeWorkerCapabilities(parsed);
}
export function hashWorkerCapabilities(serialized: string): string {
const canonical = serializeWorkerCapabilities(
parseWorkerCapabilities(serialized),
);
return createHash('sha256').update(canonical).digest('hex');
}
export function assertWorkerConcurrency(
maxConcurrentRuns: number,
availableSlots: number,
): void {
positiveInteger(
maxConcurrentRuns,
'maxConcurrentRuns',
MAX_WORKER_CONCURRENT_RUNS,
);
if (
!Number.isSafeInteger(availableSlots) ||
availableSlots < 0 ||
availableSlots > maxConcurrentRuns
) {
invalid('availableSlots must be between 0 and maxConcurrentRuns');
}
}
export function isWorkerLeaseActive(
worker: WorkerRecord,
observedAtMs: number,
): boolean {
if (!Number.isSafeInteger(observedAtMs) || observedAtMs < 0) {
invalid('observedAtMs must be a non-negative safe integer');
}
return worker.status !== 'offline' && worker.leaseExpiresAtMs > observedAtMs;
}
@@ -0,0 +1,50 @@
import { createHash, timingSafeEqual } from 'crypto';
import type { CompletionReceipt } from './completionReceipt';
import type { ExecutionContext } from './execution';
export const WORKER_COMPLETION_RECEIPT_TOKEN_DIGEST_PATTERN = /^[0-9a-f]{64}$/;
const CALLBACK_TOKEN_PATTERN = /^[A-Za-z0-9_-]{32,128}$/;
export interface WorkerExecutionCompletionReceiptAuthentication {
callbackSequence: number;
tokenDigest: string;
}
export function createWorkerExecutionCompletionReceiptAuthentication(
callback: ExecutionContext['completionCallback'],
): WorkerExecutionCompletionReceiptAuthentication | undefined {
if (callback === undefined) return undefined;
if (!CALLBACK_TOKEN_PATTERN.test(callback.token)) {
throw new TypeError('Worker completion callback token is invalid');
}
if (
!Number.isSafeInteger(callback.callbackSequence) ||
callback.callbackSequence < 1
) {
throw new TypeError(
'Worker completion callback sequence must be a positive safe integer',
);
}
return {
callbackSequence: callback.callbackSequence,
tokenDigest: createHash('sha256')
.update(callback.token, 'utf8')
.digest('hex'),
};
}
export function matchesWorkerExecutionCompletionReceiptAuthentication(
receipt: CompletionReceipt,
expected: WorkerExecutionCompletionReceiptAuthentication,
): boolean {
if (
receipt.callbackSequence !== expected.callbackSequence ||
!WORKER_COMPLETION_RECEIPT_TOKEN_DIGEST_PATTERN.test(expected.tokenDigest)
) {
return false;
}
const actual = createHash('sha256').update(receipt.token, 'utf8').digest();
const expectedBytes = Buffer.from(expected.tokenDigest, 'hex');
return timingSafeEqual(actual, expectedBytes);
}
+450
View File
@@ -0,0 +1,450 @@
import type { ExecutionHandle } from './execution';
import { cloneExecutionSpec } from './executionSpec';
import {
assertRunDispatchCandidate,
type RunDispatchCandidate,
} from './runDispatchCandidate';
import {
assertRunDispatchLeaseRecord,
type RunDispatchLeaseRecord,
} from './runDispatchLease';
import {
assertRunDispatchOfferId,
createExecutionSpecDigest,
createRunDispatchOfferId,
type ClaimedExecutionOffer,
} from './runDispatchOffer';
import {
MAX_EXECUTOR_HANDLE_LENGTH,
MAX_LOG_ARTIFACT_ID_LENGTH,
} from './runStateMachine';
import { WORKER_COMPLETION_RECEIPT_TOKEN_DIGEST_PATTERN } from './workerExecutionCompletionReceiptAuthentication';
export const WORKER_EXECUTION_OFFER_JOURNAL_STATES = [
'accepted',
'starting_acknowledged',
'launching',
'started',
'running_acknowledged',
'start_failed',
'start_failure_acknowledged',
'completion_acknowledged',
'recovery_required',
] as const;
export type WorkerExecutionOfferJournalState =
(typeof WORKER_EXECUTION_OFFER_JOURNAL_STATES)[number];
export const MAX_WORKER_EXECUTION_OFFER_RECORD_BYTES = 192 * 1024;
export const MAX_WORKER_EXECUTION_OFFER_JOURNAL_ENTRIES = 1024;
export const MAX_WORKER_EXECUTION_OFFER_JOURNAL_PAGE_SIZE = 64;
export interface WorkerExecutionOfferJournalRecord {
schemaVersion: 1;
revision: number;
state: WorkerExecutionOfferJournalState;
offer: ClaimedExecutionOffer;
acceptedAtMs: number;
updatedAtMs: number;
executorHandle?: string;
executorStartedAtMs?: number;
logArtifactId?: string;
completionReceiptCallbackSequence?: number;
completionReceiptTokenDigest?: string;
completionAcknowledgedAtMs?: number;
recoveryReason?:
| 'launch_outcome_unknown'
| 'control_plane_already_running'
| 'control_plane_terminal'
| 'lease_lost_local_execution_stopped'
| 'lease_lost_local_execution_unverified';
}
export class InvalidWorkerExecutionOfferError extends TypeError {
constructor(message: string) {
super(`Worker execution offer is invalid: ${message}`);
this.name = 'InvalidWorkerExecutionOfferError';
}
}
export class WorkerExecutionOfferConflictError extends Error {
constructor(readonly offerId: string) {
super(`Worker execution offer ${offerId} conflicts with durable state`);
this.name = 'WorkerExecutionOfferConflictError';
}
}
function invalid(message: string): never {
throw new InvalidWorkerExecutionOfferError(message);
}
function safeInteger(value: unknown, name: string): number {
if (!Number.isSafeInteger(value) || (value as number) < 0) {
invalid(`${name} must be a non-negative safe integer`);
}
return value as number;
}
function boundedString(value: unknown, name: string, maximum: number): string {
if (
typeof value !== 'string' ||
value.length < 1 ||
value.length > maximum ||
/[\u0000-\u001f\u007f]/.test(value)
) {
invalid(`${name} is invalid`);
}
return value;
}
function sameCandidate(
left: RunDispatchCandidate,
right: RunDispatchCandidate,
): boolean {
return (
left.runId === right.runId &&
left.attemptId === right.attemptId &&
left.projectId === right.projectId &&
left.taskId === right.taskId &&
left.taskRevision === right.taskRevision &&
left.executorType === right.executorType &&
left.priority === right.priority &&
left.queuedAtMs === right.queuedAtMs &&
left.attemptCreatedAtMs === right.attemptCreatedAtMs
);
}
function sameLeaseAuthority(
left: RunDispatchLeaseRecord,
right: RunDispatchLeaseRecord,
): boolean {
return (
left.attemptId === right.attemptId &&
left.runId === right.runId &&
left.workerId === right.workerId &&
left.workerSessionId === right.workerSessionId &&
left.workerGeneration === right.workerGeneration &&
left.leaseGeneration === right.leaseGeneration &&
left.leaseToken === right.leaseToken
);
}
export function assertClaimedExecutionOffer(
offer: ClaimedExecutionOffer,
): void {
if (!offer || typeof offer !== 'object' || Array.isArray(offer)) {
invalid('offer must be an object');
}
assertRunDispatchOfferId(offer.offerId);
assertRunDispatchCandidate(offer.candidate);
assertRunDispatchLeaseRecord(offer.lease);
if (offer.lease.status !== 'leased') invalid('lease must be active');
cloneExecutionSpec(offer.executionSpec);
if (
offer.deliveryKind !== 'new_claim' &&
offer.deliveryKind !== 'lease_recovery'
) {
invalid('deliveryKind is invalid');
}
if (
!offer.worker ||
typeof offer.worker !== 'object' ||
offer.worker.id !== offer.lease.workerId ||
offer.worker.sessionId !== offer.lease.workerSessionId ||
offer.worker.generation !== offer.lease.workerGeneration
) {
invalid('Worker target does not match the lease fence');
}
if (
offer.candidate.runId !== offer.lease.runId ||
offer.candidate.attemptId !== offer.lease.attemptId ||
offer.executionSpec.runId !== offer.candidate.runId ||
offer.executionSpec.attemptId !== offer.candidate.attemptId ||
offer.executionSpec.projectId !== offer.candidate.projectId ||
offer.executionSpec.taskId !== offer.candidate.taskId ||
offer.executionSpec.taskRevision !== offer.candidate.taskRevision
) {
invalid('Run, Attempt, Project, Task or revision identity drifted');
}
if (createRunDispatchOfferId(offer.lease) !== offer.offerId) {
invalid('offerId does not match the lease authority');
}
if (
createExecutionSpecDigest(offer.executionSpec) !== offer.executionSpecDigest
) {
invalid('ExecutionSpec digest does not match the payload');
}
if (
offer.placementScore !== undefined &&
(!Number.isFinite(offer.placementScore) || offer.placementScore < 0)
) {
invalid('placementScore is invalid');
}
}
export function cloneClaimedExecutionOffer(
offer: ClaimedExecutionOffer,
): ClaimedExecutionOffer {
assertClaimedExecutionOffer(offer);
return {
offerId: offer.offerId,
executionSpecDigest: offer.executionSpecDigest,
deliveryKind: offer.deliveryKind,
candidate: { ...offer.candidate },
worker: { ...offer.worker },
lease: { ...offer.lease },
executionSpec: cloneExecutionSpec(offer.executionSpec),
...(offer.placementScore === undefined
? {}
: { placementScore: offer.placementScore }),
};
}
export function assertSameWorkerExecutionOffer(
persisted: ClaimedExecutionOffer,
delivered: ClaimedExecutionOffer,
): void {
assertClaimedExecutionOffer(persisted);
assertClaimedExecutionOffer(delivered);
if (
persisted.offerId !== delivered.offerId ||
persisted.executionSpecDigest !== delivered.executionSpecDigest ||
!sameCandidate(persisted.candidate, delivered.candidate) ||
!sameLeaseAuthority(persisted.lease, delivered.lease) ||
persisted.worker.id !== delivered.worker.id ||
persisted.worker.sessionId !== delivered.worker.sessionId ||
persisted.worker.generation !== delivered.worker.generation
) {
throw new WorkerExecutionOfferConflictError(delivered.offerId);
}
}
export function mergeWorkerExecutionOffer(
persisted: ClaimedExecutionOffer,
delivered: ClaimedExecutionOffer,
): ClaimedExecutionOffer {
assertSameWorkerExecutionOffer(persisted, delivered);
const selectedLease =
delivered.lease.version > persisted.lease.version
? delivered.lease
: persisted.lease;
return cloneClaimedExecutionOffer({
...persisted,
deliveryKind: delivered.deliveryKind,
lease: selectedLease,
});
}
export function createWorkerExecutionOfferJournalRecord(
offer: ClaimedExecutionOffer,
acceptedAtMs: number,
): WorkerExecutionOfferJournalRecord {
safeInteger(acceptedAtMs, 'acceptedAtMs');
return {
schemaVersion: 1,
revision: 0,
state: 'accepted',
offer: cloneClaimedExecutionOffer(offer),
acceptedAtMs,
updatedAtMs: acceptedAtMs,
};
}
export function cloneWorkerExecutionOfferJournalRecord(
record: WorkerExecutionOfferJournalRecord,
): WorkerExecutionOfferJournalRecord {
assertWorkerExecutionOfferJournalRecord(record);
return {
schemaVersion: 1,
revision: record.revision,
state: record.state,
offer: cloneClaimedExecutionOffer(record.offer),
acceptedAtMs: record.acceptedAtMs,
updatedAtMs: record.updatedAtMs,
...(record.executorHandle === undefined
? {}
: { executorHandle: record.executorHandle }),
...(record.executorStartedAtMs === undefined
? {}
: { executorStartedAtMs: record.executorStartedAtMs }),
...(record.logArtifactId === undefined
? {}
: { logArtifactId: record.logArtifactId }),
...(record.completionReceiptCallbackSequence === undefined
? {}
: {
completionReceiptCallbackSequence:
record.completionReceiptCallbackSequence,
}),
...(record.completionReceiptTokenDigest === undefined
? {}
: {
completionReceiptTokenDigest: record.completionReceiptTokenDigest,
}),
...(record.completionAcknowledgedAtMs === undefined
? {}
: {
completionAcknowledgedAtMs: record.completionAcknowledgedAtMs,
}),
...(record.recoveryReason === undefined
? {}
: { recoveryReason: record.recoveryReason }),
};
}
export function assertWorkerExecutionOfferJournalRecord(
record: WorkerExecutionOfferJournalRecord,
): void {
if (!record || typeof record !== 'object' || Array.isArray(record)) {
invalid('journal record must be an object');
}
if (record.schemaVersion !== 1) invalid('schemaVersion is unsupported');
safeInteger(record.revision, 'revision');
safeInteger(record.acceptedAtMs, 'acceptedAtMs');
safeInteger(record.updatedAtMs, 'updatedAtMs');
if (record.updatedAtMs < record.acceptedAtMs) {
invalid('journal timestamps are inconsistent');
}
if (!WORKER_EXECUTION_OFFER_JOURNAL_STATES.includes(record.state)) {
invalid('journal state is invalid');
}
assertClaimedExecutionOffer(record.offer);
const hasExecutorMetadata =
record.executorHandle !== undefined ||
record.executorStartedAtMs !== undefined ||
record.logArtifactId !== undefined;
const requiresExecutorMetadata =
record.state === 'started' || record.state === 'running_acknowledged';
if (requiresExecutorMetadata || hasExecutorMetadata) {
boundedString(
record.executorHandle,
'executorHandle',
MAX_EXECUTOR_HANDLE_LENGTH,
);
safeInteger(record.executorStartedAtMs, 'executorStartedAtMs');
if (
!requiresExecutorMetadata &&
record.state !== 'recovery_required' &&
record.state !== 'completion_acknowledged'
) {
invalid('executor metadata is not allowed in this journal state');
}
}
if (record.logArtifactId !== undefined) {
boundedString(
record.logArtifactId,
'logArtifactId',
MAX_LOG_ARTIFACT_ID_LENGTH,
);
}
const hasCompletionCallbackSequence =
record.completionReceiptCallbackSequence !== undefined;
const hasCompletionTokenDigest =
record.completionReceiptTokenDigest !== undefined;
if (hasCompletionCallbackSequence !== hasCompletionTokenDigest) {
invalid('completion receipt authentication metadata must be complete');
}
if (hasCompletionCallbackSequence) {
if (
!Number.isSafeInteger(record.completionReceiptCallbackSequence) ||
record.completionReceiptCallbackSequence! < 1
) {
invalid('completionReceiptCallbackSequence must be positive');
}
if (
typeof record.completionReceiptTokenDigest !== 'string' ||
!WORKER_COMPLETION_RECEIPT_TOKEN_DIGEST_PATTERN.test(
record.completionReceiptTokenDigest,
)
) {
invalid('completionReceiptTokenDigest is invalid');
}
if (
record.state === 'accepted' ||
record.state === 'starting_acknowledged'
) {
invalid(
'completion receipt authentication is not allowed before launching',
);
}
}
if (record.state === 'completion_acknowledged') {
if (!hasCompletionCallbackSequence) {
invalid('completion acknowledgement requires authentication metadata');
}
safeInteger(
record.completionAcknowledgedAtMs,
'completionAcknowledgedAtMs',
);
if (record.completionAcknowledgedAtMs !== record.updatedAtMs) {
invalid('completion acknowledgement timestamp must match updatedAtMs');
}
} else if (record.completionAcknowledgedAtMs !== undefined) {
invalid(
'completionAcknowledgedAtMs is only allowed for completion_acknowledged',
);
}
if (record.state === 'recovery_required') {
if (
record.recoveryReason !== 'launch_outcome_unknown' &&
record.recoveryReason !== 'control_plane_already_running' &&
record.recoveryReason !== 'control_plane_terminal' &&
record.recoveryReason !== 'lease_lost_local_execution_stopped' &&
record.recoveryReason !== 'lease_lost_local_execution_unverified'
) {
invalid('recoveryReason is invalid');
}
} else if (record.recoveryReason !== undefined) {
invalid('recoveryReason is only allowed for recovery_required');
}
}
export function workerExecutionHandleMetadata(handle: ExecutionHandle): {
executorHandle: string;
executorStartedAtMs: number;
} {
const executorHandle = boundedString(
handle.durableHandle ?? handle.id,
'executorHandle',
MAX_EXECUTOR_HANDLE_LENGTH,
);
const executorStartedAtMs = safeInteger(handle.startedAtMs, 'startedAtMs');
return { executorHandle, executorStartedAtMs };
}
export function serializeWorkerExecutionOfferJournalRecord(
record: WorkerExecutionOfferJournalRecord,
): string {
const serialized = JSON.stringify(
cloneWorkerExecutionOfferJournalRecord(record),
);
if (
Buffer.byteLength(serialized, 'utf8') >
MAX_WORKER_EXECUTION_OFFER_RECORD_BYTES
) {
invalid('journal record exceeds the byte limit');
}
return serialized;
}
export function parseWorkerExecutionOfferJournalRecord(
value: Uint8Array | string,
): WorkerExecutionOfferJournalRecord {
const bytes =
typeof value === 'string' ? Buffer.from(value) : Buffer.from(value);
if (
bytes.length < 2 ||
bytes.length > MAX_WORKER_EXECUTION_OFFER_RECORD_BYTES
) {
invalid('serialized journal record size is outside the allowed range');
}
let parsed: unknown;
try {
parsed = JSON.parse(bytes.toString('utf8'));
} catch {
return invalid('serialized journal record is not valid JSON');
}
return cloneWorkerExecutionOfferJournalRecord(
parsed as WorkerExecutionOfferJournalRecord,
);
}
+430
View File
@@ -0,0 +1,430 @@
import { satisfies, valid, validRange } from 'semver';
import {
isWorkerLeaseActive,
type WorkerCapabilities,
type WorkerRecord,
} from './worker';
export const MAX_PLACEMENT_VALUES = 16;
export const MAX_PLACEMENT_PREFERENCES = 16;
export const MAX_PLACEMENT_CANDIDATES = 64;
export interface WorkerRuntimeRequirement {
name: string;
versionRange?: string;
}
export interface WorkerPlacementRequired {
architectures?: readonly string[];
operatingSystems?: readonly string[];
executors?: readonly string[];
runtimes?: readonly WorkerRuntimeRequirement[];
labels?: Readonly<Record<string, string>>;
minMemoryBytes?: number;
minDiskBytes?: number;
gpuVendor?: string;
features?: readonly string[];
}
export interface WorkerPlacementPreference {
labels: Readonly<Record<string, string>>;
weight: number;
}
export interface WorkerPlacementSpec {
required?: WorkerPlacementRequired;
preferred?: readonly WorkerPlacementPreference[];
}
export type WorkerPlacementMismatch =
| 'worker_unavailable'
| 'architecture'
| 'operating_system'
| 'executor'
| 'runtime'
| 'label'
| 'memory'
| 'disk'
| 'gpu'
| 'feature';
export interface WorkerPlacementDecision {
matches: boolean;
score: number;
mismatches: readonly WorkerPlacementMismatch[];
}
export interface WorkerPlacementCandidate {
worker: WorkerRecord;
score: number;
}
function invalid(message: string): never {
throw new TypeError(`Worker PlacementSpec is invalid: ${message}`);
}
function object(value: unknown, name: string): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
invalid(`${name} must be an object`);
}
return value as Record<string, unknown>;
}
function assertKeys(
value: Record<string, unknown>,
name: string,
keys: readonly string[],
): void {
if (Object.keys(value).some((key) => !keys.includes(key))) {
invalid(`${name} contains an unknown field`);
}
}
function stringValue(value: unknown, name: string, maximum = 128): string {
if (
typeof value !== 'string' ||
value.length < 1 ||
value.length > maximum ||
value.includes('\0') ||
/[\u0000-\u001f\u007f]/.test(value)
) {
invalid(`${name} is invalid`);
}
return value;
}
function stringList(value: unknown, name: string): string[] | undefined {
if (value === undefined) return undefined;
if (!Array.isArray(value) || value.length > MAX_PLACEMENT_VALUES) {
invalid(`${name} must contain at most ${MAX_PLACEMENT_VALUES} values`);
}
const values = value.map((item, index) =>
stringValue(item, `${name}[${index}]`, 64),
);
if (new Set(values).size !== values.length) {
invalid(`${name} must not contain duplicates`);
}
return values.sort();
}
function labels(
value: unknown,
name: string,
): Record<string, string> | undefined {
if (value === undefined) return undefined;
const source = object(value, name);
const entries = Object.entries(source);
if (entries.length > MAX_PLACEMENT_VALUES) {
invalid(`${name} must contain at most ${MAX_PLACEMENT_VALUES} labels`);
}
return Object.fromEntries(
entries
.map(([key, candidate]) => [
stringValue(key, `${name} key`, 128),
stringValue(candidate, `${name}.${key}`, 256),
])
.sort(([left], [right]) => left.localeCompare(right)),
);
}
function positiveInteger(value: unknown, name: string): number | undefined {
if (value === undefined) return undefined;
if (!Number.isSafeInteger(value) || (value as number) < 1) {
invalid(`${name} must be a positive safe integer`);
}
return value as number;
}
function runtimes(value: unknown): WorkerRuntimeRequirement[] | undefined {
if (value === undefined) return undefined;
if (!Array.isArray(value) || value.length > MAX_PLACEMENT_VALUES) {
invalid(
`required.runtimes must contain at most ${MAX_PLACEMENT_VALUES} values`,
);
}
const requirements = value.map((candidate, index) => {
const requirement = object(candidate, `required.runtimes[${index}]`);
assertKeys(requirement, `required.runtimes[${index}]`, [
'name',
'versionRange',
]);
const name = stringValue(
requirement.name,
`required.runtimes[${index}].name`,
64,
);
if (requirement.versionRange === undefined) return { name };
const versionRange = stringValue(
requirement.versionRange,
`required.runtimes[${index}].versionRange`,
128,
);
if (!validRange(versionRange)) {
invalid(`required.runtimes[${index}].versionRange is not semver`);
}
return { name, versionRange };
});
if (
new Set(requirements.map((item) => item.name)).size !== requirements.length
) {
invalid('required.runtimes must not repeat a runtime name');
}
return requirements.sort((left, right) =>
left.name.localeCompare(right.name),
);
}
function normalizeRequired(
value: unknown,
): WorkerPlacementRequired | undefined {
if (value === undefined) return undefined;
const required = object(value, 'required');
assertKeys(required, 'required', [
'architectures',
'operatingSystems',
'executors',
'runtimes',
'labels',
'minMemoryBytes',
'minDiskBytes',
'gpuVendor',
'features',
]);
const architectures = stringList(
required.architectures,
'required.architectures',
);
const operatingSystems = stringList(
required.operatingSystems,
'required.operatingSystems',
);
const executors = stringList(required.executors, 'required.executors');
const runtimeRequirements = runtimes(required.runtimes);
const requiredLabels = labels(required.labels, 'required.labels');
const minMemoryBytes = positiveInteger(
required.minMemoryBytes,
'required.minMemoryBytes',
);
const minDiskBytes = positiveInteger(
required.minDiskBytes,
'required.minDiskBytes',
);
const gpuVendor =
required.gpuVendor === undefined
? undefined
: stringValue(required.gpuVendor, 'required.gpuVendor', 64);
const features = stringList(required.features, 'required.features');
return {
...(architectures === undefined ? {} : { architectures }),
...(operatingSystems === undefined ? {} : { operatingSystems }),
...(executors === undefined ? {} : { executors }),
...(runtimeRequirements === undefined
? {}
: { runtimes: runtimeRequirements }),
...(requiredLabels === undefined ? {} : { labels: requiredLabels }),
...(minMemoryBytes === undefined ? {} : { minMemoryBytes }),
...(minDiskBytes === undefined ? {} : { minDiskBytes }),
...(gpuVendor === undefined ? {} : { gpuVendor }),
...(features === undefined ? {} : { features }),
};
}
function normalizePreferred(
value: unknown,
): WorkerPlacementPreference[] | undefined {
if (value === undefined) return undefined;
if (!Array.isArray(value) || value.length > MAX_PLACEMENT_PREFERENCES) {
invalid(
`preferred must contain at most ${MAX_PLACEMENT_PREFERENCES} values`,
);
}
return value.map((candidate, index) => {
const preference = object(candidate, `preferred[${index}]`);
assertKeys(preference, `preferred[${index}]`, ['labels', 'weight']);
const preferredLabels = labels(
preference.labels,
`preferred[${index}].labels`,
);
if (!preferredLabels || Object.keys(preferredLabels).length === 0) {
invalid(`preferred[${index}].labels must not be empty`);
}
if (
!Number.isSafeInteger(preference.weight) ||
(preference.weight as number) < 1 ||
(preference.weight as number) > 100
) {
invalid(`preferred[${index}].weight must be between 1 and 100`);
}
return {
labels: preferredLabels,
weight: preference.weight as number,
};
});
}
export function normalizeWorkerPlacementSpec(
value: unknown,
): WorkerPlacementSpec {
const placement = object(value, 'placement');
assertKeys(placement, 'placement', ['required', 'preferred']);
const required = normalizeRequired(placement.required);
const preferred = normalizePreferred(placement.preferred);
return {
...(required === undefined ? {} : { required }),
...(preferred === undefined ? {} : { preferred }),
};
}
function hasLabels(
capabilities: WorkerCapabilities,
expected: Readonly<Record<string, string>>,
): boolean {
return Object.entries(expected).every(
([key, value]) => capabilities.labels[key] === value,
);
}
function hasRuntime(
capabilities: WorkerCapabilities,
requirement: WorkerRuntimeRequirement,
): boolean {
return capabilities.runtimes.some((runtime) => {
if (runtime.name !== requirement.name) return false;
if (!requirement.versionRange) return true;
return (
valid(runtime.version) !== null &&
satisfies(runtime.version, requirement.versionRange, {
includePrerelease: true,
})
);
});
}
function matchNormalizedWorkerPlacement(
worker: WorkerRecord,
placement: WorkerPlacementSpec,
observedAtMs: number,
): WorkerPlacementDecision {
const required = placement.required ?? {};
const capabilities = worker.capabilities;
const mismatches: WorkerPlacementMismatch[] = [];
if (
worker.status !== 'online' ||
worker.availableSlots < 1 ||
!isWorkerLeaseActive(worker, observedAtMs)
) {
mismatches.push('worker_unavailable');
}
if (
required.architectures?.length &&
!required.architectures.includes(capabilities.architecture)
) {
mismatches.push('architecture');
}
if (
required.operatingSystems?.length &&
!required.operatingSystems.includes(capabilities.operatingSystem)
) {
mismatches.push('operating_system');
}
if (
required.executors?.some(
(executor) => !capabilities.executors.includes(executor),
)
) {
mismatches.push('executor');
}
if (
required.runtimes?.some((runtime) => !hasRuntime(capabilities, runtime))
) {
mismatches.push('runtime');
}
if (required.labels && !hasLabels(capabilities, required.labels)) {
mismatches.push('label');
}
if (
required.minMemoryBytes !== undefined &&
(capabilities.capacity.memoryBytes ?? 0) < required.minMemoryBytes
) {
mismatches.push('memory');
}
if (
required.minDiskBytes !== undefined &&
(capabilities.capacity.diskBytes ?? 0) < required.minDiskBytes
) {
mismatches.push('disk');
}
if (
required.gpuVendor !== undefined &&
!capabilities.capacity.gpu?.some((gpu) => gpu.vendor === required.gpuVendor)
) {
mismatches.push('gpu');
}
if (
required.features?.some(
(feature) => !capabilities.features.includes(feature),
)
) {
mismatches.push('feature');
}
const score = (placement.preferred ?? []).reduce(
(total, preference) =>
total +
(hasLabels(capabilities, preference.labels) ? preference.weight : 0),
0,
);
return { matches: mismatches.length === 0, score, mismatches };
}
export function matchesWorkerPlacement(
worker: WorkerRecord,
placementValue: unknown,
observedAtMs: number,
): WorkerPlacementDecision {
return matchNormalizedWorkerPlacement(
worker,
normalizeWorkerPlacementSpec(placementValue),
observedAtMs,
);
}
export function selectWorkerCandidates(
workers: readonly WorkerRecord[],
placement: unknown,
observedAtMs: number,
limit = 16,
): WorkerPlacementCandidate[] {
if (workers.length > MAX_PLACEMENT_CANDIDATES) {
throw new RangeError(
`workers must contain at most ${MAX_PLACEMENT_CANDIDATES} candidates`,
);
}
if (
!Number.isSafeInteger(limit) ||
limit < 1 ||
limit > MAX_PLACEMENT_CANDIDATES
) {
throw new RangeError(
`limit must be between 1 and ${MAX_PLACEMENT_CANDIDATES}`,
);
}
const normalizedPlacement = normalizeWorkerPlacementSpec(placement);
return workers
.map((worker) => ({
worker,
decision: matchNormalizedWorkerPlacement(
worker,
normalizedPlacement,
observedAtMs,
),
}))
.filter((candidate) => candidate.decision.matches)
.sort(
(left, right) =>
right.decision.score - left.decision.score ||
right.worker.availableSlots - left.worker.availableSlots ||
left.worker.id.localeCompare(right.worker.id),
)
.slice(0, limit)
.map(({ worker, decision }) => ({ worker, score: decision.score }));
}