import { createHash } from 'node:crypto'; import { MAX_PLUGIN_PACKAGE_MANIFEST_BYTES, normalizePluginPackageManifest, type PluginPackageManifest, } from './pluginPackage'; import { pluginPackageContentTreeDigest, type PluginPackageContentEntryDescriptor, } from './pluginPackageBundle'; import { normalizePluginPackageLock, pluginPackageManifestDigest, serializePluginPackageManifest, type PluginPackageLock, } from './installation/pluginPackageInstall'; import { normalizePluginPackageResourceGeneration, pluginPackageResourceReferencesFromContents, type PluginPackageResourceGeneration, type PluginPackageResourceGenerationSource, type PluginPackageResourceKind, type PluginPackageResourceReference, } from './pluginPackageResourceGeneration'; import { assertPluginPackageSecretBindingMatches, type PluginPackageSecretBinding, type PluginPackageSecretBindingRepository, } from './secret-binding/binding'; import { normalizeTaskDefinitionLabels, normalizeTaskDefinitionSpec, type TaskDefinitionJson, type TaskDefinitionKind, type TaskDefinitionSpec, } from '../task-definition/taskDefinition'; import { BUILT_IN_COMMAND_TASK_SPEC_SCHEMA, TaskSpecSemanticRegistry, } from '../task-definition/taskSpecSemantic'; import { MAX_TOOL_DEFINITIONS, normalizeToolDefinition, type ToolDefinition, } from '../tool-execution/tool-registry/toolRegistry'; export const PLUGIN_PACKAGE_TASK_RESOURCE_SCHEMA = 'qinglong/plugin-package-task-resource@v1' as const; export const PLUGIN_PACKAGE_WORKFLOW_RESOURCE_SCHEMA = 'qinglong/plugin-package-workflow-resource@v1' as const; export const PLUGIN_PACKAGE_PROMPT_RESOURCE_SCHEMA = 'qinglong/plugin-package-prompt-resource@v1' as const; export const PLUGIN_PACKAGE_TOOL_RESOURCE_SCHEMA = 'qinglong/plugin-package-tool-resource@v1' as const; export const PLUGIN_PACKAGE_MATERIALIZED_REVISION_SCHEMA = 'qinglong/plugin-package-materialized-revision@v1' as const; export const MAX_PLUGIN_PACKAGE_MATERIALIZED_RESOURCE_BYTES = 1024 * 1024; export const MAX_PLUGIN_PACKAGE_MATERIALIZED_TOTAL_BYTES = 8 * 1024 * 1024; export const MAX_PLUGIN_PACKAGE_MATERIALIZED_REVISION_JSON_BYTES = 24 * 1024 * 1024; export const MAX_PLUGIN_PACKAGE_WORKFLOW_STEPS = 128; export const MAX_PLUGIN_PACKAGE_PROMPT_PARAMETERS = 64; export const MAX_PLUGIN_PACKAGE_PROMPT_TEMPLATE_BYTES = 512 * 1024; export interface PluginPackageTaskResource { readonly schema: typeof PLUGIN_PACKAGE_TASK_RESOURCE_SCHEMA; readonly id: string; readonly name: string; readonly description?: string; readonly labels: Readonly>; readonly enabled: boolean; readonly kind: TaskDefinitionKind; readonly spec: TaskDefinitionSpec; } export interface PluginPackageWorkflowStep { readonly id: string; readonly task: string; readonly needs: readonly string[]; } export interface PluginPackageWorkflowResource { readonly schema: typeof PLUGIN_PACKAGE_WORKFLOW_RESOURCE_SCHEMA; readonly id: string; readonly name: string; readonly description?: string; readonly enabled: boolean; readonly steps: readonly Readonly[]; } export interface PluginPackagePromptParameter { readonly name: string; readonly description?: string; readonly required: boolean; } export interface PluginPackagePromptResource { readonly schema: typeof PLUGIN_PACKAGE_PROMPT_RESOURCE_SCHEMA; readonly id: string; readonly name: string; readonly description?: string; readonly template: string; readonly parameters: readonly Readonly[]; } export interface PluginPackageToolResource { readonly schema: typeof PLUGIN_PACKAGE_TOOL_RESOURCE_SCHEMA; readonly definition: Readonly; } export type PluginPackageMaterializedResourceValue = | Readonly | Readonly | Readonly | Readonly; export interface PluginPackageMaterializedResource { readonly kind: PluginPackageResourceKind; readonly path: string; readonly sourceBytes: number; readonly sourceDigest: string; readonly value: PluginPackageMaterializedResourceValue; } export interface PluginPackageMaterializedRevision { readonly schema: typeof PLUGIN_PACKAGE_MATERIALIZED_REVISION_SCHEMA; readonly generation: Readonly; readonly lock: Readonly; readonly manifest: Readonly; readonly manifestDigest: string; readonly secretBinding?: Readonly; readonly resources: readonly Readonly[]; readonly revisionDigest: string; } export interface PluginPackageResourceMaterializationEntry { readonly reference: Readonly; readonly bytes: Uint8Array; } export interface MaterializePluginPackageResourcesInput { readonly generation: Readonly; readonly lock: Readonly; readonly manifestBytes: Uint8Array; readonly secretBinding?: Readonly; readonly resources: readonly Readonly[]; readonly taskSpecSemanticRegistry: TaskSpecSemanticRegistry; } export interface PluginPackageResourceLockSource { findLock(lockDigest: string): Promise | null>; } export interface PluginPackageResourceByteReader { /** * Reads one immutable staged entry. The reader must apply maximumBytes before * buffering the complete entry and stay bound to the generation it opened. */ read(path: string, maximumBytes: number): Promise; close(): void | Promise; } export interface PluginPackageResourceByteSource { /** * Opens one bounded, caller-owned read session. Implementations must not * retain watchers, timers or authoritative cache after close. */ open( generation: Readonly, ): Promise; } export interface MaterializeActivePluginPackageResourcesOptions { readonly projectId: string; readonly packageName: string; readonly generationSource: PluginPackageResourceGenerationSource; readonly lockSource: PluginPackageResourceLockSource; readonly byteSource: PluginPackageResourceByteSource; readonly secretBindingSource?: Pick< PluginPackageSecretBindingRepository, 'find' >; readonly taskSpecSemanticRegistry: TaskSpecSemanticRegistry; } export interface PluginPackageMaterializedRevisionRepository { find( generationDigest: string, ): Promise | null>; publish(revision: Readonly): Promise< Readonly<{ status: 'created' | 'existing'; revision: Readonly; }> >; } export interface PluginPackageTaskDefinitionDraft { readonly projectId: string; readonly taskId: string; readonly name: string; readonly description?: string; readonly kind: TaskDefinitionKind; readonly spec: TaskDefinitionSpec; readonly labels: Readonly>; readonly enabled: boolean; } export class InvalidPluginPackageResourceMaterializationError extends TypeError { readonly code = 'PLUGIN_PACKAGE_RESOURCE_MATERIALIZATION_INVALID'; constructor(message: string) { super(`Plugin Package resource materialization is invalid: ${message}`); this.name = 'InvalidPluginPackageResourceMaterializationError'; } } export class PluginPackageResourceMaterializationConflictError extends Error { readonly code = 'PLUGIN_PACKAGE_RESOURCE_MATERIALIZATION_CONFLICT'; constructor(message: string) { super( `Plugin Package resource materialization conflicts with state: ${message}`, ); this.name = 'PluginPackageResourceMaterializationConflictError'; } } export class PluginPackageResourceMaterializationUnavailableError extends Error { readonly code = 'PLUGIN_PACKAGE_RESOURCE_MATERIALIZATION_UNAVAILABLE'; constructor(options?: ErrorOptions) { super('Plugin Package resource materialization is unavailable', options); this.name = 'PluginPackageResourceMaterializationUnavailableError'; } } const DIGEST = /^[0-9a-f]{64}$/; const PACKAGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/; const RESOURCE_ID = /^[a-z][a-z0-9-]{0,62}$/; const STEP_ID = /^[a-z][a-z0-9-]{0,62}$/; const PARAMETER_NAME = /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/; const CONTROL = /[\u0000-\u001f\u007f]/; const REVISION_DIGEST_DOMAIN = Buffer.from( 'qinglong/plugin-package-materialized-revision-digest@v1\0', 'utf8', ); const PROJECT_PERMISSION_TO_PACKAGE_PERMISSION: Readonly< Record > = Object.freeze({ 'artifact.read': 'artifact.read', 'run.read': 'run.read', 'run.start': 'run.start', 'run.stop': 'run.stop', 'run.retry': 'run.retry', 'secret.use': 'secret.use', 'task.read': 'task.read', 'task.update': 'task.update', }); function invalid(message: string): never { throw new InvalidPluginPackageResourceMaterializationError(message); } function dataRecord(value: unknown, label: string): Record { 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; } function exactKeys( value: object, required: readonly string[], optional: readonly string[], label: string, ): void { const actual = Reflect.ownKeys(value); const strings = actual.filter( (key): key is string => typeof key === 'string', ); const allowed = new Set([...required, ...optional]); if ( actual.length !== strings.length || required.some((key) => !strings.includes(key)) || strings.some((key) => !allowed.has(key)) ) { invalid(`${label} shape is invalid`); } } function boundedDenseArray( value: unknown, maximum: number, label: string, ): readonly unknown[] { if (!Array.isArray(value) || value.length > maximum) { return invalid(`${label} is invalid`); } const keys = Object.keys(value); const ownKeys = Reflect.ownKeys(value); if ( ownKeys.length !== value.length + 1 || !ownKeys.includes('length') || keys.length !== value.length || keys.some((key, index) => key !== String(index)) ) { return invalid(`${label} must be a dense data array`); } const descriptors = Object.getOwnPropertyDescriptors(value); if ( keys.some((key) => { const descriptor = descriptors[key]; return ( descriptor === undefined || descriptor.get !== undefined || descriptor.set !== undefined || descriptor.enumerable !== true ); }) ) { return invalid(`${label} must be a dense data array`); } return value; } function assertDataTree( value: unknown, budget: { nodes: number }, depth = 0, seen = new Set(), ): void { budget.nodes += 1; if (budget.nodes > 100_000 || depth > 32) { invalid('materialized revision exceeds its structure budget'); } if ( value === null || typeof value === 'string' || typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value)) ) { return; } if (!value || typeof value !== 'object' || seen.has(value)) { invalid('materialized revision contains a non-data or cyclic value'); } seen.add(value); if (Array.isArray(value)) { const entries = boundedDenseArray(value, 100_000, 'revision array'); for (const entry of entries) { assertDataTree(entry, budget, depth + 1, seen); } seen.delete(value); return; } const record = dataRecord(value, 'revision object'); for (const entry of Object.values(record)) { assertDataTree(entry, budget, depth + 1, seen); } seen.delete(value); } function boundedText( value: unknown, label: string, maximumBytes: number, allowEmpty = false, ): string { if ( typeof value !== 'string' || (!allowEmpty && value.length === 0) || value.includes('\0') || CONTROL.test(value) || Buffer.byteLength(value, 'utf8') > maximumBytes ) { return invalid(`${label} is invalid`); } return value; } function boundedTemplate(value: unknown): string { if ( typeof value !== 'string' || value.includes('\0') || /[\u0001-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(value) || Buffer.byteLength(value, 'utf8') > MAX_PLUGIN_PACKAGE_PROMPT_TEMPLATE_BYTES ) { return invalid('Prompt template is invalid'); } return value; } function optionalDescription(value: unknown): string | undefined { return value === undefined ? undefined : boundedText(value, 'description', 4096); } function resourceId(value: unknown, label = 'resource id'): string { const id = boundedText(value, label, 63); if (!RESOURCE_ID.test(id)) return invalid(`${label} is invalid`); return id; } function digest(value: unknown, label: string): string { if (typeof value !== 'string' || !DIGEST.test(value)) { return invalid(`${label} is invalid`); } return value; } function byteCount(value: unknown, label: string): number { if ( !Number.isSafeInteger(value) || (value as number) < 1 || (value as number) > MAX_PLUGIN_PACKAGE_MATERIALIZED_RESOURCE_BYTES ) { return invalid(`${label} is invalid`); } return value as number; } function bytes(value: unknown, maximum: number, label: string): Buffer { if ( !(value instanceof Uint8Array) || value.byteLength < 1 || value.byteLength > maximum ) { return invalid(`${label} is invalid`); } return Buffer.from(value); } function parseJson(value: Buffer, label: string): unknown { let text: string; try { text = new TextDecoder('utf-8', { fatal: true }).decode(value); } catch { return invalid(`${label} is not strict UTF-8`); } let parsed: unknown; try { parsed = JSON.parse(text); } catch { return invalid(`${label} is not JSON`); } return parsed; } function canonicalJson(value: unknown): string { if ( value === null || typeof value === 'boolean' || typeof value === 'number' || typeof value === 'string' ) { 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 materializedRevisionDigest( value: Omit, ): string { return createHash('sha256') .update(REVISION_DIGEST_DOMAIN) .update(canonicalJson(value), 'utf8') .digest('hex'); } function taskIdentity(packageName: string, id: string): string { const taskId = `pkg:${packageName}:${id}`; if (Buffer.byteLength(taskId, 'utf8') > 128) { return invalid('package Task identity exceeds the TaskDefinition budget'); } return taskId; } function normalizeTaskResource( value: unknown, packageName: string, projectId: string, manifest: Readonly, registry: TaskSpecSemanticRegistry, secretBinding: Readonly | undefined, source: boolean, ): Readonly { const task = dataRecord(value, 'Task resource'); exactKeys( task, ['schema', 'id', 'name', 'labels', 'enabled', 'kind', 'spec'], ['description'], 'Task resource', ); if (task.schema !== PLUGIN_PACKAGE_TASK_RESOURCE_SCHEMA) { return invalid('Task resource schema is unsupported'); } const id = resourceId(task.id, 'Task id'); const taskId = taskIdentity(packageName, id); const kind = task.kind; let spec = normalizeTaskDefinitionSpec(task.spec as TaskDefinitionSpec); if (kind !== 'command' || spec.schema !== BUILT_IN_COMMAND_TASK_SPEC_SCHEMA) { return invalid( 'Task resource v1 only supports qinglong/command@v1 command tasks', ); } if (!manifest.spec.permissions.tools.includes('system.command')) { return invalid( 'command Task requires the approved system.command permission', ); } if (!(registry instanceof TaskSpecSemanticRegistry)) { return invalid('TaskSpec semantic registry is invalid'); } if (source) { spec = compilePackageSecretEnvironment(spec, secretBinding); } let normalizedSpec: TaskDefinitionSpec; try { normalizedSpec = registry.normalize({ projectId, taskId, kind, spec, }); } catch { return invalid('Task resource spec semantics are invalid or unsupported'); } assertResolvedPackageSecretEnvironment(normalizedSpec, secretBinding); const description = optionalDescription(task.description); if (typeof task.enabled !== 'boolean') { return invalid('Task enabled is invalid'); } return Object.freeze({ schema: PLUGIN_PACKAGE_TASK_RESOURCE_SCHEMA, id, name: boundedText(task.name, 'Task name', 255), ...(description === undefined ? {} : { description }), labels: normalizeTaskDefinitionLabels( task.labels as Readonly>, ), enabled: task.enabled, kind, spec: normalizedSpec, }); } function compilePackageSecretEnvironment( spec: Readonly, secretBinding: Readonly | undefined, ): Readonly { const environment = spec.config.environment; if (!Array.isArray(environment)) return spec; const entries = new Map( (secretBinding?.entries ?? []).map((entry) => [entry.name, entry]), ); const compiled = environment.flatMap((value) => { const entry = dataRecord(value, 'Task environment'); if (entry.kind === 'secret') { return invalid('Package Task source cannot contain a direct SecretRef'); } if (entry.kind !== 'package-secret') return [value]; exactKeys( entry, ['kind', 'name', 'requirement'], [], 'Package Secret environment', ); const requirement = boundedText( entry.requirement, 'Package Secret requirement', 128, ); const binding = entries.get(requirement); if (!binding) { return invalid( 'Package Task references an undeclared Secret requirement', ); } if (binding.secretRef === null) return []; return [ Object.freeze({ name: entry.name as TaskDefinitionJson, kind: 'secret' as const, secretRef: binding.secretRef, }), ]; }); return normalizeTaskDefinitionSpec( Object.freeze({ schema: spec.schema, config: Object.freeze({ ...spec.config, environment: Object.freeze(compiled), }), }), ); } function assertResolvedPackageSecretEnvironment( spec: Readonly, secretBinding: Readonly | undefined, ): void { const allowed = new Set( (secretBinding?.entries ?? []).flatMap((entry) => entry.secretRef === null ? [] : [entry.secretRef], ), ); const environment = spec.config.environment; if (!Array.isArray(environment)) return; for (const value of environment) { if (!value || typeof value !== 'object' || Array.isArray(value)) continue; const entry = value as Readonly>; if (entry.kind === 'secret' && !allowed.has(entry.secretRef as string)) { invalid('Task SecretRef is not authorized by the Package binding'); } } } function normalizeWorkflowResource( value: unknown, ): Readonly { const workflow = dataRecord(value, 'Workflow resource'); exactKeys( workflow, ['schema', 'id', 'name', 'enabled', 'steps'], ['description'], 'Workflow resource', ); if (workflow.schema !== PLUGIN_PACKAGE_WORKFLOW_RESOURCE_SCHEMA) { return invalid('Workflow resource schema is unsupported'); } if (typeof workflow.enabled !== 'boolean') { return invalid('Workflow enabled is invalid'); } const ids = new Set(); const steps = boundedDenseArray( workflow.steps, MAX_PLUGIN_PACKAGE_WORKFLOW_STEPS, 'Workflow steps', ).map((stepValue) => { const step = dataRecord(stepValue, 'Workflow step'); exactKeys(step, ['id', 'task', 'needs'], [], 'Workflow step'); const id = boundedText(step.id, 'Workflow step id', 63); if (!STEP_ID.test(id) || ids.has(id)) { return invalid('Workflow step id is invalid or duplicated'); } ids.add(id); const task = resourceId(step.task, 'Workflow Task reference'); const needs = boundedDenseArray( step.needs, MAX_PLUGIN_PACKAGE_WORKFLOW_STEPS, 'Workflow step needs', ).map((need) => { const dependency = boundedText(need, 'Workflow step dependency', 63); if (!STEP_ID.test(dependency)) { return invalid('Workflow step dependency is invalid'); } return dependency; }); if (new Set(needs).size !== needs.length || needs.includes(id)) { return invalid('Workflow step dependencies are duplicated or recursive'); } return Object.freeze({ id, task, needs: Object.freeze([...needs].sort()), }); }); const byId = new Map(steps.map((step) => [step.id, step])); for (const step of steps) { if (step.needs.some((need) => !byId.has(need))) { return invalid('Workflow step references an unknown dependency'); } } const pending = new Map(steps.map((step) => [step.id, new Set(step.needs)])); const ready = [...pending.entries()] .filter(([, needs]) => needs.size === 0) .map(([id]) => id); let visited = 0; while (ready.length > 0) { const current = ready.pop()!; if (!pending.delete(current)) continue; visited += 1; for (const [id, needs] of pending) { if (needs.delete(current) && needs.size === 0) ready.push(id); } } if (visited !== steps.length) { return invalid('Workflow graph contains a cycle'); } const description = optionalDescription(workflow.description); return Object.freeze({ schema: PLUGIN_PACKAGE_WORKFLOW_RESOURCE_SCHEMA, id: resourceId(workflow.id, 'Workflow id'), name: boundedText(workflow.name, 'Workflow name', 255), ...(description === undefined ? {} : { description }), enabled: workflow.enabled, steps: Object.freeze( [...steps].sort((left, right) => left.id.localeCompare(right.id)), ), }); } function normalizePromptResource( value: unknown, ): Readonly { const prompt = dataRecord(value, 'Prompt resource'); exactKeys( prompt, ['schema', 'id', 'name', 'template', 'parameters'], ['description'], 'Prompt resource', ); if (prompt.schema !== PLUGIN_PACKAGE_PROMPT_RESOURCE_SCHEMA) { return invalid('Prompt resource schema is unsupported'); } const names = new Set(); const parameters = boundedDenseArray( prompt.parameters, MAX_PLUGIN_PACKAGE_PROMPT_PARAMETERS, 'Prompt parameters', ).map((parameterValue) => { const parameter = dataRecord(parameterValue, 'Prompt parameter'); exactKeys( parameter, ['name', 'required'], ['description'], 'Prompt parameter', ); const name = boundedText(parameter.name, 'Prompt parameter name', 64); if (!PARAMETER_NAME.test(name) || names.has(name)) { return invalid('Prompt parameter name is invalid or duplicated'); } if (typeof parameter.required !== 'boolean') { return invalid('Prompt parameter required flag is invalid'); } names.add(name); const description = optionalDescription(parameter.description); return Object.freeze({ name, ...(description === undefined ? {} : { description }), required: parameter.required, }); }); const template = boundedTemplate(prompt.template); const referenced = new Set(); for (const match of template.matchAll( /\{\{([A-Za-z][A-Za-z0-9_.-]{0,63})\}\}/g, )) { referenced.add(match[1]!); } if ( [...referenced].some((name) => !names.has(name)) || [...names].some((name) => !referenced.has(name)) ) { return invalid( 'Prompt placeholders and declared parameters must match exactly', ); } const description = optionalDescription(prompt.description); return Object.freeze({ schema: PLUGIN_PACKAGE_PROMPT_RESOURCE_SCHEMA, id: resourceId(prompt.id, 'Prompt id'), name: boundedText(prompt.name, 'Prompt name', 255), ...(description === undefined ? {} : { description }), template, parameters: Object.freeze( [...parameters].sort((left, right) => left.name.localeCompare(right.name), ), ), }); } export function normalizePluginPackageWorkflowResource( value: unknown, ): Readonly { return normalizeWorkflowResource(value); } export function normalizePluginPackagePromptResource( value: unknown, ): Readonly { return normalizePromptResource(value); } function normalizeToolResource( value: unknown, packageName: string, manifest: Readonly, ): Readonly { const tool = dataRecord(value, 'Tool resource'); exactKeys(tool, ['schema', 'definition'], [], 'Tool resource'); if (tool.schema !== PLUGIN_PACKAGE_TOOL_RESOURCE_SCHEMA) { return invalid('Tool resource schema is unsupported'); } let definition: Readonly; try { definition = normalizeToolDefinition(tool.definition); } catch { return invalid('Tool definition is invalid'); } if (!definition.name.startsWith(`${packageName}.`)) { return invalid('Tool name must be namespaced by its Package name'); } const approved = new Set(manifest.spec.permissions.tools); for (const permission of definition.requiredPermissions) { const packagePermission = PROJECT_PERMISSION_TO_PACKAGE_PERMISSION[permission]; if ( packagePermission === undefined || !approved.has(packagePermission as never) ) { return invalid( 'Tool required permission is not present in the approved Package manifest', ); } } return Object.freeze({ schema: PLUGIN_PACKAGE_TOOL_RESOURCE_SCHEMA, definition, }); } function normalizeMaterializedValue( kind: PluginPackageResourceKind, value: unknown, packageName: string, projectId: string, manifest: Readonly, registry: TaskSpecSemanticRegistry, secretBinding: Readonly | undefined, source: boolean, ): PluginPackageMaterializedResourceValue { switch (kind) { case 'task': return normalizeTaskResource( value, packageName, projectId, manifest, registry, secretBinding, source, ); case 'workflow': return normalizeWorkflowResource(value); case 'prompt': return normalizePromptResource(value); case 'tool': return normalizeToolResource(value, packageName, manifest); } } function validateCrossResourceSemantics( resources: readonly Readonly[], ): void { const ids = new Map>([ ['task', new Set()], ['workflow', new Set()], ['prompt', new Set()], ['tool', new Set()], ]); for (const resource of resources) { const identity = resource.kind === 'tool' ? (resource.value as PluginPackageToolResource).definition.name : ( resource.value as | PluginPackageTaskResource | PluginPackageWorkflowResource | PluginPackagePromptResource ).id; const kindIds = ids.get(resource.kind)!; if (kindIds.has(identity)) { invalid(`${resource.kind} resource identity is duplicated`); } kindIds.add(identity); } const tasks = ids.get('task')!; for (const resource of resources) { if (resource.kind !== 'workflow') continue; const workflow = resource.value as PluginPackageWorkflowResource; if (workflow.steps.some((step) => !tasks.has(step.task))) { invalid('Workflow references an unknown package Task'); } } if (ids.get('tool')!.size > MAX_TOOL_DEFINITIONS) { invalid('Package Tool resources exceed the immutable registry budget'); } } function normalizeManifestBytes( value: unknown, lock: Readonly, ): Readonly { const material = bytes( value, MAX_PLUGIN_PACKAGE_MANIFEST_BYTES, 'manifest bytes', ); let manifest: Readonly; try { manifest = normalizePluginPackageManifest( parseJson(material, 'Package manifest'), ); } catch (error) { if (error instanceof InvalidPluginPackageResourceMaterializationError) { throw error; } return invalid('Package manifest semantics are invalid'); } if ( serializePluginPackageManifest(manifest) !== material.toString('utf8') || pluginPackageManifestDigest(manifest) !== lock.manifestDigest || manifest.metadata.name !== lock.packageName || manifest.metadata.version !== lock.packageVersion ) { return invalid('Package manifest does not match its immutable lock'); } return manifest; } function normalizeRevisionResources( values: unknown, generation: Readonly, manifest: Readonly, registry: TaskSpecSemanticRegistry, secretBinding: Readonly | undefined, ): readonly Readonly[] { const entries = boundedDenseArray( values, generation.resources.length, 'materialized resources', ); if (entries.length !== generation.resources.length) { return invalid('materialized resources are incomplete'); } let totalBytes = 0; const resources = entries.map((entryValue, index) => { const entry = dataRecord(entryValue, 'materialized resource'); exactKeys( entry, ['kind', 'path', 'sourceBytes', 'sourceDigest', 'value'], [], 'materialized resource', ); const reference = generation.resources[index]!; if (entry.kind !== reference.kind || entry.path !== reference.path) { return invalid( 'materialized resource order or identity does not match generation', ); } const sourceBytes = byteCount(entry.sourceBytes, 'resource source bytes'); totalBytes += sourceBytes; if (totalBytes > MAX_PLUGIN_PACKAGE_MATERIALIZED_TOTAL_BYTES) { return invalid('materialized resources exceed the total byte budget'); } return Object.freeze({ kind: reference.kind, path: reference.path, sourceBytes, sourceDigest: digest(entry.sourceDigest, 'resource source digest'), value: normalizeMaterializedValue( reference.kind, entry.value, generation.packageName, generation.projectId, manifest, registry, secretBinding, false, ), }); }); validateCrossResourceSemantics(resources); return Object.freeze(resources); } function assertGenerationMatchesLock( generation: Readonly, lock: Readonly, ): void { const previousLockDigest = lock.previousLockDigest ?? null; if ( generation.lockDigest !== lock.lockDigest || generation.projectId !== lock.projectId || generation.packageName !== lock.packageName || generation.generation !== lock.targetGeneration || generation.previousActiveLockDigest !== previousLockDigest || generation.contentDigest !== lock.source.contentDigest || JSON.stringify(generation.resources) !== JSON.stringify(lock.resources) ) { invalid('active generation does not match its immutable lock'); } } function assertManifestMatchesGeneration( manifest: Readonly, generation: Readonly, ): void { const references = pluginPackageResourceReferencesFromContents( manifest.spec.contents, ); if (JSON.stringify(references) !== JSON.stringify(generation.resources)) { invalid('Package manifest resources do not match active generation'); } } export function normalizePluginPackageMaterializedRevision( value: PluginPackageMaterializedRevision, taskSpecSemanticRegistry: TaskSpecSemanticRegistry, ): Readonly { assertDataTree(value, { nodes: 0 }); const revision = dataRecord(value, 'materialized revision'); exactKeys( revision, [ 'schema', 'generation', 'lock', 'manifest', 'manifestDigest', 'resources', 'revisionDigest', ], ['secretBinding'], 'materialized revision', ); if (revision.schema !== PLUGIN_PACKAGE_MATERIALIZED_REVISION_SCHEMA) { return invalid('materialized revision schema is unsupported'); } const generation = normalizePluginPackageResourceGeneration( revision.generation as PluginPackageResourceGeneration, ); const lock = normalizePluginPackageLock(revision.lock as PluginPackageLock); assertGenerationMatchesLock(generation, lock); const manifest = normalizePluginPackageManifest(revision.manifest); const manifestDigest = digest(revision.manifestDigest, 'manifest digest'); if ( pluginPackageManifestDigest(manifest) !== manifestDigest || manifestDigest !== lock.manifestDigest || manifest.metadata.name !== generation.packageName || manifest.metadata.version !== lock.packageVersion ) { return invalid('materialized manifest identity or digest is invalid'); } assertManifestMatchesGeneration(manifest, generation); const secretBinding = normalizeMaterializationSecretBinding( revision.secretBinding as Readonly | undefined, generation, manifest, ); const resources = normalizeRevisionResources( revision.resources, generation, manifest, taskSpecSemanticRegistry, secretBinding, ); const descriptors: PluginPackageContentEntryDescriptor[] = resources .map((resource) => Object.freeze({ path: resource.path, bytes: resource.sourceBytes, digest: resource.sourceDigest, }), ) .sort((left, right) => left.path.localeCompare(right.path)); if ( pluginPackageContentTreeDigest(descriptors) !== generation.contentDigest ) { return invalid('materialized resource bytes do not match the content tree'); } const unsigned = Object.freeze({ schema: PLUGIN_PACKAGE_MATERIALIZED_REVISION_SCHEMA, generation, lock, manifest, manifestDigest, ...(secretBinding === undefined ? {} : { secretBinding }), resources, }); const revisionDigest = digest(revision.revisionDigest, 'revision digest'); if (materializedRevisionDigest(unsigned) !== revisionDigest) { return invalid('materialized revision digest does not match'); } return Object.freeze({ ...unsigned, revisionDigest }); } export function materializePluginPackageResources( value: MaterializePluginPackageResourcesInput, ): Readonly { const input = dataRecord(value, 'materialization input'); exactKeys( input, [ 'generation', 'lock', 'manifestBytes', 'resources', 'taskSpecSemanticRegistry', ], ['secretBinding'], 'materialization input', ); const generation = normalizePluginPackageResourceGeneration(value.generation); const lock = normalizePluginPackageLock(value.lock); assertGenerationMatchesLock(generation, lock); const manifest = normalizeManifestBytes(value.manifestBytes, lock); assertManifestMatchesGeneration(manifest, generation); const secretBinding = normalizeMaterializationSecretBinding( value.secretBinding, generation, manifest, ); const entries = boundedDenseArray( value.resources, generation.resources.length, 'resource byte entries', ); if (entries.length !== generation.resources.length) { return invalid('resource byte entries are incomplete'); } let totalBytes = 0; const resources = entries.map((entryValue, index) => { const entry = dataRecord(entryValue, 'resource byte entry'); exactKeys(entry, ['reference', 'bytes'], [], 'resource byte entry'); const reference = dataRecord(entry.reference, 'resource reference'); exactKeys(reference, ['kind', 'path'], [], 'resource reference'); const expected = generation.resources[index]!; if ( reference.kind !== expected.kind || reference.path !== expected.path || !expected.path.endsWith('.json') ) { return invalid( 'resource byte entry identity, order or JSON extension is invalid', ); } const material = bytes( entry.bytes, MAX_PLUGIN_PACKAGE_MATERIALIZED_RESOURCE_BYTES, 'resource bytes', ); totalBytes += material.byteLength; if (totalBytes > MAX_PLUGIN_PACKAGE_MATERIALIZED_TOTAL_BYTES) { return invalid('resource bytes exceed the total materialization budget'); } return Object.freeze({ kind: expected.kind, path: expected.path, sourceBytes: material.byteLength, sourceDigest: createHash('sha256').update(material).digest('hex'), value: normalizeMaterializedValue( expected.kind, parseJson(material, `${expected.kind} resource`), generation.packageName, generation.projectId, manifest, value.taskSpecSemanticRegistry, secretBinding, true, ), }); }); validateCrossResourceSemantics(resources); const descriptors = resources .map((resource) => Object.freeze({ path: resource.path, bytes: resource.sourceBytes, digest: resource.sourceDigest, }), ) .sort((left, right) => left.path.localeCompare(right.path)); if ( pluginPackageContentTreeDigest(descriptors) !== generation.contentDigest ) { return invalid('resource bytes do not match the active content tree'); } const unsigned = Object.freeze({ schema: PLUGIN_PACKAGE_MATERIALIZED_REVISION_SCHEMA, generation, lock, manifest, manifestDigest: lock.manifestDigest, ...(secretBinding === undefined ? {} : { secretBinding }), resources: Object.freeze(resources), }); return normalizePluginPackageMaterializedRevision( Object.freeze({ ...unsigned, revisionDigest: materializedRevisionDigest(unsigned), }), value.taskSpecSemanticRegistry, ); } function normalizeMaterializationSecretBinding( value: Readonly | undefined, generation: Readonly, manifest: Readonly, ): Readonly | undefined { if (manifest.spec.permissions.secrets.length === 0) { if (value !== undefined) { invalid('Secret binding is forbidden when Manifest declares no Secrets'); } return undefined; } if (!manifest.spec.permissions.tools.includes('secret.use')) { return invalid( 'Secret-aware Package requires the approved secret.use permission', ); } if (value === undefined) { return invalid('Secret-aware Package requires an approved binding'); } try { return assertPluginPackageSecretBindingMatches(value, generation, manifest); } catch { return invalid('Secret binding does not match generation and Manifest'); } } function materializationSources( value: MaterializeActivePluginPackageResourcesOptions, ): void { if ( !value || typeof value !== 'object' || Array.isArray(value) || !value.generationSource || typeof value.generationSource.findActiveResourceGeneration !== 'function' || !value.lockSource || typeof value.lockSource.findLock !== 'function' || !value.byteSource || typeof value.byteSource.open !== 'function' || (value.secretBindingSource !== undefined && (!value.secretBindingSource || typeof value.secretBindingSource.find !== 'function')) || !(value.taskSpecSemanticRegistry instanceof TaskSpecSemanticRegistry) ) { invalid('active materialization sources are invalid'); } if ( typeof value.projectId !== 'string' || value.projectId.length < 1 || Buffer.byteLength(value.projectId, 'utf8') > 128 || typeof value.packageName !== 'string' || !PACKAGE_NAME.test(value.packageName) ) { invalid('active materialization identity is invalid'); } } export async function materializeActivePluginPackageResources( value: MaterializeActivePluginPackageResourcesOptions, ): Promise | null> { materializationSources(value); try { const first = await value.generationSource.findActiveResourceGeneration( value.projectId, value.packageName, ); if (first === null) return null; const generation = normalizePluginPackageResourceGeneration(first); if ( generation.projectId !== value.projectId || generation.packageName !== value.packageName ) { throw new PluginPackageResourceMaterializationConflictError( 'generation source returned another Package identity', ); } const lockValue = await value.lockSource.findLock(generation.lockDigest); if (lockValue === null) { throw new PluginPackageResourceMaterializationConflictError( 'active generation lock is missing', ); } const lock = normalizePluginPackageLock(lockValue); const reader = await value.byteSource.open(generation); if ( !reader || typeof reader !== 'object' || typeof reader.read !== 'function' || typeof reader.close !== 'function' ) { invalid('resource byte source returned an invalid reader'); } let manifestBytes: Uint8Array; const resources: PluginPackageResourceMaterializationEntry[] = []; try { manifestBytes = await reader.read( 'package.json', MAX_PLUGIN_PACKAGE_MANIFEST_BYTES, ); let totalBytes = 0; for (const reference of generation.resources) { const material = await reader.read( reference.path, MAX_PLUGIN_PACKAGE_MATERIALIZED_RESOURCE_BYTES, ); if (!(material instanceof Uint8Array)) { invalid('resource byte source returned a non-byte value'); } totalBytes += material.byteLength; if (totalBytes > MAX_PLUGIN_PACKAGE_MATERIALIZED_TOTAL_BYTES) { invalid( 'resource byte source exceeded the total materialization budget', ); } resources.push(Object.freeze({ reference, bytes: material })); } } finally { try { await reader.close(); } catch (error) { throw new PluginPackageResourceMaterializationUnavailableError({ cause: error instanceof Error ? error : undefined, }); } } const activeManifest = normalizeManifestBytes(manifestBytes, lock); const activeSecretBindingValue = activeManifest.spec.permissions.secrets.length === 0 ? undefined : await value.secretBindingSource?.find(generation.generationDigest); const activeSecretBinding = activeSecretBindingValue ?? undefined; const revision = materializePluginPackageResources({ generation, lock, manifestBytes, ...(activeSecretBinding === undefined ? {} : { secretBinding: activeSecretBinding }), resources: Object.freeze(resources), taskSpecSemanticRegistry: value.taskSpecSemanticRegistry, }); const secondValue = await value.generationSource.findActiveResourceGeneration( value.projectId, value.packageName, ); if (secondValue === null) { throw new PluginPackageResourceMaterializationConflictError( 'active generation disappeared during materialization', ); } const second = normalizePluginPackageResourceGeneration(secondValue); if (second.generationDigest !== generation.generationDigest) { throw new PluginPackageResourceMaterializationConflictError( 'active generation changed during materialization', ); } return revision; } catch (error) { if ( error instanceof InvalidPluginPackageResourceMaterializationError || error instanceof PluginPackageResourceMaterializationConflictError ) { throw error; } throw new PluginPackageResourceMaterializationUnavailableError({ cause: error instanceof Error ? error : undefined, }); } } export function pluginPackageTaskDefinitionDrafts( revisionValue: PluginPackageMaterializedRevision, taskSpecSemanticRegistry: TaskSpecSemanticRegistry, ): readonly Readonly[] { const revision = normalizePluginPackageMaterializedRevision( revisionValue, taskSpecSemanticRegistry, ); return Object.freeze( revision.resources .filter( ( resource, ): resource is Readonly & { readonly kind: 'task'; readonly value: Readonly; } => resource.kind === 'task', ) .map((resource) => { const task = resource.value; return Object.freeze({ projectId: revision.generation.projectId, taskId: taskIdentity(revision.generation.packageName, task.id), name: task.name, ...(task.description === undefined ? {} : { description: task.description }), kind: task.kind, spec: task.spec, labels: task.labels, enabled: task.enabled, }); }), ); } export function pluginPackageToolDefinitions( revisionValue: PluginPackageMaterializedRevision, taskSpecSemanticRegistry: TaskSpecSemanticRegistry, ): readonly Readonly[] { const revision = normalizePluginPackageMaterializedRevision( revisionValue, taskSpecSemanticRegistry, ); return Object.freeze( revision.resources .filter((resource) => resource.kind === 'tool') .map( (resource) => (resource.value as Readonly).definition, ), ); }