tasq/node_modules/@claude-flow/guidance/dist/artifacts.js

356 lines
12 KiB
JavaScript

/**
* Artifact Ledger - Signed, Versioned Production Outputs
*
* Every production output (code, reports, datasets, memory deltas) is recorded
* in a tamper-evident ledger. Each artifact captures:
*
* - SHA-256 content hash for integrity verification
* - HMAC-SHA256 signature over the artifact envelope
* - Full lineage tracking (parent artifacts, source traces, tool calls, memory reads)
* - Multi-dimensional search (by kind, run, cell, tags, time range)
* - Export/import for portability and replay
*
* @module @claude-flow/guidance/artifacts
*/
import { createHash, createHmac, randomUUID } from 'node:crypto';
// ============================================================================
// Constants
// ============================================================================
const DEFAULT_MAX_ARTIFACTS = 10_000;
const SERIALIZATION_VERSION = 1;
/**
* Constant-time string comparison to prevent timing attacks on HMAC signatures.
*/
function timingSafeEqual(a, b) {
if (a.length !== b.length)
return false;
let mismatch = 0;
for (let i = 0; i < a.length; i++) {
mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return mismatch === 0;
}
const ALL_KINDS = [
'code',
'report',
'dataset',
'model-output',
'memory-delta',
'config',
'trace-export',
'checkpoint',
];
/**
* A tamper-evident ledger for production artifacts.
*
* Every artifact is signed and content-hashed on creation. The ledger
* supports retrieval by ID, run, kind, cell, and arbitrary search queries.
* Full lineage traversal allows tracing any artifact back through its
* entire ancestry chain.
*/
export class ArtifactLedger {
artifacts = new Map();
signingKey;
maxArtifacts;
constructor(config = {}) {
if (!config.signingKey) {
throw new Error('ArtifactLedger requires an explicit signingKey — hardcoded defaults are not secure');
}
this.signingKey = config.signingKey;
this.maxArtifacts = config.maxArtifacts ?? DEFAULT_MAX_ARTIFACTS;
}
/**
* Record a new artifact in the ledger.
*
* Computes the content hash, signs the envelope, and stores the artifact.
* If the ledger exceeds maxArtifacts, the oldest artifact is evicted.
*
* @param params - Artifact creation parameters
* @returns The fully signed and stored Artifact
*/
record(params) {
const contentHash = this.computeContentHash(params.content);
const contentSize = this.computeContentSize(params.content);
const artifact = {
artifactId: randomUUID(),
runId: params.runId,
cellId: params.cellId,
tenantId: params.tenantId,
kind: params.kind,
name: params.name,
description: params.description,
contentHash,
contentSize,
content: params.content,
metadata: params.metadata ?? {},
lineage: params.lineage,
signature: '', // placeholder; signed below
createdAt: Date.now(),
tags: params.tags ?? [],
};
artifact.signature = this.signArtifact(artifact);
this.artifacts.set(artifact.artifactId, artifact);
// Evict oldest if over capacity
if (this.artifacts.size > this.maxArtifacts) {
this.evictOldest();
}
return artifact;
}
/**
* Verify an artifact's signature, content integrity, and lineage completeness.
*
* @param artifactId - The artifact to verify
* @returns Verification result with individual check outcomes
*/
verify(artifactId) {
const artifact = this.artifacts.get(artifactId);
if (!artifact) {
return {
verified: false,
signatureValid: false,
contentIntact: false,
lineageComplete: false,
verifiedAt: Date.now(),
};
}
const expectedSignature = this.signArtifact(artifact);
const signatureValid = timingSafeEqual(artifact.signature, expectedSignature);
const expectedHash = this.computeContentHash(artifact.content);
const contentIntact = artifact.contentHash === expectedHash;
const lineageComplete = artifact.lineage.parentArtifacts.every(parentId => this.artifacts.has(parentId));
return {
verified: signatureValid && contentIntact && lineageComplete,
signatureValid,
contentIntact,
lineageComplete,
verifiedAt: Date.now(),
};
}
/**
* Retrieve an artifact by its ID.
*
* @param artifactId - The artifact to retrieve
* @returns The artifact, or undefined if not found
*/
get(artifactId) {
return this.artifacts.get(artifactId);
}
/**
* Retrieve all artifacts produced by a specific run.
*
* @param runId - The run ID to filter by
* @returns Artifacts matching the run, ordered by creation time
*/
getByRun(runId) {
return this.filterAndSort(a => a.runId === runId);
}
/**
* Retrieve all artifacts of a specific kind.
*
* @param kind - The artifact kind to filter by
* @returns Artifacts matching the kind, ordered by creation time
*/
getByKind(kind) {
return this.filterAndSort(a => a.kind === kind);
}
/**
* Retrieve all artifacts produced by a specific agent cell.
*
* @param cellId - The cell ID to filter by
* @returns Artifacts matching the cell, ordered by creation time
*/
getByCell(cellId) {
return this.filterAndSort(a => a.cellId === cellId);
}
/**
* Traverse the full ancestry of an artifact, depth-first.
*
* Returns all ancestor artifacts reachable through the lineage
* parentArtifacts chain. Handles cycles by tracking visited IDs.
*
* @param artifactId - The artifact whose lineage to traverse
* @returns All ancestor artifacts in depth-first order
*/
getLineage(artifactId) {
const result = [];
const visited = new Set();
const traverse = (id) => {
if (visited.has(id))
return;
visited.add(id);
const artifact = this.artifacts.get(id);
if (!artifact)
return;
for (const parentId of artifact.lineage.parentArtifacts) {
if (!visited.has(parentId)) {
const parent = this.artifacts.get(parentId);
if (parent) {
result.push(parent);
traverse(parentId);
}
}
}
};
traverse(artifactId);
return result;
}
/**
* Search artifacts using a multi-dimensional query.
*
* All specified filters are ANDed together.
*
* @param query - Search criteria
* @returns Matching artifacts ordered by creation time
*/
search(query) {
return this.filterAndSort(a => {
if (query.kind !== undefined && a.kind !== query.kind)
return false;
if (query.runId !== undefined && a.runId !== query.runId)
return false;
if (query.since !== undefined && a.createdAt < query.since)
return false;
if (query.until !== undefined && a.createdAt > query.until)
return false;
if (query.tags !== undefined && query.tags.length > 0) {
const artifactTags = new Set(a.tags);
if (!query.tags.every(t => artifactTags.has(t)))
return false;
}
return true;
});
}
/**
* Export the entire ledger as a serializable object.
*
* @returns Serialized ledger data suitable for JSON.stringify
*/
export() {
return {
artifacts: Array.from(this.artifacts.values()).map(a => ({ ...a })),
createdAt: new Date().toISOString(),
version: SERIALIZATION_VERSION,
};
}
/**
* Import a previously exported ledger, replacing all current contents.
*
* @param data - Serialized ledger data
* @throws If the version is unsupported
*/
import(data) {
if (data.version !== SERIALIZATION_VERSION) {
throw new Error(`Unsupported artifact ledger version: ${data.version} (expected ${SERIALIZATION_VERSION})`);
}
this.artifacts.clear();
for (const artifact of data.artifacts) {
this.artifacts.set(artifact.artifactId, { ...artifact });
}
}
/**
* Get aggregate statistics about the ledger.
*
* @returns Counts by kind and total content size
*/
getStats() {
const byKind = Object.fromEntries(ALL_KINDS.map(k => [k, 0]));
let totalSize = 0;
for (const artifact of this.artifacts.values()) {
byKind[artifact.kind]++;
totalSize += artifact.contentSize;
}
return {
totalArtifacts: this.artifacts.size,
byKind,
totalSize,
};
}
// ===========================================================================
// Private helpers
// ===========================================================================
/**
* Produce the HMAC-SHA256 signature for an artifact.
*
* The signature covers every field except `signature` and `content` itself
* (content is covered by contentHash).
*/
signArtifact(artifact) {
const body = {
artifactId: artifact.artifactId,
runId: artifact.runId,
cellId: artifact.cellId,
tenantId: artifact.tenantId,
kind: artifact.kind,
name: artifact.name,
description: artifact.description,
contentHash: artifact.contentHash,
contentSize: artifact.contentSize,
metadata: artifact.metadata,
lineage: artifact.lineage,
createdAt: artifact.createdAt,
tags: artifact.tags,
};
const payload = JSON.stringify(body);
return createHmac('sha256', this.signingKey).update(payload).digest('hex');
}
/**
* Compute the SHA-256 hash of artifact content.
*
* Handles strings directly and serializes everything else to JSON.
*/
computeContentHash(content) {
const payload = typeof content === 'string'
? content
: JSON.stringify(content);
return createHash('sha256').update(payload).digest('hex');
}
/**
* Compute the byte size of artifact content.
*/
computeContentSize(content) {
if (typeof content === 'string') {
return Buffer.byteLength(content, 'utf-8');
}
return Buffer.byteLength(JSON.stringify(content), 'utf-8');
}
/**
* Filter artifacts and return them sorted by creation time ascending.
*/
filterAndSort(predicate) {
const results = [];
for (const artifact of this.artifacts.values()) {
if (predicate(artifact)) {
results.push(artifact);
}
}
return results.sort((a, b) => a.createdAt - b.createdAt);
}
/**
* Evict the oldest artifact when capacity is exceeded.
*/
evictOldest() {
let oldest;
for (const artifact of this.artifacts.values()) {
if (!oldest || artifact.createdAt < oldest.createdAt) {
oldest = artifact;
}
}
if (oldest) {
this.artifacts.delete(oldest.artifactId);
}
}
}
// ============================================================================
// Factory
// ============================================================================
/**
* Create a new ArtifactLedger instance.
*
* @param config - Optional configuration. `signingKey` sets the HMAC key,
* `maxArtifacts` sets capacity before FIFO eviction.
* @returns A fresh ArtifactLedger
*/
export function createArtifactLedger(config) {
return new ArtifactLedger(config);
}
//# sourceMappingURL=artifacts.js.map