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
@@ -0,0 +1,150 @@
import { EXECUTION_ORIGINS, RUN_STATUSES } from '../../run/run';
import type { RunRepositoryReader } from '../../run/runRepository';
import {
BoundedRunReadProjectionUnavailableError,
executeBoundedRunReadProjection,
} from '../../run/projection/boundedRunReadProjection';
import {
normalizeToolDefinition,
type ToolJsonValue,
} from '../tool-registry/toolRegistry';
export const BUILTIN_RUN_READ_TOOL = Object.freeze({
name: 'qinglong.run.get',
version: '1.0.0',
});
export const BUILTIN_RUN_READ_TIMEOUT_SECONDS = 5;
const MAX_INT = 2_147_483_647;
const MIN_INT = -2_147_483_648;
export const BUILTIN_RUN_READ_TOOL_DEFINITION = normalizeToolDefinition({
name: BUILTIN_RUN_READ_TOOL.name,
version: BUILTIN_RUN_READ_TOOL.version,
description: 'Read one low-sensitive Project-scoped Run projection',
inputSchema: {
type: 'object',
properties: {
runId: { type: 'string', minLength: 1, maxLength: 128 },
},
required: ['runId'],
additionalProperties: false,
},
outputSchema: {
type: 'object',
properties: {
found: { type: 'boolean' },
id: { type: 'string', minLength: 1, maxLength: 128 },
taskId: { type: 'string', minLength: 1, maxLength: 255 },
taskRevision: { type: 'string', minLength: 1, maxLength: 255 },
status: {
type: 'string',
maxLength: 32,
enum: RUN_STATUSES,
},
version: { type: 'integer', minimum: 0, maximum: MAX_INT },
eventSequence: { type: 'integer', minimum: 0, maximum: MAX_INT },
priority: { type: 'integer', minimum: MIN_INT, maximum: MAX_INT },
executionOrigin: {
type: 'string',
maxLength: 32,
enum: EXECUTION_ORIGINS,
},
executionOwner: {
type: 'string',
maxLength: 16,
enum: ['legacy', 'runtime'],
},
createdAtMs: {
type: 'integer',
minimum: 0,
maximum: Number.MAX_SAFE_INTEGER,
},
queuedAtMs: {
type: 'integer',
minimum: 0,
maximum: Number.MAX_SAFE_INTEGER,
},
startedAtMs: {
type: 'integer',
minimum: 0,
maximum: Number.MAX_SAFE_INTEGER,
},
finishedAtMs: {
type: 'integer',
minimum: 0,
maximum: Number.MAX_SAFE_INTEGER,
},
},
required: ['found'],
additionalProperties: false,
},
effect: 'read',
risk: 'low',
requiredPermissions: ['run.read'],
timeoutSeconds: BUILTIN_RUN_READ_TIMEOUT_SECONDS,
});
export class InvalidBuiltInRunReadToolError extends TypeError {
readonly code = 'BUILTIN_RUN_READ_TOOL_INVALID';
constructor(message: string) {
super(`Built-in Run read Tool is invalid: ${message}`);
this.name = 'InvalidBuiltInRunReadToolError';
}
}
export class BuiltInRunReadToolUnavailableError extends Error {
readonly code = 'BUILTIN_RUN_READ_TOOL_UNAVAILABLE';
constructor() {
super('Built-in Run read Tool is unavailable');
this.name = 'BuiltInRunReadToolUnavailableError';
}
}
const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
function invalid(message: string): never {
throw new InvalidBuiltInRunReadToolError(message);
}
function boundedText(value: unknown, maximum: number): value is string {
return (
typeof value === 'string' &&
value.length > 0 &&
value.length <= maximum &&
!CONTROL_PATTERN.test(value)
);
}
export async function executeBuiltInRunReadTool(
runs: Pick<RunRepositoryReader, 'findRunById'>,
projectId: string,
input: ToolJsonValue,
): Promise<Readonly<Record<string, ToolJsonValue>>> {
const inputRecord =
input && typeof input === 'object' && !Array.isArray(input)
? (input as Readonly<Record<string, ToolJsonValue>>)
: null;
if (
!runs ||
typeof runs.findRunById !== 'function' ||
!boundedText(projectId, 128) ||
!inputRecord ||
Reflect.ownKeys(inputRecord).length !== 1 ||
!Object.hasOwn(inputRecord, 'runId') ||
!boundedText(inputRecord.runId, 128)
) {
return invalid('execution context or input is invalid');
}
const runId = inputRecord.runId;
try {
return await executeBoundedRunReadProjection(runs, projectId, runId);
} catch (error) {
if (!(error instanceof BoundedRunReadProjectionUnavailableError)) {
return invalid('execution context or input is invalid');
}
throw new BuiltInRunReadToolUnavailableError();
}
}
@@ -0,0 +1,156 @@
import type { RunRepositoryReader } from '../../run/runRepository';
import {
normalizeProjectToolDefinitionSnapshot,
type ProjectToolDefinitionSnapshot,
} from '../tool-registry/projectToolDefinitionSnapshot';
import {
ToolDefinitionRegistry,
type ToolJsonValue,
} from '../tool-registry/toolRegistry';
import {
createTrustedToolHandlerBinding,
normalizeTrustedToolHandlerBinding,
type TrustedToolHandlerBinding,
} from '../trustedToolInvocation';
import type { DeploymentProfile } from '../../cluster-control/clusterControlActivation';
import type {
TrustedToolExecutionAdapter,
TrustedToolExecutionAdapterContext,
} from '../trustedToolExecution';
import {
BUILTIN_RUN_READ_TOOL,
BUILTIN_RUN_READ_TOOL_DEFINITION,
BUILTIN_RUN_READ_TIMEOUT_SECONDS,
InvalidBuiltInRunReadToolError,
executeBuiltInRunReadTool,
} from './builtInRunReadProjection';
export {
BUILTIN_RUN_READ_TOOL,
BUILTIN_RUN_READ_TOOL_DEFINITION,
BUILTIN_RUN_READ_TIMEOUT_SECONDS,
BuiltInRunReadToolUnavailableError,
InvalidBuiltInRunReadToolError,
executeBuiltInRunReadTool,
} from './builtInRunReadProjection';
export const BUILTIN_RUN_READ_ADAPTER = Object.freeze({
id: 'builtin.qinglong.run-get',
version: '1.0.0',
});
export const BUILTIN_RUN_READ_REDACTION_CONTRACT = Object.freeze({
id: 'redaction.qinglong.run-get',
version: '1.0.0',
});
export const BUILTIN_RUN_READ_AUDIT_CONTRACT = Object.freeze({
id: 'audit.qinglong.tool-call',
version: '1.0.0',
});
const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
function invalid(message: string): never {
throw new InvalidBuiltInRunReadToolError(message);
}
function sameValue(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function boundedText(value: unknown, maximum: number): value is string {
return (
typeof value === 'string' &&
value.length > 0 &&
value.length <= maximum &&
!CONTROL_PATTERN.test(value)
);
}
export function createBuiltInRunReadToolHandlerBinding(
snapshotValue: ProjectToolDefinitionSnapshot,
profiles: readonly DeploymentProfile[],
): Readonly<TrustedToolHandlerBinding> {
const snapshot = normalizeProjectToolDefinitionSnapshot(snapshotValue);
const definition = snapshot.definitions.find(
(entry) =>
entry.definition.name === BUILTIN_RUN_READ_TOOL.name &&
entry.definition.version === BUILTIN_RUN_READ_TOOL.version,
)?.definition;
if (!definition || !sameValue(definition, BUILTIN_RUN_READ_TOOL_DEFINITION)) {
return invalid('reviewed Tool definition is absent or changed');
}
return createTrustedToolHandlerBinding(snapshot, {
tool: BUILTIN_RUN_READ_TOOL,
adapter: BUILTIN_RUN_READ_ADAPTER,
executionClass: 'builtin_in_process',
profiles,
authorities: ['database.read'],
timeoutSeconds: BUILTIN_RUN_READ_TIMEOUT_SECONDS,
redactionContract: BUILTIN_RUN_READ_REDACTION_CONTRACT,
auditContract: BUILTIN_RUN_READ_AUDIT_CONTRACT,
});
}
export class BuiltInRunReadToolAdapter implements TrustedToolExecutionAdapter {
readonly binding!: Readonly<TrustedToolHandlerBinding>;
readonly profile!: DeploymentProfile;
readonly recoveryMode = 'retry_safe_read' as const;
readonly #runs!: Pick<RunRepositoryReader, 'findRunById'>;
constructor(
bindingValue: TrustedToolHandlerBinding,
profile: DeploymentProfile,
definitions: ToolDefinitionRegistry,
runs: Pick<RunRepositoryReader, 'findRunById'>,
) {
const binding = normalizeTrustedToolHandlerBinding(bindingValue);
if (!(definitions instanceof ToolDefinitionRegistry)) {
return invalid('Tool Definition registry is invalid');
}
let definition;
try {
definition = definitions.resolve(
BUILTIN_RUN_READ_TOOL.name,
BUILTIN_RUN_READ_TOOL.version,
);
} catch {
return invalid('reviewed Tool definition is unavailable');
}
if (
!sameValue(binding.tool, BUILTIN_RUN_READ_TOOL) ||
!sameValue(binding.adapter, BUILTIN_RUN_READ_ADAPTER) ||
binding.executionClass !== 'builtin_in_process' ||
!sameValue(binding.authorities, ['database.read']) ||
binding.timeoutSeconds !== BUILTIN_RUN_READ_TIMEOUT_SECONDS ||
!sameValue(
binding.redactionContract,
BUILTIN_RUN_READ_REDACTION_CONTRACT,
) ||
!sameValue(binding.auditContract, BUILTIN_RUN_READ_AUDIT_CONTRACT) ||
!binding.profiles.includes(profile) ||
!sameValue(definition, BUILTIN_RUN_READ_TOOL_DEFINITION)
) {
return invalid('binding does not match the reviewed adapter contract');
}
if (!runs || typeof runs.findRunById !== 'function') {
return invalid('Run repository is invalid');
}
this.binding = binding;
this.profile = profile;
this.#runs = runs;
Object.freeze(this);
}
async execute(
context: Readonly<TrustedToolExecutionAdapterContext>,
input: ToolJsonValue,
): Promise<unknown> {
if (
!context ||
typeof context !== 'object' ||
!boundedText(context.projectId, 128)
) {
return invalid('execution context or input is invalid');
}
return executeBuiltInRunReadTool(this.#runs, context.projectId, input);
}
}
@@ -0,0 +1,176 @@
import type { ProjectPermission } from '../../security/project-policy/projectPolicy';
import type {
SecurityPolicyDecision,
SecurityPolicyFence,
SecurityPrincipal,
SecuritySubject,
} from '../../security/security';
export const TOOL_INVOCATION_SCHEMA = 'qinglong/tool-invocation@v1';
export const MAX_TOOL_DEFINITIONS = 128;
export const MAX_TOOL_REQUIRED_PERMISSIONS = 16;
export const MAX_TOOL_SCHEMA_DEPTH = 8;
export const MAX_TOOL_SCHEMA_NODES = 256;
export const MAX_TOOL_SCHEMA_PROPERTIES = 64;
export const MAX_TOOL_SCHEMA_ENUM_VALUES = 64;
export const MAX_TOOL_ARRAY_ITEMS = 256;
export const MAX_TOOL_INPUT_BYTES = 64 * 1024;
export const MAX_TOOL_OUTPUT_BYTES = 256 * 1024;
export const MAX_TOOL_TIMEOUT_SECONDS = 60 * 60;
export const TOOL_EFFECTS = ['read', 'write', 'execute', 'external'] as const;
export const TOOL_RISKS = ['low', 'medium', 'high', 'critical'] as const;
export const TOOL_JSON_SCHEMA_TYPES = [
'null',
'boolean',
'string',
'number',
'integer',
'array',
'object',
] as const;
export type ToolEffect = (typeof TOOL_EFFECTS)[number];
export type ToolRisk = (typeof TOOL_RISKS)[number];
export type ToolJsonSchemaType = (typeof TOOL_JSON_SCHEMA_TYPES)[number];
export type ToolInvocationStatus = 'ready' | 'approval_required';
export type ToolJsonValue =
| null
| boolean
| number
| string
| readonly ToolJsonValue[]
| Readonly<{ [key: string]: ToolJsonValue }>;
export type ToolJsonSchema =
| Readonly<{ type: 'null' }>
| Readonly<{ type: 'boolean' }>
| Readonly<{
type: 'string';
minLength?: number;
maxLength: number;
enum?: readonly string[];
}>
| Readonly<{
type: 'number';
minimum: number;
maximum: number;
}>
| Readonly<{
type: 'integer';
minimum: number;
maximum: number;
}>
| Readonly<{
type: 'array';
items: ToolJsonSchema;
minItems?: number;
maxItems: number;
uniqueItems?: boolean;
}>
| Readonly<{
type: 'object';
properties: Readonly<Record<string, ToolJsonSchema>>;
required: readonly string[];
additionalProperties: false;
}>;
export interface ToolDefinition {
readonly name: string;
readonly version: string;
readonly description: string;
readonly inputSchema: ToolJsonSchema;
readonly outputSchema?: ToolJsonSchema;
readonly effect: ToolEffect;
readonly risk: ToolRisk;
readonly requiredPermissions: readonly ProjectPermission[];
readonly timeoutSeconds: number;
}
export interface ToolPolicyAuthorizer {
authorize(
principal: Readonly<SecurityPrincipal>,
projectId: string,
permission: ProjectPermission,
): Promise<SecurityPolicyDecision>;
}
export interface ToolInvocationRequest {
readonly projectId: string;
readonly principal: SecurityPrincipal;
readonly nowMs: number;
readonly tool: Readonly<{ name: string; version: string }>;
readonly input: unknown;
}
export interface DeniedToolInvocation {
readonly status: 'denied';
readonly tool: Readonly<{ name: string; version: string }>;
readonly permission: ProjectPermission;
}
export interface PreparedToolInvocation {
readonly status: ToolInvocationStatus;
readonly schema: typeof TOOL_INVOCATION_SCHEMA;
readonly projectId: string;
readonly requestedBy: Readonly<SecuritySubject>;
readonly tool: Readonly<{ name: string; version: string }>;
readonly permission: ProjectPermission;
readonly requiredPermissions: readonly ProjectPermission[];
readonly effect: ToolEffect;
readonly risk: ToolRisk;
readonly timeoutSeconds: number;
readonly fence: Readonly<SecurityPolicyFence>;
readonly input: ToolJsonValue;
readonly inputDigest: string;
readonly actionDigest: string;
}
export type ToolInvocationPlan =
| Readonly<DeniedToolInvocation>
| Readonly<PreparedToolInvocation>;
export class InvalidToolDefinitionError extends TypeError {
readonly code = 'TOOL_DEFINITION_INVALID';
constructor(message: string) {
super(`Tool definition is invalid: ${message}`);
this.name = 'InvalidToolDefinitionError';
}
}
export class InvalidToolJsonValueError extends TypeError {
readonly code = 'TOOL_JSON_VALUE_INVALID';
constructor(message: string) {
super(`Tool JSON value is invalid: ${message}`);
this.name = 'InvalidToolJsonValueError';
}
}
export class UnsupportedToolError extends Error {
readonly code = 'TOOL_UNSUPPORTED';
constructor() {
super('Tool is not registered');
this.name = 'UnsupportedToolError';
}
}
export class ToolPolicyUnavailableError extends Error {
readonly code = 'TOOL_POLICY_UNAVAILABLE';
constructor() {
super('Tool policy authorization is unavailable');
this.name = 'ToolPolicyUnavailableError';
}
}
export class ToolPolicySnapshotConflictError extends Error {
readonly code = 'TOOL_POLICY_SNAPSHOT_CONFLICT';
constructor() {
super('Tool permissions were not authorized by one policy snapshot');
this.name = 'ToolPolicySnapshotConflictError';
}
}
@@ -0,0 +1,391 @@
import {
normalizeProjectPermission,
type ProjectPermission,
} from '../../security/project-policy/projectPolicy';
import { semver } from '../../versioning/pinnedSemver';
import {
InvalidToolDefinitionError,
MAX_TOOL_ARRAY_ITEMS,
MAX_TOOL_OUTPUT_BYTES,
MAX_TOOL_REQUIRED_PERMISSIONS,
MAX_TOOL_SCHEMA_DEPTH,
MAX_TOOL_SCHEMA_ENUM_VALUES,
MAX_TOOL_SCHEMA_NODES,
MAX_TOOL_SCHEMA_PROPERTIES,
MAX_TOOL_TIMEOUT_SECONDS,
TOOL_EFFECTS,
TOOL_JSON_SCHEMA_TYPES,
TOOL_RISKS,
type ToolDefinition,
type ToolEffect,
type ToolJsonSchema,
type ToolJsonSchemaType,
type ToolRisk,
} from './contracts';
const TOOL_NAME_PATTERN =
/^[a-z][a-z0-9-]{0,62}(?:\.[a-z][a-z0-9-]{0,62}){1,7}$/;
const PROPERTY_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/;
const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
function definitionRecord(
value: unknown,
label: string,
): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidToolDefinitionError(`${label} must be an object`);
}
return value as Record<string, unknown>;
}
function exactDefinitionKeys(
value: Record<string, unknown>,
required: readonly string[],
optional: readonly string[],
label: string,
): void {
const keys = Object.keys(value);
const allowed = new Set([...required, ...optional]);
if (
required.some((key) => !Object.hasOwn(value, key)) ||
keys.some((key) => !allowed.has(key))
) {
throw new InvalidToolDefinitionError(`${label} shape is invalid`);
}
}
function boundedDefinitionText(
value: unknown,
label: string,
maximumBytes: number,
): string {
if (
typeof value !== 'string' ||
value.length === 0 ||
CONTROL_PATTERN.test(value) ||
Buffer.byteLength(value, 'utf8') > maximumBytes
) {
throw new InvalidToolDefinitionError(`${label} is invalid`);
}
return value;
}
function boundedInteger(
value: unknown,
minimum: number,
maximum: number,
label: string,
): number {
if (
!Number.isSafeInteger(value) ||
(value as number) < minimum ||
(value as number) > maximum
) {
throw new InvalidToolDefinitionError(`${label} is invalid`);
}
return value as number;
}
interface SchemaBudget {
nodes: number;
}
function normalizeSchema(
value: unknown,
depth: number,
budget: SchemaBudget,
): ToolJsonSchema {
if (depth > MAX_TOOL_SCHEMA_DEPTH) {
throw new InvalidToolDefinitionError('JSON Schema depth exceeded');
}
budget.nodes += 1;
if (budget.nodes > MAX_TOOL_SCHEMA_NODES) {
throw new InvalidToolDefinitionError('JSON Schema node budget exceeded');
}
const schema = definitionRecord(value, 'JSON Schema');
const type = schema.type;
if (
typeof type !== 'string' ||
!TOOL_JSON_SCHEMA_TYPES.includes(type as ToolJsonSchemaType)
) {
throw new InvalidToolDefinitionError('JSON Schema type is invalid');
}
if (type === 'null' || type === 'boolean') {
exactDefinitionKeys(schema, ['type'], [], 'JSON Schema');
return Object.freeze({ type });
}
if (type === 'string') {
exactDefinitionKeys(
schema,
['maxLength', 'type'],
['enum', 'minLength'],
'string JSON Schema',
);
const maxLength = boundedInteger(
schema.maxLength,
0,
MAX_TOOL_OUTPUT_BYTES,
'string maxLength',
);
const minLength =
schema.minLength === undefined
? undefined
: boundedInteger(schema.minLength, 0, maxLength, 'string minLength');
let values: readonly string[] | undefined;
if (schema.enum !== undefined) {
if (
!Array.isArray(schema.enum) ||
schema.enum.length < 1 ||
schema.enum.length > MAX_TOOL_SCHEMA_ENUM_VALUES
) {
throw new InvalidToolDefinitionError('string enum is invalid');
}
const unique = new Set<string>();
for (const item of schema.enum) {
if (
typeof item !== 'string' ||
Array.from(item).length > maxLength ||
(minLength !== undefined && Array.from(item).length < minLength) ||
unique.has(item)
) {
throw new InvalidToolDefinitionError(
'string enum contains an invalid or duplicate value',
);
}
unique.add(item);
}
values = Object.freeze([...unique].sort());
}
return Object.freeze({
type: 'string',
...(minLength === undefined ? {} : { minLength }),
maxLength,
...(values === undefined ? {} : { enum: values }),
});
}
if (type === 'number' || type === 'integer') {
exactDefinitionKeys(
schema,
['maximum', 'minimum', 'type'],
[],
'numeric JSON Schema',
);
if (
typeof schema.minimum !== 'number' ||
!Number.isFinite(schema.minimum) ||
Math.abs(schema.minimum) > Number.MAX_SAFE_INTEGER ||
typeof schema.maximum !== 'number' ||
!Number.isFinite(schema.maximum) ||
Math.abs(schema.maximum) > Number.MAX_SAFE_INTEGER ||
schema.maximum < schema.minimum ||
(type === 'integer' &&
(!Number.isSafeInteger(schema.minimum) ||
!Number.isSafeInteger(schema.maximum)))
) {
throw new InvalidToolDefinitionError(
'numeric JSON Schema bounds are invalid',
);
}
return type === 'number'
? Object.freeze({
type: 'number',
minimum: schema.minimum,
maximum: schema.maximum,
})
: Object.freeze({
type: 'integer',
minimum: schema.minimum,
maximum: schema.maximum,
});
}
if (type === 'array') {
exactDefinitionKeys(
schema,
['items', 'maxItems', 'type'],
['minItems', 'uniqueItems'],
'array JSON Schema',
);
const maxItems = boundedInteger(
schema.maxItems,
0,
MAX_TOOL_ARRAY_ITEMS,
'array maxItems',
);
const minItems =
schema.minItems === undefined
? undefined
: boundedInteger(schema.minItems, 0, maxItems, 'array minItems');
if (
schema.uniqueItems !== undefined &&
typeof schema.uniqueItems !== 'boolean'
) {
throw new InvalidToolDefinitionError('array uniqueItems is invalid');
}
return Object.freeze({
type: 'array',
items: normalizeSchema(schema.items, depth + 1, budget),
...(minItems === undefined ? {} : { minItems }),
maxItems,
...(schema.uniqueItems === undefined
? {}
: { uniqueItems: schema.uniqueItems }),
});
}
exactDefinitionKeys(
schema,
['additionalProperties', 'properties', 'required', 'type'],
[],
'object JSON Schema',
);
if (schema.additionalProperties !== false) {
throw new InvalidToolDefinitionError(
'object additionalProperties must be false',
);
}
const properties = definitionRecord(
schema.properties,
'JSON Schema properties',
);
const propertyEntries = Object.entries(properties);
if (propertyEntries.length > MAX_TOOL_SCHEMA_PROPERTIES) {
throw new InvalidToolDefinitionError(
'JSON Schema property budget exceeded',
);
}
const normalizedProperties: Record<string, ToolJsonSchema> = {};
for (const [name, propertySchema] of propertyEntries.sort((left, right) =>
left[0].localeCompare(right[0]),
)) {
if (!PROPERTY_NAME_PATTERN.test(name)) {
throw new InvalidToolDefinitionError(
'JSON Schema property name is invalid',
);
}
normalizedProperties[name] = normalizeSchema(
propertySchema,
depth + 1,
budget,
);
}
if (
!Array.isArray(schema.required) ||
schema.required.length > propertyEntries.length
) {
throw new InvalidToolDefinitionError(
'JSON Schema required list is invalid',
);
}
const required = new Set<string>();
for (const item of schema.required) {
if (
typeof item !== 'string' ||
!Object.hasOwn(normalizedProperties, item) ||
required.has(item)
) {
throw new InvalidToolDefinitionError(
'JSON Schema required list is invalid or duplicated',
);
}
required.add(item);
}
return Object.freeze({
type: 'object',
properties: Object.freeze(normalizedProperties),
required: Object.freeze([...required].sort()),
additionalProperties: false,
});
}
function normalizePermissions(value: unknown): readonly ProjectPermission[] {
if (!Array.isArray(value) || value.length > MAX_TOOL_REQUIRED_PERMISSIONS) {
throw new InvalidToolDefinitionError('required permissions are invalid');
}
const permissions = new Set<ProjectPermission>();
for (const item of value) {
if (typeof item !== 'string') {
throw new InvalidToolDefinitionError('required permission is invalid');
}
let permission: ProjectPermission;
try {
permission = normalizeProjectPermission(item);
} catch {
throw new InvalidToolDefinitionError('required permission is invalid');
}
if (permission.startsWith('tool.call:') || permissions.has(permission)) {
throw new InvalidToolDefinitionError(
'nested or duplicate Tool permission is invalid',
);
}
permissions.add(permission);
}
return Object.freeze([...permissions].sort());
}
export function normalizeToolDefinition(
value: unknown,
): Readonly<ToolDefinition> {
const definition = definitionRecord(value, 'definition');
exactDefinitionKeys(
definition,
[
'description',
'effect',
'inputSchema',
'name',
'requiredPermissions',
'risk',
'timeoutSeconds',
'version',
],
['outputSchema'],
'definition',
);
const name = boundedDefinitionText(definition.name, 'name', 255);
if (!TOOL_NAME_PATTERN.test(name)) {
throw new InvalidToolDefinitionError('name is invalid');
}
const version = boundedDefinitionText(definition.version, 'version', 128);
if (semver().valid(version) !== version) {
throw new InvalidToolDefinitionError('version is invalid');
}
if (
typeof definition.effect !== 'string' ||
!TOOL_EFFECTS.includes(definition.effect as ToolEffect) ||
typeof definition.risk !== 'string' ||
!TOOL_RISKS.includes(definition.risk as ToolRisk)
) {
throw new InvalidToolDefinitionError('effect or risk is invalid');
}
const budget: SchemaBudget = { nodes: 0 };
const inputSchema = normalizeSchema(definition.inputSchema, 1, budget);
if (inputSchema.type !== 'object') {
throw new InvalidToolDefinitionError(
'input JSON Schema root must be an object',
);
}
const outputSchema =
definition.outputSchema === undefined
? undefined
: normalizeSchema(definition.outputSchema, 1, budget);
return Object.freeze({
name,
version,
description: boundedDefinitionText(
definition.description,
'description',
4096,
),
inputSchema,
...(outputSchema === undefined ? {} : { outputSchema }),
effect: definition.effect as ToolEffect,
risk: definition.risk as ToolRisk,
requiredPermissions: normalizePermissions(definition.requiredPermissions),
timeoutSeconds: boundedInteger(
definition.timeoutSeconds,
1,
MAX_TOOL_TIMEOUT_SECONDS,
'timeoutSeconds',
),
});
}
@@ -0,0 +1,165 @@
import { createHash } from 'node:crypto';
import {
assertProjectPolicyProjectId,
normalizeProjectPermission,
} from '../../security/project-policy/projectPolicy';
import {
normalizeSecurityPolicyDecision,
normalizeSecurityPrincipal,
type SecurityPolicyDecision,
type SecurityPolicyFence,
} from '../../security/security';
import {
InvalidToolJsonValueError,
TOOL_INVOCATION_SCHEMA,
ToolPolicySnapshotConflictError,
ToolPolicyUnavailableError,
type ToolInvocationPlan,
type ToolInvocationRequest,
type ToolPolicyAuthorizer,
} from './contracts';
import { ToolDefinitionRegistry } from './registryProtocol';
const SHA256_PATTERN = /^[0-9a-f]{64}$/;
function sameFence(
left: Readonly<SecurityPolicyFence>,
right: Readonly<SecurityPolicyFence>,
): boolean {
return (
left.projectVersion === right.projectVersion &&
left.bindingVersion === right.bindingVersion
);
}
function digest(value: unknown): string {
const result = createHash('sha256')
.update(JSON.stringify(value))
.digest('hex');
if (!SHA256_PATTERN.test(result)) {
throw new Error('unreachable SHA-256 result');
}
return result;
}
export async function prepareToolInvocation(
registry: ToolDefinitionRegistry,
request: ToolInvocationRequest,
authorizer: ToolPolicyAuthorizer,
): Promise<ToolInvocationPlan> {
if (!(registry instanceof ToolDefinitionRegistry)) {
throw new TypeError('Tool registry is invalid');
}
if (!request || typeof request !== 'object' || Array.isArray(request)) {
throw new InvalidToolJsonValueError('request must be an object');
}
const requestRecord = request as unknown as Record<string, unknown>;
const keys = Object.keys(requestRecord).sort();
const expected = ['input', 'nowMs', 'principal', 'projectId', 'tool'].sort();
if (
keys.length !== expected.length ||
keys.some((key, index) => key !== expected[index])
) {
throw new InvalidToolJsonValueError('request shape is invalid');
}
if (
!request.tool ||
typeof request.tool !== 'object' ||
Array.isArray(request.tool) ||
Object.keys(request.tool).sort().join(',') !== 'name,version'
) {
throw new InvalidToolJsonValueError('request Tool identity is invalid');
}
assertProjectPolicyProjectId(request.projectId);
const principal = normalizeSecurityPrincipal(
request.principal,
request.nowMs,
);
const definition = registry.resolve(request.tool.name, request.tool.version);
if (!authorizer || typeof authorizer.authorize !== 'function') {
throw new ToolPolicyUnavailableError();
}
const permission = normalizeProjectPermission(`tool.call:${definition.name}`);
const permissions = Object.freeze([
permission,
...definition.requiredPermissions,
]);
const decisions: Readonly<SecurityPolicyDecision>[] = [];
for (const requiredPermission of permissions) {
try {
const decision = normalizeSecurityPolicyDecision(
await authorizer.authorize(
principal,
request.projectId,
requiredPermission,
),
);
if (decision.effect === 'deny') {
return Object.freeze({
status: 'denied',
tool: Object.freeze({
name: definition.name,
version: definition.version,
}),
permission,
});
}
decisions.push(decision);
} catch {
throw new ToolPolicyUnavailableError();
}
}
const fences = decisions.map((decision) => decision.fence);
const fence = fences[0];
if (
!fence ||
fences.some(
(candidate) => candidate === null || !sameFence(fence, candidate),
)
) {
throw new ToolPolicySnapshotConflictError();
}
const input = registry.normalizeInput(
definition.name,
definition.version,
request.input,
);
const inputDigest = digest(input);
const actionDigest = digest({
schema: TOOL_INVOCATION_SCHEMA,
projectId: request.projectId,
requestedBy: principal.subject,
tool: {
name: definition.name,
version: definition.version,
},
permission,
requiredPermissions: definition.requiredPermissions,
effect: definition.effect,
risk: definition.risk,
timeoutSeconds: definition.timeoutSeconds,
inputDigest,
});
return Object.freeze({
status: decisions.some((decision) => decision.effect === 'require_approval')
? 'approval_required'
: 'ready',
schema: TOOL_INVOCATION_SCHEMA,
projectId: request.projectId,
requestedBy: principal.subject,
tool: Object.freeze({
name: definition.name,
version: definition.version,
}),
permission,
requiredPermissions: definition.requiredPermissions,
effect: definition.effect,
risk: definition.risk,
timeoutSeconds: definition.timeoutSeconds,
fence,
input,
inputDigest,
actionDigest,
});
}
@@ -0,0 +1,234 @@
import { semver } from '../../versioning/pinnedSemver';
import {
InvalidToolDefinitionError,
InvalidToolJsonValueError,
MAX_TOOL_DEFINITIONS,
MAX_TOOL_INPUT_BYTES,
MAX_TOOL_OUTPUT_BYTES,
UnsupportedToolError,
type ToolDefinition,
type ToolJsonSchema,
type ToolJsonValue,
} from './contracts';
import { normalizeToolDefinition } from './definitionProtocol';
function valueRecord(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidToolJsonValueError(`${label} must be an object`);
}
const prototype = Object.getPrototypeOf(value);
if (prototype !== Object.prototype && prototype !== null) {
throw new InvalidToolJsonValueError(`${label} must be a plain JSON object`);
}
const descriptors = Object.getOwnPropertyDescriptors(value);
if (
Object.values(descriptors).some(
(descriptor) =>
!Object.hasOwn(descriptor, 'value') ||
descriptor.get !== undefined ||
descriptor.set !== undefined,
)
) {
throw new InvalidToolJsonValueError(
`${label} must contain only JSON data properties`,
);
}
return value as Record<string, unknown>;
}
function normalizeJson(
value: unknown,
schema: ToolJsonSchema,
path: string,
): ToolJsonValue {
if (schema.type === 'null') {
if (value !== null) {
throw new InvalidToolJsonValueError(`${path} must be null`);
}
return null;
}
if (schema.type === 'boolean') {
if (typeof value !== 'boolean') {
throw new InvalidToolJsonValueError(`${path} must be boolean`);
}
return value;
}
if (schema.type === 'string') {
if (typeof value !== 'string') {
throw new InvalidToolJsonValueError(`${path} must be a string`);
}
const length = Array.from(value).length;
if (
length > schema.maxLength ||
(schema.minLength !== undefined && length < schema.minLength) ||
(schema.enum !== undefined && !schema.enum.includes(value))
) {
throw new InvalidToolJsonValueError(`${path} violates its string bounds`);
}
return value;
}
if (schema.type === 'number' || schema.type === 'integer') {
if (
typeof value !== 'number' ||
!Number.isFinite(value) ||
Math.abs(value) > Number.MAX_SAFE_INTEGER ||
value < schema.minimum ||
value > schema.maximum ||
(schema.type === 'integer' && !Number.isSafeInteger(value))
) {
throw new InvalidToolJsonValueError(
`${path} violates its numeric bounds`,
);
}
return value;
}
if (schema.type === 'array') {
if (
!Array.isArray(value) ||
value.length > schema.maxItems ||
(schema.minItems !== undefined && value.length < schema.minItems)
) {
throw new InvalidToolJsonValueError(`${path} violates its array bounds`);
}
const enumerableKeys = Object.keys(value);
if (
enumerableKeys.length !== value.length ||
enumerableKeys.some(
(key, index) =>
key !== String(index) ||
!Object.hasOwn(value, index) ||
!Object.hasOwn(
Object.getOwnPropertyDescriptor(value, key) ?? {},
'value',
),
)
) {
throw new InvalidToolJsonValueError(`${path} must be a dense JSON array`);
}
const normalized = value.map((item, index) =>
normalizeJson(item, schema.items, `${path}[${index}]`),
);
if (schema.uniqueItems) {
const identities = normalized.map((item) => JSON.stringify(item));
if (new Set(identities).size !== identities.length) {
throw new InvalidToolJsonValueError(`${path} contains duplicate items`);
}
}
return Object.freeze(normalized);
}
const source = valueRecord(value, path);
const keys = Object.keys(source);
if (
keys.some((key) => !Object.hasOwn(schema.properties, key)) ||
schema.required.some((key) => !Object.hasOwn(source, key))
) {
throw new InvalidToolJsonValueError(
`${path} has missing or unknown properties`,
);
}
const normalized: Record<string, ToolJsonValue> = {};
for (const key of keys.sort()) {
normalized[key] = normalizeJson(
source[key],
schema.properties[key]!,
`${path}.${key}`,
);
}
return Object.freeze(normalized);
}
function normalizeBoundedJson(
value: unknown,
schema: ToolJsonSchema,
maximumBytes: number,
label: string,
): ToolJsonValue {
const normalized = normalizeJson(value, schema, label);
if (Buffer.byteLength(JSON.stringify(normalized), 'utf8') > maximumBytes) {
throw new InvalidToolJsonValueError(`${label} byte budget exceeded`);
}
return normalized;
}
function toolIdentity(name: string, version: string): string {
return `${name}@${version}`;
}
export class ToolDefinitionRegistry {
readonly #definitions: ReadonlyMap<string, Readonly<ToolDefinition>>;
readonly #metadata: readonly Readonly<ToolDefinition>[];
constructor(definitions: readonly unknown[]) {
if (
!Array.isArray(definitions) ||
definitions.length > MAX_TOOL_DEFINITIONS
) {
throw new InvalidToolDefinitionError(
'registry definition count is invalid',
);
}
const byIdentity = new Map<string, Readonly<ToolDefinition>>();
for (const value of definitions) {
const definition = normalizeToolDefinition(value);
const identity = toolIdentity(definition.name, definition.version);
if (byIdentity.has(identity)) {
throw new InvalidToolDefinitionError(
'registry definition is duplicated',
);
}
byIdentity.set(identity, definition);
}
this.#definitions = byIdentity;
this.#metadata = Object.freeze(
[...byIdentity.values()].sort(
(left, right) =>
left.name.localeCompare(right.name) ||
semver().compare(left.version, right.version),
),
);
Object.freeze(this);
}
list(): readonly Readonly<ToolDefinition>[] {
return this.#metadata;
}
resolve(name: string, version: string): Readonly<ToolDefinition> {
const definition = this.#definitions.get(toolIdentity(name, version));
if (!definition) throw new UnsupportedToolError();
return definition;
}
normalizeInput(name: string, version: string, input: unknown): ToolJsonValue {
const definition = this.resolve(name, version);
return normalizeBoundedJson(
input,
definition.inputSchema,
MAX_TOOL_INPUT_BYTES,
'input',
);
}
normalizeOutput(
name: string,
version: string,
output: unknown,
): ToolJsonValue {
const definition = this.resolve(name, version);
if (definition.outputSchema === undefined) {
if (output !== null) {
throw new InvalidToolJsonValueError(
'output must be null when outputSchema is absent',
);
}
return null;
}
return normalizeBoundedJson(
output,
definition.outputSchema,
MAX_TOOL_OUTPUT_BYTES,
'output',
);
}
}
@@ -0,0 +1,37 @@
// Stable Tool Registry facade.
export {
InvalidToolDefinitionError,
InvalidToolJsonValueError,
MAX_TOOL_ARRAY_ITEMS,
MAX_TOOL_DEFINITIONS,
MAX_TOOL_INPUT_BYTES,
MAX_TOOL_OUTPUT_BYTES,
MAX_TOOL_REQUIRED_PERMISSIONS,
MAX_TOOL_SCHEMA_DEPTH,
MAX_TOOL_SCHEMA_ENUM_VALUES,
MAX_TOOL_SCHEMA_NODES,
MAX_TOOL_SCHEMA_PROPERTIES,
MAX_TOOL_TIMEOUT_SECONDS,
TOOL_EFFECTS,
TOOL_INVOCATION_SCHEMA,
TOOL_JSON_SCHEMA_TYPES,
TOOL_RISKS,
ToolPolicySnapshotConflictError,
ToolPolicyUnavailableError,
UnsupportedToolError,
type DeniedToolInvocation,
type PreparedToolInvocation,
type ToolDefinition,
type ToolEffect,
type ToolInvocationPlan,
type ToolInvocationRequest,
type ToolInvocationStatus,
type ToolJsonSchema,
type ToolJsonSchemaType,
type ToolJsonValue,
type ToolPolicyAuthorizer,
type ToolRisk,
} from './contracts';
export { normalizeToolDefinition } from './definitionProtocol';
export { prepareToolInvocation } from './invocationAdmission';
export { ToolDefinitionRegistry } from './registryProtocol';
@@ -0,0 +1,986 @@
import {
createCipheriv,
createDecipheriv,
createHash,
randomBytes,
} from 'node:crypto';
import { normalizeStepRunMutation, type StepRunMutation } from '../run/stepRun';
import {
normalizeToolExecutionStartBarrierRecord,
type ToolExecutionStartBarrierRecord,
} from './toolExecutionStartBarrier';
import {
normalizeToolResultKeyCatalogFence,
type ToolResultKeyCatalogFence,
} from './toolResultKeyCatalog';
import {
MAX_TOOL_OUTPUT_BYTES,
ToolDefinitionRegistry,
type ToolJsonValue,
} from './tool-registry/toolRegistry';
import {
TRUSTED_TOOL_EXECUTION_RESULT_SCHEMA,
type TrustedToolExecutionResult,
} from './trustedToolExecution';
export const TOOL_EXECUTION_RESULT_ARTIFACT_SCHEMA =
'qinglong/tool-execution-result-artifact@v1' as const;
export const TOOL_EXECUTION_COMPLETION_SCHEMA =
'qinglong/tool-execution-completion@v1' as const;
export const TOOL_EXECUTION_COMPLETION_COMMAND_SCHEMA =
'qinglong/tool-execution-completion-command@v2' as const;
export const TOOL_EXECUTION_RESULT_KEY_BINDING_SCHEMA =
'qinglong/tool-execution-result-key-binding@v1' as const;
export const TOOL_EXECUTION_RESULT_ARTIFACT_ALGORITHM = 'aes-256-gcm' as const;
export const MAX_TOOL_EXECUTION_RESULT_ARTIFACT_JSON_BYTES = 384 * 1024;
export const MAX_TOOL_EXECUTION_COMPLETION_JSON_BYTES = 24 * 1024;
export interface ToolExecutionResultArtifact {
readonly schema: typeof TOOL_EXECUTION_RESULT_ARTIFACT_SCHEMA;
readonly artifactId: string;
readonly projectId: string;
readonly startId: string;
readonly runId: string;
readonly stepRunId: string;
readonly tool: Readonly<{ name: string; version: string }>;
readonly barrierDigest: string;
readonly adapterDigest: string;
readonly outputDigest: string;
readonly executionResultDigest: string;
readonly keyId: string;
readonly algorithm: typeof TOOL_EXECUTION_RESULT_ARTIFACT_ALGORITHM;
readonly nonce: string;
readonly ciphertext: string;
readonly authTag: string;
readonly plaintextBytes: number;
readonly sealedAtMs: number;
readonly artifactDigest: string;
}
export interface ToolExecutionResultArtifactReference {
readonly artifactId: string;
readonly artifactDigest: string;
readonly outputDigest: string;
readonly executionResultDigest: string;
}
export interface ToolExecutionCompletionRecord {
readonly schema: typeof TOOL_EXECUTION_COMPLETION_SCHEMA;
readonly startId: string;
readonly projectId: string;
readonly runId: string;
readonly stepRunId: string;
readonly startedStepRunVersion: number;
readonly completedStepRunVersion: number;
readonly barrierDigest: string;
readonly adapterDigest: string;
readonly resultArtifact: Readonly<ToolExecutionResultArtifactReference>;
readonly stepRunMutationId: string;
readonly stepRunMutationDigest: string;
readonly completedStepRunDigest: string;
readonly runEventId: string;
readonly completedAtMs: number;
readonly completionDigest: string;
}
export interface ToolExecutionCompletionCommand {
readonly schema: typeof TOOL_EXECUTION_COMPLETION_COMMAND_SCHEMA;
readonly barrier: Readonly<ToolExecutionStartBarrierRecord>;
readonly executionResult: Readonly<TrustedToolExecutionResult>;
readonly resultArtifact: Readonly<ToolExecutionResultArtifact>;
readonly resultKeyCatalogFence: Readonly<ToolResultKeyCatalogFence>;
readonly stepRunMutation: Readonly<StepRunMutation>;
readonly commandDigest: string;
}
export interface ToolExecutionResultKeyBinding {
readonly schema: typeof TOOL_EXECUTION_RESULT_KEY_BINDING_SCHEMA;
readonly startId: string;
readonly artifactId: string;
readonly artifactDigest: string;
readonly catalogGeneration: number;
readonly catalogDigest: string;
readonly keyId: string;
readonly materialProof: string;
readonly bindingDigest: string;
}
export interface CommitToolExecutionCompletionResult {
readonly status: 'created' | 'existing';
readonly completion: Readonly<ToolExecutionCompletionRecord>;
}
export interface ToolExecutionCompletionRepository {
findByStartId(
startId: string,
): Promise<Readonly<ToolExecutionCompletionRecord> | null>;
findResultArtifact(
artifactId: string,
): Promise<Readonly<ToolExecutionResultArtifact> | null>;
commit(
command: ToolExecutionCompletionCommand,
): Promise<Readonly<CommitToolExecutionCompletionResult>>;
}
export class InvalidToolExecutionCompletionError extends TypeError {
readonly code = 'TOOL_EXECUTION_COMPLETION_INVALID';
constructor(message: string) {
super(`Tool execution completion is invalid: ${message}`);
this.name = 'InvalidToolExecutionCompletionError';
}
}
export class ToolExecutionCompletionConflictError extends Error {
readonly code = 'TOOL_EXECUTION_COMPLETION_CONFLICT';
constructor() {
super('Tool execution completion conflicts with durable state');
this.name = 'ToolExecutionCompletionConflictError';
}
}
export class ToolExecutionCompletionUnavailableError extends Error {
readonly code = 'TOOL_EXECUTION_COMPLETION_UNAVAILABLE';
constructor(options?: ErrorOptions) {
super('Tool execution completion authority is unavailable', options);
this.name = 'ToolExecutionCompletionUnavailableError';
}
}
const IDENTITY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const TOOL_NAME_PATTERN = /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/;
const TOOL_VERSION_PATTERN =
/^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)$/;
const KEY_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
const BASE64URL_PATTERN = /^[A-Za-z0-9_-]*$/;
const OUTPUT_DIGEST_DOMAIN = Buffer.from(
'qinglong/trusted-tool-execution-output-digest@v1\0',
'utf8',
);
const EXECUTION_RESULT_DIGEST_DOMAIN = Buffer.from(
'qinglong/trusted-tool-execution-result-digest@v1\0',
'utf8',
);
const RESULT_ARTIFACT_DIGEST_DOMAIN = Buffer.from(
'qinglong/tool-execution-result-artifact-digest@v1\0',
'utf8',
);
const COMPLETION_DIGEST_DOMAIN = Buffer.from(
'qinglong/tool-execution-completion-digest@v1\0',
'utf8',
);
const COMPLETION_COMMAND_DIGEST_DOMAIN = Buffer.from(
'qinglong/tool-execution-completion-command-digest@v1\0',
'utf8',
);
const RESULT_KEY_BINDING_DIGEST_DOMAIN = Buffer.from(
'qinglong/tool-execution-result-key-binding-digest@v1\0',
'utf8',
);
function invalid(message: string): never {
throw new InvalidToolExecutionCompletionError(message);
}
function hash(domain: Uint8Array, value: unknown): string {
return createHash('sha256')
.update(domain)
.update(JSON.stringify(value))
.digest('hex');
}
function exactKeys(
value: Record<string, unknown>,
keys: readonly string[],
label: string,
): void {
const actual = Object.keys(value).sort();
const expected = [...keys].sort();
if (
actual.length !== expected.length ||
actual.some((key, index) => key !== expected[index])
) {
invalid(`${label} shape is invalid`);
}
}
function record(value: unknown, label: string): Record<string, unknown> {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
Object.getPrototypeOf(value) !== Object.prototype
) {
return invalid(`${label} is not a plain object`);
}
return value as Record<string, unknown>;
}
function identity(value: unknown, label: string): string {
if (typeof value !== 'string' || !IDENTITY_PATTERN.test(value)) {
return invalid(`${label} is invalid`);
}
return value;
}
function digest(value: unknown, label: string): string {
if (typeof value !== 'string' || !DIGEST_PATTERN.test(value)) {
return invalid(`${label} is invalid`);
}
return value;
}
function timestamp(value: unknown, label: string): number {
if (!Number.isSafeInteger(value) || (value as number) < 0) {
return invalid(`${label} is invalid`);
}
return value as number;
}
function version(value: unknown, label: string): number {
if (
!Number.isSafeInteger(value) ||
(value as number) < 2 ||
(value as number) > 2_147_483_647
) {
return invalid(`${label} is invalid`);
}
return value as number;
}
function toolIdentity(
value: Readonly<{ name: string; version: string }>,
): Readonly<{ name: string; version: string }> {
const candidate = record(value, 'Tool identity');
exactKeys(candidate, ['name', 'version'], 'Tool identity');
if (
typeof value.name !== 'string' ||
!TOOL_NAME_PATTERN.test(value.name) ||
typeof value.version !== 'string' ||
!TOOL_VERSION_PATTERN.test(value.version)
) {
return invalid('Tool identity is invalid');
}
return Object.freeze({ name: value.name, version: value.version });
}
function jsonValue(value: unknown, depth = 0): ToolJsonValue {
if (depth > 16) return invalid('output nesting is too deep');
if (
value === null ||
typeof value === 'boolean' ||
typeof value === 'string'
) {
return value;
}
if (typeof value === 'number') {
if (!Number.isFinite(value)) return invalid('output number is invalid');
return value;
}
if (Array.isArray(value)) {
return Object.freeze(value.map((item) => jsonValue(item, depth + 1)));
}
const source = record(value, 'output value');
const normalized: Record<string, ToolJsonValue> = {};
for (const key of Object.keys(source)) {
normalized[key] = jsonValue(source[key], depth + 1);
}
return Object.freeze(normalized);
}
function base64url(value: unknown, label: string, exactBytes?: number): Buffer {
if (
typeof value !== 'string' ||
!BASE64URL_PATTERN.test(value) ||
value.length === 0
) {
return invalid(`${label} is invalid`);
}
const decoded = Buffer.from(value, 'base64url');
if (
decoded.toString('base64url') !== value ||
(exactBytes !== undefined && decoded.length !== exactBytes)
) {
decoded.fill(0);
return invalid(`${label} is invalid`);
}
return decoded;
}
function ownedKey(value: Uint8Array): Buffer {
if (!(value instanceof Uint8Array) || value.byteLength !== 32) {
return invalid('result Artifact key is invalid');
}
return Buffer.from(value);
}
function normalizeExecutionResult(
value: TrustedToolExecutionResult,
): Readonly<TrustedToolExecutionResult> {
const candidate = record(value, 'execution result');
exactKeys(
candidate,
[
'adapterDigest',
'barrierDigest',
'completedAtMs',
'output',
'outputDigest',
'resultDigest',
'schema',
'startId',
],
'execution result',
);
if (value.schema !== TRUSTED_TOOL_EXECUTION_RESULT_SCHEMA) {
return invalid('execution result schema is invalid');
}
const output = jsonValue(value.output);
const outputJson = JSON.stringify(output);
if (Buffer.byteLength(outputJson, 'utf8') > MAX_TOOL_OUTPUT_BYTES) {
return invalid('execution result output exceeds its budget');
}
const outputDigest = digest(value.outputDigest, 'output digest');
if (hash(OUTPUT_DIGEST_DOMAIN, output) !== outputDigest) {
return invalid('execution result output digest does not match');
}
const unsigned = Object.freeze({
schema: TRUSTED_TOOL_EXECUTION_RESULT_SCHEMA,
startId: identity(value.startId, 'execution result start id'),
barrierDigest: digest(value.barrierDigest, 'barrier digest'),
adapterDigest: digest(value.adapterDigest, 'adapter digest'),
output,
outputDigest,
completedAtMs: timestamp(value.completedAtMs, 'completion time'),
});
const resultDigest = digest(value.resultDigest, 'execution result digest');
if (hash(EXECUTION_RESULT_DIGEST_DOMAIN, unsigned) !== resultDigest) {
return invalid('execution result digest does not match');
}
return Object.freeze({ ...unsigned, resultDigest });
}
function artifactMetadata(
artifact: Omit<
ToolExecutionResultArtifact,
'artifactDigest' | 'authTag' | 'ciphertext' | 'nonce'
>,
): Readonly<
Omit<
ToolExecutionResultArtifact,
'artifactDigest' | 'authTag' | 'ciphertext' | 'nonce'
>
> {
return Object.freeze(artifact);
}
function artifactAad(metadata: ReturnType<typeof artifactMetadata>): Buffer {
return Buffer.from(JSON.stringify(metadata), 'utf8');
}
export function normalizeToolExecutionResultArtifact(
value: ToolExecutionResultArtifact,
): Readonly<ToolExecutionResultArtifact> {
const candidate = record(value, 'result Artifact');
exactKeys(
candidate,
[
'adapterDigest',
'algorithm',
'artifactDigest',
'artifactId',
'authTag',
'barrierDigest',
'ciphertext',
'executionResultDigest',
'keyId',
'nonce',
'outputDigest',
'plaintextBytes',
'projectId',
'runId',
'schema',
'sealedAtMs',
'startId',
'stepRunId',
'tool',
],
'result Artifact',
);
if (
value.schema !== TOOL_EXECUTION_RESULT_ARTIFACT_SCHEMA ||
value.algorithm !== TOOL_EXECUTION_RESULT_ARTIFACT_ALGORITHM ||
typeof value.keyId !== 'string' ||
!KEY_ID_PATTERN.test(value.keyId)
) {
return invalid('result Artifact schema, algorithm or key id is invalid');
}
const nonce = base64url(value.nonce, 'result Artifact nonce', 12);
const ciphertext = base64url(value.ciphertext, 'result Artifact ciphertext');
const authTag = base64url(value.authTag, 'result Artifact auth tag', 16);
nonce.fill(0);
ciphertext.fill(0);
authTag.fill(0);
const unsigned = Object.freeze({
schema: TOOL_EXECUTION_RESULT_ARTIFACT_SCHEMA,
artifactId: identity(value.artifactId, 'result Artifact id'),
projectId: identity(value.projectId, 'result Artifact project id'),
startId: identity(value.startId, 'result Artifact start id'),
runId: identity(value.runId, 'result Artifact Run id'),
stepRunId: identity(value.stepRunId, 'result Artifact StepRun id'),
tool: toolIdentity(value.tool),
barrierDigest: digest(
value.barrierDigest,
'result Artifact barrier digest',
),
adapterDigest: digest(
value.adapterDigest,
'result Artifact adapter digest',
),
outputDigest: digest(value.outputDigest, 'result Artifact output digest'),
executionResultDigest: digest(
value.executionResultDigest,
'execution result digest',
),
keyId: value.keyId,
algorithm: TOOL_EXECUTION_RESULT_ARTIFACT_ALGORITHM,
nonce: value.nonce,
ciphertext: value.ciphertext,
authTag: value.authTag,
plaintextBytes: timestamp(value.plaintextBytes, 'plaintext byte count'),
sealedAtMs: timestamp(value.sealedAtMs, 'result Artifact seal time'),
});
if (
unsigned.plaintextBytes > MAX_TOOL_OUTPUT_BYTES ||
Buffer.byteLength(JSON.stringify(unsigned), 'utf8') >
MAX_TOOL_EXECUTION_RESULT_ARTIFACT_JSON_BYTES
) {
return invalid('result Artifact exceeds its budget');
}
const artifactDigest = digest(value.artifactDigest, 'result Artifact digest');
if (hash(RESULT_ARTIFACT_DIGEST_DOMAIN, unsigned) !== artifactDigest) {
return invalid('result Artifact digest does not match');
}
return Object.freeze({ ...unsigned, artifactDigest });
}
export function toolExecutionResultArtifactReference(
value: ToolExecutionResultArtifact,
): Readonly<ToolExecutionResultArtifactReference> {
const artifact = normalizeToolExecutionResultArtifact(value);
return Object.freeze({
artifactId: artifact.artifactId,
artifactDigest: artifact.artifactDigest,
outputDigest: artifact.outputDigest,
executionResultDigest: artifact.executionResultDigest,
});
}
export function createToolExecutionResultArtifact(
input: Readonly<{
artifactId: string;
projectId: string;
runId: string;
stepRunId: string;
tool: Readonly<{ name: string; version: string }>;
executionResult: Readonly<TrustedToolExecutionResult>;
keyId: string;
key: Uint8Array;
}>,
registry: ToolDefinitionRegistry,
nonceFactory: () => Uint8Array = () => randomBytes(12),
): Readonly<ToolExecutionResultArtifact> {
if (!(registry instanceof ToolDefinitionRegistry)) {
return invalid('Tool registry is invalid');
}
const result = normalizeExecutionResult(input.executionResult);
const tool = toolIdentity(input.tool);
const output = registry.normalizeOutput(
tool.name,
tool.version,
result.output,
);
if (JSON.stringify(output) !== JSON.stringify(result.output)) {
return invalid('execution output is not canonical for the Tool');
}
const plaintext = Buffer.from(JSON.stringify(output), 'utf8');
const key = ownedKey(input.key);
let nonce: Buffer | undefined;
try {
nonce = Buffer.from(nonceFactory());
if (nonce.length !== 12) {
throw new ToolExecutionCompletionUnavailableError();
}
const metadata = artifactMetadata({
schema: TOOL_EXECUTION_RESULT_ARTIFACT_SCHEMA,
artifactId: identity(input.artifactId, 'result Artifact id'),
projectId: identity(input.projectId, 'result Artifact project id'),
startId: result.startId,
runId: identity(input.runId, 'result Artifact Run id'),
stepRunId: identity(input.stepRunId, 'result Artifact StepRun id'),
tool,
barrierDigest: result.barrierDigest,
adapterDigest: result.adapterDigest,
outputDigest: result.outputDigest,
executionResultDigest: result.resultDigest,
keyId:
typeof input.keyId === 'string' && KEY_ID_PATTERN.test(input.keyId)
? input.keyId
: invalid('result Artifact key id is invalid'),
algorithm: TOOL_EXECUTION_RESULT_ARTIFACT_ALGORITHM,
plaintextBytes: plaintext.length,
sealedAtMs: result.completedAtMs,
});
const cipher = createCipheriv(
TOOL_EXECUTION_RESULT_ARTIFACT_ALGORITHM,
key,
nonce,
{ authTagLength: 16 },
);
const aad = artifactAad(metadata);
try {
cipher.setAAD(aad);
} finally {
aad.fill(0);
}
const ciphertext = Buffer.concat([
cipher.update(plaintext),
cipher.final(),
]);
try {
const unsigned = Object.freeze({
schema: metadata.schema,
artifactId: metadata.artifactId,
projectId: metadata.projectId,
startId: metadata.startId,
runId: metadata.runId,
stepRunId: metadata.stepRunId,
tool: metadata.tool,
barrierDigest: metadata.barrierDigest,
adapterDigest: metadata.adapterDigest,
outputDigest: metadata.outputDigest,
executionResultDigest: metadata.executionResultDigest,
keyId: metadata.keyId,
algorithm: metadata.algorithm,
nonce: nonce.toString('base64url'),
ciphertext: ciphertext.toString('base64url'),
authTag: cipher.getAuthTag().toString('base64url'),
plaintextBytes: metadata.plaintextBytes,
sealedAtMs: metadata.sealedAtMs,
});
return normalizeToolExecutionResultArtifact({
...unsigned,
artifactDigest: hash(RESULT_ARTIFACT_DIGEST_DOMAIN, unsigned),
});
} finally {
ciphertext.fill(0);
}
} catch (cause) {
if (
cause instanceof InvalidToolExecutionCompletionError ||
cause instanceof ToolExecutionCompletionUnavailableError
) {
throw cause;
}
throw new ToolExecutionCompletionUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
} finally {
key.fill(0);
plaintext.fill(0);
nonce?.fill(0);
}
}
export function openToolExecutionResultArtifact(
artifactValue: ToolExecutionResultArtifact,
keyValue: Uint8Array,
registry: ToolDefinitionRegistry,
): ToolJsonValue {
const artifact = normalizeToolExecutionResultArtifact(artifactValue);
if (!(registry instanceof ToolDefinitionRegistry)) {
return invalid('Tool registry is invalid');
}
const key = ownedKey(keyValue);
const nonce = base64url(artifact.nonce, 'result Artifact nonce', 12);
const ciphertext = base64url(
artifact.ciphertext,
'result Artifact ciphertext',
);
const authTag = base64url(artifact.authTag, 'result Artifact auth tag', 16);
const aad = artifactAad(
artifactMetadata({
schema: artifact.schema,
artifactId: artifact.artifactId,
projectId: artifact.projectId,
startId: artifact.startId,
runId: artifact.runId,
stepRunId: artifact.stepRunId,
tool: artifact.tool,
barrierDigest: artifact.barrierDigest,
adapterDigest: artifact.adapterDigest,
outputDigest: artifact.outputDigest,
executionResultDigest: artifact.executionResultDigest,
keyId: artifact.keyId,
algorithm: artifact.algorithm,
plaintextBytes: artifact.plaintextBytes,
sealedAtMs: artifact.sealedAtMs,
}),
);
let plaintext: Buffer | undefined;
try {
const decipher = createDecipheriv(
TOOL_EXECUTION_RESULT_ARTIFACT_ALGORITHM,
key,
nonce,
{ authTagLength: 16 },
);
decipher.setAAD(aad);
decipher.setAuthTag(authTag);
plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
if (plaintext.length !== artifact.plaintextBytes) {
throw new ToolExecutionCompletionUnavailableError();
}
const parsed = JSON.parse(plaintext.toString('utf8')) as unknown;
const output = registry.normalizeOutput(
artifact.tool.name,
artifact.tool.version,
parsed,
);
if (
JSON.stringify(output) !== plaintext.toString('utf8') ||
hash(OUTPUT_DIGEST_DOMAIN, output) !== artifact.outputDigest
) {
throw new ToolExecutionCompletionUnavailableError();
}
return output;
} catch (cause) {
if (cause instanceof InvalidToolExecutionCompletionError) throw cause;
throw new ToolExecutionCompletionUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
} finally {
key.fill(0);
nonce.fill(0);
ciphertext.fill(0);
authTag.fill(0);
aad.fill(0);
plaintext?.fill(0);
}
}
export function normalizeToolExecutionCompletionRecord(
value: ToolExecutionCompletionRecord,
): Readonly<ToolExecutionCompletionRecord> {
const candidate = record(value, 'completion');
exactKeys(
candidate,
[
'adapterDigest',
'barrierDigest',
'completedAtMs',
'completedStepRunDigest',
'completedStepRunVersion',
'completionDigest',
'projectId',
'resultArtifact',
'runEventId',
'runId',
'schema',
'startId',
'startedStepRunVersion',
'stepRunId',
'stepRunMutationDigest',
'stepRunMutationId',
],
'completion',
);
if (value.schema !== TOOL_EXECUTION_COMPLETION_SCHEMA) {
return invalid('completion schema is invalid');
}
const referenceValue = record(
value.resultArtifact,
'result Artifact reference',
);
exactKeys(
referenceValue,
['artifactDigest', 'artifactId', 'executionResultDigest', 'outputDigest'],
'result Artifact reference',
);
const startedStepRunVersion = version(
value.startedStepRunVersion,
'started StepRun version',
);
const completedStepRunVersion = version(
value.completedStepRunVersion,
'completed StepRun version',
);
if (completedStepRunVersion !== startedStepRunVersion + 1) {
return invalid('completion StepRun version fence is invalid');
}
const unsigned = Object.freeze({
schema: TOOL_EXECUTION_COMPLETION_SCHEMA,
startId: identity(value.startId, 'completion start id'),
projectId: identity(value.projectId, 'completion project id'),
runId: identity(value.runId, 'completion Run id'),
stepRunId: identity(value.stepRunId, 'completion StepRun id'),
startedStepRunVersion,
completedStepRunVersion,
barrierDigest: digest(value.barrierDigest, 'completion barrier digest'),
adapterDigest: digest(value.adapterDigest, 'completion adapter digest'),
resultArtifact: Object.freeze({
artifactId: identity(
value.resultArtifact.artifactId,
'result Artifact id',
),
artifactDigest: digest(
value.resultArtifact.artifactDigest,
'result Artifact digest',
),
outputDigest: digest(
value.resultArtifact.outputDigest,
'result Artifact output digest',
),
executionResultDigest: digest(
value.resultArtifact.executionResultDigest,
'execution result digest',
),
}),
stepRunMutationId: identity(
value.stepRunMutationId,
'completion mutation id',
),
stepRunMutationDigest: digest(
value.stepRunMutationDigest,
'completion mutation digest',
),
completedStepRunDigest: digest(
value.completedStepRunDigest,
'completed StepRun digest',
),
runEventId: identity(value.runEventId, 'completion Run event id'),
completedAtMs: timestamp(value.completedAtMs, 'completion time'),
});
if (
Buffer.byteLength(JSON.stringify(unsigned), 'utf8') >
MAX_TOOL_EXECUTION_COMPLETION_JSON_BYTES
) {
return invalid('completion exceeds its budget');
}
const completionDigest = digest(value.completionDigest, 'completion digest');
if (hash(COMPLETION_DIGEST_DOMAIN, unsigned) !== completionDigest) {
return invalid('completion digest does not match');
}
return Object.freeze({ ...unsigned, completionDigest });
}
function completionFromParts(
barrier: Readonly<ToolExecutionStartBarrierRecord>,
result: Readonly<TrustedToolExecutionResult>,
artifact: Readonly<ToolExecutionResultArtifact>,
mutation: Readonly<StepRunMutation>,
): Readonly<ToolExecutionCompletionRecord> {
const reference = toolExecutionResultArtifactReference(artifact);
const unsigned = Object.freeze({
schema: TOOL_EXECUTION_COMPLETION_SCHEMA,
startId: barrier.startId,
projectId: barrier.projectId,
runId: barrier.runId,
stepRunId: barrier.stepRunId,
startedStepRunVersion: barrier.startedStepRunVersion,
completedStepRunVersion: mutation.stepRun.version,
barrierDigest: barrier.barrierDigest,
adapterDigest: barrier.adapterDigest,
resultArtifact: reference,
stepRunMutationId: mutation.mutationId,
stepRunMutationDigest: mutation.mutationDigest,
completedStepRunDigest: mutation.stepRun.stepRunDigest,
runEventId: mutation.event.id,
completedAtMs: result.completedAtMs,
});
return normalizeToolExecutionCompletionRecord({
...unsigned,
completionDigest: hash(COMPLETION_DIGEST_DOMAIN, unsigned),
});
}
export function normalizeToolExecutionCompletionCommand(
value: ToolExecutionCompletionCommand,
): Readonly<ToolExecutionCompletionCommand> {
const candidate = record(value, 'completion command');
exactKeys(
candidate,
[
'barrier',
'commandDigest',
'executionResult',
'resultArtifact',
'resultKeyCatalogFence',
'schema',
'stepRunMutation',
],
'completion command',
);
if (value.schema !== TOOL_EXECUTION_COMPLETION_COMMAND_SCHEMA) {
return invalid('completion command schema is invalid');
}
const barrier = normalizeToolExecutionStartBarrierRecord(value.barrier);
const executionResult = normalizeExecutionResult(value.executionResult);
const resultArtifact = normalizeToolExecutionResultArtifact(
value.resultArtifact,
);
const resultKeyCatalogFence = normalizeToolResultKeyCatalogFence(
value.resultKeyCatalogFence,
);
const stepRunMutation = normalizeStepRunMutation(value.stepRunMutation);
if (
executionResult.startId !== barrier.startId ||
executionResult.barrierDigest !== barrier.barrierDigest ||
executionResult.adapterDigest !== barrier.adapterDigest ||
executionResult.completedAtMs < barrier.startedAtMs ||
resultArtifact.projectId !== barrier.projectId ||
resultArtifact.startId !== barrier.startId ||
resultArtifact.runId !== barrier.runId ||
resultArtifact.stepRunId !== barrier.stepRunId ||
resultArtifact.barrierDigest !== barrier.barrierDigest ||
resultArtifact.adapterDigest !== barrier.adapterDigest ||
resultArtifact.outputDigest !== executionResult.outputDigest ||
resultArtifact.executionResultDigest !== executionResult.resultDigest ||
resultArtifact.keyId !== resultKeyCatalogFence.keyId ||
resultArtifact.sealedAtMs !== executionResult.completedAtMs ||
stepRunMutation.runId !== barrier.runId ||
stepRunMutation.stepRun.id !== barrier.stepRunId ||
stepRunMutation.stepRun.kind !== 'tool' ||
stepRunMutation.previousStatus !== 'running' ||
stepRunMutation.expectedStepRunVersion !== barrier.startedStepRunVersion ||
stepRunMutation.expectedStepRunDigest !== barrier.startedStepRunDigest ||
stepRunMutation.stepRun.status !== 'succeeded' ||
stepRunMutation.stepRun.outputRef !== resultArtifact.artifactId ||
stepRunMutation.stepRun.finishedAtMs !== executionResult.completedAtMs ||
stepRunMutation.stepRun.updatedAtMs !== executionResult.completedAtMs
) {
throw new ToolExecutionCompletionConflictError();
}
const unsigned = Object.freeze({
schema: TOOL_EXECUTION_COMPLETION_COMMAND_SCHEMA,
barrier,
executionResult,
resultArtifact,
resultKeyCatalogFence,
stepRunMutation,
});
const commandDigest = digest(
value.commandDigest,
'completion command digest',
);
if (hash(COMPLETION_COMMAND_DIGEST_DOMAIN, unsigned) !== commandDigest) {
return invalid('completion command digest does not match');
}
return Object.freeze({ ...unsigned, commandDigest });
}
export function createToolExecutionCompletionCommand(
value: Omit<ToolExecutionCompletionCommand, 'commandDigest' | 'schema'>,
): Readonly<ToolExecutionCompletionCommand> {
const unsigned = Object.freeze({
schema: TOOL_EXECUTION_COMPLETION_COMMAND_SCHEMA,
barrier: normalizeToolExecutionStartBarrierRecord(value.barrier),
executionResult: normalizeExecutionResult(value.executionResult),
resultArtifact: normalizeToolExecutionResultArtifact(value.resultArtifact),
resultKeyCatalogFence: normalizeToolResultKeyCatalogFence(
value.resultKeyCatalogFence,
),
stepRunMutation: normalizeStepRunMutation(value.stepRunMutation),
});
return normalizeToolExecutionCompletionCommand({
...unsigned,
commandDigest: hash(COMPLETION_COMMAND_DIGEST_DOMAIN, unsigned),
});
}
export function normalizeToolExecutionResultKeyBinding(
value: ToolExecutionResultKeyBinding,
): Readonly<ToolExecutionResultKeyBinding> {
const candidate = record(value, 'result key binding');
exactKeys(
candidate,
[
'artifactDigest',
'artifactId',
'bindingDigest',
'catalogDigest',
'catalogGeneration',
'keyId',
'materialProof',
'schema',
'startId',
],
'result key binding',
);
const unsigned = Object.freeze({
schema: TOOL_EXECUTION_RESULT_KEY_BINDING_SCHEMA,
startId: identity(value.startId, 'result key binding start id'),
artifactId: identity(value.artifactId, 'result key binding Artifact id'),
artifactDigest: digest(
value.artifactDigest,
'result key binding Artifact digest',
),
catalogGeneration: timestamp(
value.catalogGeneration,
'result key catalog generation',
),
catalogDigest: digest(value.catalogDigest, 'result key catalog digest'),
keyId:
typeof value.keyId === 'string' && KEY_ID_PATTERN.test(value.keyId)
? value.keyId
: invalid('result key binding key id is invalid'),
materialProof: digest(value.materialProof, 'result key material proof'),
});
if (unsigned.catalogGeneration < 1) {
return invalid('result key catalog generation is invalid');
}
const bindingDigest = digest(
value.bindingDigest,
'result key binding digest',
);
if (hash(RESULT_KEY_BINDING_DIGEST_DOMAIN, unsigned) !== bindingDigest) {
return invalid('result key binding digest does not match');
}
return Object.freeze({ ...unsigned, bindingDigest });
}
export function toolExecutionResultKeyBinding(
commandValue: ToolExecutionCompletionCommand,
): Readonly<ToolExecutionResultKeyBinding> {
const command = normalizeToolExecutionCompletionCommand(commandValue);
const unsigned = Object.freeze({
schema: TOOL_EXECUTION_RESULT_KEY_BINDING_SCHEMA,
startId: command.barrier.startId,
artifactId: command.resultArtifact.artifactId,
artifactDigest: command.resultArtifact.artifactDigest,
catalogGeneration: command.resultKeyCatalogFence.generation,
catalogDigest: command.resultKeyCatalogFence.catalogDigest,
keyId: command.resultKeyCatalogFence.keyId,
materialProof: command.resultKeyCatalogFence.materialProof,
});
return normalizeToolExecutionResultKeyBinding({
...unsigned,
bindingDigest: hash(RESULT_KEY_BINDING_DIGEST_DOMAIN, unsigned),
});
}
export function toolExecutionCompletionRecord(
commandValue: ToolExecutionCompletionCommand,
): Readonly<ToolExecutionCompletionRecord> {
const command = normalizeToolExecutionCompletionCommand(commandValue);
return completionFromParts(
command.barrier,
command.executionResult,
command.resultArtifact,
command.stepRunMutation,
);
}
@@ -0,0 +1,673 @@
import { createHash } from 'node:crypto';
import {
normalizeSecurityAuditRecord,
type SecurityAuditRecord,
} from '../security/audit/securityAudit';
export const TOOL_EXECUTION_TRACE_ANCHOR_SCHEMA =
'qinglong/tool-execution-trace-anchor@v1' as const;
export const TOOL_EXECUTION_AUDIT_RECEIPT_SCHEMA =
'qinglong/tool-execution-audit-receipt@v1' as const;
export const TOOL_EXECUTION_EVIDENCE_BUNDLE_SCHEMA =
'qinglong/tool-execution-evidence-bundle@v1' as const;
export const TOOL_EXECUTION_START_AUDIT_OPERATION =
'tool.invoke.start' as const;
export const MAX_TOOL_EXECUTION_EVIDENCE_BYTES = 16 * 1024;
export const MAX_TOOL_EXECUTION_EVIDENCE_PAGE_SIZE = 128;
export interface ToolExecutionTraceAnchor {
readonly schema: typeof TOOL_EXECUTION_TRACE_ANCHOR_SCHEMA;
readonly traceId: string;
readonly spanId: string;
readonly parentSpanId: string | null;
readonly projectId: string;
readonly runId: string;
readonly stepRunId: string;
readonly invocationPlanDigest: string;
readonly bindingDigest: string;
readonly adapterDigest: string;
readonly redactionContractDigest: string;
readonly auditContractDigest: string;
readonly createdAtMs: number;
readonly traceDigest: string;
}
export interface ToolExecutionAuditReceipt {
readonly schema: typeof TOOL_EXECUTION_AUDIT_RECEIPT_SCHEMA;
readonly eventId: string;
readonly projectId: string;
readonly runId: string;
readonly stepRunId: string;
readonly traceId: string;
readonly spanId: string;
readonly traceDigest: string;
readonly invocationPlanDigest: string;
readonly bindingDigest: string;
readonly auditRecordDigest: string;
readonly createdAtMs: number;
readonly receiptDigest: string;
}
export interface ToolExecutionEvidenceBundle {
readonly schema: typeof TOOL_EXECUTION_EVIDENCE_BUNDLE_SCHEMA;
readonly trace: Readonly<ToolExecutionTraceAnchor>;
readonly audit: Readonly<SecurityAuditRecord>;
readonly receipt: Readonly<ToolExecutionAuditReceipt>;
}
export interface CreateToolExecutionEvidenceInput {
readonly traceId: string;
readonly spanId: string;
readonly parentSpanId?: string;
readonly projectId: string;
readonly runId: string;
readonly stepRunId: string;
readonly invocationPlanDigest: string;
readonly bindingDigest: string;
readonly adapterDigest: string;
readonly redactionContractDigest: string;
readonly auditContractDigest: string;
readonly audit: SecurityAuditRecord;
readonly createdAtMs: number;
}
export interface ToolExecutionEvidenceCursor {
readonly createdAtMs: number;
readonly traceId: string;
readonly spanId: string;
}
export interface ListToolExecutionEvidenceQuery {
readonly runId: string;
readonly limit: number;
readonly after?: ToolExecutionEvidenceCursor;
}
export interface ListToolExecutionEvidenceResult {
readonly bundles: readonly Readonly<ToolExecutionEvidenceBundle>[];
readonly truncated: boolean;
readonly next?: Readonly<ToolExecutionEvidenceCursor>;
}
export interface PrepareToolExecutionEvidenceResult {
readonly status: 'created' | 'existing';
readonly bundle: Readonly<ToolExecutionEvidenceBundle>;
}
export interface ToolExecutionEvidenceRepository {
findByTrace(
traceId: string,
spanId: string,
): Promise<Readonly<ToolExecutionEvidenceBundle> | null>;
findByAuditEventId(
eventId: string,
): Promise<Readonly<ToolExecutionEvidenceBundle> | null>;
listByRun(
query: ListToolExecutionEvidenceQuery,
): Promise<ListToolExecutionEvidenceResult>;
prepare(
bundle: ToolExecutionEvidenceBundle,
): Promise<Readonly<PrepareToolExecutionEvidenceResult>>;
}
export class InvalidToolExecutionEvidenceError extends TypeError {
readonly code = 'TOOL_EXECUTION_EVIDENCE_INVALID';
constructor(message: string) {
super(`Tool execution evidence is invalid: ${message}`);
this.name = 'InvalidToolExecutionEvidenceError';
}
}
export class ToolExecutionEvidenceConflictError extends Error {
readonly code = 'TOOL_EXECUTION_EVIDENCE_CONFLICT';
constructor() {
super('Tool execution evidence identity is bound to different content');
this.name = 'ToolExecutionEvidenceConflictError';
}
}
export class ToolExecutionEvidenceUnavailableError extends Error {
readonly code = 'TOOL_EXECUTION_EVIDENCE_UNAVAILABLE';
constructor(options?: ErrorOptions) {
super('Tool execution evidence repository is unavailable', options);
this.name = 'ToolExecutionEvidenceUnavailableError';
}
}
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const TRACE_ID_PATTERN = /^[0-9a-f]{32}$/;
const SPAN_ID_PATTERN = /^[0-9a-f]{16}$/;
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
const TRACE_DIGEST_DOMAIN = Buffer.from(
'qinglong/tool-execution-trace-anchor-digest@v1\0',
'utf8',
);
const AUDIT_RECORD_DIGEST_DOMAIN = Buffer.from(
'qinglong/tool-execution-audit-record-digest@v1\0',
'utf8',
);
const AUDIT_RECEIPT_DIGEST_DOMAIN = Buffer.from(
'qinglong/tool-execution-audit-receipt-digest@v1\0',
'utf8',
);
function invalid(message: string): never {
throw new InvalidToolExecutionEvidenceError(message);
}
function dataRecord(value: unknown, label: string): Record<string, unknown> {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
(Object.getPrototypeOf(value) !== Object.prototype &&
Object.getPrototypeOf(value) !== null)
) {
return invalid(`${label} must be an object`);
}
const descriptors = Object.getOwnPropertyDescriptors(value);
if (
Object.values(descriptors).some(
(descriptor) =>
descriptor.get !== undefined ||
descriptor.set !== undefined ||
descriptor.enumerable !== true,
)
) {
return invalid(`${label} must contain enumerable data properties`);
}
return value as Record<string, unknown>;
}
function exactKeys(
value: object,
required: readonly string[],
optional: readonly string[],
label: string,
): void {
const keys = Object.keys(value).sort();
const requiredKeys = [...required].sort();
const allowed = new Set([...required, ...optional]);
if (
requiredKeys.some((key) => !keys.includes(key)) ||
keys.some((key) => !allowed.has(key))
) {
invalid(`${label} shape is invalid`);
}
}
function identifier(value: unknown, label: string): string {
if (typeof value !== 'string' || !IDENTIFIER_PATTERN.test(value)) {
return invalid(`${label} is invalid`);
}
return value;
}
function traceId(value: unknown): string {
if (typeof value !== 'string' || !TRACE_ID_PATTERN.test(value)) {
return invalid('traceId is invalid');
}
return value;
}
function spanId(value: unknown, label = 'spanId'): string {
if (typeof value !== 'string' || !SPAN_ID_PATTERN.test(value)) {
return invalid(`${label} is invalid`);
}
return value;
}
function digest(value: unknown, label: string): string {
if (typeof value !== 'string' || !DIGEST_PATTERN.test(value)) {
return invalid(`${label} is invalid`);
}
return value;
}
function timestamp(value: unknown, label: string): number {
if (!Number.isSafeInteger(value) || (value as number) < 0) {
return invalid(`${label} is invalid`);
}
return value as number;
}
function canonicalJson(value: unknown): string {
if (
value === null ||
typeof value === 'boolean' ||
typeof value === 'string'
) {
return JSON.stringify(value);
}
if (typeof value === 'number') {
if (!Number.isSafeInteger(value)) invalid('canonical value is invalid');
return JSON.stringify(value);
}
if (Array.isArray(value)) {
return `[${value.map((entry) => canonicalJson(entry)).join(',')}]`;
}
const record = dataRecord(value, 'canonical value');
return `{${Object.keys(record)
.sort()
.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
.join(',')}}`;
}
function hash(domain: Buffer, value: unknown): string {
return createHash('sha256')
.update(domain)
.update(canonicalJson(value), 'utf8')
.digest('hex');
}
function traceUnsigned(
value: Readonly<ToolExecutionTraceAnchor>,
): Omit<ToolExecutionTraceAnchor, 'traceDigest'> {
const {
traceDigest: _traceDigest,
...unsigned
} = value;
return unsigned;
}
function receiptUnsigned(
value: Readonly<ToolExecutionAuditReceipt>,
): Omit<ToolExecutionAuditReceipt, 'receiptDigest'> {
const {
receiptDigest: _receiptDigest,
...unsigned
} = value;
return unsigned;
}
export function toolExecutionAuditRecordDigest(
value: SecurityAuditRecord,
): string {
return hash(
AUDIT_RECORD_DIGEST_DOMAIN,
normalizeSecurityAuditRecord(value),
);
}
export function normalizeToolExecutionTraceAnchor(
value: ToolExecutionTraceAnchor,
): Readonly<ToolExecutionTraceAnchor> {
const record = dataRecord(value, 'trace anchor');
exactKeys(
record,
[
'adapterDigest',
'auditContractDigest',
'bindingDigest',
'createdAtMs',
'invocationPlanDigest',
'parentSpanId',
'projectId',
'redactionContractDigest',
'runId',
'schema',
'spanId',
'stepRunId',
'traceDigest',
'traceId',
],
[],
'trace anchor',
);
if (value.schema !== TOOL_EXECUTION_TRACE_ANCHOR_SCHEMA) {
return invalid('trace anchor schema is invalid');
}
const normalized = Object.freeze({
schema: TOOL_EXECUTION_TRACE_ANCHOR_SCHEMA,
traceId: traceId(value.traceId),
spanId: spanId(value.spanId),
parentSpanId:
value.parentSpanId === null
? null
: spanId(value.parentSpanId, 'parentSpanId'),
projectId: identifier(value.projectId, 'projectId'),
runId: identifier(value.runId, 'runId'),
stepRunId: identifier(value.stepRunId, 'stepRunId'),
invocationPlanDigest: digest(
value.invocationPlanDigest,
'invocationPlanDigest',
),
bindingDigest: digest(value.bindingDigest, 'bindingDigest'),
adapterDigest: digest(value.adapterDigest, 'adapterDigest'),
redactionContractDigest: digest(
value.redactionContractDigest,
'redactionContractDigest',
),
auditContractDigest: digest(
value.auditContractDigest,
'auditContractDigest',
),
createdAtMs: timestamp(value.createdAtMs, 'createdAtMs'),
traceDigest: digest(value.traceDigest, 'traceDigest'),
});
if (
normalized.parentSpanId === normalized.spanId ||
hash(TRACE_DIGEST_DOMAIN, traceUnsigned(normalized)) !==
normalized.traceDigest ||
Buffer.byteLength(canonicalJson(normalized), 'utf8') >
MAX_TOOL_EXECUTION_EVIDENCE_BYTES
) {
return invalid('trace anchor semantic digest is invalid');
}
return normalized;
}
export function normalizeToolExecutionAuditReceipt(
value: ToolExecutionAuditReceipt,
): Readonly<ToolExecutionAuditReceipt> {
const record = dataRecord(value, 'audit receipt');
exactKeys(
record,
[
'auditRecordDigest',
'bindingDigest',
'createdAtMs',
'eventId',
'invocationPlanDigest',
'projectId',
'receiptDigest',
'runId',
'schema',
'spanId',
'stepRunId',
'traceDigest',
'traceId',
],
[],
'audit receipt',
);
if (value.schema !== TOOL_EXECUTION_AUDIT_RECEIPT_SCHEMA) {
return invalid('audit receipt schema is invalid');
}
const normalized = Object.freeze({
schema: TOOL_EXECUTION_AUDIT_RECEIPT_SCHEMA,
eventId: identifier(value.eventId, 'eventId'),
projectId: identifier(value.projectId, 'projectId'),
runId: identifier(value.runId, 'runId'),
stepRunId: identifier(value.stepRunId, 'stepRunId'),
traceId: traceId(value.traceId),
spanId: spanId(value.spanId),
traceDigest: digest(value.traceDigest, 'traceDigest'),
invocationPlanDigest: digest(
value.invocationPlanDigest,
'invocationPlanDigest',
),
bindingDigest: digest(value.bindingDigest, 'bindingDigest'),
auditRecordDigest: digest(
value.auditRecordDigest,
'auditRecordDigest',
),
createdAtMs: timestamp(value.createdAtMs, 'createdAtMs'),
receiptDigest: digest(value.receiptDigest, 'receiptDigest'),
});
if (
hash(AUDIT_RECEIPT_DIGEST_DOMAIN, receiptUnsigned(normalized)) !==
normalized.receiptDigest ||
Buffer.byteLength(canonicalJson(normalized), 'utf8') >
MAX_TOOL_EXECUTION_EVIDENCE_BYTES
) {
return invalid('audit receipt semantic digest is invalid');
}
return normalized;
}
export function normalizeToolExecutionEvidenceBundle(
value: ToolExecutionEvidenceBundle,
): Readonly<ToolExecutionEvidenceBundle> {
const record = dataRecord(value, 'evidence bundle');
exactKeys(record, ['audit', 'receipt', 'schema', 'trace'], [], 'evidence bundle');
if (value.schema !== TOOL_EXECUTION_EVIDENCE_BUNDLE_SCHEMA) {
return invalid('evidence bundle schema is invalid');
}
let audit: Readonly<SecurityAuditRecord>;
try {
audit = normalizeSecurityAuditRecord(value.audit);
} catch {
return invalid('security audit record is invalid');
}
const trace = normalizeToolExecutionTraceAnchor(value.trace);
const receipt = normalizeToolExecutionAuditReceipt(value.receipt);
if (
audit.operationId !== TOOL_EXECUTION_START_AUDIT_OPERATION ||
audit.outcome !== 'allowed' ||
audit.projectId !== trace.projectId ||
audit.fence === null ||
audit.occurredAtMs !== trace.createdAtMs ||
receipt.eventId !== audit.eventId ||
receipt.projectId !== trace.projectId ||
receipt.runId !== trace.runId ||
receipt.stepRunId !== trace.stepRunId ||
receipt.traceId !== trace.traceId ||
receipt.spanId !== trace.spanId ||
receipt.traceDigest !== trace.traceDigest ||
receipt.invocationPlanDigest !== trace.invocationPlanDigest ||
receipt.bindingDigest !== trace.bindingDigest ||
receipt.auditRecordDigest !== toolExecutionAuditRecordDigest(audit) ||
receipt.createdAtMs !== trace.createdAtMs
) {
return invalid('evidence bundle bindings are inconsistent');
}
const bundle = Object.freeze({
schema: TOOL_EXECUTION_EVIDENCE_BUNDLE_SCHEMA,
trace,
audit,
receipt,
});
if (
Buffer.byteLength(canonicalJson(bundle), 'utf8') >
MAX_TOOL_EXECUTION_EVIDENCE_BYTES
) {
return invalid('evidence bundle exceeds the byte limit');
}
return bundle;
}
export function createToolExecutionEvidenceBundle(
inputValue: CreateToolExecutionEvidenceInput,
): Readonly<ToolExecutionEvidenceBundle> {
const input = dataRecord(inputValue, 'create input');
exactKeys(
input,
[
'adapterDigest',
'audit',
'auditContractDigest',
'bindingDigest',
'createdAtMs',
'invocationPlanDigest',
'projectId',
'redactionContractDigest',
'runId',
'spanId',
'stepRunId',
'traceId',
],
['parentSpanId'],
'create input',
);
let audit: Readonly<SecurityAuditRecord>;
try {
audit = normalizeSecurityAuditRecord(inputValue.audit);
} catch {
return invalid('security audit record is invalid');
}
const traceWithoutDigest = Object.freeze({
schema: TOOL_EXECUTION_TRACE_ANCHOR_SCHEMA,
traceId: traceId(inputValue.traceId),
spanId: spanId(inputValue.spanId),
parentSpanId:
inputValue.parentSpanId === undefined
? null
: spanId(inputValue.parentSpanId, 'parentSpanId'),
projectId: identifier(inputValue.projectId, 'projectId'),
runId: identifier(inputValue.runId, 'runId'),
stepRunId: identifier(inputValue.stepRunId, 'stepRunId'),
invocationPlanDigest: digest(
inputValue.invocationPlanDigest,
'invocationPlanDigest',
),
bindingDigest: digest(inputValue.bindingDigest, 'bindingDigest'),
adapterDigest: digest(inputValue.adapterDigest, 'adapterDigest'),
redactionContractDigest: digest(
inputValue.redactionContractDigest,
'redactionContractDigest',
),
auditContractDigest: digest(
inputValue.auditContractDigest,
'auditContractDigest',
),
createdAtMs: timestamp(inputValue.createdAtMs, 'createdAtMs'),
});
const trace = normalizeToolExecutionTraceAnchor({
...traceWithoutDigest,
traceDigest: hash(TRACE_DIGEST_DOMAIN, traceWithoutDigest),
});
const receiptWithoutDigest = Object.freeze({
schema: TOOL_EXECUTION_AUDIT_RECEIPT_SCHEMA,
eventId: audit.eventId,
projectId: trace.projectId,
runId: trace.runId,
stepRunId: trace.stepRunId,
traceId: trace.traceId,
spanId: trace.spanId,
traceDigest: trace.traceDigest,
invocationPlanDigest: trace.invocationPlanDigest,
bindingDigest: trace.bindingDigest,
auditRecordDigest: toolExecutionAuditRecordDigest(audit),
createdAtMs: trace.createdAtMs,
});
const receipt = normalizeToolExecutionAuditReceipt({
...receiptWithoutDigest,
receiptDigest: hash(
AUDIT_RECEIPT_DIGEST_DOMAIN,
receiptWithoutDigest,
),
});
return normalizeToolExecutionEvidenceBundle({
schema: TOOL_EXECUTION_EVIDENCE_BUNDLE_SCHEMA,
trace,
audit,
receipt,
});
}
export function toolExecutionAdmissionEvidence(
value: ToolExecutionEvidenceBundle,
): Readonly<{
trace: Readonly<{ traceId: string; spanId: string; digest: string }>;
audit: Readonly<{ eventId: string; digest: string }>;
}> {
const bundle = normalizeToolExecutionEvidenceBundle(value);
return Object.freeze({
trace: Object.freeze({
traceId: bundle.trace.traceId,
spanId: bundle.trace.spanId,
digest: bundle.trace.traceDigest,
}),
audit: Object.freeze({
eventId: bundle.receipt.eventId,
digest: bundle.receipt.receiptDigest,
}),
});
}
export function normalizeListToolExecutionEvidenceQuery(
value: ListToolExecutionEvidenceQuery,
): Readonly<ListToolExecutionEvidenceQuery> {
const query = dataRecord(value, 'list query');
exactKeys(query, ['limit', 'runId'], ['after'], 'list query');
const limit = value.limit;
if (
!Number.isSafeInteger(limit) ||
limit < 1 ||
limit > MAX_TOOL_EXECUTION_EVIDENCE_PAGE_SIZE
) {
return invalid('list limit is invalid');
}
let after: Readonly<ToolExecutionEvidenceCursor> | undefined;
if (value.after !== undefined) {
const cursor = dataRecord(value.after, 'list cursor');
exactKeys(
cursor,
['createdAtMs', 'spanId', 'traceId'],
[],
'list cursor',
);
after = Object.freeze({
createdAtMs: timestamp(value.after.createdAtMs, 'cursor createdAtMs'),
traceId: traceId(value.after.traceId),
spanId: spanId(value.after.spanId),
});
}
return Object.freeze({
runId: identifier(value.runId, 'runId'),
limit,
...(after ? { after } : {}),
});
}
export function normalizeListToolExecutionEvidenceResult(
value: ListToolExecutionEvidenceResult,
queryValue: ListToolExecutionEvidenceQuery,
): Readonly<ListToolExecutionEvidenceResult> {
const query = normalizeListToolExecutionEvidenceQuery(queryValue);
const result = dataRecord(value, 'list result');
exactKeys(result, ['bundles', 'truncated'], ['next'], 'list result');
if (
!Array.isArray(value.bundles) ||
value.bundles.length > query.limit ||
typeof value.truncated !== 'boolean'
) {
return invalid('list result is invalid');
}
const bundles = value.bundles.map(normalizeToolExecutionEvidenceBundle);
if (
bundles.some((bundle) => bundle.trace.runId !== query.runId) ||
bundles.some(
(bundle, index) =>
index > 0 &&
(bundle.trace.createdAtMs < bundles[index - 1]!.trace.createdAtMs ||
(bundle.trace.createdAtMs ===
bundles[index - 1]!.trace.createdAtMs &&
`${bundle.trace.traceId}:${bundle.trace.spanId}` <=
`${bundles[index - 1]!.trace.traceId}:${
bundles[index - 1]!.trace.spanId
}`)),
)
) {
return invalid('list result ordering is invalid');
}
const last = bundles.at(-1);
const expectedNext =
value.truncated && last
? Object.freeze({
createdAtMs: last.trace.createdAtMs,
traceId: last.trace.traceId,
spanId: last.trace.spanId,
})
: undefined;
if (
(expectedNext === undefined) !== (value.next === undefined) ||
(expectedNext &&
(value.next!.createdAtMs !== expectedNext.createdAtMs ||
value.next!.traceId !== expectedNext.traceId ||
value.next!.spanId !== expectedNext.spanId))
) {
return invalid('list continuation is invalid');
}
return Object.freeze({
bundles: Object.freeze(bundles),
truncated: value.truncated,
...(expectedNext ? { next: expectedNext } : {}),
});
}
@@ -0,0 +1,510 @@
import { createHash } from 'node:crypto';
import { normalizeStepRunMutation, type StepRunMutation } from '../run/stepRun';
import {
normalizeToolExecutionStartBarrierRecord,
type ToolExecutionStartBarrierRecord,
} from './toolExecutionStartBarrier';
export const TOOL_EXECUTION_FAILURE_RESULT_SCHEMA =
'qinglong/tool-execution-failure-result@v1' as const;
export const TOOL_EXECUTION_FAILURE_COMPLETION_SCHEMA =
'qinglong/tool-execution-failure-completion@v1' as const;
export const TOOL_EXECUTION_FAILURE_COMPLETION_COMMAND_SCHEMA =
'qinglong/tool-execution-failure-completion-command@v1' as const;
export const MAX_TOOL_EXECUTION_FAILURE_COMPLETION_JSON_BYTES = 24 * 1024;
export const TOOL_EXECUTION_FAILURE_OUTCOMES = ['failed', 'timed_out'] as const;
export type ToolExecutionFailureOutcome =
(typeof TOOL_EXECUTION_FAILURE_OUTCOMES)[number];
export const TOOL_EXECUTION_FAILURE_FACTS = Object.freeze({
failed: Object.freeze({
resultCode: 'tool_adapter_failed',
errorSummary: 'Trusted Tool execution failed',
}),
timed_out: Object.freeze({
resultCode: 'tool_deadline_exceeded',
errorSummary: 'Trusted Tool execution deadline exceeded',
}),
} satisfies Record<ToolExecutionFailureOutcome, Readonly<{ resultCode: string; errorSummary: string }>>);
export interface ToolExecutionFailureResult {
readonly schema: typeof TOOL_EXECUTION_FAILURE_RESULT_SCHEMA;
readonly startId: string;
readonly barrierDigest: string;
readonly adapterDigest: string;
readonly outcome: ToolExecutionFailureOutcome;
readonly resultCode: string;
readonly errorSummary: string;
readonly completedAtMs: number;
readonly failureDigest: string;
}
export interface ToolExecutionFailureCompletionRecord {
readonly schema: typeof TOOL_EXECUTION_FAILURE_COMPLETION_SCHEMA;
readonly startId: string;
readonly projectId: string;
readonly runId: string;
readonly stepRunId: string;
readonly startedStepRunVersion: number;
readonly completedStepRunVersion: number;
readonly barrierDigest: string;
readonly adapterDigest: string;
readonly outcome: ToolExecutionFailureOutcome;
readonly resultCode: string;
readonly errorSummary: string;
readonly stepRunMutationId: string;
readonly stepRunMutationDigest: string;
readonly completedStepRunDigest: string;
readonly runEventId: string;
readonly completedAtMs: number;
readonly completionDigest: string;
}
export interface ToolExecutionFailureCompletionCommand {
readonly schema: typeof TOOL_EXECUTION_FAILURE_COMPLETION_COMMAND_SCHEMA;
readonly barrier: Readonly<ToolExecutionStartBarrierRecord>;
readonly failure: Readonly<ToolExecutionFailureResult>;
readonly stepRunMutation: Readonly<StepRunMutation>;
readonly commandDigest: string;
}
export interface CommitToolExecutionFailureCompletionResult {
readonly status: 'created' | 'existing';
readonly completion: Readonly<ToolExecutionFailureCompletionRecord>;
}
export interface ToolExecutionFailureCompletionRepository {
findByStartId(
startId: string,
): Promise<Readonly<ToolExecutionFailureCompletionRecord> | null>;
commit(
command: ToolExecutionFailureCompletionCommand,
): Promise<Readonly<CommitToolExecutionFailureCompletionResult>>;
}
export class InvalidToolExecutionFailureCompletionError extends TypeError {
readonly code = 'TOOL_EXECUTION_FAILURE_COMPLETION_INVALID';
constructor(message: string) {
super(`Tool execution failure completion is invalid: ${message}`);
this.name = 'InvalidToolExecutionFailureCompletionError';
}
}
export class ToolExecutionFailureCompletionConflictError extends Error {
readonly code = 'TOOL_EXECUTION_FAILURE_COMPLETION_CONFLICT';
constructor() {
super('Tool execution failure completion conflicts with durable state');
this.name = 'ToolExecutionFailureCompletionConflictError';
}
}
export class ToolExecutionFailureCompletionUnavailableError extends Error {
readonly code = 'TOOL_EXECUTION_FAILURE_COMPLETION_UNAVAILABLE';
constructor(options?: ErrorOptions) {
super(
'Tool execution failure completion authority is unavailable',
options,
);
this.name = 'ToolExecutionFailureCompletionUnavailableError';
}
}
const IDENTITY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
const FAILURE_DIGEST_DOMAIN = Buffer.from(
'qinglong/tool-execution-failure-result-digest@v1\0',
'utf8',
);
const COMPLETION_DIGEST_DOMAIN = Buffer.from(
'qinglong/tool-execution-failure-completion-digest@v1\0',
'utf8',
);
const COMMAND_DIGEST_DOMAIN = Buffer.from(
'qinglong/tool-execution-failure-completion-command-digest@v1\0',
'utf8',
);
function invalid(message: string): never {
throw new InvalidToolExecutionFailureCompletionError(message);
}
function hash(domain: Uint8Array, value: unknown): string {
return createHash('sha256')
.update(domain)
.update(JSON.stringify(value))
.digest('hex');
}
function record(value: unknown, label: string): Record<string, unknown> {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
Object.getPrototypeOf(value) !== Object.prototype
) {
return invalid(`${label} is not a plain object`);
}
return value as Record<string, unknown>;
}
function exactKeys(
value: Record<string, unknown>,
keys: readonly string[],
label: string,
): void {
const actual = Object.keys(value).sort();
const expected = [...keys].sort();
if (
actual.length !== expected.length ||
actual.some((key, index) => key !== expected[index])
) {
invalid(`${label} shape is invalid`);
}
}
function identity(value: unknown, label: string): string {
if (typeof value !== 'string' || !IDENTITY_PATTERN.test(value)) {
return invalid(`${label} is invalid`);
}
return value;
}
function digest(value: unknown, label: string): string {
if (typeof value !== 'string' || !DIGEST_PATTERN.test(value)) {
return invalid(`${label} is invalid`);
}
return value;
}
function timestamp(value: unknown, label: string): number {
if (!Number.isSafeInteger(value) || (value as number) < 0) {
return invalid(`${label} is invalid`);
}
return value as number;
}
function version(value: unknown, label: string): number {
if (
!Number.isSafeInteger(value) ||
(value as number) < 2 ||
(value as number) > 2_147_483_647
) {
return invalid(`${label} is invalid`);
}
return value as number;
}
function outcome(value: unknown): ToolExecutionFailureOutcome {
if (
typeof value !== 'string' ||
!TOOL_EXECUTION_FAILURE_OUTCOMES.includes(
value as ToolExecutionFailureOutcome,
)
) {
return invalid('failure outcome is invalid');
}
return value as ToolExecutionFailureOutcome;
}
function exactFailureFacts(
value: Readonly<{
outcome: ToolExecutionFailureOutcome;
resultCode: unknown;
errorSummary: unknown;
}>,
): Readonly<{ resultCode: string; errorSummary: string }> {
const expected = TOOL_EXECUTION_FAILURE_FACTS[value.outcome];
if (
value.resultCode !== expected.resultCode ||
value.errorSummary !== expected.errorSummary
) {
return invalid('failure facts are invalid');
}
return expected;
}
export function normalizeToolExecutionFailureResult(
value: ToolExecutionFailureResult,
): Readonly<ToolExecutionFailureResult> {
const candidate = record(value, 'failure result');
exactKeys(
candidate,
[
'adapterDigest',
'barrierDigest',
'completedAtMs',
'errorSummary',
'failureDigest',
'outcome',
'resultCode',
'schema',
'startId',
],
'failure result',
);
if (value.schema !== TOOL_EXECUTION_FAILURE_RESULT_SCHEMA) {
return invalid('failure result schema is invalid');
}
const normalizedOutcome = outcome(value.outcome);
const facts = exactFailureFacts({
outcome: normalizedOutcome,
resultCode: value.resultCode,
errorSummary: value.errorSummary,
});
const unsigned = Object.freeze({
schema: TOOL_EXECUTION_FAILURE_RESULT_SCHEMA,
startId: identity(value.startId, 'failure start id'),
barrierDigest: digest(value.barrierDigest, 'failure barrier digest'),
adapterDigest: digest(value.adapterDigest, 'failure adapter digest'),
outcome: normalizedOutcome,
resultCode: facts.resultCode,
errorSummary: facts.errorSummary,
completedAtMs: timestamp(value.completedAtMs, 'failure completion time'),
});
const failureDigest = digest(value.failureDigest, 'failure digest');
if (hash(FAILURE_DIGEST_DOMAIN, unsigned) !== failureDigest) {
return invalid('failure digest does not match');
}
return Object.freeze({ ...unsigned, failureDigest });
}
export function createToolExecutionFailureResult(
barrierValue: ToolExecutionStartBarrierRecord,
failureOutcome: ToolExecutionFailureOutcome,
completedAtMs: number,
): Readonly<ToolExecutionFailureResult> {
const barrier = normalizeToolExecutionStartBarrierRecord(barrierValue);
const normalizedOutcome = outcome(failureOutcome);
const facts = TOOL_EXECUTION_FAILURE_FACTS[normalizedOutcome];
const unsigned = Object.freeze({
schema: TOOL_EXECUTION_FAILURE_RESULT_SCHEMA,
startId: barrier.startId,
barrierDigest: barrier.barrierDigest,
adapterDigest: barrier.adapterDigest,
outcome: normalizedOutcome,
resultCode: facts.resultCode,
errorSummary: facts.errorSummary,
completedAtMs: timestamp(completedAtMs, 'failure completion time'),
});
if (unsigned.completedAtMs < barrier.startedAtMs) {
return invalid('failure completion precedes durable start');
}
return normalizeToolExecutionFailureResult({
...unsigned,
failureDigest: hash(FAILURE_DIGEST_DOMAIN, unsigned),
});
}
export function normalizeToolExecutionFailureCompletionRecord(
value: ToolExecutionFailureCompletionRecord,
): Readonly<ToolExecutionFailureCompletionRecord> {
const candidate = record(value, 'failure completion');
exactKeys(
candidate,
[
'adapterDigest',
'barrierDigest',
'completedAtMs',
'completedStepRunDigest',
'completedStepRunVersion',
'completionDigest',
'errorSummary',
'outcome',
'projectId',
'resultCode',
'runEventId',
'runId',
'schema',
'startId',
'startedStepRunVersion',
'stepRunId',
'stepRunMutationDigest',
'stepRunMutationId',
],
'failure completion',
);
if (value.schema !== TOOL_EXECUTION_FAILURE_COMPLETION_SCHEMA) {
return invalid('failure completion schema is invalid');
}
const normalizedOutcome = outcome(value.outcome);
const facts = exactFailureFacts({
outcome: normalizedOutcome,
resultCode: value.resultCode,
errorSummary: value.errorSummary,
});
const startedStepRunVersion = version(
value.startedStepRunVersion,
'started StepRun version',
);
const completedStepRunVersion = version(
value.completedStepRunVersion,
'completed StepRun version',
);
if (completedStepRunVersion !== startedStepRunVersion + 1) {
return invalid('failure completion StepRun version fence is invalid');
}
const unsigned = Object.freeze({
schema: TOOL_EXECUTION_FAILURE_COMPLETION_SCHEMA,
startId: identity(value.startId, 'failure completion start id'),
projectId: identity(value.projectId, 'failure completion project id'),
runId: identity(value.runId, 'failure completion Run id'),
stepRunId: identity(value.stepRunId, 'failure completion StepRun id'),
startedStepRunVersion,
completedStepRunVersion,
barrierDigest: digest(
value.barrierDigest,
'failure completion barrier digest',
),
adapterDigest: digest(
value.adapterDigest,
'failure completion adapter digest',
),
outcome: normalizedOutcome,
resultCode: facts.resultCode,
errorSummary: facts.errorSummary,
stepRunMutationId: identity(
value.stepRunMutationId,
'failure completion mutation id',
),
stepRunMutationDigest: digest(
value.stepRunMutationDigest,
'failure completion mutation digest',
),
completedStepRunDigest: digest(
value.completedStepRunDigest,
'failed StepRun digest',
),
runEventId: identity(value.runEventId, 'failure completion Run event id'),
completedAtMs: timestamp(value.completedAtMs, 'failure completion time'),
});
if (
Buffer.byteLength(JSON.stringify(unsigned), 'utf8') >
MAX_TOOL_EXECUTION_FAILURE_COMPLETION_JSON_BYTES
) {
return invalid('failure completion exceeds its budget');
}
const completionDigest = digest(
value.completionDigest,
'failure completion digest',
);
if (hash(COMPLETION_DIGEST_DOMAIN, unsigned) !== completionDigest) {
return invalid('failure completion digest does not match');
}
return Object.freeze({ ...unsigned, completionDigest });
}
function completionFromParts(
barrier: Readonly<ToolExecutionStartBarrierRecord>,
failure: Readonly<ToolExecutionFailureResult>,
mutation: Readonly<StepRunMutation>,
): Readonly<ToolExecutionFailureCompletionRecord> {
const unsigned = Object.freeze({
schema: TOOL_EXECUTION_FAILURE_COMPLETION_SCHEMA,
startId: barrier.startId,
projectId: barrier.projectId,
runId: barrier.runId,
stepRunId: barrier.stepRunId,
startedStepRunVersion: barrier.startedStepRunVersion,
completedStepRunVersion: mutation.stepRun.version,
barrierDigest: barrier.barrierDigest,
adapterDigest: barrier.adapterDigest,
outcome: failure.outcome,
resultCode: failure.resultCode,
errorSummary: failure.errorSummary,
stepRunMutationId: mutation.mutationId,
stepRunMutationDigest: mutation.mutationDigest,
completedStepRunDigest: mutation.stepRun.stepRunDigest,
runEventId: mutation.event.id,
completedAtMs: failure.completedAtMs,
});
return normalizeToolExecutionFailureCompletionRecord({
...unsigned,
completionDigest: hash(COMPLETION_DIGEST_DOMAIN, unsigned),
});
}
export function normalizeToolExecutionFailureCompletionCommand(
value: ToolExecutionFailureCompletionCommand,
): Readonly<ToolExecutionFailureCompletionCommand> {
const candidate = record(value, 'failure completion command');
exactKeys(
candidate,
['barrier', 'commandDigest', 'failure', 'schema', 'stepRunMutation'],
'failure completion command',
);
if (value.schema !== TOOL_EXECUTION_FAILURE_COMPLETION_COMMAND_SCHEMA) {
return invalid('failure completion command schema is invalid');
}
const barrier = normalizeToolExecutionStartBarrierRecord(value.barrier);
const failure = normalizeToolExecutionFailureResult(value.failure);
const stepRunMutation = normalizeStepRunMutation(value.stepRunMutation);
if (
failure.startId !== barrier.startId ||
failure.barrierDigest !== barrier.barrierDigest ||
failure.adapterDigest !== barrier.adapterDigest ||
failure.completedAtMs < barrier.startedAtMs ||
stepRunMutation.runId !== barrier.runId ||
stepRunMutation.stepRun.id !== barrier.stepRunId ||
stepRunMutation.stepRun.kind !== 'tool' ||
stepRunMutation.previousStatus !== 'running' ||
stepRunMutation.expectedStepRunVersion !== barrier.startedStepRunVersion ||
stepRunMutation.expectedStepRunDigest !== barrier.startedStepRunDigest ||
stepRunMutation.stepRun.status !== failure.outcome ||
stepRunMutation.stepRun.outputRef !== null ||
stepRunMutation.stepRun.resultCode !== failure.resultCode ||
stepRunMutation.stepRun.errorSummary !== failure.errorSummary ||
stepRunMutation.stepRun.finishedAtMs !== failure.completedAtMs ||
stepRunMutation.stepRun.updatedAtMs !== failure.completedAtMs
) {
throw new ToolExecutionFailureCompletionConflictError();
}
const unsigned = Object.freeze({
schema: TOOL_EXECUTION_FAILURE_COMPLETION_COMMAND_SCHEMA,
barrier,
failure,
stepRunMutation,
});
const commandDigest = digest(
value.commandDigest,
'failure completion command digest',
);
if (hash(COMMAND_DIGEST_DOMAIN, unsigned) !== commandDigest) {
return invalid('failure completion command digest does not match');
}
return Object.freeze({ ...unsigned, commandDigest });
}
export function createToolExecutionFailureCompletionCommand(
value: Omit<
ToolExecutionFailureCompletionCommand,
'commandDigest' | 'schema'
>,
): Readonly<ToolExecutionFailureCompletionCommand> {
const unsigned = Object.freeze({
schema: TOOL_EXECUTION_FAILURE_COMPLETION_COMMAND_SCHEMA,
barrier: normalizeToolExecutionStartBarrierRecord(value.barrier),
failure: normalizeToolExecutionFailureResult(value.failure),
stepRunMutation: normalizeStepRunMutation(value.stepRunMutation),
});
return normalizeToolExecutionFailureCompletionCommand({
...unsigned,
commandDigest: hash(COMMAND_DIGEST_DOMAIN, unsigned),
});
}
export function toolExecutionFailureCompletionRecord(
commandValue: ToolExecutionFailureCompletionCommand,
): Readonly<ToolExecutionFailureCompletionRecord> {
const command = normalizeToolExecutionFailureCompletionCommand(commandValue);
return completionFromParts(
command.barrier,
command.failure,
command.stepRunMutation,
);
}
@@ -0,0 +1,733 @@
import { createHash } from 'node:crypto';
import {
normalizeProjectPolicySubject,
} from '../security/project-policy/projectPolicy';
import type { SecuritySubject } from '../security/security';
import {
normalizeStepRunMutation,
type StepRunMutation,
} from '../run/stepRun';
import {
normalizeToolExecutionEvidenceBundle,
type ToolExecutionEvidenceBundle,
} from './toolExecutionEvidence';
import {
normalizeToolInvocationInputArtifactReference,
normalizeToolInvocationPreviewArtifactReference,
type ToolInvocationInputArtifactReference,
type ToolInvocationPreviewArtifactReference,
} from './toolInvocationArtifact';
import {
TRUSTED_TOOL_DEPLOYMENT_PROFILES,
TRUSTED_TOOL_EXECUTION_CLASSES,
normalizeTrustedToolExecutionAdmission,
trustedToolContractIdentityDigest,
type TrustedToolContractIdentity,
type TrustedToolExecutionAdmission,
type TrustedToolExecutionClass,
} from './trustedToolInvocation';
import type { DeploymentProfile } from '../cluster-control/clusterControlActivation';
import type { SecurityPolicyFence } from '../security/security';
export const TOOL_EXECUTION_START_COMMAND_SCHEMA =
'qinglong/tool-execution-start-command@v1' as const;
export const TOOL_EXECUTION_START_BARRIER_SCHEMA =
'qinglong/tool-execution-start-barrier@v1' as const;
export const MAX_TOOL_EXECUTION_START_COMMAND_BYTES = 64 * 1024;
export const MAX_TOOL_EXECUTION_START_BARRIER_BYTES = 16 * 1024;
export interface CreateToolExecutionStartCommandInput {
readonly startId: string;
readonly admission: TrustedToolExecutionAdmission;
readonly evidence: ToolExecutionEvidenceBundle;
readonly stepRunMutation: StepRunMutation;
}
export interface ToolExecutionStartCommand {
readonly schema: typeof TOOL_EXECUTION_START_COMMAND_SCHEMA;
readonly startId: string;
readonly admission: Readonly<TrustedToolExecutionAdmission>;
readonly evidence: Readonly<ToolExecutionEvidenceBundle>;
readonly stepRunMutation: Readonly<StepRunMutation>;
readonly commandDigest: string;
}
export interface ToolExecutionStartBarrierRecord {
readonly schema: typeof TOOL_EXECUTION_START_BARRIER_SCHEMA;
readonly startId: string;
readonly projectId: string;
readonly runId: string;
readonly stepRunId: string;
readonly actionRef: string;
readonly planDigest: string;
readonly actionDigest: string;
readonly snapshotDigest: string;
readonly definitionDigest: string;
readonly bindingDigest: string;
readonly admissionDigest: string;
readonly invocationArtifact: Readonly<ToolInvocationInputArtifactReference>;
readonly previewArtifact: Readonly<ToolInvocationPreviewArtifactReference>;
readonly requestedBy: Readonly<SecuritySubject>;
readonly profile: DeploymentProfile;
readonly adapter: Readonly<TrustedToolContractIdentity>;
readonly adapterDigest: string;
readonly redactionContract: Readonly<TrustedToolContractIdentity>;
readonly redactionContractDigest: string;
readonly auditContract: Readonly<TrustedToolContractIdentity>;
readonly auditContractDigest: string;
readonly executionClass: TrustedToolExecutionClass;
readonly timeoutSeconds: number;
readonly policyFence: Readonly<SecurityPolicyFence>;
readonly approvalRequestId: string | null;
readonly approvalDispatchId: string | null;
readonly approvalDispatchDigest: string | null;
readonly previousStepRunVersion: number;
readonly previousStepRunDigest: string;
readonly startedStepRunVersion: number;
readonly startedStepRunDigest: string;
readonly stepRunMutationId: string;
readonly stepRunMutationDigest: string;
readonly runEventId: string;
readonly traceId: string;
readonly spanId: string;
readonly traceDigest: string;
readonly auditEventId: string;
readonly auditReceiptDigest: string;
readonly startedAtMs: number;
readonly commandDigest: string;
readonly barrierDigest: string;
}
export interface PrepareToolExecutionStartResult {
readonly status: 'created' | 'existing';
readonly barrier: Readonly<ToolExecutionStartBarrierRecord>;
}
export interface ToolExecutionStartBarrierRepository {
findByStartId(
startId: string,
): Promise<Readonly<ToolExecutionStartBarrierRecord> | null>;
findByStepRun(
runId: string,
stepRunId: string,
startedStepRunVersion: number,
): Promise<Readonly<ToolExecutionStartBarrierRecord> | null>;
prepare(
command: ToolExecutionStartCommand,
): Promise<Readonly<PrepareToolExecutionStartResult>>;
}
export class InvalidToolExecutionStartBarrierError extends TypeError {
readonly code = 'TOOL_EXECUTION_START_BARRIER_INVALID';
constructor(message: string) {
super(`Tool execution start barrier is invalid: ${message}`);
this.name = 'InvalidToolExecutionStartBarrierError';
}
}
export class ToolExecutionStartBarrierConflictError extends Error {
readonly code = 'TOOL_EXECUTION_START_BARRIER_CONFLICT';
constructor() {
super('Tool execution start identity is bound to different content');
this.name = 'ToolExecutionStartBarrierConflictError';
}
}
export class ToolExecutionStartBarrierUnavailableError extends Error {
readonly code = 'TOOL_EXECUTION_START_BARRIER_UNAVAILABLE';
constructor(options?: ErrorOptions) {
super('Tool execution start barrier repository is unavailable', options);
this.name = 'ToolExecutionStartBarrierUnavailableError';
}
}
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const ACTION_REF_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$/;
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
const TRACE_ID_PATTERN = /^[0-9a-f]{32}$/;
const SPAN_ID_PATTERN = /^[0-9a-f]{16}$/;
const UUID_V4_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const COMMAND_DIGEST_DOMAIN = Buffer.from(
'qinglong/tool-execution-start-command-digest@v1\0',
'utf8',
);
const BARRIER_DIGEST_DOMAIN = Buffer.from(
'qinglong/tool-execution-start-barrier-digest@v1\0',
'utf8',
);
function invalid(message: string): never {
throw new InvalidToolExecutionStartBarrierError(message);
}
function dataRecord(value: unknown, label: string): Record<string, unknown> {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
(Object.getPrototypeOf(value) !== Object.prototype &&
Object.getPrototypeOf(value) !== null)
) {
return invalid(`${label} must be an object`);
}
const descriptors = Object.getOwnPropertyDescriptors(value);
if (
Object.values(descriptors).some(
(descriptor) =>
descriptor.get !== undefined ||
descriptor.set !== undefined ||
descriptor.enumerable !== true,
)
) {
return invalid(`${label} must contain enumerable data properties`);
}
return value as Record<string, unknown>;
}
function exactKeys(
value: object,
required: readonly string[],
label: string,
): void {
const keys = Reflect.ownKeys(value);
const allowed = new Set(required);
if (
keys.length !== required.length ||
keys.some((key) => typeof key !== 'string' || !allowed.has(key)) ||
required.some((key) => !keys.includes(key))
) {
invalid(`${label} shape is invalid`);
}
}
function hash(domain: Buffer, value: unknown): string {
return createHash('sha256')
.update(domain)
.update(JSON.stringify(value))
.digest('hex');
}
function identifier(value: unknown, label: string): string {
if (typeof value !== 'string' || !IDENTIFIER_PATTERN.test(value)) {
return invalid(`${label} is invalid`);
}
return value;
}
function actionRef(value: unknown): string {
if (typeof value !== 'string' || !ACTION_REF_PATTERN.test(value)) {
return invalid('action reference is invalid');
}
return value;
}
function digest(value: unknown, label: string): string {
if (typeof value !== 'string' || !DIGEST_PATTERN.test(value)) {
return invalid(`${label} is invalid`);
}
return value;
}
function integer(
value: unknown,
minimum: number,
maximum: number,
label: string,
): number {
if (
!Number.isSafeInteger(value) ||
(value as number) < minimum ||
(value as number) > maximum
) {
return invalid(`${label} is invalid`);
}
return value as number;
}
function traceIdentity(
value: unknown,
pattern: RegExp,
label: string,
): string {
if (typeof value !== 'string' || !pattern.test(value)) {
return invalid(`${label} is invalid`);
}
return value;
}
function sameFence(
left: Readonly<SecurityPolicyFence>,
right: Readonly<SecurityPolicyFence>,
): boolean {
return (
left.projectVersion === right.projectVersion &&
left.bindingVersion === right.bindingVersion
);
}
function normalizeFence(
value: SecurityPolicyFence,
): Readonly<SecurityPolicyFence> {
const record = dataRecord(value, 'policy fence');
exactKeys(record, ['bindingVersion', 'projectVersion'], 'policy fence');
if (
!Number.isSafeInteger(value.projectVersion) ||
value.projectVersion < 1 ||
(value.bindingVersion !== null &&
(!Number.isSafeInteger(value.bindingVersion) ||
value.bindingVersion < 1))
) {
return invalid('policy fence is invalid');
}
return Object.freeze({
projectVersion: value.projectVersion,
bindingVersion: value.bindingVersion,
});
}
function normalizeContractIdentity(
value: TrustedToolContractIdentity,
label: string,
): Readonly<TrustedToolContractIdentity> {
try {
trustedToolContractIdentityDigest(value);
} catch {
return invalid(`${label} is invalid`);
}
return Object.freeze({ id: value.id, version: value.version });
}
function commandUnsigned(
value: Readonly<ToolExecutionStartCommand>,
): Omit<ToolExecutionStartCommand, 'commandDigest'> {
return Object.freeze({
schema: value.schema,
startId: value.startId,
admission: value.admission,
evidence: value.evidence,
stepRunMutation: value.stepRunMutation,
});
}
function validateCommandBindings(
admission: Readonly<TrustedToolExecutionAdmission>,
evidence: Readonly<ToolExecutionEvidenceBundle>,
mutation: Readonly<StepRunMutation>,
): void {
const trace = evidence.trace;
const audit = evidence.audit;
const receipt = evidence.receipt;
const stepRun = mutation.stepRun;
const expectedDefinitionRef =
`tool:${admission.tool.name}@${admission.tool.version}`;
if (
admission.projectId !== trace.projectId ||
admission.planDigest !== trace.invocationPlanDigest ||
admission.bindingDigest !== trace.bindingDigest ||
trustedToolContractIdentityDigest(admission.adapter) !==
trace.adapterDigest ||
trustedToolContractIdentityDigest(admission.redactionContract) !==
trace.redactionContractDigest ||
trustedToolContractIdentityDigest(admission.auditContract) !==
trace.auditContractDigest ||
admission.evidence.trace.traceId !== trace.traceId ||
admission.evidence.trace.spanId !== trace.spanId ||
admission.evidence.trace.digest !== trace.traceDigest ||
admission.evidence.audit.eventId !== receipt.eventId ||
admission.evidence.audit.digest !== receipt.receiptDigest ||
admission.evidence.stepRun.id !== stepRun.id ||
admission.evidence.stepRun.version !==
mutation.expectedStepRunVersion ||
admission.evidence.stepRun.digest !==
mutation.expectedStepRunDigest ||
admission.requestedBy.type !== audit.subject?.type ||
admission.requestedBy.id !== audit.subject?.id ||
audit.fence === null ||
!sameFence(admission.policyFence, audit.fence) ||
audit.occurredAtMs !== admission.admittedAtMs ||
trace.createdAtMs !== admission.admittedAtMs ||
mutation.runId !== trace.runId ||
stepRun.runId !== trace.runId ||
stepRun.id !== trace.stepRunId ||
stepRun.kind !== 'tool' ||
stepRun.definitionRef !== expectedDefinitionRef ||
stepRun.definitionDigest !== admission.definitionDigest ||
(mutation.previousStatus !== 'ready' &&
mutation.previousStatus !== 'waiting_approval') ||
mutation.expectedStepRunVersion === null ||
mutation.expectedStepRunDigest === null ||
stepRun.status !== 'running' ||
stepRun.startedAtMs !== admission.admittedAtMs ||
stepRun.updatedAtMs !== admission.admittedAtMs ||
mutation.event.createdAtMs !== admission.admittedAtMs ||
mutation.event.type !== 'step.running' ||
(admission.approvalRequestId === null) !==
(admission.approvalDispatchId === null) ||
(admission.approvalDispatchId === null) !==
(mutation.previousStatus === 'ready') ||
(admission.approvalRequestId === null
? stepRun.approvalRequestId !== null
: stepRun.approvalRequestId !== admission.approvalRequestId)
) {
return invalid('start command bindings are inconsistent');
}
}
export function normalizeToolExecutionStartCommand(
value: ToolExecutionStartCommand,
): Readonly<ToolExecutionStartCommand> {
const record = dataRecord(value, 'start command');
exactKeys(
record,
[
'admission',
'commandDigest',
'evidence',
'schema',
'startId',
'stepRunMutation',
],
'start command',
);
if (value.schema !== TOOL_EXECUTION_START_COMMAND_SCHEMA) {
return invalid('start command schema is invalid');
}
let admission: Readonly<TrustedToolExecutionAdmission>;
let evidence: Readonly<ToolExecutionEvidenceBundle>;
let stepRunMutation: Readonly<StepRunMutation>;
try {
admission = normalizeTrustedToolExecutionAdmission(value.admission);
evidence = normalizeToolExecutionEvidenceBundle(value.evidence);
stepRunMutation = normalizeStepRunMutation(value.stepRunMutation);
} catch {
return invalid('start command contains invalid durable facts');
}
const normalized = Object.freeze({
schema: TOOL_EXECUTION_START_COMMAND_SCHEMA,
startId: identifier(value.startId, 'start id'),
admission,
evidence,
stepRunMutation,
commandDigest: digest(value.commandDigest, 'command digest'),
});
validateCommandBindings(admission, evidence, stepRunMutation);
if (
hash(COMMAND_DIGEST_DOMAIN, commandUnsigned(normalized)) !==
normalized.commandDigest ||
Buffer.byteLength(JSON.stringify(normalized), 'utf8') >
MAX_TOOL_EXECUTION_START_COMMAND_BYTES
) {
return invalid('start command digest or size is invalid');
}
return normalized;
}
export function createToolExecutionStartCommand(
inputValue: CreateToolExecutionStartCommandInput,
): Readonly<ToolExecutionStartCommand> {
const input = dataRecord(inputValue, 'create start command input');
exactKeys(
input,
['admission', 'evidence', 'startId', 'stepRunMutation'],
'create start command input',
);
let admission: Readonly<TrustedToolExecutionAdmission>;
let evidence: Readonly<ToolExecutionEvidenceBundle>;
let stepRunMutation: Readonly<StepRunMutation>;
try {
admission = normalizeTrustedToolExecutionAdmission(inputValue.admission);
evidence = normalizeToolExecutionEvidenceBundle(inputValue.evidence);
stepRunMutation = normalizeStepRunMutation(inputValue.stepRunMutation);
} catch {
return invalid('create start command input contains invalid facts');
}
validateCommandBindings(admission, evidence, stepRunMutation);
const unsigned = Object.freeze({
schema: TOOL_EXECUTION_START_COMMAND_SCHEMA,
startId: identifier(inputValue.startId, 'start id'),
admission,
evidence,
stepRunMutation,
});
return normalizeToolExecutionStartCommand({
...unsigned,
commandDigest: hash(COMMAND_DIGEST_DOMAIN, unsigned),
});
}
function barrierUnsigned(
value: Readonly<ToolExecutionStartBarrierRecord>,
): Omit<ToolExecutionStartBarrierRecord, 'barrierDigest'> {
const { barrierDigest: _barrierDigest, ...unsigned } = value;
return Object.freeze(unsigned);
}
export function toolExecutionStartBarrierRecord(
commandValue: ToolExecutionStartCommand,
): Readonly<ToolExecutionStartBarrierRecord> {
const command = normalizeToolExecutionStartCommand(commandValue);
const admission = command.admission;
const evidence = command.evidence;
const mutation = command.stepRunMutation;
const unsigned = Object.freeze({
schema: TOOL_EXECUTION_START_BARRIER_SCHEMA,
startId: command.startId,
projectId: admission.projectId,
runId: mutation.runId,
stepRunId: mutation.stepRun.id,
actionRef: admission.actionRef,
planDigest: admission.planDigest,
actionDigest: admission.actionDigest,
snapshotDigest: admission.snapshotDigest,
definitionDigest: admission.definitionDigest,
bindingDigest: admission.bindingDigest,
admissionDigest: admission.admissionDigest,
invocationArtifact: admission.invocationArtifact,
previewArtifact: admission.previewArtifact,
requestedBy: admission.requestedBy,
profile: admission.profile,
adapter: admission.adapter,
adapterDigest: evidence.trace.adapterDigest,
redactionContract: admission.redactionContract,
redactionContractDigest: evidence.trace.redactionContractDigest,
auditContract: admission.auditContract,
auditContractDigest: evidence.trace.auditContractDigest,
executionClass: admission.executionClass,
timeoutSeconds: admission.timeoutSeconds,
policyFence: admission.policyFence,
approvalRequestId: admission.approvalRequestId,
approvalDispatchId: admission.approvalDispatchId,
approvalDispatchDigest: admission.approvalDispatchDigest,
previousStepRunVersion: mutation.expectedStepRunVersion!,
previousStepRunDigest: mutation.expectedStepRunDigest!,
startedStepRunVersion: mutation.stepRun.version,
startedStepRunDigest: mutation.stepRun.stepRunDigest,
stepRunMutationId: mutation.mutationId,
stepRunMutationDigest: mutation.mutationDigest,
runEventId: mutation.event.id,
traceId: evidence.trace.traceId,
spanId: evidence.trace.spanId,
traceDigest: evidence.trace.traceDigest,
auditEventId: evidence.audit.eventId,
auditReceiptDigest: evidence.receipt.receiptDigest,
startedAtMs: admission.admittedAtMs,
commandDigest: command.commandDigest,
} satisfies Omit<ToolExecutionStartBarrierRecord, 'barrierDigest'>);
return normalizeToolExecutionStartBarrierRecord({
...unsigned,
barrierDigest: hash(BARRIER_DIGEST_DOMAIN, unsigned),
});
}
export function normalizeToolExecutionStartBarrierRecord(
value: ToolExecutionStartBarrierRecord,
): Readonly<ToolExecutionStartBarrierRecord> {
const record = dataRecord(value, 'start barrier');
exactKeys(
record,
[
'actionDigest',
'actionRef',
'adapter',
'adapterDigest',
'admissionDigest',
'approvalDispatchDigest',
'approvalDispatchId',
'approvalRequestId',
'auditContract',
'auditContractDigest',
'auditEventId',
'auditReceiptDigest',
'barrierDigest',
'bindingDigest',
'commandDigest',
'definitionDigest',
'executionClass',
'invocationArtifact',
'planDigest',
'policyFence',
'previousStepRunDigest',
'previousStepRunVersion',
'profile',
'projectId',
'previewArtifact',
'redactionContract',
'redactionContractDigest',
'requestedBy',
'runEventId',
'runId',
'schema',
'snapshotDigest',
'spanId',
'startId',
'startedAtMs',
'startedStepRunDigest',
'startedStepRunVersion',
'stepRunId',
'stepRunMutationDigest',
'stepRunMutationId',
'timeoutSeconds',
'traceDigest',
'traceId',
],
'start barrier',
);
if (value.schema !== TOOL_EXECUTION_START_BARRIER_SCHEMA) {
return invalid('start barrier schema is invalid');
}
const approvalRequestId =
value.approvalRequestId === null
? null
: identifier(value.approvalRequestId, 'approval request id');
const approvalDispatchId =
value.approvalDispatchId === null
? null
: identifier(value.approvalDispatchId, 'approval dispatch id');
const approvalDispatchDigest =
value.approvalDispatchDigest === null
? null
: digest(value.approvalDispatchDigest, 'approval dispatch digest');
if (
(approvalRequestId === null) !== (approvalDispatchId === null) ||
(approvalDispatchId === null) !== (approvalDispatchDigest === null)
) {
return invalid('start barrier approval binding is incomplete');
}
if (!TRUSTED_TOOL_DEPLOYMENT_PROFILES.includes(value.profile)) {
return invalid('start barrier profile is invalid');
}
if (!TRUSTED_TOOL_EXECUTION_CLASSES.includes(value.executionClass)) {
return invalid('start barrier execution class is invalid');
}
let requestedBy: Readonly<SecuritySubject>;
try {
requestedBy = normalizeProjectPolicySubject(value.requestedBy);
} catch {
return invalid('start barrier subject is invalid');
}
const normalized = Object.freeze({
schema: TOOL_EXECUTION_START_BARRIER_SCHEMA,
startId: identifier(value.startId, 'start id'),
projectId: identifier(value.projectId, 'project id'),
runId: identifier(value.runId, 'Run id'),
stepRunId: identifier(value.stepRunId, 'StepRun id'),
actionRef: actionRef(value.actionRef),
planDigest: digest(value.planDigest, 'plan digest'),
actionDigest: digest(value.actionDigest, 'action digest'),
snapshotDigest: digest(value.snapshotDigest, 'snapshot digest'),
definitionDigest: digest(value.definitionDigest, 'definition digest'),
bindingDigest: digest(value.bindingDigest, 'binding digest'),
admissionDigest: digest(value.admissionDigest, 'admission digest'),
invocationArtifact: normalizeToolInvocationInputArtifactReference(
value.invocationArtifact,
),
previewArtifact: normalizeToolInvocationPreviewArtifactReference(
value.previewArtifact,
),
requestedBy,
profile: value.profile,
adapter: normalizeContractIdentity(value.adapter, 'adapter identity'),
adapterDigest: digest(value.adapterDigest, 'adapter digest'),
redactionContract: normalizeContractIdentity(
value.redactionContract,
'redaction contract identity',
),
redactionContractDigest: digest(
value.redactionContractDigest,
'redaction contract digest',
),
auditContract: normalizeContractIdentity(
value.auditContract,
'audit contract identity',
),
auditContractDigest: digest(
value.auditContractDigest,
'audit contract digest',
),
executionClass: value.executionClass,
timeoutSeconds: integer(
value.timeoutSeconds,
1,
60 * 60,
'timeout',
),
policyFence: normalizeFence(value.policyFence),
approvalRequestId,
approvalDispatchId,
approvalDispatchDigest,
previousStepRunVersion: integer(
value.previousStepRunVersion,
1,
2_147_483_646,
'previous StepRun version',
),
previousStepRunDigest: digest(
value.previousStepRunDigest,
'previous StepRun digest',
),
startedStepRunVersion: integer(
value.startedStepRunVersion,
2,
2_147_483_647,
'started StepRun version',
),
startedStepRunDigest: digest(
value.startedStepRunDigest,
'started StepRun digest',
),
stepRunMutationId: identifier(
value.stepRunMutationId,
'StepRun mutation id',
),
stepRunMutationDigest: digest(
value.stepRunMutationDigest,
'StepRun mutation digest',
),
runEventId: identifier(value.runEventId, 'Run event id'),
traceId: traceIdentity(value.traceId, TRACE_ID_PATTERN, 'trace id'),
spanId: traceIdentity(value.spanId, SPAN_ID_PATTERN, 'span id'),
traceDigest: digest(value.traceDigest, 'trace digest'),
auditEventId: traceIdentity(
value.auditEventId,
UUID_V4_PATTERN,
'audit event id',
),
auditReceiptDigest: digest(
value.auditReceiptDigest,
'audit receipt digest',
),
startedAtMs: integer(
value.startedAtMs,
0,
Number.MAX_SAFE_INTEGER,
'start time',
),
commandDigest: digest(value.commandDigest, 'command digest'),
barrierDigest: digest(value.barrierDigest, 'barrier digest'),
});
if (
trustedToolContractIdentityDigest(normalized.adapter) !==
normalized.adapterDigest ||
trustedToolContractIdentityDigest(normalized.redactionContract) !==
normalized.redactionContractDigest ||
trustedToolContractIdentityDigest(normalized.auditContract) !==
normalized.auditContractDigest ||
normalized.previewArtifact.actionDigest !== normalized.actionDigest ||
normalized.previewArtifact.redactionContractDigest !==
normalized.redactionContractDigest ||
normalized.startedStepRunVersion !==
normalized.previousStepRunVersion + 1 ||
hash(BARRIER_DIGEST_DOMAIN, barrierUnsigned(normalized)) !==
normalized.barrierDigest ||
Buffer.byteLength(JSON.stringify(normalized), 'utf8') >
MAX_TOOL_EXECUTION_START_BARRIER_BYTES
) {
return invalid('start barrier digest, version or size is invalid');
}
return normalized;
}
@@ -0,0 +1,861 @@
import {
createCipheriv,
createDecipheriv,
createHash,
randomBytes,
} from 'node:crypto';
import { normalizeProjectPolicySubject } from '../security/project-policy/projectPolicy';
import type { SecuritySubject } from '../security/security';
import {
MAX_TOOL_INPUT_BYTES,
ToolDefinitionRegistry,
type ToolJsonValue,
} from './tool-registry/toolRegistry';
export const TOOL_INVOCATION_INPUT_ARTIFACT_SCHEMA =
'qinglong/tool-invocation-input-artifact@v1' as const;
export const TOOL_INVOCATION_PREVIEW_ARTIFACT_SCHEMA =
'qinglong/tool-invocation-preview-artifact@v1' as const;
export const TOOL_INVOCATION_ARTIFACT_ALGORITHM = 'aes-256-gcm' as const;
export const MAX_TOOL_INVOCATION_ARTIFACT_ID_BYTES = 128;
export const MAX_TOOL_INVOCATION_ARTIFACT_KEY_ID_BYTES = 128;
export const MAX_TOOL_INVOCATION_PREVIEW_ARTIFACT_BYTES = 8 * 1024;
export const MAX_TOOL_INVOCATION_INPUT_ARTIFACT_JSON_BYTES = 96 * 1024;
export const MAX_TOOL_INVOCATION_PREVIEW_ARTIFACT_JSON_BYTES = 16 * 1024;
export interface ToolInvocationPreviewField {
readonly kind: 'count' | 'identifier' | 'redacted' | 'text';
readonly label: string;
readonly value: string | null;
}
export interface ToolInvocationPreviewDocument {
readonly title: string;
readonly summary: string;
readonly fields: readonly Readonly<ToolInvocationPreviewField>[];
readonly warnings: readonly string[];
}
export interface ToolInvocationInputArtifact {
readonly schema: typeof TOOL_INVOCATION_INPUT_ARTIFACT_SCHEMA;
readonly artifactId: string;
readonly projectId: string;
readonly actionRef: string;
readonly requestedBy: Readonly<SecuritySubject>;
readonly tool: Readonly<{ name: string; version: string }>;
readonly inputDigest: string;
readonly invocationActionDigest: string;
readonly keyId: string;
readonly algorithm: typeof TOOL_INVOCATION_ARTIFACT_ALGORITHM;
readonly nonce: string;
readonly ciphertext: string;
readonly authTag: string;
readonly plaintextBytes: number;
readonly sealedAtMs: number;
readonly artifactDigest: string;
}
export interface ToolInvocationPreviewArtifact {
readonly schema: typeof TOOL_INVOCATION_PREVIEW_ARTIFACT_SCHEMA;
readonly artifactId: string;
readonly projectId: string;
readonly actionRef: string;
readonly actionDigest: string;
readonly redactionContractDigest: string;
readonly preview: Readonly<ToolInvocationPreviewDocument>;
readonly previewDigest: string;
readonly byteLength: number;
readonly sealedAtMs: number;
readonly artifactDigest: string;
}
export interface ToolInvocationInputArtifactReference {
readonly artifactId: string;
readonly artifactDigest: string;
readonly inputDigest: string;
readonly keyId: string;
readonly algorithm: typeof TOOL_INVOCATION_ARTIFACT_ALGORITHM;
readonly plaintextBytes: number;
}
export interface ToolInvocationPreviewArtifactReference {
readonly artifactId: string;
readonly artifactDigest: string;
readonly actionDigest: string;
readonly previewDigest: string;
readonly redactionContractDigest: string;
readonly byteLength: number;
}
export interface ToolInvocationArtifactKeyMaterial {
readonly keyId: string;
/** Exactly 32 bytes. The consumer owns this copy and must wipe it. */
readonly key: Uint8Array;
}
export interface ToolInvocationArtifactKeyProvider {
active(): Promise<ToolInvocationArtifactKeyMaterial>;
resolve(keyId: string): Promise<ToolInvocationArtifactKeyMaterial | null>;
}
export interface ToolInvocationArtifactRepository {
put(
inputArtifact: ToolInvocationInputArtifact,
previewArtifact: ToolInvocationPreviewArtifact,
): Promise<Readonly<{ status: 'inserted' | 'existing' }>>;
findInput(
artifactId: string,
): Promise<Readonly<ToolInvocationInputArtifact> | null>;
findPreview(
artifactId: string,
): Promise<Readonly<ToolInvocationPreviewArtifact> | null>;
}
export class InvalidToolInvocationArtifactError extends TypeError {
readonly code = 'TOOL_INVOCATION_ARTIFACT_INVALID';
constructor(message: string) {
super(`Tool invocation Artifact is invalid: ${message}`);
this.name = 'InvalidToolInvocationArtifactError';
}
}
export class ToolInvocationArtifactUnavailableError extends Error {
readonly code = 'TOOL_INVOCATION_ARTIFACT_UNAVAILABLE';
constructor(options?: ErrorOptions) {
super('Tool invocation Artifact is unavailable', options);
this.name = 'ToolInvocationArtifactUnavailableError';
}
}
export class ToolInvocationArtifactConflictError extends Error {
readonly code = 'TOOL_INVOCATION_ARTIFACT_CONFLICT';
constructor() {
super('Tool invocation Artifact identity is bound to different content');
this.name = 'ToolInvocationArtifactConflictError';
}
}
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const ACTION_REF_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$/;
const TOOL_NAME_PATTERN = /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/;
const VERSION_PATTERN = /^[0-9A-Za-z][0-9A-Za-z.+-]{0,63}$/;
const KEY_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;
const WARNING_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
const PREVIEW_FIELD_KINDS = [
'count',
'identifier',
'redacted',
'text',
] as const;
const INPUT_DIGEST_DOMAIN = Buffer.alloc(0);
const INPUT_ARTIFACT_DIGEST_DOMAIN = Buffer.from(
'qinglong/tool-invocation-input-artifact-digest@v1\0',
'utf8',
);
const INPUT_ARTIFACT_AAD_DOMAIN = Buffer.from(
'qinglong/tool-invocation-input-artifact-aad@v1\0',
'utf8',
);
const PREVIEW_DIGEST_DOMAIN = Buffer.from(
'qinglong/trusted-tool-invocation-preview-digest@v1\0',
'utf8',
);
const PREVIEW_ARTIFACT_DIGEST_DOMAIN = Buffer.from(
'qinglong/tool-invocation-preview-artifact-digest@v1\0',
'utf8',
);
function invalid(message: string): never {
throw new InvalidToolInvocationArtifactError(message);
}
function hash(domain: Buffer, value: unknown): string {
return createHash('sha256')
.update(domain)
.update(JSON.stringify(value))
.digest('hex');
}
function dataRecord(value: unknown, label: string): Record<string, unknown> {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
(Object.getPrototypeOf(value) !== Object.prototype &&
Object.getPrototypeOf(value) !== null)
) {
return invalid(`${label} must be an object`);
}
return value as Record<string, unknown>;
}
function exactKeys(
value: object,
expected: readonly string[],
label: string,
): void {
const actual = Reflect.ownKeys(value);
const allowed = new Set(expected);
if (
actual.length !== expected.length ||
actual.some((key) => typeof key !== 'string' || !allowed.has(key)) ||
expected.some((key) => !actual.includes(key))
) {
invalid(`${label} shape is invalid`);
}
}
function identifier(value: unknown, label: string): string {
if (typeof value !== 'string' || !IDENTIFIER_PATTERN.test(value)) {
return invalid(`${label} is invalid`);
}
return value;
}
function actionRef(value: unknown): string {
if (typeof value !== 'string' || !ACTION_REF_PATTERN.test(value)) {
return invalid('action reference is invalid');
}
return value;
}
function digest(value: unknown, label: string): string {
if (typeof value !== 'string' || !DIGEST_PATTERN.test(value)) {
return invalid(`${label} is invalid`);
}
return value;
}
function timestamp(value: unknown, label: string): number {
if (!Number.isSafeInteger(value) || (value as number) < 0) {
return invalid(`${label} is invalid`);
}
return value as number;
}
function boundedInteger(
value: unknown,
maximum: number,
label: string,
): number {
if (
!Number.isSafeInteger(value) ||
(value as number) < 0 ||
(value as number) > maximum
) {
return invalid(`${label} is invalid`);
}
return value as number;
}
function boundedText(
value: unknown,
maximumBytes: number,
label: string,
): string {
if (
typeof value !== 'string' ||
value.length === 0 ||
CONTROL_PATTERN.test(value) ||
Buffer.byteLength(value, 'utf8') > maximumBytes
) {
return invalid(`${label} is invalid`);
}
return value;
}
export function normalizeToolInvocationPreviewDocument(
value: ToolInvocationPreviewDocument,
): Readonly<ToolInvocationPreviewDocument> {
const record = dataRecord(value, 'preview document');
exactKeys(
record,
['fields', 'summary', 'title', 'warnings'],
'preview document',
);
if (
!Array.isArray(value.fields) ||
value.fields.length > 16 ||
!Array.isArray(value.warnings) ||
value.warnings.length > 8
) {
return invalid('preview document collections are invalid');
}
const fields = value.fields.map((fieldValue) => {
const field = dataRecord(fieldValue, 'preview field');
exactKeys(field, ['kind', 'label', 'value'], 'preview field');
if (!PREVIEW_FIELD_KINDS.includes(fieldValue.kind)) {
return invalid('preview field kind is invalid');
}
if (
(fieldValue.kind === 'redacted' && fieldValue.value !== null) ||
(fieldValue.kind !== 'redacted' && fieldValue.value === null)
) {
return invalid('preview field redaction is invalid');
}
return Object.freeze({
kind: fieldValue.kind,
label: boundedText(fieldValue.label, 128, 'preview field label'),
value:
fieldValue.value === null
? null
: boundedText(fieldValue.value, 512, 'preview field value'),
});
});
const warnings = value.warnings.map((warning) => {
if (typeof warning !== 'string' || !WARNING_PATTERN.test(warning)) {
return invalid('preview warning is invalid');
}
return warning;
});
if (new Set(warnings).size !== warnings.length) {
return invalid('preview warnings are duplicated');
}
return Object.freeze({
title: boundedText(value.title, 256, 'preview title'),
summary: boundedText(value.summary, 2048, 'preview summary'),
fields: Object.freeze(fields),
warnings: Object.freeze([...warnings].sort()),
});
}
function toolIdentity(
value: Readonly<{ name: string; version: string }>,
): Readonly<{ name: string; version: string }> {
const record = dataRecord(value, 'Tool identity');
exactKeys(record, ['name', 'version'], 'Tool identity');
if (
typeof value.name !== 'string' ||
!TOOL_NAME_PATTERN.test(value.name) ||
typeof value.version !== 'string' ||
!VERSION_PATTERN.test(value.version)
) {
return invalid('Tool identity is invalid');
}
return Object.freeze({ name: value.name, version: value.version });
}
function base64url(
value: unknown,
label: string,
expectedBytes?: number,
): Buffer {
if (
typeof value !== 'string' ||
value.length === 0 ||
!BASE64URL_PATTERN.test(value)
) {
return invalid(`${label} is invalid`);
}
const bytes = Buffer.from(value, 'base64url');
if (
bytes.toString('base64url') !== value ||
(expectedBytes !== undefined && bytes.length !== expectedBytes)
) {
bytes.fill(0);
return invalid(`${label} is invalid`);
}
return bytes;
}
function ownedKey(value: Uint8Array): Buffer {
if (!(value instanceof Uint8Array) || value.byteLength !== 32) {
throw new ToolInvocationArtifactUnavailableError();
}
return Buffer.from(value);
}
function inputArtifactAad(
value: Omit<
ToolInvocationInputArtifact,
'artifactDigest' | 'authTag' | 'ciphertext' | 'nonce'
>,
): Buffer {
return Buffer.concat([
INPUT_ARTIFACT_AAD_DOMAIN,
Buffer.from(JSON.stringify(value), 'utf8'),
]);
}
export function normalizeToolInvocationInputArtifact(
value: ToolInvocationInputArtifact,
): Readonly<ToolInvocationInputArtifact> {
const record = dataRecord(value, 'input Artifact');
exactKeys(
record,
[
'actionRef',
'algorithm',
'artifactDigest',
'artifactId',
'authTag',
'ciphertext',
'inputDigest',
'invocationActionDigest',
'keyId',
'nonce',
'plaintextBytes',
'projectId',
'requestedBy',
'schema',
'sealedAtMs',
'tool',
],
'input Artifact',
);
if (
value.schema !== TOOL_INVOCATION_INPUT_ARTIFACT_SCHEMA ||
value.algorithm !== TOOL_INVOCATION_ARTIFACT_ALGORITHM ||
typeof value.keyId !== 'string' ||
!KEY_ID_PATTERN.test(value.keyId)
) {
return invalid('input Artifact schema, algorithm or key is invalid');
}
const nonce = base64url(value.nonce, 'input Artifact nonce', 12);
const ciphertext = base64url(value.ciphertext, 'input Artifact ciphertext');
const authTag = base64url(value.authTag, 'input Artifact auth tag', 16);
const plaintextBytes = boundedInteger(
value.plaintextBytes,
MAX_TOOL_INPUT_BYTES,
'input Artifact plaintext bytes',
);
try {
if (ciphertext.length !== plaintextBytes) {
return invalid('input Artifact ciphertext length does not match');
}
} finally {
nonce.fill(0);
ciphertext.fill(0);
authTag.fill(0);
}
const unsigned = Object.freeze({
schema: TOOL_INVOCATION_INPUT_ARTIFACT_SCHEMA,
artifactId: identifier(value.artifactId, 'input Artifact id'),
projectId: identifier(value.projectId, 'input Artifact project id'),
actionRef: actionRef(value.actionRef),
requestedBy: normalizeProjectPolicySubject(value.requestedBy),
tool: toolIdentity(value.tool),
inputDigest: digest(value.inputDigest, 'input digest'),
invocationActionDigest: digest(
value.invocationActionDigest,
'invocation action digest',
),
keyId: value.keyId,
algorithm: TOOL_INVOCATION_ARTIFACT_ALGORITHM,
nonce: value.nonce,
ciphertext: value.ciphertext,
authTag: value.authTag,
plaintextBytes,
sealedAtMs: timestamp(value.sealedAtMs, 'input Artifact seal time'),
} satisfies Omit<ToolInvocationInputArtifact, 'artifactDigest'>);
const artifactDigest = digest(value.artifactDigest, 'input Artifact digest');
if (
hash(INPUT_ARTIFACT_DIGEST_DOMAIN, unsigned) !== artifactDigest ||
Buffer.byteLength(JSON.stringify({ ...unsigned, artifactDigest }), 'utf8') >
MAX_TOOL_INVOCATION_INPUT_ARTIFACT_JSON_BYTES
) {
return invalid('input Artifact digest or size does not match');
}
return Object.freeze({ ...unsigned, artifactDigest });
}
export function createToolInvocationInputArtifact(
inputValue: Readonly<{
artifactId: string;
projectId: string;
actionRef: string;
requestedBy: Readonly<SecuritySubject>;
tool: Readonly<{ name: string; version: string }>;
input: ToolJsonValue;
inputDigest: string;
invocationActionDigest: string;
keyId: string;
key: Uint8Array;
sealedAtMs: number;
}>,
nonceFactory: () => Uint8Array = () => randomBytes(12),
): Readonly<ToolInvocationInputArtifact> {
const inputDigest = digest(inputValue.inputDigest, 'input digest');
if (hash(INPUT_DIGEST_DOMAIN, inputValue.input) !== inputDigest) {
return invalid('input digest does not match plaintext');
}
const plaintext = Buffer.from(JSON.stringify(inputValue.input), 'utf8');
if (plaintext.length > MAX_TOOL_INPUT_BYTES) {
plaintext.fill(0);
return invalid('input plaintext is too large');
}
const key = ownedKey(inputValue.key);
let nonce: Buffer | undefined;
try {
nonce = Buffer.from(nonceFactory());
if (nonce.length !== 12) {
throw new ToolInvocationArtifactUnavailableError();
}
const metadata = Object.freeze({
schema: TOOL_INVOCATION_INPUT_ARTIFACT_SCHEMA,
artifactId: identifier(inputValue.artifactId, 'input Artifact id'),
projectId: identifier(inputValue.projectId, 'input Artifact project id'),
actionRef: actionRef(inputValue.actionRef),
requestedBy: normalizeProjectPolicySubject(inputValue.requestedBy),
tool: toolIdentity(inputValue.tool),
inputDigest,
invocationActionDigest: digest(
inputValue.invocationActionDigest,
'invocation action digest',
),
keyId:
typeof inputValue.keyId === 'string' &&
KEY_ID_PATTERN.test(inputValue.keyId)
? inputValue.keyId
: invalid('input Artifact key id is invalid'),
algorithm: TOOL_INVOCATION_ARTIFACT_ALGORITHM,
plaintextBytes: plaintext.length,
sealedAtMs: timestamp(inputValue.sealedAtMs, 'input Artifact seal time'),
});
const cipher = createCipheriv(
TOOL_INVOCATION_ARTIFACT_ALGORITHM,
key,
nonce,
{ authTagLength: 16 },
);
const aad = inputArtifactAad(metadata);
try {
cipher.setAAD(aad);
} finally {
aad.fill(0);
}
const ciphertext = Buffer.concat([
cipher.update(plaintext),
cipher.final(),
]);
try {
const unsigned = Object.freeze({
schema: metadata.schema,
artifactId: metadata.artifactId,
projectId: metadata.projectId,
actionRef: metadata.actionRef,
requestedBy: metadata.requestedBy,
tool: metadata.tool,
inputDigest: metadata.inputDigest,
invocationActionDigest: metadata.invocationActionDigest,
keyId: metadata.keyId,
algorithm: metadata.algorithm,
nonce: nonce.toString('base64url'),
ciphertext: ciphertext.toString('base64url'),
authTag: cipher.getAuthTag().toString('base64url'),
plaintextBytes: metadata.plaintextBytes,
sealedAtMs: metadata.sealedAtMs,
});
return normalizeToolInvocationInputArtifact({
...unsigned,
artifactDigest: hash(INPUT_ARTIFACT_DIGEST_DOMAIN, unsigned),
});
} finally {
ciphertext.fill(0);
}
} catch (cause) {
if (
cause instanceof InvalidToolInvocationArtifactError ||
cause instanceof ToolInvocationArtifactUnavailableError
) {
throw cause;
}
throw new ToolInvocationArtifactUnavailableError({ cause });
} finally {
key.fill(0);
plaintext.fill(0);
nonce?.fill(0);
}
}
export function openToolInvocationInputArtifact(
artifactValue: ToolInvocationInputArtifact,
keyValue: Uint8Array,
registry: ToolDefinitionRegistry,
): ToolJsonValue {
const artifact = normalizeToolInvocationInputArtifact(artifactValue);
if (!(registry instanceof ToolDefinitionRegistry)) {
return invalid('Tool registry is invalid');
}
const key = ownedKey(keyValue);
const nonce = base64url(artifact.nonce, 'input Artifact nonce', 12);
const ciphertext = base64url(
artifact.ciphertext,
'input Artifact ciphertext',
);
const authTag = base64url(artifact.authTag, 'input Artifact auth tag', 16);
const metadata = {
schema: artifact.schema,
artifactId: artifact.artifactId,
projectId: artifact.projectId,
actionRef: artifact.actionRef,
requestedBy: artifact.requestedBy,
tool: artifact.tool,
inputDigest: artifact.inputDigest,
invocationActionDigest: artifact.invocationActionDigest,
keyId: artifact.keyId,
algorithm: artifact.algorithm,
plaintextBytes: artifact.plaintextBytes,
sealedAtMs: artifact.sealedAtMs,
};
const aad = inputArtifactAad(metadata);
let plaintext: Buffer | undefined;
try {
const decipher = createDecipheriv(
TOOL_INVOCATION_ARTIFACT_ALGORITHM,
key,
nonce,
{ authTagLength: 16 },
);
decipher.setAAD(aad);
decipher.setAuthTag(authTag);
plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
if (plaintext.length !== artifact.plaintextBytes) {
throw new ToolInvocationArtifactUnavailableError();
}
const parsed = JSON.parse(plaintext.toString('utf8')) as unknown;
const normalized = registry.normalizeInput(
artifact.tool.name,
artifact.tool.version,
parsed,
);
if (hash(INPUT_DIGEST_DOMAIN, normalized) !== artifact.inputDigest) {
throw new ToolInvocationArtifactUnavailableError();
}
return normalized;
} catch (cause) {
if (cause instanceof ToolInvocationArtifactUnavailableError) throw cause;
throw new ToolInvocationArtifactUnavailableError({ cause });
} finally {
key.fill(0);
nonce.fill(0);
ciphertext.fill(0);
authTag.fill(0);
aad.fill(0);
plaintext?.fill(0);
}
}
export function toolInvocationInputArtifactReference(
value: ToolInvocationInputArtifact,
): Readonly<ToolInvocationInputArtifactReference> {
const artifact = normalizeToolInvocationInputArtifact(value);
return Object.freeze({
artifactId: artifact.artifactId,
artifactDigest: artifact.artifactDigest,
inputDigest: artifact.inputDigest,
keyId: artifact.keyId,
algorithm: artifact.algorithm,
plaintextBytes: artifact.plaintextBytes,
});
}
export function normalizeToolInvocationInputArtifactReference(
value: ToolInvocationInputArtifactReference,
): Readonly<ToolInvocationInputArtifactReference> {
const record = dataRecord(value, 'input Artifact reference');
exactKeys(
record,
[
'algorithm',
'artifactDigest',
'artifactId',
'inputDigest',
'keyId',
'plaintextBytes',
],
'input Artifact reference',
);
if (
value.algorithm !== TOOL_INVOCATION_ARTIFACT_ALGORITHM ||
typeof value.keyId !== 'string' ||
!KEY_ID_PATTERN.test(value.keyId)
) {
return invalid('input Artifact reference algorithm or key is invalid');
}
return Object.freeze({
artifactId: identifier(value.artifactId, 'input Artifact id'),
artifactDigest: digest(value.artifactDigest, 'input Artifact digest'),
inputDigest: digest(value.inputDigest, 'input digest'),
keyId: value.keyId,
algorithm: TOOL_INVOCATION_ARTIFACT_ALGORITHM,
plaintextBytes: boundedInteger(
value.plaintextBytes,
MAX_TOOL_INPUT_BYTES,
'input Artifact plaintext bytes',
),
});
}
function previewArtifactUnsigned(
value: ToolInvocationPreviewArtifact,
): Omit<ToolInvocationPreviewArtifact, 'artifactDigest'> {
const { artifactDigest: _artifactDigest, ...unsigned } = value;
return unsigned;
}
export function createToolInvocationPreviewArtifact(
inputValue: Readonly<{
artifactId: string;
projectId: string;
actionRef: string;
actionDigest: string;
redactionContractDigest: string;
preview: Readonly<ToolInvocationPreviewDocument>;
sealedAtMs: number;
}>,
): Readonly<ToolInvocationPreviewArtifact> {
const actionDigest = digest(inputValue.actionDigest, 'action digest');
const preview = normalizeToolInvocationPreviewDocument(inputValue.preview);
const previewDigest = hash(PREVIEW_DIGEST_DOMAIN, {
actionDigest,
preview,
});
const byteLength = Buffer.byteLength(JSON.stringify(preview), 'utf8');
if (byteLength > MAX_TOOL_INVOCATION_PREVIEW_ARTIFACT_BYTES) {
return invalid('preview Artifact is too large');
}
const unsigned = Object.freeze({
schema: TOOL_INVOCATION_PREVIEW_ARTIFACT_SCHEMA,
artifactId: identifier(inputValue.artifactId, 'preview Artifact id'),
projectId: identifier(inputValue.projectId, 'preview Artifact project id'),
actionRef: actionRef(inputValue.actionRef),
actionDigest,
redactionContractDigest: digest(
inputValue.redactionContractDigest,
'redaction contract digest',
),
preview,
previewDigest,
byteLength,
sealedAtMs: timestamp(inputValue.sealedAtMs, 'preview Artifact seal time'),
} satisfies Omit<ToolInvocationPreviewArtifact, 'artifactDigest'>);
return Object.freeze({
...unsigned,
artifactDigest: hash(PREVIEW_ARTIFACT_DIGEST_DOMAIN, unsigned),
});
}
export function normalizeToolInvocationPreviewArtifact(
value: ToolInvocationPreviewArtifact,
): Readonly<ToolInvocationPreviewArtifact> {
const record = dataRecord(value, 'preview Artifact');
exactKeys(
record,
[
'actionDigest',
'actionRef',
'artifactDigest',
'artifactId',
'byteLength',
'preview',
'previewDigest',
'projectId',
'redactionContractDigest',
'schema',
'sealedAtMs',
],
'preview Artifact',
);
if (value.schema !== TOOL_INVOCATION_PREVIEW_ARTIFACT_SCHEMA) {
return invalid('preview Artifact schema is invalid');
}
const preview = normalizeToolInvocationPreviewDocument(value.preview);
const byteLength = boundedInteger(
value.byteLength,
MAX_TOOL_INVOCATION_PREVIEW_ARTIFACT_BYTES,
'preview Artifact bytes',
);
if (Buffer.byteLength(JSON.stringify(preview), 'utf8') !== byteLength) {
return invalid('preview Artifact byte length does not match');
}
const unsigned = Object.freeze({
schema: TOOL_INVOCATION_PREVIEW_ARTIFACT_SCHEMA,
artifactId: identifier(value.artifactId, 'preview Artifact id'),
projectId: identifier(value.projectId, 'preview Artifact project id'),
actionRef: actionRef(value.actionRef),
actionDigest: digest(value.actionDigest, 'action digest'),
redactionContractDigest: digest(
value.redactionContractDigest,
'redaction contract digest',
),
preview,
previewDigest: digest(value.previewDigest, 'preview digest'),
byteLength,
sealedAtMs: timestamp(value.sealedAtMs, 'preview Artifact seal time'),
} satisfies Omit<ToolInvocationPreviewArtifact, 'artifactDigest'>);
if (
hash(PREVIEW_DIGEST_DOMAIN, {
actionDigest: unsigned.actionDigest,
preview: unsigned.preview,
}) !== unsigned.previewDigest
) {
return invalid('preview digest does not match');
}
const artifactDigest = digest(
value.artifactDigest,
'preview Artifact digest',
);
if (
hash(PREVIEW_ARTIFACT_DIGEST_DOMAIN, unsigned) !== artifactDigest ||
Buffer.byteLength(JSON.stringify({ ...unsigned, artifactDigest }), 'utf8') >
MAX_TOOL_INVOCATION_PREVIEW_ARTIFACT_JSON_BYTES
) {
return invalid('preview Artifact digest or size does not match');
}
return Object.freeze({ ...unsigned, artifactDigest });
}
export function toolInvocationPreviewArtifactReference(
value: ToolInvocationPreviewArtifact,
): Readonly<ToolInvocationPreviewArtifactReference> {
const artifact = normalizeToolInvocationPreviewArtifact(value);
return Object.freeze({
artifactId: artifact.artifactId,
artifactDigest: artifact.artifactDigest,
actionDigest: artifact.actionDigest,
previewDigest: artifact.previewDigest,
redactionContractDigest: artifact.redactionContractDigest,
byteLength: artifact.byteLength,
});
}
export function normalizeToolInvocationPreviewArtifactReference(
value: ToolInvocationPreviewArtifactReference,
): Readonly<ToolInvocationPreviewArtifactReference> {
const record = dataRecord(value, 'preview Artifact reference');
exactKeys(
record,
[
'artifactDigest',
'artifactId',
'actionDigest',
'byteLength',
'previewDigest',
'redactionContractDigest',
],
'preview Artifact reference',
);
return Object.freeze({
artifactId: identifier(value.artifactId, 'preview Artifact id'),
artifactDigest: digest(value.artifactDigest, 'preview Artifact digest'),
actionDigest: digest(value.actionDigest, 'action digest'),
previewDigest: digest(value.previewDigest, 'preview digest'),
redactionContractDigest: digest(
value.redactionContractDigest,
'redaction contract digest',
),
byteLength: boundedInteger(
value.byteLength,
MAX_TOOL_INVOCATION_PREVIEW_ARTIFACT_BYTES,
'preview Artifact bytes',
),
});
}
@@ -0,0 +1,865 @@
import { createHash, createHmac } from 'node:crypto';
export const TOOL_RESULT_KEY_CATALOG_SCHEMA =
'qinglong/tool-result-key-catalog@v1' as const;
export const TOOL_RESULT_KEY_CATALOG_COMMAND_SCHEMA =
'qinglong/tool-result-key-catalog-command@v1' as const;
export const MAX_TOOL_RESULT_DECRYPTABLE_KEYS = 16;
export const MAX_TOOL_RESULT_CATALOG_KEYS = 64;
export type ToolResultKeyState = 'active' | 'decrypt_only' | 'retired' | 'lost';
export type ToolResultKeyCatalogMutationKind =
| 'bootstrap'
| 'rotate'
| 'retire'
| 'mark_lost'
| 'restore';
export interface ToolResultKeyCatalogEntry {
readonly keyId: string;
readonly state: ToolResultKeyState;
readonly materialProof: string;
readonly introducedGeneration: number;
readonly stateChangedGeneration: number;
readonly retirementReceiptDigest: string | null;
}
export interface ToolResultKeyCatalogSnapshot {
readonly schema: typeof TOOL_RESULT_KEY_CATALOG_SCHEMA;
readonly generation: number;
readonly previousCatalogDigest: string | null;
readonly activeKeyId: string | null;
readonly keys: readonly Readonly<ToolResultKeyCatalogEntry>[];
readonly mutationKind: ToolResultKeyCatalogMutationKind;
readonly mutationId: string;
readonly catalogDigest: string;
}
export interface ToolResultKeyCatalogRecord
extends ToolResultKeyCatalogSnapshot {
readonly committedAtMs: number;
}
export interface ToolResultKeyCatalogFence {
readonly generation: number;
readonly catalogDigest: string;
readonly keyId: string;
readonly materialProof: string;
}
export interface ToolResultKeyCatalogCommand {
readonly schema: typeof TOOL_RESULT_KEY_CATALOG_COMMAND_SCHEMA;
readonly expectedGeneration: number;
readonly expectedCatalogDigest: string | null;
readonly next: Readonly<ToolResultKeyCatalogSnapshot>;
readonly commandDigest: string;
}
export interface CommitToolResultKeyCatalogResult {
readonly status: 'created' | 'existing';
readonly catalog: Readonly<ToolResultKeyCatalogRecord>;
}
export interface ToolResultKeyCatalogReader {
findCurrent(): Promise<Readonly<ToolResultKeyCatalogRecord> | null>;
}
export interface ToolResultKeyCatalogRepository
extends ToolResultKeyCatalogReader {
append(
command: Readonly<ToolResultKeyCatalogCommand>,
): Promise<Readonly<CommitToolResultKeyCatalogResult>>;
}
export class InvalidToolResultKeyCatalogError extends TypeError {
readonly code = 'TOOL_RESULT_KEY_CATALOG_INVALID';
constructor(message: string) {
super(`Tool result key catalog is invalid: ${message}`);
this.name = 'InvalidToolResultKeyCatalogError';
}
}
export class ToolResultKeyCatalogConflictError extends Error {
readonly code = 'TOOL_RESULT_KEY_CATALOG_CONFLICT';
constructor() {
super('Tool result key catalog conflicts with durable state');
this.name = 'ToolResultKeyCatalogConflictError';
}
}
export class ToolResultKeyCatalogUnavailableError extends Error {
readonly code = 'TOOL_RESULT_KEY_CATALOG_UNAVAILABLE';
constructor(options?: ErrorOptions) {
super('Tool result key catalog is unavailable', options);
this.name = 'ToolResultKeyCatalogUnavailableError';
}
}
export class ToolResultKeyLostError extends Error {
readonly code = 'TOOL_RESULT_KEY_LOST';
readonly keyId: string;
constructor(keyId: string) {
super('Tool result key material is lost');
this.name = 'ToolResultKeyLostError';
this.keyId = keyId;
}
}
const KEY_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
const IDENTITY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
const CATALOG_DIGEST_DOMAIN = Buffer.from(
'qinglong/tool-result-key-catalog-digest@v1\0',
'utf8',
);
const COMMAND_DIGEST_DOMAIN = Buffer.from(
'qinglong/tool-result-key-catalog-command-digest@v1\0',
'utf8',
);
const MATERIAL_PROOF_DOMAIN = Buffer.from(
'qinglong/tool-result-key-material-proof@v1\0',
'utf8',
);
function invalid(message: string): never {
throw new InvalidToolResultKeyCatalogError(message);
}
function hash(domain: Uint8Array, value: unknown): string {
return createHash('sha256')
.update(domain)
.update(JSON.stringify(value))
.digest('hex');
}
function exactKeys(
value: Record<string, unknown>,
expected: readonly string[],
label: string,
): void {
const actual = Object.keys(value).sort();
const sortedExpected = [...expected].sort();
if (
actual.length !== sortedExpected.length ||
actual.some((key, index) => key !== sortedExpected[index])
) {
invalid(`${label} shape is invalid`);
}
}
function record(value: unknown, label: string): Record<string, unknown> {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
Object.getPrototypeOf(value) !== Object.prototype
) {
return invalid(`${label} is not a plain object`);
}
return value as Record<string, unknown>;
}
function keyId(value: unknown): string {
if (typeof value !== 'string' || !KEY_ID_PATTERN.test(value)) {
return invalid('key id is invalid');
}
return value;
}
function identity(value: unknown, label: string): string {
if (typeof value !== 'string' || !IDENTITY_PATTERN.test(value)) {
return invalid(`${label} is invalid`);
}
return value;
}
function digest(value: unknown, label: string): string {
if (typeof value !== 'string' || !DIGEST_PATTERN.test(value)) {
return invalid(`${label} is invalid`);
}
return value;
}
function nullableDigest(value: unknown, label: string): string | null {
return value === null ? null : digest(value, label);
}
function generation(value: unknown, minimum = 1): number {
if (
!Number.isSafeInteger(value) ||
(value as number) < minimum ||
(value as number) > 2_147_483_647
) {
return invalid('generation is invalid');
}
return value as number;
}
function state(value: unknown): ToolResultKeyState {
if (
value !== 'active' &&
value !== 'decrypt_only' &&
value !== 'retired' &&
value !== 'lost'
) {
return invalid('key state is invalid');
}
return value;
}
function mutationKind(value: unknown): ToolResultKeyCatalogMutationKind {
if (
value !== 'bootstrap' &&
value !== 'rotate' &&
value !== 'retire' &&
value !== 'mark_lost' &&
value !== 'restore'
) {
return invalid('mutation kind is invalid');
}
return value;
}
function normalizedEntry(
value: ToolResultKeyCatalogEntry,
catalogGeneration: number,
): Readonly<ToolResultKeyCatalogEntry> {
const candidate = record(value, 'catalog key');
exactKeys(
candidate,
[
'introducedGeneration',
'keyId',
'materialProof',
'retirementReceiptDigest',
'state',
'stateChangedGeneration',
],
'catalog key',
);
const normalizedState = state(value.state);
const introducedGeneration = generation(value.introducedGeneration);
const stateChangedGeneration = generation(value.stateChangedGeneration);
if (
introducedGeneration > stateChangedGeneration ||
stateChangedGeneration > catalogGeneration
) {
return invalid('key generation fence is invalid');
}
const retirementReceiptDigest = nullableDigest(
value.retirementReceiptDigest,
'retirement receipt digest',
);
if ((normalizedState === 'retired') !== (retirementReceiptDigest !== null)) {
return invalid('retired key receipt is invalid');
}
return Object.freeze({
keyId: keyId(value.keyId),
state: normalizedState,
materialProof: digest(value.materialProof, 'material proof'),
introducedGeneration,
stateChangedGeneration,
retirementReceiptDigest,
});
}
function unsignedSnapshot(
value: Omit<ToolResultKeyCatalogSnapshot, 'catalogDigest'>,
): Omit<ToolResultKeyCatalogSnapshot, 'catalogDigest'> {
return Object.freeze(value);
}
export function normalizeToolResultKeyCatalogSnapshot(
value: ToolResultKeyCatalogSnapshot,
): Readonly<ToolResultKeyCatalogSnapshot> {
const candidate = record(value, 'catalog snapshot');
exactKeys(
candidate,
[
'activeKeyId',
'catalogDigest',
'generation',
'keys',
'mutationId',
'mutationKind',
'previousCatalogDigest',
'schema',
],
'catalog snapshot',
);
if (
value.schema !== TOOL_RESULT_KEY_CATALOG_SCHEMA ||
!Array.isArray(value.keys) ||
value.keys.length < 1 ||
value.keys.length > MAX_TOOL_RESULT_CATALOG_KEYS
) {
return invalid('catalog snapshot header is invalid');
}
const normalizedGeneration = generation(value.generation);
const keys = value.keys
.map((entry) => normalizedEntry(entry, normalizedGeneration))
.sort((left, right) => left.keyId.localeCompare(right.keyId));
if (
keys.some(
(entry, index) => index > 0 && entry.keyId === keys[index - 1]!.keyId,
)
) {
return invalid('catalog key id is duplicated');
}
const decryptable = keys.filter(
(entry) => entry.state === 'active' || entry.state === 'decrypt_only',
);
if (decryptable.length > MAX_TOOL_RESULT_DECRYPTABLE_KEYS) {
return invalid('decryptable key budget is exceeded');
}
const active = keys.filter((entry) => entry.state === 'active');
const activeKeyId =
value.activeKeyId === null ? null : keyId(value.activeKeyId);
if (
active.length > 1 ||
(activeKeyId === null && active.length !== 0) ||
(activeKeyId !== null &&
(active.length !== 1 || active[0]!.keyId !== activeKeyId))
) {
return invalid('active key projection is invalid');
}
const previousCatalogDigest = nullableDigest(
value.previousCatalogDigest,
'previous catalog digest',
);
if ((normalizedGeneration === 1) !== (previousCatalogDigest === null)) {
return invalid('previous catalog digest fence is invalid');
}
const unsigned = unsignedSnapshot({
schema: TOOL_RESULT_KEY_CATALOG_SCHEMA,
generation: normalizedGeneration,
previousCatalogDigest,
activeKeyId,
keys: Object.freeze(keys),
mutationKind: mutationKind(value.mutationKind),
mutationId: identity(value.mutationId, 'mutation id'),
});
const catalogDigest = digest(value.catalogDigest, 'catalog digest');
if (hash(CATALOG_DIGEST_DOMAIN, unsigned) !== catalogDigest) {
return invalid('catalog digest does not match');
}
return Object.freeze({ ...unsigned, catalogDigest });
}
export function normalizeToolResultKeyCatalogRecord(
value: ToolResultKeyCatalogRecord,
): Readonly<ToolResultKeyCatalogRecord> {
const candidate = record(value, 'catalog record');
exactKeys(
candidate,
[
'activeKeyId',
'catalogDigest',
'committedAtMs',
'generation',
'keys',
'mutationId',
'mutationKind',
'previousCatalogDigest',
'schema',
],
'catalog record',
);
if (!Number.isSafeInteger(value.committedAtMs) || value.committedAtMs < 0) {
return invalid('commit time is invalid');
}
const snapshot = normalizeToolResultKeyCatalogSnapshot({
schema: value.schema,
generation: value.generation,
previousCatalogDigest: value.previousCatalogDigest,
activeKeyId: value.activeKeyId,
keys: value.keys,
mutationKind: value.mutationKind,
mutationId: value.mutationId,
catalogDigest: value.catalogDigest,
});
return Object.freeze({ ...snapshot, committedAtMs: value.committedAtMs });
}
export function normalizeToolResultKeyCatalogFence(
value: ToolResultKeyCatalogFence,
): Readonly<ToolResultKeyCatalogFence> {
const candidate = record(value, 'catalog fence');
exactKeys(
candidate,
['catalogDigest', 'generation', 'keyId', 'materialProof'],
'catalog fence',
);
return Object.freeze({
generation: generation(value.generation),
catalogDigest: digest(value.catalogDigest, 'catalog digest'),
keyId: keyId(value.keyId),
materialProof: digest(value.materialProof, 'material proof'),
});
}
function snapshot(
input: Omit<ToolResultKeyCatalogSnapshot, 'catalogDigest' | 'schema'>,
): Readonly<ToolResultKeyCatalogSnapshot> {
const unsigned = unsignedSnapshot({
schema: TOOL_RESULT_KEY_CATALOG_SCHEMA,
...input,
keys: Object.freeze(
[...input.keys].sort((left, right) =>
left.keyId.localeCompare(right.keyId),
),
),
});
return normalizeToolResultKeyCatalogSnapshot({
...unsigned,
catalogDigest: hash(CATALOG_DIGEST_DOMAIN, unsigned),
});
}
function command(
expectedGeneration: number,
expectedCatalogDigest: string | null,
next: Readonly<ToolResultKeyCatalogSnapshot>,
): Readonly<ToolResultKeyCatalogCommand> {
const unsigned = Object.freeze({
schema: TOOL_RESULT_KEY_CATALOG_COMMAND_SCHEMA,
expectedGeneration,
expectedCatalogDigest,
next,
});
return normalizeToolResultKeyCatalogCommand({
...unsigned,
commandDigest: hash(COMMAND_DIGEST_DOMAIN, unsigned),
});
}
export function normalizeToolResultKeyCatalogCommand(
value: ToolResultKeyCatalogCommand,
): Readonly<ToolResultKeyCatalogCommand> {
const candidate = record(value, 'catalog command');
exactKeys(
candidate,
[
'commandDigest',
'expectedCatalogDigest',
'expectedGeneration',
'next',
'schema',
],
'catalog command',
);
if (
value.schema !== TOOL_RESULT_KEY_CATALOG_COMMAND_SCHEMA ||
!Number.isSafeInteger(value.expectedGeneration) ||
value.expectedGeneration < 0
) {
return invalid('catalog command header is invalid');
}
const expectedCatalogDigest = nullableDigest(
value.expectedCatalogDigest,
'expected catalog digest',
);
const next = normalizeToolResultKeyCatalogSnapshot(value.next);
if (
next.generation !== value.expectedGeneration + 1 ||
next.previousCatalogDigest !== expectedCatalogDigest ||
(value.expectedGeneration === 0) !==
(expectedCatalogDigest === null && next.mutationKind === 'bootstrap')
) {
return invalid('catalog command fence is invalid');
}
const unsigned = Object.freeze({
schema: TOOL_RESULT_KEY_CATALOG_COMMAND_SCHEMA,
expectedGeneration: value.expectedGeneration,
expectedCatalogDigest,
next,
});
const commandDigest = digest(value.commandDigest, 'command digest');
if (hash(COMMAND_DIGEST_DOMAIN, unsigned) !== commandDigest) {
return invalid('command digest does not match');
}
return Object.freeze({ ...unsigned, commandDigest });
}
export function toolResultKeyMaterialProof(
candidateKeyId: string,
value: Uint8Array,
): string {
const normalizedKeyId = keyId(candidateKeyId);
if (!(value instanceof Uint8Array) || value.byteLength !== 32) {
return invalid('key material is invalid');
}
const key = Buffer.from(value);
try {
return createHmac('sha256', key)
.update(MATERIAL_PROOF_DOMAIN)
.update(normalizedKeyId)
.digest('hex');
} finally {
key.fill(0);
}
}
export function createToolResultKeyCatalogBootstrapCommand(input: {
readonly keyId: string;
readonly materialProof: string;
readonly mutationId: string;
}): Readonly<ToolResultKeyCatalogCommand> {
const next = snapshot({
generation: 1,
previousCatalogDigest: null,
activeKeyId: keyId(input.keyId),
keys: Object.freeze([
Object.freeze({
keyId: keyId(input.keyId),
state: 'active' as const,
materialProof: digest(input.materialProof, 'material proof'),
introducedGeneration: 1,
stateChangedGeneration: 1,
retirementReceiptDigest: null,
}),
]),
mutationKind: 'bootstrap',
mutationId: identity(input.mutationId, 'mutation id'),
});
return command(0, null, next);
}
function currentRecord(
value: ToolResultKeyCatalogRecord,
): Readonly<ToolResultKeyCatalogRecord> {
return normalizeToolResultKeyCatalogRecord(value);
}
function nextKeys(
current: Readonly<ToolResultKeyCatalogRecord>,
): ToolResultKeyCatalogEntry[] {
return current.keys
.filter((entry) => entry.state !== 'retired')
.map((entry) => ({ ...entry }));
}
export function createToolResultKeyRotationCommand(
value: ToolResultKeyCatalogRecord,
input: {
readonly keyId: string;
readonly materialProof: string;
readonly mutationId: string;
},
): Readonly<ToolResultKeyCatalogCommand> {
const current = currentRecord(value);
const candidateKeyId = keyId(input.keyId);
if (current.keys.some((entry) => entry.keyId === candidateKeyId)) {
return invalid('rotation key id already exists');
}
const nextGeneration = current.generation + 1;
const keys = nextKeys(current).map((entry) =>
entry.state === 'active'
? {
...entry,
state: 'decrypt_only' as const,
stateChangedGeneration: nextGeneration,
}
: entry,
);
keys.push({
keyId: candidateKeyId,
state: 'active',
materialProof: digest(input.materialProof, 'material proof'),
introducedGeneration: nextGeneration,
stateChangedGeneration: nextGeneration,
retirementReceiptDigest: null,
});
const next = snapshot({
generation: nextGeneration,
previousCatalogDigest: current.catalogDigest,
activeKeyId: candidateKeyId,
keys: Object.freeze(keys),
mutationKind: 'rotate',
mutationId: identity(input.mutationId, 'mutation id'),
});
return command(current.generation, current.catalogDigest, next);
}
export function createToolResultKeyRetirementCommand(
value: ToolResultKeyCatalogRecord,
input: {
readonly keyId: string;
readonly retirementReceiptDigest: string;
readonly mutationId: string;
},
): Readonly<ToolResultKeyCatalogCommand> {
const current = currentRecord(value);
const candidateKeyId = keyId(input.keyId);
const receipt = digest(
input.retirementReceiptDigest,
'retirement receipt digest',
);
const target = current.keys.find((entry) => entry.keyId === candidateKeyId);
if (!target || target.state !== 'decrypt_only') {
return invalid('only a decrypt-only key can be retired');
}
const nextGeneration = current.generation + 1;
const keys = nextKeys(current).map((entry) =>
entry.keyId === candidateKeyId
? {
...entry,
state: 'retired' as const,
stateChangedGeneration: nextGeneration,
retirementReceiptDigest: receipt,
}
: entry,
);
const next = snapshot({
generation: nextGeneration,
previousCatalogDigest: current.catalogDigest,
activeKeyId: current.activeKeyId,
keys: Object.freeze(keys),
mutationKind: 'retire',
mutationId: identity(input.mutationId, 'mutation id'),
});
return command(current.generation, current.catalogDigest, next);
}
export function createToolResultKeyLostCommand(
value: ToolResultKeyCatalogRecord,
input: {
readonly keyId: string;
readonly mutationId: string;
},
): Readonly<ToolResultKeyCatalogCommand> {
const current = currentRecord(value);
const candidateKeyId = keyId(input.keyId);
const target = current.keys.find((entry) => entry.keyId === candidateKeyId);
if (
!target ||
(target.state !== 'active' && target.state !== 'decrypt_only')
) {
return invalid('only a decryptable key can be marked lost');
}
const nextGeneration = current.generation + 1;
const keys = nextKeys(current).map((entry) =>
entry.keyId === candidateKeyId
? {
...entry,
state: 'lost' as const,
stateChangedGeneration: nextGeneration,
}
: entry,
);
const next = snapshot({
generation: nextGeneration,
previousCatalogDigest: current.catalogDigest,
activeKeyId:
current.activeKeyId === candidateKeyId ? null : current.activeKeyId,
keys: Object.freeze(keys),
mutationKind: 'mark_lost',
mutationId: identity(input.mutationId, 'mutation id'),
});
return command(current.generation, current.catalogDigest, next);
}
export function createToolResultKeyRestoreCommand(
value: ToolResultKeyCatalogRecord,
input: {
readonly keyId: string;
readonly materialProof: string;
readonly mutationId: string;
},
): Readonly<ToolResultKeyCatalogCommand> {
const current = currentRecord(value);
const candidateKeyId = keyId(input.keyId);
const target = current.keys.find((entry) => entry.keyId === candidateKeyId);
if (
!target ||
target.state !== 'lost' ||
target.materialProof !== digest(input.materialProof, 'material proof')
) {
return invalid('lost key restore proof does not match');
}
const nextGeneration = current.generation + 1;
const restoredState = 'decrypt_only' as const;
const keys = nextKeys(current).map((entry) =>
entry.keyId === candidateKeyId
? {
...entry,
state: restoredState,
stateChangedGeneration: nextGeneration,
}
: entry,
);
const next = snapshot({
generation: nextGeneration,
previousCatalogDigest: current.catalogDigest,
activeKeyId: current.activeKeyId,
keys: Object.freeze(keys),
mutationKind: 'restore',
mutationId: identity(input.mutationId, 'mutation id'),
});
return command(current.generation, current.catalogDigest, next);
}
export function findToolResultKeyCatalogEntry(
value: ToolResultKeyCatalogRecord,
candidateKeyId: string,
): Readonly<ToolResultKeyCatalogEntry> | null {
const catalog = currentRecord(value);
const normalizedKeyId = keyId(candidateKeyId);
return catalog.keys.find((entry) => entry.keyId === normalizedKeyId) ?? null;
}
export function requireActiveToolResultKey(
value: ToolResultKeyCatalogRecord,
): Readonly<ToolResultKeyCatalogEntry> {
const catalog = currentRecord(value);
if (catalog.activeKeyId === null) {
throw new ToolResultKeyCatalogUnavailableError();
}
const entry = catalog.keys.find(
(candidate) => candidate.keyId === catalog.activeKeyId,
);
if (!entry || entry.state !== 'active') {
throw new ToolResultKeyCatalogUnavailableError();
}
return entry;
}
export function requireDecryptableToolResultKey(
value: ToolResultKeyCatalogRecord,
candidateKeyId: string,
): Readonly<ToolResultKeyCatalogEntry> {
const entry = findToolResultKeyCatalogEntry(value, candidateKeyId);
if (!entry) throw new ToolResultKeyCatalogUnavailableError();
if (entry.state === 'lost') throw new ToolResultKeyLostError(entry.keyId);
if (entry.state !== 'active' && entry.state !== 'decrypt_only') {
throw new ToolResultKeyCatalogUnavailableError();
}
return entry;
}
export function toolResultKeyCatalogFence(
value: ToolResultKeyCatalogRecord,
entryValue: ToolResultKeyCatalogEntry,
): Readonly<ToolResultKeyCatalogFence> {
const catalog = currentRecord(value);
const entry = normalizedEntry(entryValue, catalog.generation);
const stored = catalog.keys.find(
(candidate) => candidate.keyId === entry.keyId,
);
if (
!stored ||
JSON.stringify(stored) !== JSON.stringify(entry) ||
entry.state !== 'active' ||
catalog.activeKeyId !== entry.keyId
) {
return invalid('catalog fence key is not active');
}
return normalizeToolResultKeyCatalogFence({
generation: catalog.generation,
catalogDigest: catalog.catalogDigest,
keyId: entry.keyId,
materialProof: entry.materialProof,
});
}
export function assertToolResultKeyCatalogTransition(
currentValue: ToolResultKeyCatalogRecord | null,
commandValue: ToolResultKeyCatalogCommand,
): void {
const candidate = normalizeToolResultKeyCatalogCommand(commandValue);
let expected: Readonly<ToolResultKeyCatalogCommand>;
if (currentValue === null) {
if (
candidate.next.mutationKind !== 'bootstrap' ||
candidate.next.keys.length !== 1
) {
throw new ToolResultKeyCatalogConflictError();
}
const entry = candidate.next.keys[0]!;
expected = createToolResultKeyCatalogBootstrapCommand({
keyId: entry.keyId,
materialProof: entry.materialProof,
mutationId: candidate.next.mutationId,
});
} else {
const current = currentRecord(currentValue);
if (
candidate.expectedGeneration !== current.generation ||
candidate.expectedCatalogDigest !== current.catalogDigest
) {
throw new ToolResultKeyCatalogConflictError();
}
const changed = candidate.next.keys.filter((entry) => {
const previous = current.keys.find(
(candidateEntry) => candidateEntry.keyId === entry.keyId,
);
return !previous || JSON.stringify(previous) !== JSON.stringify(entry);
});
switch (candidate.next.mutationKind) {
case 'rotate': {
const added = changed.find(
(entry) =>
!current.keys.some(
(currentEntry) => currentEntry.keyId === entry.keyId,
),
);
if (!added) throw new ToolResultKeyCatalogConflictError();
expected = createToolResultKeyRotationCommand(current, {
keyId: added.keyId,
materialProof: added.materialProof,
mutationId: candidate.next.mutationId,
});
break;
}
case 'retire': {
const retired = changed.find((entry) => entry.state === 'retired');
if (!retired || retired.retirementReceiptDigest === null) {
throw new ToolResultKeyCatalogConflictError();
}
expected = createToolResultKeyRetirementCommand(current, {
keyId: retired.keyId,
retirementReceiptDigest: retired.retirementReceiptDigest,
mutationId: candidate.next.mutationId,
});
break;
}
case 'mark_lost': {
const lost = changed.find((entry) => entry.state === 'lost');
if (!lost) throw new ToolResultKeyCatalogConflictError();
expected = createToolResultKeyLostCommand(current, {
keyId: lost.keyId,
mutationId: candidate.next.mutationId,
});
break;
}
case 'restore': {
const restored = changed.find((entry) => {
const previous = current.keys.find(
(currentEntry) => currentEntry.keyId === entry.keyId,
);
return (
previous?.state === 'lost' &&
(entry.state === 'active' || entry.state === 'decrypt_only')
);
});
if (!restored) throw new ToolResultKeyCatalogConflictError();
expected = createToolResultKeyRestoreCommand(current, {
keyId: restored.keyId,
materialProof: restored.materialProof,
mutationId: candidate.next.mutationId,
});
break;
}
default:
throw new ToolResultKeyCatalogConflictError();
}
}
if (JSON.stringify(expected) !== JSON.stringify(candidate)) {
throw new ToolResultKeyCatalogConflictError();
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,341 @@
import {
approvedActionDispatchDigest,
type ApprovedActionDispatchRecord,
} from '../../approved-action/approvedAction';
import {
assertProjectPolicyProjectId,
normalizeProjectPolicySubject,
} from '../../security/project-policy/projectPolicy';
import {
normalizeSecurityPolicyDecision,
normalizeSecurityPrincipal,
type SecurityPolicyDecision,
type SecurityPolicyFence,
type SecurityPrincipal,
} from '../../security/security';
import type { ToolPolicyAuthorizer } from '../tool-registry/toolRegistry';
import {
normalizeToolInvocationInputArtifactReference,
normalizeToolInvocationPreviewArtifactReference,
} from '../toolInvocationArtifact';
import { TrustedToolHandlerBindingRegistry } from './binding';
import {
ADMISSION_DIGEST_DOMAIN,
dataRecord,
digest,
exactKeys,
hash,
identifier,
invalid,
normalizeContractIdentity,
normalizeFence,
normalizeProfile,
normalizeToolIdentity,
positiveInteger,
sameFence,
sameSubject,
timestamp,
} from './codec';
import {
TRUSTED_TOOL_EXECUTION_ADMISSION_SCHEMA,
TRUSTED_TOOL_EXECUTION_CLASSES,
TrustedToolExecutionApprovalRequiredError,
TrustedToolExecutionPolicyDeniedError,
TrustedToolExecutionPolicyUnavailableError,
TrustedToolInvocationBindingConflictError,
type AdmitTrustedToolExecutionInput,
type ToolExecutionStartEvidence,
type TrustedToolExecutionAdmission,
type TrustedToolInvocationPlan,
} from './contracts';
import {
assertTrustedToolApprovedDispatch,
normalizeTrustedToolInvocationPlan,
} from './plan';
function normalizeExecutionEvidence(
value: ToolExecutionStartEvidence,
): Readonly<ToolExecutionStartEvidence> {
const evidence = dataRecord(value, 'execution evidence');
exactKeys(evidence, ['audit', 'stepRun', 'trace'], [], 'execution evidence');
const stepRun = dataRecord(value.stepRun, 'StepRun evidence');
exactKeys(stepRun, ['digest', 'id', 'version'], [], 'StepRun evidence');
const trace = dataRecord(value.trace, 'Trace evidence');
exactKeys(trace, ['digest', 'spanId', 'traceId'], [], 'Trace evidence');
const audit = dataRecord(value.audit, 'Audit evidence');
exactKeys(audit, ['digest', 'eventId'], [], 'Audit evidence');
return Object.freeze({
stepRun: Object.freeze({
id: identifier(value.stepRun.id, 'StepRun id'),
version: positiveInteger(
value.stepRun.version,
2_147_483_647,
'StepRun version',
),
digest: digest(value.stepRun.digest, 'StepRun digest'),
}),
trace: Object.freeze({
traceId: identifier(value.trace.traceId, 'Trace id'),
spanId: identifier(value.trace.spanId, 'Trace span id'),
digest: digest(value.trace.digest, 'Trace digest'),
}),
audit: Object.freeze({
eventId: identifier(value.audit.eventId, 'Audit event id'),
digest: digest(value.audit.digest, 'Audit digest'),
}),
});
}
export function normalizeTrustedToolExecutionAdmission(
value: TrustedToolExecutionAdmission,
): Readonly<TrustedToolExecutionAdmission> {
const record = dataRecord(value, 'execution admission');
exactKeys(
record,
[
'actionRef',
'actionDigest',
'adapter',
'admissionDigest',
'admittedAtMs',
'approvalRequestId',
'approvalDispatchDigest',
'approvalDispatchId',
'auditContract',
'bindingDigest',
'evidence',
'executionClass',
'definitionDigest',
'invocationArtifact',
'planDigest',
'policyFence',
'profile',
'projectId',
'previewArtifact',
'requestedBy',
'redactionContract',
'schema',
'snapshotDigest',
'timeoutSeconds',
'tool',
],
[],
'execution admission',
);
if (value.schema !== TRUSTED_TOOL_EXECUTION_ADMISSION_SCHEMA) {
return invalid('execution admission schema is invalid');
}
assertProjectPolicyProjectId(value.projectId);
const requestedBy = normalizeProjectPolicySubject(value.requestedBy);
const tool = normalizeToolIdentity(value.tool, 'admitted Tool');
const profile = normalizeProfile(value.profile);
const adapter = normalizeContractIdentity(value.adapter, 'admitted adapter');
if (!TRUSTED_TOOL_EXECUTION_CLASSES.includes(value.executionClass)) {
return invalid('execution admission class is invalid');
}
const approvalDispatchId =
value.approvalDispatchId === null
? null
: identifier(value.approvalDispatchId, 'approval dispatch id');
const approvalRequestId =
value.approvalRequestId === null
? null
: identifier(value.approvalRequestId, 'approval request id');
const approvalDispatchDigest =
value.approvalDispatchDigest === null
? null
: digest(value.approvalDispatchDigest, 'approval dispatch digest');
if (
(approvalRequestId === null) !== (approvalDispatchId === null) ||
(approvalDispatchId === null) !== (approvalDispatchDigest === null)
) {
return invalid('execution admission approval binding is incomplete');
}
const unsigned = Object.freeze({
schema: TRUSTED_TOOL_EXECUTION_ADMISSION_SCHEMA,
actionRef: identifier(value.actionRef, 'admitted action reference'),
planDigest: digest(value.planDigest, 'admitted plan digest'),
actionDigest: digest(value.actionDigest, 'admitted action digest'),
projectId: value.projectId,
requestedBy,
tool,
profile,
snapshotDigest: digest(value.snapshotDigest, 'admitted snapshot digest'),
definitionDigest: digest(
value.definitionDigest,
'admitted definition digest',
),
bindingDigest: digest(value.bindingDigest, 'admitted binding digest'),
invocationArtifact: normalizeToolInvocationInputArtifactReference(
value.invocationArtifact,
),
previewArtifact: normalizeToolInvocationPreviewArtifactReference(
value.previewArtifact,
),
adapter,
redactionContract: normalizeContractIdentity(
value.redactionContract,
'admitted redaction contract',
),
auditContract: normalizeContractIdentity(
value.auditContract,
'admitted audit contract',
),
executionClass: value.executionClass,
timeoutSeconds: positiveInteger(
value.timeoutSeconds,
60 * 60,
'admitted timeout',
),
policyFence: normalizeFence(value.policyFence),
approvalRequestId,
approvalDispatchId,
approvalDispatchDigest,
evidence: normalizeExecutionEvidence(value.evidence),
admittedAtMs: timestamp(value.admittedAtMs, 'admission time'),
} satisfies Omit<TrustedToolExecutionAdmission, 'admissionDigest'>);
if (unsigned.previewArtifact.actionDigest !== unsigned.actionDigest) {
return invalid('execution admission Artifact bindings do not match');
}
const admissionDigest = digest(value.admissionDigest, 'admission digest');
if (hash(ADMISSION_DIGEST_DOMAIN, unsigned) !== admissionDigest) {
return invalid('execution admission digest does not match');
}
return Object.freeze({ ...unsigned, admissionDigest });
}
async function currentPolicyFence(
plan: Readonly<TrustedToolInvocationPlan>,
principal: Readonly<SecurityPrincipal>,
authorizer: ToolPolicyAuthorizer,
): Promise<
Readonly<{
fence: Readonly<SecurityPolicyFence>;
decisions: readonly Readonly<SecurityPolicyDecision>[];
}>
> {
if (!authorizer || typeof authorizer.authorize !== 'function') {
throw new TrustedToolExecutionPolicyUnavailableError();
}
const permissions = [plan.permission, ...plan.requiredPermissions];
const decisions: Readonly<SecurityPolicyDecision>[] = [];
for (const permission of permissions) {
let decision: Readonly<SecurityPolicyDecision>;
try {
decision = normalizeSecurityPolicyDecision(
await authorizer.authorize(principal, plan.projectId, permission),
);
} catch (cause) {
throw new TrustedToolExecutionPolicyUnavailableError({ cause });
}
if (decision.effect === 'deny') {
throw new TrustedToolExecutionPolicyDeniedError();
}
decisions.push(decision);
}
const fence = decisions[0]?.fence;
if (
!fence ||
decisions.some(
(decision) => !decision.fence || !sameFence(fence, decision.fence),
)
) {
throw new TrustedToolExecutionPolicyUnavailableError();
}
return Object.freeze({
fence,
decisions: Object.freeze(decisions),
});
}
export async function admitTrustedToolExecution(
bindings: TrustedToolHandlerBindingRegistry,
planValue: TrustedToolInvocationPlan,
inputValue: AdmitTrustedToolExecutionInput,
): Promise<Readonly<TrustedToolExecutionAdmission>> {
if (!(bindings instanceof TrustedToolHandlerBindingRegistry)) {
return invalid('handler binding registry is invalid');
}
const input = dataRecord(inputValue, 'execution admission input');
exactKeys(
input,
['authorizer', 'evidence', 'nowMs', 'principal', 'profile'],
['dispatch'],
'execution admission input',
);
const plan = normalizeTrustedToolInvocationPlan(planValue, bindings);
const nowMs = timestamp(inputValue.nowMs, 'admission time');
const principal = normalizeSecurityPrincipal(inputValue.principal, nowMs);
if (!sameSubject(principal.subject, plan.requestedBy)) {
throw new TrustedToolInvocationBindingConflictError();
}
const profile = normalizeProfile(inputValue.profile);
const currentBinding = bindings.resolve(
plan.tool.name,
plan.tool.version,
profile,
);
if (
profile !== plan.profile ||
currentBinding.bindingDigest !== plan.binding.bindingDigest
) {
throw new TrustedToolInvocationBindingConflictError();
}
const policy = await currentPolicyFence(
plan,
principal,
inputValue.authorizer,
);
let dispatch: Readonly<ApprovedActionDispatchRecord> | null = null;
if (plan.status === 'approval_required') {
if (!inputValue.dispatch) {
throw new TrustedToolExecutionApprovalRequiredError();
}
dispatch = assertTrustedToolApprovedDispatch(
plan,
bindings,
inputValue.dispatch,
);
} else {
if (
inputValue.dispatch !== undefined ||
policy.decisions.some(
(decision) => decision.effect === 'require_approval',
)
) {
throw new TrustedToolExecutionApprovalRequiredError();
}
}
const evidence = normalizeExecutionEvidence(inputValue.evidence);
const unsigned = Object.freeze({
schema: TRUSTED_TOOL_EXECUTION_ADMISSION_SCHEMA,
actionRef: plan.actionRef,
planDigest: plan.planDigest,
actionDigest: plan.actionDigest,
projectId: plan.projectId,
requestedBy: plan.requestedBy,
tool: plan.tool,
profile,
snapshotDigest: plan.snapshotDigest,
definitionDigest: plan.definitionDigest,
bindingDigest: currentBinding.bindingDigest,
invocationArtifact: plan.invocationArtifact,
previewArtifact: plan.previewArtifact,
adapter: currentBinding.adapter,
redactionContract: currentBinding.redactionContract,
auditContract: currentBinding.auditContract,
executionClass: currentBinding.executionClass,
timeoutSeconds: plan.timeoutSeconds,
policyFence: policy.fence,
approvalRequestId: dispatch?.approvalRequestId ?? null,
approvalDispatchId: dispatch?.id ?? null,
approvalDispatchDigest:
dispatch === null ? null : approvedActionDispatchDigest(dispatch),
evidence,
admittedAtMs: nowMs,
} satisfies Omit<TrustedToolExecutionAdmission, 'admissionDigest'>);
return Object.freeze({
...unsigned,
admissionDigest: hash(ADMISSION_DIGEST_DOMAIN, unsigned),
});
}
@@ -0,0 +1,258 @@
import type { DeploymentProfile } from '../../cluster-control/clusterControlActivation';
import {
normalizeProjectToolDefinitionSnapshot,
type ProjectToolDefinitionSnapshot,
type ProjectToolDefinitionSnapshotEntry,
} from '../tool-registry/projectToolDefinitionSnapshot';
import { ToolDefinitionRegistry } from '../tool-registry/toolRegistry';
import {
MAX_TRUSTED_TOOL_HANDLER_BINDINGS,
TRUSTED_TOOL_HANDLER_BINDING_SCHEMA,
TRUSTED_TOOL_EXECUTION_CLASSES,
TrustedToolHandlerUnavailableError,
TrustedToolInvocationBindingConflictError,
type CreateTrustedToolHandlerBindingInput,
type TrustedToolHandlerBinding,
} from './contracts';
import {
BINDING_DIGEST_DOMAIN,
dataRecord,
digest,
exactKeys,
hash,
invalid,
normalizeAuthorities,
normalizeContractIdentity,
normalizeProfile,
normalizeProfiles,
normalizeToolIdentity,
positiveInteger,
} from './codec';
function definitionEntry(
snapshot: Readonly<ProjectToolDefinitionSnapshot>,
tool: Readonly<{ name: string; version: string }>,
): Readonly<ProjectToolDefinitionSnapshotEntry> {
const entry = snapshot.definitions.find(
(candidate) =>
candidate.definition.name === tool.name &&
candidate.definition.version === tool.version,
);
if (!entry) throw new TrustedToolHandlerUnavailableError();
return entry;
}
function bindingWithoutDigest(
value: Readonly<TrustedToolHandlerBinding>,
): Omit<TrustedToolHandlerBinding, 'bindingDigest'> {
const { bindingDigest: _bindingDigest, ...unsigned } = value;
return unsigned;
}
function normalizeBindingUnsigned(
value: Omit<TrustedToolHandlerBinding, 'bindingDigest'>,
): Omit<TrustedToolHandlerBinding, 'bindingDigest'> {
const record = dataRecord(value, 'handler binding');
exactKeys(
record,
[
'adapter',
'auditContract',
'authorities',
'definitionDigest',
'executionClass',
'profiles',
'redactionContract',
'schema',
'snapshotDigest',
'timeoutSeconds',
'tool',
],
[],
'handler binding',
);
if (
value.schema !== TRUSTED_TOOL_HANDLER_BINDING_SCHEMA ||
!TRUSTED_TOOL_EXECUTION_CLASSES.includes(value.executionClass)
) {
return invalid('handler binding schema or execution class is invalid');
}
return Object.freeze({
schema: TRUSTED_TOOL_HANDLER_BINDING_SCHEMA,
snapshotDigest: digest(value.snapshotDigest, 'snapshot digest'),
definitionDigest: digest(value.definitionDigest, 'definition digest'),
tool: normalizeToolIdentity(value.tool, 'handler Tool'),
adapter: normalizeContractIdentity(value.adapter, 'handler adapter'),
executionClass: value.executionClass,
profiles: normalizeProfiles(value.profiles),
authorities: normalizeAuthorities(value.authorities),
timeoutSeconds: positiveInteger(
value.timeoutSeconds,
60 * 60,
'handler timeout',
),
redactionContract: normalizeContractIdentity(
value.redactionContract,
'redaction contract',
),
auditContract: normalizeContractIdentity(
value.auditContract,
'audit contract',
),
});
}
export function normalizeTrustedToolHandlerBinding(
value: TrustedToolHandlerBinding,
): Readonly<TrustedToolHandlerBinding> {
const record = dataRecord(value, 'handler binding');
exactKeys(
record,
[
'adapter',
'auditContract',
'authorities',
'bindingDigest',
'definitionDigest',
'executionClass',
'profiles',
'redactionContract',
'schema',
'snapshotDigest',
'timeoutSeconds',
'tool',
],
[],
'handler binding',
);
const unsigned = normalizeBindingUnsigned(bindingWithoutDigest(value));
const bindingDigest = digest(value.bindingDigest, 'binding digest');
if (hash(BINDING_DIGEST_DOMAIN, unsigned) !== bindingDigest) {
return invalid('handler binding digest does not match');
}
return Object.freeze({ ...unsigned, bindingDigest });
}
export function createTrustedToolHandlerBinding(
snapshotValue: ProjectToolDefinitionSnapshot,
inputValue: CreateTrustedToolHandlerBindingInput,
): Readonly<TrustedToolHandlerBinding> {
const snapshot = normalizeProjectToolDefinitionSnapshot(snapshotValue);
const input = dataRecord(inputValue, 'handler binding input');
exactKeys(
input,
[
'adapter',
'auditContract',
'authorities',
'executionClass',
'profiles',
'redactionContract',
'timeoutSeconds',
'tool',
],
[],
'handler binding input',
);
const tool = normalizeToolIdentity(inputValue.tool, 'handler Tool');
const entry = definitionEntry(snapshot, tool);
const unsigned = normalizeBindingUnsigned({
schema: TRUSTED_TOOL_HANDLER_BINDING_SCHEMA,
snapshotDigest: snapshot.snapshotDigest,
definitionDigest: entry.definitionDigest,
tool,
adapter: inputValue.adapter,
executionClass: inputValue.executionClass,
profiles: inputValue.profiles,
authorities: inputValue.authorities,
timeoutSeconds: inputValue.timeoutSeconds,
redactionContract: inputValue.redactionContract,
auditContract: inputValue.auditContract,
});
if (unsigned.timeoutSeconds > entry.definition.timeoutSeconds) {
return invalid('handler timeout widens the Tool definition timeout');
}
return Object.freeze({
...unsigned,
bindingDigest: hash(BINDING_DIGEST_DOMAIN, unsigned),
});
}
function toolKey(tool: Readonly<{ name: string; version: string }>): string {
return `${tool.name}@${tool.version}`;
}
export class TrustedToolHandlerBindingRegistry {
readonly #snapshot: Readonly<ProjectToolDefinitionSnapshot>;
readonly #bindings: ReadonlyMap<string, Readonly<TrustedToolHandlerBinding>>;
readonly #metadata: readonly Readonly<TrustedToolHandlerBinding>[];
constructor(
snapshotValue: ProjectToolDefinitionSnapshot,
bindingValues: readonly TrustedToolHandlerBinding[],
) {
const snapshot = normalizeProjectToolDefinitionSnapshot(snapshotValue);
if (
!Array.isArray(bindingValues) ||
bindingValues.length > MAX_TRUSTED_TOOL_HANDLER_BINDINGS
) {
invalid('handler binding count is invalid');
}
const bindings = new Map<string, Readonly<TrustedToolHandlerBinding>>();
for (const bindingValue of bindingValues) {
const binding = normalizeTrustedToolHandlerBinding(bindingValue);
const entry = definitionEntry(snapshot, binding.tool);
if (
binding.snapshotDigest !== snapshot.snapshotDigest ||
binding.definitionDigest !== entry.definitionDigest ||
binding.timeoutSeconds > entry.definition.timeoutSeconds
) {
throw new TrustedToolInvocationBindingConflictError();
}
const key = toolKey(binding.tool);
if (bindings.has(key)) {
invalid('handler binding is duplicated');
}
bindings.set(key, binding);
}
this.#snapshot = snapshot;
this.#bindings = bindings;
this.#metadata = Object.freeze(
[...bindings.values()].sort((left, right) =>
toolKey(left.tool).localeCompare(toolKey(right.tool)),
),
);
Object.freeze(this);
}
get projectId(): string {
return this.#snapshot.projectId;
}
get snapshotDigest(): string {
return this.#snapshot.snapshotDigest;
}
list(): readonly Readonly<TrustedToolHandlerBinding>[] {
return this.#metadata;
}
resolve(
name: string,
version: string,
profile: DeploymentProfile,
): Readonly<TrustedToolHandlerBinding> {
const normalizedProfile = normalizeProfile(profile);
const binding = this.#bindings.get(toolKey({ name, version }));
if (!binding || !binding.profiles.includes(normalizedProfile)) {
throw new TrustedToolHandlerUnavailableError();
}
return binding;
}
definitionRegistry(): ToolDefinitionRegistry {
return new ToolDefinitionRegistry(
this.#snapshot.definitions.map((entry) => entry.definition),
);
}
}
@@ -0,0 +1,284 @@
import { createHash } from 'node:crypto';
import type { DeploymentProfile } from '../../cluster-control/clusterControlActivation';
import type {
SecurityPolicyFence,
SecuritySubject,
} from '../../security/security';
import { semver } from '../../versioning/pinnedSemver';
import {
InvalidTrustedToolInvocationError,
MAX_TRUSTED_TOOL_HANDLER_AUTHORITIES,
TRUSTED_TOOL_DEPLOYMENT_PROFILES,
TRUSTED_TOOL_HANDLER_AUTHORITIES,
type TrustedToolContractIdentity,
type TrustedToolHandlerAuthority,
} from './contracts';
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const CONTRACT_ID_PATTERN =
/^[a-z][a-z0-9-]{0,62}(?:\.[a-z][a-z0-9-]{0,62}){1,7}$/;
export const WARNING_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
const PROFILE_ORDER = new Map(
TRUSTED_TOOL_DEPLOYMENT_PROFILES.map((profile, index) => [profile, index]),
);
export const BINDING_DIGEST_DOMAIN = Buffer.from(
'qinglong/trusted-tool-handler-binding-digest@v1\0',
'utf8',
);
export const ACTION_DIGEST_DOMAIN = Buffer.from(
'qinglong/trusted-tool-invocation-action-digest@v1\0',
'utf8',
);
export const PLAN_DIGEST_DOMAIN = Buffer.from(
'qinglong/trusted-tool-invocation-plan-digest@v1\0',
'utf8',
);
export const ADMISSION_DIGEST_DOMAIN = Buffer.from(
'qinglong/trusted-tool-execution-admission-digest@v1\0',
'utf8',
);
const CONTRACT_IDENTITY_DIGEST_DOMAIN = Buffer.from(
'qinglong/trusted-tool-contract-identity-digest@v1\0',
'utf8',
);
export function invalid(message: string): never {
throw new InvalidTrustedToolInvocationError(message);
}
export function dataRecord(
value: unknown,
label: string,
): Record<string, unknown> {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
(Object.getPrototypeOf(value) !== Object.prototype &&
Object.getPrototypeOf(value) !== null)
) {
return invalid(`${label} must be an object`);
}
const descriptors = Object.getOwnPropertyDescriptors(value);
if (
Object.values(descriptors).some(
(descriptor) =>
descriptor.get !== undefined ||
descriptor.set !== undefined ||
descriptor.enumerable !== true,
)
) {
return invalid(`${label} must contain enumerable data properties`);
}
return value as Record<string, unknown>;
}
export function exactKeys(
value: object,
required: readonly string[],
optional: readonly string[],
label: string,
): void {
const keys = Reflect.ownKeys(value);
const allowed = new Set([...required, ...optional]);
if (
keys.some((key) => typeof key !== 'string' || !allowed.has(key)) ||
required.some((key) => !keys.includes(key))
) {
invalid(`${label} shape is invalid`);
}
}
export function digest(value: unknown, label: string): string {
if (typeof value !== 'string' || !DIGEST_PATTERN.test(value)) {
return invalid(`${label} is invalid`);
}
return value;
}
export function hash(domain: Buffer, value: unknown): string {
return createHash('sha256')
.update(domain)
.update(JSON.stringify(value))
.digest('hex');
}
export function identifier(value: unknown, label: string): string {
if (typeof value !== 'string' || !IDENTIFIER_PATTERN.test(value)) {
return invalid(`${label} is invalid`);
}
return value;
}
export function timestamp(value: unknown, label: string): number {
if (!Number.isSafeInteger(value) || (value as number) < 0) {
return invalid(`${label} is invalid`);
}
return value as number;
}
export function positiveInteger(
value: unknown,
maximum: number,
label: string,
): number {
if (
!Number.isSafeInteger(value) ||
(value as number) < 1 ||
(value as number) > maximum
) {
return invalid(`${label} is invalid`);
}
return value as number;
}
export function boundedText(
value: unknown,
maximumBytes: number,
label: string,
): string {
if (
typeof value !== 'string' ||
value.length < 1 ||
CONTROL_PATTERN.test(value) ||
Buffer.byteLength(value, 'utf8') > maximumBytes
) {
return invalid(`${label} is invalid`);
}
return value;
}
export function sameSubject(
left: Readonly<SecuritySubject>,
right: Readonly<SecuritySubject>,
): boolean {
return left.type === right.type && left.id === right.id;
}
export function sameFence(
left: Readonly<SecurityPolicyFence>,
right: Readonly<SecurityPolicyFence>,
): boolean {
return (
left.projectVersion === right.projectVersion &&
left.bindingVersion === right.bindingVersion
);
}
export function normalizeFence(
value: SecurityPolicyFence,
): Readonly<SecurityPolicyFence> {
const record = dataRecord(value, 'policy fence');
exactKeys(record, ['bindingVersion', 'projectVersion'], [], 'policy fence');
if (
!Number.isSafeInteger(value.projectVersion) ||
value.projectVersion < 1 ||
(value.bindingVersion !== null &&
(!Number.isSafeInteger(value.bindingVersion) || value.bindingVersion < 1))
) {
return invalid('policy fence is invalid');
}
return Object.freeze({
projectVersion: value.projectVersion,
bindingVersion: value.bindingVersion,
});
}
export function normalizeToolIdentity(
value: Readonly<{ name: string; version: string }>,
label: string,
): Readonly<{ name: string; version: string }> {
const record = dataRecord(value, label);
exactKeys(record, ['name', 'version'], [], label);
const name = boundedText(value.name, 255, `${label} name`);
const version = boundedText(value.version, 128, `${label} version`);
if (!CONTRACT_ID_PATTERN.test(name) || semver().valid(version) !== version) {
return invalid(`${label} is invalid`);
}
return Object.freeze({ name, version });
}
export function normalizeContractIdentity(
value: TrustedToolContractIdentity,
label: string,
): Readonly<TrustedToolContractIdentity> {
const record = dataRecord(value, label);
exactKeys(record, ['id', 'version'], [], label);
const id = boundedText(value.id, 255, `${label} id`);
const version = boundedText(value.version, 128, `${label} version`);
if (!CONTRACT_ID_PATTERN.test(id) || semver().valid(version) !== version) {
return invalid(`${label} is invalid`);
}
return Object.freeze({ id, version });
}
export function trustedToolContractIdentityDigest(
value: TrustedToolContractIdentity,
): string {
return hash(
CONTRACT_IDENTITY_DIGEST_DOMAIN,
normalizeContractIdentity(value, 'Tool contract identity'),
);
}
export function normalizeProfile(value: unknown): DeploymentProfile {
if (
typeof value !== 'string' ||
!TRUSTED_TOOL_DEPLOYMENT_PROFILES.includes(value as DeploymentProfile)
) {
return invalid('deployment profile is invalid');
}
return value as DeploymentProfile;
}
export function normalizeProfiles(
value: readonly DeploymentProfile[],
): readonly DeploymentProfile[] {
if (
!Array.isArray(value) ||
value.length < 1 ||
value.length > TRUSTED_TOOL_DEPLOYMENT_PROFILES.length
) {
return invalid('handler profiles are invalid');
}
const profiles = value.map(normalizeProfile);
if (new Set(profiles).size !== profiles.length) {
return invalid('handler profiles are duplicated');
}
return Object.freeze(
profiles.sort(
(left, right) =>
(PROFILE_ORDER.get(left) ?? Number.MAX_SAFE_INTEGER) -
(PROFILE_ORDER.get(right) ?? Number.MAX_SAFE_INTEGER),
),
);
}
export function normalizeAuthorities(
value: readonly TrustedToolHandlerAuthority[],
): readonly TrustedToolHandlerAuthority[] {
if (
!Array.isArray(value) ||
value.length > MAX_TRUSTED_TOOL_HANDLER_AUTHORITIES
) {
return invalid('handler authorities are invalid');
}
const authorities = value.map((authority) => {
if (
typeof authority !== 'string' ||
!TRUSTED_TOOL_HANDLER_AUTHORITIES.includes(
authority as TrustedToolHandlerAuthority,
)
) {
return invalid('handler authority is invalid');
}
return authority as TrustedToolHandlerAuthority;
});
if (new Set(authorities).size !== authorities.length) {
return invalid('handler authorities are duplicated');
}
return Object.freeze(authorities.sort());
}
@@ -0,0 +1,251 @@
import type { ApprovedActionDispatchRecord } from '../../approved-action/approvedAction';
import type { DeploymentProfile } from '../../cluster-control/clusterControlActivation';
import type { ProjectPermission } from '../../security/project-policy/projectPolicy';
import type {
SecurityPolicyFence,
SecurityPrincipal,
SecuritySubject,
} from '../../security/security';
import type {
ToolEffect,
ToolInvocationStatus,
ToolPolicyAuthorizer,
ToolRisk,
} from '../tool-registry/toolRegistry';
import type {
ToolInvocationInputArtifact,
ToolInvocationInputArtifactReference,
ToolInvocationPreviewArtifact,
ToolInvocationPreviewArtifactReference,
} from '../toolInvocationArtifact';
export const TRUSTED_TOOL_HANDLER_BINDING_SCHEMA =
'qinglong/trusted-tool-handler-binding@v1' as const;
export const TRUSTED_TOOL_INVOCATION_PLAN_SCHEMA =
'qinglong/trusted-tool-invocation-plan@v1' as const;
export const TRUSTED_TOOL_EXECUTION_ADMISSION_SCHEMA =
'qinglong/trusted-tool-execution-admission@v1' as const;
export const TOOL_INVOKE_ACTION_TYPE = 'tool.invoke' as const;
export const TRUSTED_TOOL_EXECUTION_CLASSES = [
'builtin_in_process',
'isolated_process',
'remote_worker',
'mcp_client',
'http_connector',
] as const;
export const TRUSTED_TOOL_HANDLER_AUTHORITIES = [
'artifact.read',
'artifact.write',
'database.read',
'database.write',
'filesystem.read',
'filesystem.write',
'mcp.call',
'model.invoke',
'network.connect',
'process.spawn',
'run.control',
'secret.use',
] as const;
export const TRUSTED_TOOL_PREVIEW_FIELD_KINDS = [
'count',
'identifier',
'redacted',
'text',
] as const;
export const TRUSTED_TOOL_DEPLOYMENT_PROFILES = [
'edge',
'standalone',
'cluster-control',
'worker',
] as const satisfies readonly DeploymentProfile[];
export const MAX_TRUSTED_TOOL_HANDLER_BINDINGS = 128;
export const MAX_TRUSTED_TOOL_HANDLER_AUTHORITIES = 16;
export const MAX_TRUSTED_TOOL_PREVIEW_FIELDS = 16;
export const MAX_TRUSTED_TOOL_PREVIEW_WARNINGS = 8;
export type TrustedToolExecutionClass =
(typeof TRUSTED_TOOL_EXECUTION_CLASSES)[number];
export type TrustedToolHandlerAuthority =
(typeof TRUSTED_TOOL_HANDLER_AUTHORITIES)[number];
export type TrustedToolPreviewFieldKind =
(typeof TRUSTED_TOOL_PREVIEW_FIELD_KINDS)[number];
export interface TrustedToolContractIdentity {
readonly id: string;
readonly version: string;
}
export interface CreateTrustedToolHandlerBindingInput {
readonly tool: Readonly<{ name: string; version: string }>;
readonly adapter: TrustedToolContractIdentity;
readonly executionClass: TrustedToolExecutionClass;
readonly profiles: readonly DeploymentProfile[];
readonly authorities: readonly TrustedToolHandlerAuthority[];
readonly timeoutSeconds: number;
readonly redactionContract: TrustedToolContractIdentity;
readonly auditContract: TrustedToolContractIdentity;
}
export interface TrustedToolHandlerBinding
extends CreateTrustedToolHandlerBindingInput {
readonly schema: typeof TRUSTED_TOOL_HANDLER_BINDING_SCHEMA;
readonly snapshotDigest: string;
readonly definitionDigest: string;
readonly bindingDigest: string;
}
export interface TrustedToolInvocationPreviewField {
readonly kind: TrustedToolPreviewFieldKind;
readonly label: string;
readonly value: string | null;
}
export interface TrustedToolInvocationPreview {
readonly title: string;
readonly summary: string;
readonly fields: readonly Readonly<TrustedToolInvocationPreviewField>[];
readonly warnings: readonly string[];
}
export interface TrustedToolInvocationPlan {
readonly schema: typeof TRUSTED_TOOL_INVOCATION_PLAN_SCHEMA;
readonly status: ToolInvocationStatus;
readonly actionType: typeof TOOL_INVOKE_ACTION_TYPE;
readonly actionRef: string;
readonly projectId: string;
readonly requestedBy: Readonly<SecuritySubject>;
readonly tool: Readonly<{ name: string; version: string }>;
readonly permission: ProjectPermission;
readonly requiredPermissions: readonly ProjectPermission[];
readonly effect: ToolEffect;
readonly risk: ToolRisk;
readonly policyFence: Readonly<SecurityPolicyFence>;
readonly profile: DeploymentProfile;
readonly snapshotDigest: string;
readonly definitionDigest: string;
readonly binding: Readonly<TrustedToolHandlerBinding>;
readonly timeoutSeconds: number;
readonly invocationArtifact: Readonly<ToolInvocationInputArtifactReference>;
readonly invocationActionDigest: string;
readonly previewArtifact: Readonly<ToolInvocationPreviewArtifactReference>;
readonly actionDigest: string;
readonly sealedAtMs: number;
readonly planDigest: string;
}
export interface TrustedToolInvocationPlanBundle {
readonly plan: Readonly<TrustedToolInvocationPlan>;
readonly inputArtifact: Readonly<ToolInvocationInputArtifact>;
readonly previewArtifact: Readonly<ToolInvocationPreviewArtifact>;
}
export interface ToolExecutionStartEvidence {
readonly stepRun: Readonly<{
id: string;
version: number;
digest: string;
}>;
readonly trace: Readonly<{
traceId: string;
spanId: string;
digest: string;
}>;
readonly audit: Readonly<{
eventId: string;
digest: string;
}>;
}
export interface TrustedToolExecutionAdmission {
readonly schema: typeof TRUSTED_TOOL_EXECUTION_ADMISSION_SCHEMA;
readonly actionRef: string;
readonly planDigest: string;
readonly actionDigest: string;
readonly projectId: string;
readonly requestedBy: Readonly<SecuritySubject>;
readonly tool: Readonly<{ name: string; version: string }>;
readonly profile: DeploymentProfile;
readonly snapshotDigest: string;
readonly definitionDigest: string;
readonly bindingDigest: string;
readonly invocationArtifact: Readonly<ToolInvocationInputArtifactReference>;
readonly previewArtifact: Readonly<ToolInvocationPreviewArtifactReference>;
readonly adapter: Readonly<TrustedToolContractIdentity>;
readonly redactionContract: Readonly<TrustedToolContractIdentity>;
readonly auditContract: Readonly<TrustedToolContractIdentity>;
readonly executionClass: TrustedToolExecutionClass;
readonly timeoutSeconds: number;
readonly policyFence: Readonly<SecurityPolicyFence>;
readonly approvalRequestId: string | null;
readonly approvalDispatchId: string | null;
readonly approvalDispatchDigest: string | null;
readonly evidence: Readonly<ToolExecutionStartEvidence>;
readonly admittedAtMs: number;
readonly admissionDigest: string;
}
export interface AdmitTrustedToolExecutionInput {
readonly principal: SecurityPrincipal;
readonly profile: DeploymentProfile;
readonly nowMs: number;
readonly authorizer: ToolPolicyAuthorizer;
readonly evidence: ToolExecutionStartEvidence;
readonly dispatch?: ApprovedActionDispatchRecord;
}
export class InvalidTrustedToolInvocationError extends TypeError {
readonly code = 'TRUSTED_TOOL_INVOCATION_INVALID';
constructor(message: string) {
super(`Trusted Tool invocation is invalid: ${message}`);
this.name = 'InvalidTrustedToolInvocationError';
}
}
export class TrustedToolHandlerUnavailableError extends Error {
readonly code = 'TRUSTED_TOOL_HANDLER_UNAVAILABLE';
constructor() {
super('No exact trusted Tool handler binding is available');
this.name = 'TrustedToolHandlerUnavailableError';
}
}
export class TrustedToolInvocationBindingConflictError extends Error {
readonly code = 'TRUSTED_TOOL_INVOCATION_BINDING_CONFLICT';
constructor() {
super('Trusted Tool invocation binding changed');
this.name = 'TrustedToolInvocationBindingConflictError';
}
}
export class TrustedToolExecutionPolicyDeniedError extends Error {
readonly code = 'TRUSTED_TOOL_EXECUTION_POLICY_DENIED';
constructor() {
super('Current Project Policy denies Tool execution');
this.name = 'TrustedToolExecutionPolicyDeniedError';
}
}
export class TrustedToolExecutionApprovalRequiredError extends Error {
readonly code = 'TRUSTED_TOOL_EXECUTION_APPROVAL_REQUIRED';
constructor() {
super('Current Project Policy requires a new Tool approval');
this.name = 'TrustedToolExecutionApprovalRequiredError';
}
}
export class TrustedToolExecutionPolicyUnavailableError extends Error {
readonly code = 'TRUSTED_TOOL_EXECUTION_POLICY_UNAVAILABLE';
constructor(options?: ErrorOptions) {
super('Current Project Policy is unavailable for Tool execution', options);
this.name = 'TrustedToolExecutionPolicyUnavailableError';
}
}
@@ -0,0 +1,601 @@
import {
normalizeApprovedActionDispatchRecord,
type ApprovedActionBinding,
type ApprovedActionDispatchRecord,
} from '../../approved-action/approvedAction';
import type { DeploymentProfile } from '../../cluster-control/clusterControlActivation';
import {
assertProjectPolicyProjectId,
normalizeProjectPermission,
normalizeProjectPolicySubject,
type ProjectPermission,
} from '../../security/project-policy/projectPolicy';
import type {
SecurityPolicyFence,
SecuritySubject,
} from '../../security/security';
import {
TOOL_INVOCATION_SCHEMA,
ToolDefinitionRegistry,
type PreparedToolInvocation,
} from '../tool-registry/toolRegistry';
import {
createToolInvocationInputArtifact,
createToolInvocationPreviewArtifact,
normalizeToolInvocationInputArtifactReference,
normalizeToolInvocationPreviewArtifactReference,
toolInvocationInputArtifactReference,
toolInvocationPreviewArtifactReference,
} from '../toolInvocationArtifact';
import {
TrustedToolHandlerBindingRegistry,
normalizeTrustedToolHandlerBinding,
} from './binding';
import {
ACTION_DIGEST_DOMAIN,
PLAN_DIGEST_DOMAIN,
WARNING_PATTERN,
boundedText,
dataRecord,
digest,
exactKeys,
hash,
identifier,
invalid,
normalizeFence,
normalizeProfile,
normalizeToolIdentity,
positiveInteger,
sameSubject,
timestamp,
trustedToolContractIdentityDigest,
} from './codec';
import {
MAX_TRUSTED_TOOL_PREVIEW_FIELDS,
MAX_TRUSTED_TOOL_PREVIEW_WARNINGS,
TOOL_INVOKE_ACTION_TYPE,
TRUSTED_TOOL_INVOCATION_PLAN_SCHEMA,
TRUSTED_TOOL_PREVIEW_FIELD_KINDS,
TrustedToolExecutionApprovalRequiredError,
TrustedToolInvocationBindingConflictError,
type TrustedToolInvocationPlan,
type TrustedToolInvocationPlanBundle,
type TrustedToolInvocationPreview,
type TrustedToolInvocationPreviewField,
} from './contracts';
function normalizePreviewField(
value: TrustedToolInvocationPreviewField,
): Readonly<TrustedToolInvocationPreviewField> {
const record = dataRecord(value, 'preview field');
exactKeys(record, ['kind', 'label', 'value'], [], 'preview field');
if (!TRUSTED_TOOL_PREVIEW_FIELD_KINDS.includes(value.kind)) {
return invalid('preview field kind is invalid');
}
const label = boundedText(value.label, 128, 'preview field label');
if (
(value.kind === 'redacted' && value.value !== null) ||
(value.kind !== 'redacted' && value.value === null)
) {
return invalid('preview field redaction is invalid');
}
const normalizedValue =
value.value === null
? null
: boundedText(value.value, 512, 'preview field value');
return Object.freeze({ kind: value.kind, label, value: normalizedValue });
}
export function normalizeTrustedToolInvocationPreview(
value: TrustedToolInvocationPreview,
): Readonly<TrustedToolInvocationPreview> {
const record = dataRecord(value, 'preview');
exactKeys(record, ['fields', 'summary', 'title', 'warnings'], [], 'preview');
if (
!Array.isArray(value.fields) ||
value.fields.length > MAX_TRUSTED_TOOL_PREVIEW_FIELDS ||
!Array.isArray(value.warnings) ||
value.warnings.length > MAX_TRUSTED_TOOL_PREVIEW_WARNINGS
) {
return invalid('preview collections are invalid');
}
const fields = value.fields.map(normalizePreviewField);
const warnings = value.warnings.map((warning) => {
if (typeof warning !== 'string' || !WARNING_PATTERN.test(warning)) {
return invalid('preview warning is invalid');
}
return warning;
});
if (new Set(warnings).size !== warnings.length) {
return invalid('preview warnings are duplicated');
}
return Object.freeze({
title: boundedText(value.title, 256, 'preview title'),
summary: boundedText(value.summary, 2048, 'preview summary'),
fields: Object.freeze(fields),
warnings: Object.freeze([...warnings].sort()),
});
}
function normalizePermissions(
value: readonly ProjectPermission[],
label: string,
): readonly ProjectPermission[] {
if (!Array.isArray(value) || value.length > 16) {
return invalid(`${label} are invalid`);
}
const permissions = value.map((permission) => {
try {
return normalizeProjectPermission(permission);
} catch {
return invalid(`${label} are invalid`);
}
});
if (
new Set(permissions).size !== permissions.length ||
permissions.some((permission) => permission.startsWith('tool.call:'))
) {
return invalid(`${label} are duplicated or nested`);
}
return Object.freeze([...permissions].sort());
}
function normalizePreparedInvocation(
value: PreparedToolInvocation,
registry: ToolDefinitionRegistry,
): Readonly<PreparedToolInvocation> {
const record = dataRecord(value, 'prepared invocation');
exactKeys(
record,
[
'actionDigest',
'effect',
'fence',
'input',
'inputDigest',
'permission',
'projectId',
'requestedBy',
'requiredPermissions',
'risk',
'schema',
'status',
'timeoutSeconds',
'tool',
],
[],
'prepared invocation',
);
if (
value.schema !== TOOL_INVOCATION_SCHEMA ||
(value.status !== 'ready' && value.status !== 'approval_required')
) {
return invalid('prepared invocation schema or status is invalid');
}
assertProjectPolicyProjectId(value.projectId);
const requestedBy = normalizeProjectPolicySubject(value.requestedBy);
const tool = normalizeToolIdentity(value.tool, 'prepared Tool');
const definition = registry.resolve(tool.name, tool.version);
const permission = normalizeProjectPermission(value.permission);
const expectedPermission = normalizeProjectPermission(
`tool.call:${definition.name}`,
);
const requiredPermissions = normalizePermissions(
value.requiredPermissions,
'prepared required permissions',
);
if (
permission !== expectedPermission ||
JSON.stringify(requiredPermissions) !==
JSON.stringify(definition.requiredPermissions) ||
value.effect !== definition.effect ||
value.risk !== definition.risk ||
value.timeoutSeconds !== definition.timeoutSeconds
) {
return invalid('prepared invocation drifts from its Tool definition');
}
const input = registry.normalizeInput(tool.name, tool.version, value.input);
const inputDigest = digest(value.inputDigest, 'input digest');
if (hash(Buffer.alloc(0), input) !== inputDigest) {
return invalid('prepared invocation input digest does not match');
}
const fence = normalizeFence(value.fence);
const actionDigest = digest(value.actionDigest, 'invocation action digest');
const expectedActionDigest = hash(Buffer.alloc(0), {
schema: TOOL_INVOCATION_SCHEMA,
projectId: value.projectId,
requestedBy,
tool,
permission,
requiredPermissions,
effect: definition.effect,
risk: definition.risk,
timeoutSeconds: definition.timeoutSeconds,
inputDigest,
});
if (actionDigest !== expectedActionDigest) {
return invalid('prepared invocation action digest does not match');
}
return Object.freeze({
status: value.status,
schema: TOOL_INVOCATION_SCHEMA,
projectId: value.projectId,
requestedBy,
tool,
permission,
requiredPermissions,
effect: definition.effect,
risk: definition.risk,
timeoutSeconds: definition.timeoutSeconds,
fence,
input,
inputDigest,
actionDigest,
});
}
function actionFields(
value: Omit<
TrustedToolInvocationPlan,
'actionDigest' | 'planDigest' | 'previewArtifact'
>,
): object {
return {
schema: value.schema,
status: value.status,
actionType: value.actionType,
actionRef: value.actionRef,
projectId: value.projectId,
requestedBy: value.requestedBy,
tool: value.tool,
permission: value.permission,
requiredPermissions: value.requiredPermissions,
effect: value.effect,
risk: value.risk,
policyFence: value.policyFence,
profile: value.profile,
snapshotDigest: value.snapshotDigest,
definitionDigest: value.definitionDigest,
bindingDigest: value.binding.bindingDigest,
timeoutSeconds: value.timeoutSeconds,
invocationArtifact: value.invocationArtifact,
invocationActionDigest: value.invocationActionDigest,
};
}
function planWithoutDigest(
value: Readonly<TrustedToolInvocationPlan>,
): Omit<TrustedToolInvocationPlan, 'planDigest'> {
const { planDigest: _planDigest, ...unsigned } = value;
return unsigned;
}
export function createTrustedToolInvocationPlan(
bindings: TrustedToolHandlerBindingRegistry,
invocationValue: PreparedToolInvocation,
inputValue: Readonly<{
actionRef: string;
profile: DeploymentProfile;
preview: TrustedToolInvocationPreview;
inputArtifactId: string;
previewArtifactId: string;
artifactKeyId: string;
artifactKey: Uint8Array;
artifactNonce: Uint8Array;
sealedAtMs: number;
}>,
): Readonly<TrustedToolInvocationPlanBundle> {
if (!(bindings instanceof TrustedToolHandlerBindingRegistry)) {
return invalid('handler binding registry is invalid');
}
const input = dataRecord(inputValue, 'plan input');
exactKeys(
input,
[
'actionRef',
'artifactKey',
'artifactKeyId',
'artifactNonce',
'inputArtifactId',
'preview',
'previewArtifactId',
'profile',
'sealedAtMs',
],
[],
'plan input',
);
const definitionRegistry = bindings.definitionRegistry();
const invocation = normalizePreparedInvocation(
invocationValue,
definitionRegistry,
);
if (invocation.projectId !== bindings.projectId) {
throw new TrustedToolInvocationBindingConflictError();
}
const profile = normalizeProfile(inputValue.profile);
const binding = bindings.resolve(
invocation.tool.name,
invocation.tool.version,
profile,
);
const preview = normalizeTrustedToolInvocationPreview(inputValue.preview);
const sealedAtMs = timestamp(inputValue.sealedAtMs, 'plan seal time');
const normalizedActionRef = identifier(
inputValue.actionRef,
'action reference',
);
const inputArtifact = createToolInvocationInputArtifact(
{
artifactId: inputValue.inputArtifactId,
projectId: invocation.projectId,
actionRef: normalizedActionRef,
requestedBy: invocation.requestedBy,
tool: invocation.tool,
input: invocation.input,
inputDigest: invocation.inputDigest,
invocationActionDigest: invocation.actionDigest,
keyId: inputValue.artifactKeyId,
key: inputValue.artifactKey,
sealedAtMs,
},
() => inputValue.artifactNonce,
);
const invocationArtifact =
toolInvocationInputArtifactReference(inputArtifact);
const base = Object.freeze({
schema: TRUSTED_TOOL_INVOCATION_PLAN_SCHEMA,
status: invocation.status,
actionType: TOOL_INVOKE_ACTION_TYPE,
actionRef: normalizedActionRef,
projectId: invocation.projectId,
requestedBy: invocation.requestedBy,
tool: invocation.tool,
permission: invocation.permission,
requiredPermissions: invocation.requiredPermissions,
effect: invocation.effect,
risk: invocation.risk,
policyFence: invocation.fence,
profile,
snapshotDigest: bindings.snapshotDigest,
definitionDigest: binding.definitionDigest,
binding,
timeoutSeconds: Math.min(invocation.timeoutSeconds, binding.timeoutSeconds),
invocationArtifact,
invocationActionDigest: invocation.actionDigest,
sealedAtMs,
} satisfies Omit<TrustedToolInvocationPlan, 'actionDigest' | 'planDigest' | 'previewArtifact'>);
const actionDigest = hash(ACTION_DIGEST_DOMAIN, actionFields(base));
const previewArtifact = createToolInvocationPreviewArtifact({
artifactId: inputValue.previewArtifactId,
projectId: invocation.projectId,
actionRef: normalizedActionRef,
actionDigest,
preview,
redactionContractDigest: trustedToolContractIdentityDigest(
binding.redactionContract,
),
sealedAtMs,
});
const previewArtifactReference =
toolInvocationPreviewArtifactReference(previewArtifact);
const unsigned = Object.freeze({
...base,
previewArtifact: previewArtifactReference,
actionDigest,
});
const plan = Object.freeze({
...unsigned,
planDigest: hash(PLAN_DIGEST_DOMAIN, unsigned),
});
return Object.freeze({
plan,
inputArtifact,
previewArtifact,
});
}
export function normalizeTrustedToolInvocationPlan(
value: TrustedToolInvocationPlan,
bindings: TrustedToolHandlerBindingRegistry,
): Readonly<TrustedToolInvocationPlan> {
if (!(bindings instanceof TrustedToolHandlerBindingRegistry)) {
return invalid('handler binding registry is invalid');
}
const record = dataRecord(value, 'trusted invocation plan');
exactKeys(
record,
[
'actionRef',
'actionDigest',
'actionType',
'binding',
'definitionDigest',
'effect',
'invocationArtifact',
'invocationActionDigest',
'permission',
'planDigest',
'policyFence',
'previewArtifact',
'profile',
'projectId',
'requestedBy',
'requiredPermissions',
'risk',
'schema',
'sealedAtMs',
'snapshotDigest',
'status',
'timeoutSeconds',
'tool',
],
[],
'trusted invocation plan',
);
if (
value.schema !== TRUSTED_TOOL_INVOCATION_PLAN_SCHEMA ||
value.actionType !== TOOL_INVOKE_ACTION_TYPE ||
(value.status !== 'ready' && value.status !== 'approval_required')
) {
return invalid('trusted invocation plan schema or status is invalid');
}
assertProjectPolicyProjectId(value.projectId);
const requestedBy = normalizeProjectPolicySubject(value.requestedBy);
const tool = normalizeToolIdentity(value.tool, 'trusted plan Tool');
const profile = normalizeProfile(value.profile);
const currentBinding = bindings.resolve(tool.name, tool.version, profile);
const binding = normalizeTrustedToolHandlerBinding(value.binding);
if (
value.projectId !== bindings.projectId ||
digest(value.snapshotDigest, 'snapshot digest') !==
bindings.snapshotDigest ||
binding.bindingDigest !== currentBinding.bindingDigest ||
digest(value.definitionDigest, 'definition digest') !==
binding.definitionDigest
) {
throw new TrustedToolInvocationBindingConflictError();
}
const definitionRegistry = bindings.definitionRegistry();
const definition = definitionRegistry.resolve(tool.name, tool.version);
const permission = normalizeProjectPermission(value.permission);
const requiredPermissions = normalizePermissions(
value.requiredPermissions,
'trusted plan required permissions',
);
const policyFence = normalizeFence(value.policyFence);
const invocationArtifact = normalizeToolInvocationInputArtifactReference(
value.invocationArtifact,
);
const invocationActionDigest = digest(
value.invocationActionDigest,
'invocation action digest',
);
const expectedInvocationActionDigest = hash(Buffer.alloc(0), {
schema: TOOL_INVOCATION_SCHEMA,
projectId: value.projectId,
requestedBy,
tool,
permission,
requiredPermissions,
effect: definition.effect,
risk: definition.risk,
timeoutSeconds: definition.timeoutSeconds,
inputDigest: invocationArtifact.inputDigest,
});
if (
permission !== normalizeProjectPermission(`tool.call:${definition.name}`) ||
JSON.stringify(requiredPermissions) !==
JSON.stringify(definition.requiredPermissions) ||
value.effect !== definition.effect ||
value.risk !== definition.risk ||
invocationActionDigest !== expectedInvocationActionDigest
) {
return invalid('trusted invocation plan drifts from its Tool definition');
}
const timeoutSeconds = positiveInteger(
value.timeoutSeconds,
definition.timeoutSeconds,
'trusted plan timeout',
);
if (
timeoutSeconds !==
Math.min(definition.timeoutSeconds, binding.timeoutSeconds)
) {
return invalid('trusted plan timeout does not match its binding');
}
const previewArtifact = normalizeToolInvocationPreviewArtifactReference(
value.previewArtifact,
);
if (
previewArtifact.redactionContractDigest !==
trustedToolContractIdentityDigest(binding.redactionContract)
) {
return invalid(
'trusted preview Artifact redaction contract does not match',
);
}
const base = Object.freeze({
schema: TRUSTED_TOOL_INVOCATION_PLAN_SCHEMA,
status: value.status,
actionType: TOOL_INVOKE_ACTION_TYPE,
actionRef: identifier(value.actionRef, 'action reference'),
projectId: value.projectId,
requestedBy,
tool,
permission,
requiredPermissions,
effect: definition.effect,
risk: definition.risk,
policyFence,
profile,
snapshotDigest: bindings.snapshotDigest,
definitionDigest: binding.definitionDigest,
binding,
timeoutSeconds,
invocationArtifact,
invocationActionDigest,
sealedAtMs: timestamp(value.sealedAtMs, 'plan seal time'),
} satisfies Omit<TrustedToolInvocationPlan, 'actionDigest' | 'planDigest' | 'previewArtifact'>);
const actionDigest = digest(value.actionDigest, 'action digest');
const expectedActionDigest = hash(ACTION_DIGEST_DOMAIN, actionFields(base));
if (
actionDigest !== expectedActionDigest ||
previewArtifact.actionDigest !== actionDigest
) {
return invalid('trusted invocation action digest does not match');
}
const unsigned = Object.freeze({
...base,
previewArtifact,
actionDigest,
});
const planDigest = digest(value.planDigest, 'plan digest');
if (hash(PLAN_DIGEST_DOMAIN, unsigned) !== planDigest) {
return invalid('trusted invocation plan digest does not match');
}
return Object.freeze({ ...unsigned, planDigest });
}
export function trustedToolInvocationApprovalBinding(
planValue: TrustedToolInvocationPlan,
bindings: TrustedToolHandlerBindingRegistry,
): Readonly<ApprovedActionBinding> {
const plan = normalizeTrustedToolInvocationPlan(planValue, bindings);
if (plan.status !== 'approval_required') {
throw new TrustedToolExecutionApprovalRequiredError();
}
return Object.freeze({
permission: plan.permission,
actionType: TOOL_INVOKE_ACTION_TYPE,
actionRef: plan.actionRef,
actionDigest: plan.actionDigest,
previewDigest: plan.previewArtifact.previewDigest,
});
}
export function assertTrustedToolApprovedDispatch(
planValue: TrustedToolInvocationPlan,
bindings: TrustedToolHandlerBindingRegistry,
dispatchValue: ApprovedActionDispatchRecord,
): Readonly<ApprovedActionDispatchRecord> {
const plan = normalizeTrustedToolInvocationPlan(planValue, bindings);
if (plan.status !== 'approval_required') {
throw new TrustedToolInvocationBindingConflictError();
}
const dispatch = normalizeApprovedActionDispatchRecord(dispatchValue);
const expected = trustedToolInvocationApprovalBinding(plan, bindings);
if (
dispatch.projectId !== plan.projectId ||
!sameSubject(dispatch.requestedBy, plan.requestedBy) ||
dispatch.action.permission !== expected.permission ||
dispatch.action.actionType !== expected.actionType ||
dispatch.action.actionRef !== expected.actionRef ||
dispatch.action.actionDigest !== expected.actionDigest ||
dispatch.action.previewDigest !== expected.previewDigest ||
dispatch.createdAtMs < plan.sealedAtMs
) {
throw new TrustedToolInvocationBindingConflictError();
}
return dispatch;
}
@@ -0,0 +1,392 @@
import { createHash } from 'node:crypto';
import type { RunRepositoryReader } from '../run/runRepository';
import {
normalizeStepRunRecord,
transitionStepRunMutation,
type StepRunRepository,
} from '../run/stepRun';
import {
normalizeToolExecutionCompletionRecord,
ToolExecutionCompletionConflictError,
type ToolExecutionCompletionRecord,
} from './toolExecutionCompletion';
import {
createToolExecutionFailureCompletionCommand,
createToolExecutionFailureResult,
normalizeToolExecutionFailureCompletionRecord,
ToolExecutionFailureCompletionConflictError,
ToolExecutionFailureCompletionUnavailableError,
toolExecutionFailureCompletionRecord,
type ToolExecutionFailureCompletionRecord,
type ToolExecutionFailureCompletionRepository,
type ToolExecutionFailureOutcome,
} from './toolExecutionFailureCompletion';
import {
normalizeToolExecutionStartBarrierRecord,
type ToolExecutionStartBarrierRecord,
} from './toolExecutionStartBarrier';
import {
executeAndCompleteTrustedToolSuccess,
type TrustedToolSuccessCompletionDependencies,
type TrustedToolSuccessCompletionResult,
} from './trustedToolSuccessCompletion';
import {
TrustedToolExecutionDeadlineExceededError,
TrustedToolExecutionFailedError,
} from './trustedToolExecution';
import type { ToolJsonValue } from './tool-registry/toolRegistry';
export interface TrustedToolFailureCompletionIdentities {
readonly mutationId: string;
readonly eventId: string;
}
export interface TrustedToolFailureCompletionIdentityFactory {
create(startId: string): TrustedToolFailureCompletionIdentities;
}
export interface TrustedToolCompletionDependencies
extends TrustedToolSuccessCompletionDependencies {
readonly failureCompletions: ToolExecutionFailureCompletionRepository;
readonly failureIdentities: TrustedToolFailureCompletionIdentityFactory;
readonly stepRuns: Pick<StepRunRepository, 'findById'>;
readonly runs: Pick<RunRepositoryReader, 'findRunById'>;
}
export interface TrustedToolSucceededCompletionResult {
readonly outcome: 'succeeded';
readonly status: 'created' | 'existing';
readonly completion: Readonly<ToolExecutionCompletionRecord>;
readonly output: ToolJsonValue;
}
export interface TrustedToolFailedCompletionResult {
readonly outcome: ToolExecutionFailureOutcome;
readonly status: 'created' | 'existing';
readonly completion: Readonly<ToolExecutionFailureCompletionRecord>;
}
export type TrustedToolCompletionResult =
| TrustedToolSucceededCompletionResult
| TrustedToolFailedCompletionResult;
interface DurableTerminalState {
readonly success: Readonly<ToolExecutionCompletionRecord> | null;
readonly failure: Readonly<ToolExecutionFailureCompletionRecord> | null;
}
const TERMINAL_RUN_STATUSES = new Set([
'succeeded',
'failed',
'cancelled',
'timed_out',
]);
function unavailable(cause?: unknown): never {
throw new ToolExecutionFailureCompletionUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
}
function conflict(): never {
throw new ToolExecutionFailureCompletionConflictError();
}
function sameValue(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function validateDependencies(
dependencies: TrustedToolCompletionDependencies,
): void {
if (
!dependencies ||
typeof dependencies !== 'object' ||
!dependencies.failureCompletions ||
typeof dependencies.failureCompletions.findByStartId !== 'function' ||
typeof dependencies.failureCompletions.commit !== 'function' ||
!dependencies.failureIdentities ||
typeof dependencies.failureIdentities.create !== 'function'
) {
unavailable();
}
}
async function findTerminalState(
startId: string,
dependencies: TrustedToolCompletionDependencies,
): Promise<Readonly<DurableTerminalState>> {
try {
const [successValue, failureValue] = await Promise.all([
dependencies.completions.findByStartId(startId),
dependencies.failureCompletions.findByStartId(startId),
]);
const success =
successValue === null
? null
: normalizeToolExecutionCompletionRecord(successValue);
const failure =
failureValue === null
? null
: normalizeToolExecutionFailureCompletionRecord(failureValue);
if (success && failure) return conflict();
return Object.freeze({ success, failure });
} catch (cause) {
if (
cause instanceof ToolExecutionCompletionConflictError ||
cause instanceof ToolExecutionFailureCompletionConflictError
) {
throw cause;
}
return unavailable(cause);
}
}
async function findBarrier(
startId: string,
dependencies: TrustedToolCompletionDependencies,
): Promise<Readonly<ToolExecutionStartBarrierRecord>> {
try {
const value = await dependencies.barriers.findByStartId(startId);
if (!value) return unavailable();
const barrier = normalizeToolExecutionStartBarrierRecord(value);
if (barrier.startId !== startId) return unavailable();
return barrier;
} catch (cause) {
return unavailable(cause);
}
}
function failureMatchesBarrier(
completion: Readonly<ToolExecutionFailureCompletionRecord>,
barrier: Readonly<ToolExecutionStartBarrierRecord>,
): boolean {
return (
completion.startId === barrier.startId &&
completion.projectId === barrier.projectId &&
completion.runId === barrier.runId &&
completion.stepRunId === barrier.stepRunId &&
completion.startedStepRunVersion === barrier.startedStepRunVersion &&
completion.completedStepRunVersion === barrier.startedStepRunVersion + 1 &&
completion.barrierDigest === barrier.barrierDigest &&
completion.adapterDigest === barrier.adapterDigest
);
}
async function openDurableFailure(
completion: Readonly<ToolExecutionFailureCompletionRecord>,
dependencies: TrustedToolCompletionDependencies,
): Promise<Readonly<TrustedToolFailedCompletionResult>> {
const barrier = await findBarrier(completion.startId, dependencies);
if (!failureMatchesBarrier(completion, barrier)) return conflict();
return Object.freeze({
outcome: completion.outcome,
status: 'existing' as const,
completion,
});
}
function succeeded(
result: Readonly<TrustedToolSuccessCompletionResult>,
): Readonly<TrustedToolSucceededCompletionResult> {
return Object.freeze({
outcome: 'succeeded' as const,
status: result.status,
completion: result.completion,
output: result.output,
});
}
async function openDurableSuccess(
startId: string,
dependencies: TrustedToolCompletionDependencies,
): Promise<Readonly<TrustedToolSucceededCompletionResult>> {
return succeeded(
await executeAndCompleteTrustedToolSuccess(startId, dependencies),
);
}
async function returnDurableWinner(
startId: string,
dependencies: TrustedToolCompletionDependencies,
): Promise<Readonly<TrustedToolCompletionResult> | null> {
const state = await findTerminalState(startId, dependencies);
if (state.success) return openDurableSuccess(startId, dependencies);
if (state.failure) return openDurableFailure(state.failure, dependencies);
return null;
}
function observedAtMs(
barrier: Readonly<ToolExecutionStartBarrierRecord>,
dependencies: TrustedToolCompletionDependencies,
): number {
let value: number;
try {
value = (dependencies.now ?? Date.now)();
} catch (cause) {
return unavailable(cause);
}
if (
!Number.isSafeInteger(value) ||
value < barrier.startedAtMs ||
value < 0
) {
return unavailable();
}
return value;
}
async function findRunningStepAndRun(
barrier: Readonly<ToolExecutionStartBarrierRecord>,
dependencies: TrustedToolCompletionDependencies,
) {
try {
const [stepValue, run] = await Promise.all([
dependencies.stepRuns.findById(barrier.stepRunId),
dependencies.runs.findRunById(barrier.runId),
]);
if (!stepValue || !run) return unavailable();
const stepRun = normalizeStepRunRecord(stepValue);
if (
stepRun.id !== barrier.stepRunId ||
stepRun.runId !== barrier.runId ||
stepRun.kind !== 'tool' ||
stepRun.status !== 'running' ||
stepRun.version !== barrier.startedStepRunVersion ||
stepRun.stepRunDigest !== barrier.startedStepRunDigest ||
run.id !== barrier.runId ||
run.projectId !== barrier.projectId ||
!Number.isSafeInteger(run.version) ||
run.version < 0 ||
!Number.isSafeInteger(run.eventSequence) ||
run.eventSequence < 0 ||
TERMINAL_RUN_STATUSES.has(run.status)
) {
return conflict();
}
return Object.freeze({ stepRun, run });
} catch (cause) {
return unavailable(cause);
}
}
function failureDedupeKey(startId: string): string {
return `tool-failure:${createHash('sha256').update(startId).digest('hex')}`;
}
async function commitFailure(
startId: string,
outcome: ToolExecutionFailureOutcome,
dependencies: TrustedToolCompletionDependencies,
): Promise<Readonly<TrustedToolCompletionResult>> {
const winner = await returnDurableWinner(startId, dependencies);
if (winner) return winner;
const barrier = await findBarrier(startId, dependencies);
const { stepRun, run } = await findRunningStepAndRun(barrier, dependencies);
const failure = createToolExecutionFailureResult(
barrier,
outcome,
observedAtMs(barrier, dependencies),
);
let identities: TrustedToolFailureCompletionIdentities;
try {
identities = dependencies.failureIdentities.create(startId);
} catch (cause) {
return unavailable(cause);
}
const mutation = transitionStepRunMutation(
stepRun,
{
expectedVersion: stepRun.version,
expectedDigest: stepRun.stepRunDigest,
mutationId: identities.mutationId,
to: outcome,
atMs: failure.completedAtMs,
resultCode: failure.resultCode,
errorSummary: failure.errorSummary,
},
{
expectedRunVersion: run.version,
expectedRunEventSequence: run.eventSequence,
eventId: identities.eventId,
dedupeKey: failureDedupeKey(startId),
actor: Object.freeze({
type: 'system' as const,
id: 'trusted-tool-runtime',
}),
},
);
const command = createToolExecutionFailureCompletionCommand({
barrier,
failure,
stepRunMutation: mutation,
});
const expected = toolExecutionFailureCompletionRecord(command);
try {
const committed = await dependencies.failureCompletions.commit(command);
const completion = normalizeToolExecutionFailureCompletionRecord(
committed.completion,
);
if (
!['created', 'existing'].includes(committed.status) ||
!sameValue(completion, expected)
) {
return conflict();
}
const concurrentSuccess = await dependencies.completions.findByStartId(
startId,
);
if (concurrentSuccess) return conflict();
return Object.freeze({
outcome: completion.outcome,
status: committed.status,
completion,
});
} catch (cause) {
const recovered = await returnDurableWinner(startId, dependencies);
if (recovered) return recovered;
throw cause;
}
}
/**
* Completes one already-started retry-safe Tool with exactly one durable
* succeeded, failed or timed_out outcome.
*
* Only explicit adapter failure and deadline errors become terminal failures.
* Binding, key, snapshot and storage errors stay non-terminal. A lost commit
* response is recovered exclusively from durable state and never re-executes
* the adapter in the same call.
*/
export async function executeAndCompleteTrustedTool(
startId: string,
dependencies: TrustedToolCompletionDependencies,
): Promise<Readonly<TrustedToolCompletionResult>> {
validateDependencies(dependencies);
const winner = await returnDurableWinner(startId, dependencies);
if (winner) return winner;
try {
const result = await executeAndCompleteTrustedToolSuccess(
startId,
dependencies,
);
const concurrentFailure =
await dependencies.failureCompletions.findByStartId(startId);
if (concurrentFailure) return conflict();
return succeeded(result);
} catch (cause) {
const durable = await returnDurableWinner(startId, dependencies);
if (durable) return durable;
if (cause instanceof TrustedToolExecutionDeadlineExceededError) {
return commitFailure(startId, 'timed_out', dependencies);
}
if (cause instanceof TrustedToolExecutionFailedError) {
return commitFailure(startId, 'failed', dependencies);
}
throw cause;
}
}
@@ -0,0 +1,592 @@
import { createHash } from 'node:crypto';
import {
TOOL_EXECUTION_START_BARRIER_SCHEMA,
normalizeToolExecutionStartBarrierRecord,
type ToolExecutionStartBarrierRecord,
type ToolExecutionStartBarrierRepository,
} from './toolExecutionStartBarrier';
import {
TOOL_INVOCATION_SCHEMA,
type ToolJsonValue,
} from './tool-registry/toolRegistry';
import {
normalizeToolInvocationInputArtifact,
openToolInvocationInputArtifact,
toolInvocationInputArtifactReference,
type ToolInvocationArtifactKeyProvider,
type ToolInvocationArtifactRepository,
} from './toolInvocationArtifact';
import {
TrustedToolHandlerBindingRegistry,
normalizeTrustedToolHandlerBinding,
trustedToolContractIdentityDigest,
type TrustedToolHandlerBinding,
} from './trustedToolInvocation';
import type { DeploymentProfile } from '../cluster-control/clusterControlActivation';
import type { SecuritySubject } from '../security/security';
export const TRUSTED_TOOL_EXECUTION_RESULT_SCHEMA =
'qinglong/trusted-tool-execution-result@v1' as const;
export const TRUSTED_TOOL_EXECUTION_RECOVERY_EVIDENCE_SCHEMA =
'qinglong/trusted-tool-execution-recovery-evidence@v1' as const;
export const MAX_TRUSTED_TOOL_EXECUTION_ADAPTERS = 128;
export interface TrustedToolExecutionAdapterContext {
readonly startId: string;
readonly projectId: string;
readonly runId: string;
readonly stepRunId: string;
readonly actionRef: string;
readonly requestedBy: Readonly<SecuritySubject>;
readonly profile: DeploymentProfile;
readonly startedAtMs: number;
readonly deadlineAtMs: number;
}
/**
* An executable adapter is supplied only by a trusted Profile composition
* root. Its immutable binding is Project/snapshot-specific; Package content,
* invocation input and persisted plans cannot register executable code.
*/
export interface TrustedToolExecutionAdapter {
readonly binding: Readonly<TrustedToolHandlerBinding>;
readonly profile: DeploymentProfile;
readonly recoveryMode: 'retry_safe_read';
execute(
context: Readonly<TrustedToolExecutionAdapterContext>,
input: ToolJsonValue,
): Promise<unknown>;
}
export interface TrustedToolExecutionDependencies {
readonly barriers: Pick<ToolExecutionStartBarrierRepository, 'findByStartId'>;
readonly artifacts: Pick<ToolInvocationArtifactRepository, 'findInput'>;
readonly keys: Pick<ToolInvocationArtifactKeyProvider, 'resolve'>;
readonly adapters: TrustedToolExecutionAdapterRegistry;
readonly now?: () => number;
}
export interface TrustedToolExecutionResult {
readonly schema: typeof TRUSTED_TOOL_EXECUTION_RESULT_SCHEMA;
readonly startId: string;
readonly barrierDigest: string;
readonly adapterDigest: string;
readonly output: ToolJsonValue;
readonly outputDigest: string;
readonly completedAtMs: number;
readonly resultDigest: string;
}
export interface TrustedToolExecutionRecoveryEvidence {
readonly schema: typeof TRUSTED_TOOL_EXECUTION_RECOVERY_EVIDENCE_SCHEMA;
readonly startId: string;
readonly barrierDigest: string;
readonly adapterDigest: string;
readonly disposition: 'retry_safe';
readonly reason: 'read_only_no_side_effects';
readonly inspectedAtMs: number;
readonly evidenceDigest: string;
}
export class InvalidTrustedToolExecutionError extends TypeError {
readonly code = 'TRUSTED_TOOL_EXECUTION_INVALID';
constructor(message: string) {
super(`Trusted Tool execution is invalid: ${message}`);
this.name = 'InvalidTrustedToolExecutionError';
}
}
export class TrustedToolExecutionUnavailableError extends Error {
readonly code = 'TRUSTED_TOOL_EXECUTION_UNAVAILABLE';
constructor() {
super('Trusted Tool execution prerequisites are unavailable');
this.name = 'TrustedToolExecutionUnavailableError';
}
}
export class TrustedToolExecutionBindingConflictError extends Error {
readonly code = 'TRUSTED_TOOL_EXECUTION_BINDING_CONFLICT';
constructor() {
super('Trusted Tool execution binding changed after durable start');
this.name = 'TrustedToolExecutionBindingConflictError';
}
}
export class TrustedToolExecutionDeadlineExceededError extends Error {
readonly code = 'TRUSTED_TOOL_EXECUTION_DEADLINE_EXCEEDED';
constructor() {
super('Trusted Tool execution deadline was exceeded');
this.name = 'TrustedToolExecutionDeadlineExceededError';
}
}
export class TrustedToolExecutionFailedError extends Error {
readonly code = 'TRUSTED_TOOL_EXECUTION_FAILED';
constructor() {
super('Trusted Tool adapter execution failed');
this.name = 'TrustedToolExecutionFailedError';
}
}
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const RESULT_DIGEST_DOMAIN = Buffer.from(
'qinglong/trusted-tool-execution-result-digest@v1\0',
'utf8',
);
const OUTPUT_DIGEST_DOMAIN = Buffer.from(
'qinglong/trusted-tool-execution-output-digest@v1\0',
'utf8',
);
const RECOVERY_EVIDENCE_DIGEST_DOMAIN = Buffer.from(
'qinglong/trusted-tool-execution-recovery-evidence-digest@v1\0',
'utf8',
);
function invalid(message: string): never {
throw new InvalidTrustedToolExecutionError(message);
}
function hash(domain: Uint8Array, value: unknown): string {
return createHash('sha256')
.update(domain)
.update(JSON.stringify(value))
.digest('hex');
}
function sameValue(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function now(clock: (() => number) | undefined): number {
let value: number;
try {
value = (clock ?? Date.now)();
} catch {
throw new TrustedToolExecutionUnavailableError();
}
if (!Number.isSafeInteger(value) || value < 0) {
throw new TrustedToolExecutionUnavailableError();
}
return value;
}
function startIdentity(value: string): string {
if (typeof value !== 'string' || !IDENTIFIER_PATTERN.test(value)) {
return invalid('start identity is invalid');
}
return value;
}
function adapterKey(bindingDigest: string, profile: DeploymentProfile): string {
return `${bindingDigest}:${profile}`;
}
function invocationActionDigest(
barrier: Readonly<ToolExecutionStartBarrierRecord>,
binding: Readonly<TrustedToolHandlerBinding>,
definitions: ReturnType<
TrustedToolHandlerBindingRegistry['definitionRegistry']
>,
): string {
const definition = definitions.resolve(
binding.tool.name,
binding.tool.version,
);
return createHash('sha256')
.update(
JSON.stringify({
schema: TOOL_INVOCATION_SCHEMA,
projectId: barrier.projectId,
requestedBy: barrier.requestedBy,
tool: binding.tool,
permission: `tool.call:${definition.name}`,
requiredPermissions: definition.requiredPermissions,
effect: definition.effect,
risk: definition.risk,
timeoutSeconds: definition.timeoutSeconds,
inputDigest: barrier.invocationArtifact.inputDigest,
}),
)
.digest('hex');
}
export class TrustedToolExecutionAdapterRegistry {
readonly #bindings!: TrustedToolHandlerBindingRegistry;
readonly #adapters!: ReadonlyMap<string, TrustedToolExecutionAdapter>;
constructor(
bindings: TrustedToolHandlerBindingRegistry,
adapters: readonly TrustedToolExecutionAdapter[],
) {
if (!(bindings instanceof TrustedToolHandlerBindingRegistry)) {
return invalid('handler binding registry is invalid');
}
if (
!Array.isArray(adapters) ||
adapters.length > MAX_TRUSTED_TOOL_EXECUTION_ADAPTERS
) {
return invalid('adapter count is invalid');
}
const entries = new Map<string, TrustedToolExecutionAdapter>();
const definitions = bindings.definitionRegistry();
for (const candidate of adapters) {
if (
!candidate ||
typeof candidate !== 'object' ||
typeof candidate.execute !== 'function' ||
candidate.recoveryMode !== 'retry_safe_read'
) {
return invalid('adapter shape is invalid');
}
const binding = normalizeTrustedToolHandlerBinding(candidate.binding);
const current = bindings.resolve(
binding.tool.name,
binding.tool.version,
candidate.profile,
);
const definition = definitions.resolve(
binding.tool.name,
binding.tool.version,
);
if (
current.bindingDigest !== binding.bindingDigest ||
definition.effect !== 'read' ||
binding.executionClass !== 'builtin_in_process'
) {
throw new TrustedToolExecutionBindingConflictError();
}
const key = adapterKey(binding.bindingDigest, candidate.profile);
if (entries.has(key)) {
return invalid('adapter binding is duplicated');
}
entries.set(
key,
Object.freeze({
binding,
profile: candidate.profile,
recoveryMode: candidate.recoveryMode,
execute: candidate.execute.bind(candidate),
}),
);
}
this.#bindings = bindings;
this.#adapters = entries;
Object.freeze(this);
}
resolve(
barrierValue: ToolExecutionStartBarrierRecord,
): TrustedToolExecutionAdapter {
const barrier = normalizeToolExecutionStartBarrierRecord(barrierValue);
const current = this.#bindings
.list()
.find((binding) => binding.bindingDigest === barrier.bindingDigest);
if (!current) {
throw new TrustedToolExecutionBindingConflictError();
}
let resolved: Readonly<TrustedToolHandlerBinding>;
try {
resolved = this.#bindings.resolve(
current.tool.name,
current.tool.version,
barrier.profile,
);
} catch {
throw new TrustedToolExecutionBindingConflictError();
}
if (
barrier.schema !== TOOL_EXECUTION_START_BARRIER_SCHEMA ||
barrier.projectId !== this.#bindings.projectId ||
barrier.snapshotDigest !== this.#bindings.snapshotDigest ||
barrier.definitionDigest !== current.definitionDigest ||
resolved.bindingDigest !== current.bindingDigest ||
barrier.timeoutSeconds !== current.timeoutSeconds ||
barrier.executionClass !== current.executionClass ||
!sameValue(barrier.adapter, current.adapter) ||
barrier.adapterDigest !==
trustedToolContractIdentityDigest(current.adapter) ||
!sameValue(barrier.redactionContract, current.redactionContract) ||
barrier.redactionContractDigest !==
trustedToolContractIdentityDigest(current.redactionContract) ||
!sameValue(barrier.auditContract, current.auditContract) ||
barrier.auditContractDigest !==
trustedToolContractIdentityDigest(current.auditContract)
) {
throw new TrustedToolExecutionBindingConflictError();
}
const adapter = this.#adapters.get(
adapterKey(current.bindingDigest, barrier.profile),
);
if (!adapter) {
throw new TrustedToolExecutionUnavailableError();
}
return adapter;
}
definitionRegistry(): ReturnType<
TrustedToolHandlerBindingRegistry['definitionRegistry']
> {
return this.#bindings.definitionRegistry();
}
}
async function durableBarrier(
startIdValue: string,
dependencies: Pick<TrustedToolExecutionDependencies, 'adapters' | 'barriers'>,
): Promise<
Readonly<{
barrier: Readonly<ToolExecutionStartBarrierRecord>;
adapter: TrustedToolExecutionAdapter;
}>
> {
const startId = startIdentity(startIdValue);
if (
!dependencies ||
typeof dependencies !== 'object' ||
!dependencies.barriers ||
typeof dependencies.barriers.findByStartId !== 'function' ||
!(dependencies.adapters instanceof TrustedToolExecutionAdapterRegistry)
) {
return invalid('durable execution dependencies are invalid');
}
let found: Readonly<ToolExecutionStartBarrierRecord> | null;
try {
found = await dependencies.barriers.findByStartId(startId);
} catch {
throw new TrustedToolExecutionUnavailableError();
}
if (!found) {
throw new TrustedToolExecutionUnavailableError();
}
let barrier: Readonly<ToolExecutionStartBarrierRecord>;
try {
barrier = normalizeToolExecutionStartBarrierRecord(found);
} catch {
throw new TrustedToolExecutionUnavailableError();
}
if (barrier.startId !== startId) {
throw new TrustedToolExecutionUnavailableError();
}
return Object.freeze({
barrier,
adapter: dependencies.adapters.resolve(barrier),
});
}
function executionContext(
barrier: Readonly<ToolExecutionStartBarrierRecord>,
): Readonly<TrustedToolExecutionAdapterContext> {
const deadlineAtMs = barrier.startedAtMs + barrier.timeoutSeconds * 1_000;
if (!Number.isSafeInteger(deadlineAtMs)) {
throw new TrustedToolExecutionUnavailableError();
}
return Object.freeze({
startId: barrier.startId,
projectId: barrier.projectId,
runId: barrier.runId,
stepRunId: barrier.stepRunId,
actionRef: barrier.actionRef,
requestedBy: barrier.requestedBy,
profile: barrier.profile,
startedAtMs: barrier.startedAtMs,
deadlineAtMs,
});
}
async function executeBeforeDeadline(
adapter: TrustedToolExecutionAdapter,
context: Readonly<TrustedToolExecutionAdapterContext>,
input: ToolJsonValue,
clock: (() => number) | undefined,
): Promise<unknown> {
const remainingMs = context.deadlineAtMs - now(clock);
if (remainingMs <= 0) {
throw new TrustedToolExecutionDeadlineExceededError();
}
return new Promise<unknown>((resolve, reject) => {
let settled = false;
const timer = setTimeout(() => {
if (settled) return;
settled = true;
reject(new TrustedToolExecutionDeadlineExceededError());
}, remainingMs);
Promise.resolve()
.then(() => adapter.execute(context, input))
.then(
(value) => {
if (settled) return;
settled = true;
clearTimeout(timer);
resolve(value);
},
(error: unknown) => {
if (settled) return;
settled = true;
clearTimeout(timer);
reject(error);
},
);
});
}
export async function executeTrustedToolAfterStart(
startId: string,
dependencies: TrustedToolExecutionDependencies,
): Promise<Readonly<TrustedToolExecutionResult>> {
if (
!dependencies ||
typeof dependencies !== 'object' ||
!dependencies.artifacts ||
typeof dependencies.artifacts.findInput !== 'function' ||
!dependencies.keys ||
typeof dependencies.keys.resolve !== 'function'
) {
return invalid('execution dependencies are invalid');
}
const { barrier, adapter } = await durableBarrier(startId, dependencies);
const context = executionContext(barrier);
if (now(dependencies.now) > context.deadlineAtMs) {
throw new TrustedToolExecutionDeadlineExceededError();
}
let artifactValue: Awaited<
ReturnType<ToolInvocationArtifactRepository['findInput']>
>;
try {
artifactValue = await dependencies.artifacts.findInput(
barrier.invocationArtifact.artifactId,
);
} catch {
throw new TrustedToolExecutionUnavailableError();
}
if (!artifactValue) {
throw new TrustedToolExecutionUnavailableError();
}
let artifact: ReturnType<typeof normalizeToolInvocationInputArtifact>;
try {
artifact = normalizeToolInvocationInputArtifact(artifactValue);
} catch {
throw new TrustedToolExecutionUnavailableError();
}
if (
!sameValue(
toolInvocationInputArtifactReference(artifact),
barrier.invocationArtifact,
) ||
artifact.projectId !== barrier.projectId ||
artifact.actionRef !== barrier.actionRef ||
!sameValue(artifact.requestedBy, barrier.requestedBy) ||
!sameValue(artifact.tool, adapter.binding.tool) ||
artifact.invocationActionDigest !==
invocationActionDigest(
barrier,
adapter.binding,
dependencies.adapters.definitionRegistry(),
) ||
artifact.sealedAtMs > barrier.startedAtMs
) {
throw new TrustedToolExecutionBindingConflictError();
}
let material: Awaited<
ReturnType<ToolInvocationArtifactKeyProvider['resolve']>
>;
try {
material = await dependencies.keys.resolve(artifact.keyId);
} catch {
throw new TrustedToolExecutionUnavailableError();
}
if (!material) {
throw new TrustedToolExecutionUnavailableError();
}
const key = material.key;
if (
material.keyId !== artifact.keyId ||
!(key instanceof Uint8Array) ||
key.byteLength !== 32
) {
if (key instanceof Uint8Array) key.fill(0);
throw new TrustedToolExecutionUnavailableError();
}
const definitions = dependencies.adapters.definitionRegistry();
let output: ToolJsonValue;
try {
const input = openToolInvocationInputArtifact(artifact, key, definitions);
const candidate = await executeBeforeDeadline(
adapter,
context,
input,
dependencies.now,
);
output = definitions.normalizeOutput(
adapter.binding.tool.name,
adapter.binding.tool.version,
candidate,
);
} catch (error) {
if (error instanceof TrustedToolExecutionDeadlineExceededError) {
throw error;
}
throw new TrustedToolExecutionFailedError();
} finally {
key.fill(0);
}
const completedAtMs = now(dependencies.now);
if (completedAtMs > context.deadlineAtMs) {
throw new TrustedToolExecutionDeadlineExceededError();
}
const outputDigest = hash(OUTPUT_DIGEST_DOMAIN, output);
const unsigned = Object.freeze({
schema: TRUSTED_TOOL_EXECUTION_RESULT_SCHEMA,
startId: barrier.startId,
barrierDigest: barrier.barrierDigest,
adapterDigest: barrier.adapterDigest,
output,
outputDigest,
completedAtMs,
});
return Object.freeze({
...unsigned,
resultDigest: hash(RESULT_DIGEST_DOMAIN, unsigned),
});
}
/**
* Recovery inspection deliberately does not load or decrypt invocation input.
* A reviewed read-only in-process adapter can be retried because it has no
* external side effect; later write/remote adapters require stronger,
* adapter-specific evidence and are not accepted by this registry.
*/
export async function inspectTrustedToolExecutionRecovery(
startId: string,
dependencies: Pick<
TrustedToolExecutionDependencies,
'adapters' | 'barriers' | 'now'
>,
): Promise<Readonly<TrustedToolExecutionRecoveryEvidence>> {
const { barrier, adapter } = await durableBarrier(startId, dependencies);
if (adapter.recoveryMode !== 'retry_safe_read') {
throw new TrustedToolExecutionUnavailableError();
}
const unsigned = Object.freeze({
schema: TRUSTED_TOOL_EXECUTION_RECOVERY_EVIDENCE_SCHEMA,
startId: barrier.startId,
barrierDigest: barrier.barrierDigest,
adapterDigest: barrier.adapterDigest,
disposition: 'retry_safe' as const,
reason: 'read_only_no_side_effects' as const,
inspectedAtMs: now(dependencies.now),
});
return Object.freeze({
...unsigned,
evidenceDigest: hash(RECOVERY_EVIDENCE_DIGEST_DOMAIN, unsigned),
});
}
@@ -0,0 +1,19 @@
export * from './trusted-tool-invocation/contracts';
export { trustedToolContractIdentityDigest } from './trusted-tool-invocation/codec';
export {
TrustedToolHandlerBindingRegistry,
createTrustedToolHandlerBinding,
normalizeTrustedToolHandlerBinding,
} from './trusted-tool-invocation/binding';
export {
assertTrustedToolApprovedDispatch,
createTrustedToolInvocationPlan,
normalizeTrustedToolInvocationPlan,
normalizeTrustedToolInvocationPreview,
trustedToolInvocationApprovalBinding,
} from './trusted-tool-invocation/plan';
export {
admitTrustedToolExecution,
normalizeTrustedToolExecutionAdmission,
} from './trusted-tool-invocation/admission';
@@ -0,0 +1,475 @@
import { createHash } from 'node:crypto';
import type { RunRepositoryReader } from '../run/runRepository';
import {
normalizeStepRunRecord,
transitionStepRunMutation,
type StepRunRepository,
} from '../run/stepRun';
import {
createToolExecutionCompletionCommand,
createToolExecutionResultArtifact,
normalizeToolExecutionCompletionRecord,
normalizeToolExecutionResultArtifact,
openToolExecutionResultArtifact,
ToolExecutionCompletionConflictError,
ToolExecutionCompletionUnavailableError,
toolExecutionCompletionRecord,
type ToolExecutionCompletionRecord,
type ToolExecutionCompletionRepository,
type ToolExecutionResultArtifact,
} from './toolExecutionCompletion';
import {
normalizeToolExecutionStartBarrierRecord,
type ToolExecutionStartBarrierRecord,
} from './toolExecutionStartBarrier';
import {
normalizeToolResultKeyCatalogRecord,
requireActiveToolResultKey,
requireDecryptableToolResultKey,
toolResultKeyCatalogFence,
toolResultKeyMaterialProof,
type ToolResultKeyCatalogReader,
type ToolResultKeyCatalogRecord,
} from './toolResultKeyCatalog';
import type {
ToolInvocationArtifactKeyMaterial,
ToolInvocationArtifactKeyProvider,
} from './toolInvocationArtifact';
import {
normalizeToolExecutionResultRekeyOverlay,
openToolExecutionResultRekeyOverlay,
type ToolExecutionResultRekeyOverlay,
type ToolExecutionResultRekeyReader,
} from './toolResultRekey';
import {
executeTrustedToolAfterStart,
TrustedToolExecutionAdapterRegistry,
type TrustedToolExecutionDependencies,
} from './trustedToolExecution';
import type { ToolJsonValue } from './tool-registry/toolRegistry';
export interface TrustedToolSuccessCompletionIdentities {
readonly artifactId: string;
readonly mutationId: string;
readonly eventId: string;
}
export interface TrustedToolSuccessCompletionIdentityFactory {
create(startId: string): TrustedToolSuccessCompletionIdentities;
}
export interface TrustedToolSuccessCompletionDependencies
extends TrustedToolExecutionDependencies {
readonly completions: ToolExecutionCompletionRepository;
readonly stepRuns: Pick<StepRunRepository, 'findById'>;
readonly runs: Pick<RunRepositoryReader, 'findRunById'>;
readonly resultKeyCatalog: ToolResultKeyCatalogReader;
readonly resultRekeys: ToolExecutionResultRekeyReader;
readonly resultKeys: Pick<ToolInvocationArtifactKeyProvider, 'resolve'>;
readonly identities: TrustedToolSuccessCompletionIdentityFactory;
readonly nonceFactory?: () => Uint8Array;
}
export interface TrustedToolSuccessCompletionResult {
readonly status: 'created' | 'existing';
readonly completion: Readonly<ToolExecutionCompletionRecord>;
readonly output: ToolJsonValue;
}
const TERMINAL_RUN_STATUSES = new Set([
'succeeded',
'failed',
'cancelled',
'timed_out',
]);
function unavailable(cause?: unknown): never {
throw new ToolExecutionCompletionUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
}
function conflict(): never {
throw new ToolExecutionCompletionConflictError();
}
function sameValue(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function validateDependencies(
dependencies: TrustedToolSuccessCompletionDependencies,
): void {
if (
!dependencies ||
typeof dependencies !== 'object' ||
!dependencies.completions ||
typeof dependencies.completions.findByStartId !== 'function' ||
typeof dependencies.completions.findResultArtifact !== 'function' ||
typeof dependencies.completions.commit !== 'function' ||
!dependencies.stepRuns ||
typeof dependencies.stepRuns.findById !== 'function' ||
!dependencies.runs ||
typeof dependencies.runs.findRunById !== 'function' ||
!dependencies.resultKeys ||
typeof dependencies.resultKeys.resolve !== 'function' ||
!dependencies.resultKeyCatalog ||
typeof dependencies.resultKeyCatalog.findCurrent !== 'function' ||
!dependencies.resultRekeys ||
typeof dependencies.resultRekeys.findHeadByArtifactId !== 'function' ||
!dependencies.identities ||
typeof dependencies.identities.create !== 'function' ||
!(dependencies.adapters instanceof TrustedToolExecutionAdapterRegistry) ||
(dependencies.nonceFactory !== undefined &&
typeof dependencies.nonceFactory !== 'function')
) {
unavailable();
}
}
async function findResultRekeyHead(
artifactId: string,
dependencies: TrustedToolSuccessCompletionDependencies,
): Promise<Readonly<ToolExecutionResultRekeyOverlay> | null> {
try {
const value = await dependencies.resultRekeys.findHeadByArtifactId(
artifactId,
);
return value === null
? null
: normalizeToolExecutionResultRekeyOverlay(value);
} catch (cause) {
return unavailable(cause);
}
}
async function findResultKeyCatalog(
dependencies: TrustedToolSuccessCompletionDependencies,
): Promise<Readonly<ToolResultKeyCatalogRecord>> {
try {
const value = await dependencies.resultKeyCatalog.findCurrent();
if (!value) return unavailable();
return normalizeToolResultKeyCatalogRecord(value);
} catch (cause) {
return unavailable(cause);
}
}
function validCatalogMaterial(
material: ToolInvocationArtifactKeyMaterial | null,
keyId: string,
materialProof: string,
): material is ToolInvocationArtifactKeyMaterial {
return (
validKey(material, keyId) &&
toolResultKeyMaterialProof(keyId, material.key) === materialProof
);
}
async function findCompletion(
startId: string,
dependencies: TrustedToolSuccessCompletionDependencies,
): Promise<Readonly<ToolExecutionCompletionRecord> | null> {
try {
const value = await dependencies.completions.findByStartId(startId);
return value === null
? null
: normalizeToolExecutionCompletionRecord(value);
} catch (cause) {
return unavailable(cause);
}
}
async function findBarrier(
startId: string,
dependencies: TrustedToolSuccessCompletionDependencies,
): Promise<Readonly<ToolExecutionStartBarrierRecord>> {
try {
const value = await dependencies.barriers.findByStartId(startId);
if (!value) return unavailable();
const barrier = normalizeToolExecutionStartBarrierRecord(value);
if (barrier.startId !== startId) return unavailable();
return barrier;
} catch (cause) {
return unavailable(cause);
}
}
function validKey(
material: ToolInvocationArtifactKeyMaterial | null,
expectedKeyId?: string,
): material is ToolInvocationArtifactKeyMaterial {
return (
material !== null &&
typeof material.keyId === 'string' &&
(expectedKeyId === undefined || material.keyId === expectedKeyId) &&
material.key instanceof Uint8Array &&
material.key.byteLength === 32
);
}
function wipeMaterial(
material: ToolInvocationArtifactKeyMaterial | null | undefined,
): void {
if (material?.key instanceof Uint8Array) material.key.fill(0);
}
function completionMatches(
completion: Readonly<ToolExecutionCompletionRecord>,
barrier: Readonly<ToolExecutionStartBarrierRecord>,
artifact: Readonly<ToolExecutionResultArtifact>,
): boolean {
return (
completion.startId === barrier.startId &&
completion.projectId === barrier.projectId &&
completion.runId === barrier.runId &&
completion.stepRunId === barrier.stepRunId &&
completion.startedStepRunVersion === barrier.startedStepRunVersion &&
completion.barrierDigest === barrier.barrierDigest &&
completion.adapterDigest === barrier.adapterDigest &&
artifact.artifactId === completion.resultArtifact.artifactId &&
artifact.artifactDigest === completion.resultArtifact.artifactDigest &&
artifact.projectId === completion.projectId &&
artifact.startId === completion.startId &&
artifact.runId === completion.runId &&
artifact.stepRunId === completion.stepRunId &&
artifact.barrierDigest === completion.barrierDigest &&
artifact.adapterDigest === completion.adapterDigest &&
artifact.outputDigest === completion.resultArtifact.outputDigest &&
artifact.executionResultDigest ===
completion.resultArtifact.executionResultDigest &&
artifact.sealedAtMs === completion.completedAtMs
);
}
async function openDurableCompletion(
completion: Readonly<ToolExecutionCompletionRecord>,
dependencies: TrustedToolSuccessCompletionDependencies,
): Promise<Readonly<TrustedToolSuccessCompletionResult>> {
const barrier = await findBarrier(completion.startId, dependencies);
let artifact: Readonly<ToolExecutionResultArtifact>;
try {
const value = await dependencies.completions.findResultArtifact(
completion.resultArtifact.artifactId,
);
if (!value) return unavailable();
artifact = normalizeToolExecutionResultArtifact(value);
} catch (cause) {
return unavailable(cause);
}
if (!completionMatches(completion, barrier, artifact)) {
return conflict();
}
const adapter = dependencies.adapters.resolve(barrier);
if (!sameValue(artifact.tool, adapter.binding.tool)) {
return conflict();
}
let material: ToolInvocationArtifactKeyMaterial | null | undefined;
try {
const catalog = await findResultKeyCatalog(dependencies);
const overlay = await findResultRekeyHead(
artifact.artifactId,
dependencies,
);
const keyId = overlay?.targetCatalogFence.keyId ?? artifact.keyId;
const entry = requireDecryptableToolResultKey(catalog, keyId);
const materialProof =
overlay?.targetCatalogFence.materialProof ?? entry.materialProof;
if (entry.materialProof !== materialProof) return unavailable();
material = await dependencies.resultKeys.resolve(keyId);
if (!validCatalogMaterial(material, keyId, materialProof)) {
return unavailable();
}
const output = overlay
? openToolExecutionResultRekeyOverlay(
overlay,
material.key,
dependencies.adapters.definitionRegistry(),
artifact,
)
: openToolExecutionResultArtifact(
artifact,
material.key,
dependencies.adapters.definitionRegistry(),
);
return Object.freeze({
status: 'existing' as const,
completion,
output,
});
} catch (cause) {
return unavailable(cause);
} finally {
wipeMaterial(material);
}
}
async function findStepRun(
barrier: Readonly<ToolExecutionStartBarrierRecord>,
dependencies: TrustedToolSuccessCompletionDependencies,
) {
try {
const value = await dependencies.stepRuns.findById(barrier.stepRunId);
if (!value) return unavailable();
return normalizeStepRunRecord(value);
} catch (cause) {
return unavailable(cause);
}
}
async function findRun(
barrier: Readonly<ToolExecutionStartBarrierRecord>,
dependencies: TrustedToolSuccessCompletionDependencies,
) {
try {
const value = await dependencies.runs.findRunById(barrier.runId);
if (!value) return unavailable();
return value;
} catch (cause) {
return unavailable(cause);
}
}
function completionDedupeKey(startId: string): string {
return `tool-success:${createHash('sha256').update(startId).digest('hex')}`;
}
/**
* Executes one already-started retry-safe Tool and closes its success path.
*
* Durable completion is checked before adapter execution and again after it.
* If the commit response is lost, the stored encrypted result is reopened and
* returned without executing the adapter a second time in this call.
*/
export async function executeAndCompleteTrustedToolSuccess(
startId: string,
dependencies: TrustedToolSuccessCompletionDependencies,
): Promise<Readonly<TrustedToolSuccessCompletionResult>> {
validateDependencies(dependencies);
const existing = await findCompletion(startId, dependencies);
if (existing) return openDurableCompletion(existing, dependencies);
const executionResult = await executeTrustedToolAfterStart(
startId,
dependencies,
);
const concurrent = await findCompletion(startId, dependencies);
if (concurrent) return openDurableCompletion(concurrent, dependencies);
const barrier = await findBarrier(startId, dependencies);
const adapter = dependencies.adapters.resolve(barrier);
const stepRun = await findStepRun(barrier, dependencies);
const run = await findRun(barrier, dependencies);
if (
stepRun.id !== barrier.stepRunId ||
stepRun.runId !== barrier.runId ||
stepRun.kind !== 'tool' ||
stepRun.status !== 'running' ||
stepRun.version !== barrier.startedStepRunVersion ||
stepRun.stepRunDigest !== barrier.startedStepRunDigest ||
run.id !== barrier.runId ||
run.projectId !== barrier.projectId ||
!Number.isSafeInteger(run.version) ||
run.version < 0 ||
!Number.isSafeInteger(run.eventSequence) ||
run.eventSequence < 0 ||
TERMINAL_RUN_STATUSES.has(run.status)
) {
return conflict();
}
let identities: TrustedToolSuccessCompletionIdentities;
try {
identities = dependencies.identities.create(startId);
} catch (cause) {
return unavailable(cause);
}
let material: ToolInvocationArtifactKeyMaterial | null | undefined;
let resultArtifact: Readonly<ToolExecutionResultArtifact>;
let resultKeyCatalogFence: ReturnType<typeof toolResultKeyCatalogFence>;
try {
const catalog = await findResultKeyCatalog(dependencies);
const entry = requireActiveToolResultKey(catalog);
material = await dependencies.resultKeys.resolve(entry.keyId);
if (!validCatalogMaterial(material, entry.keyId, entry.materialProof)) {
return unavailable();
}
resultKeyCatalogFence = toolResultKeyCatalogFence(catalog, entry);
resultArtifact = createToolExecutionResultArtifact(
{
artifactId: identities.artifactId,
projectId: barrier.projectId,
runId: barrier.runId,
stepRunId: barrier.stepRunId,
tool: adapter.binding.tool,
executionResult,
keyId: entry.keyId,
key: material.key,
},
dependencies.adapters.definitionRegistry(),
dependencies.nonceFactory,
);
} catch (cause) {
return unavailable(cause);
} finally {
wipeMaterial(material);
}
const stepRunMutation = transitionStepRunMutation(
stepRun,
{
expectedVersion: stepRun.version,
expectedDigest: stepRun.stepRunDigest,
mutationId: identities.mutationId,
to: 'succeeded',
atMs: executionResult.completedAtMs,
outputRef: resultArtifact.artifactId,
},
{
expectedRunVersion: run.version,
expectedRunEventSequence: run.eventSequence,
eventId: identities.eventId,
dedupeKey: completionDedupeKey(startId),
actor: Object.freeze({
type: 'system' as const,
id: 'trusted-tool-runtime',
}),
},
);
const command = createToolExecutionCompletionCommand({
barrier,
executionResult,
resultArtifact,
resultKeyCatalogFence,
stepRunMutation,
});
const expectedCompletion = toolExecutionCompletionRecord(command);
try {
const committed = await dependencies.completions.commit(command);
const completion = normalizeToolExecutionCompletionRecord(
committed.completion,
);
if (
!['created', 'existing'].includes(committed.status) ||
!sameValue(completion, expectedCompletion)
) {
return conflict();
}
return Object.freeze({
status: committed.status,
completion,
output: executionResult.output,
});
} catch (cause) {
const recovered = await findCompletion(startId, dependencies);
if (recovered) return openDurableCompletion(recovered, dependencies);
throw cause;
}
}