/*
RFC-0873: nachweis.assessment.ingest command handler — validates an AssessmentBundleV1, hashes artifacts, uploads to R2, writes PBP evidence-source, appends Bordbuch entry. Implements ADR-0054: generic normalized assessment contract between provider adapters and PBP/Bordbuch.
- Does not publish or approve — use nachweis.publish and nachweis.approve for gate progression.
- Does not create public derivatives — use nachweis.public-derivative.
- Does not delete or overwrite existing observations — observation history is immutable.
- RFC-1097: sweep — werkstatt-engine clean
Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics.
- RFC-1149: migrate ambient filesystem and process IO to the WorkspaceIO port across engine and shared
- RFC-1237: resolve dry-run via context.dryRun fallback — consumeCommonFlags strips --dry-run into context.dryRun before dispatch, so a flags-only read was permanently false under werkstatt run (enforced by KERNEL-FLAG-10).
- RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt
- RFC-1254: drop unused io-migration leftovers
RFC-0873, RFC-1097
*/
import path from "node:path";
import type {
KernelCommandInput,
KernelCommandResult,
KernelRuntimeContext,
} from "@warpgogol/werkstatt-engine/kernel";
import {
parseMarkdownFrontmatter,
stringifyMarkdownFrontmatter,
} from "@warpgogol/werkstatt-shared/content";
import { appendAndCommitBordbuch } from "../bordbuch/bordbuch-commit-helper.ts";
import { acquireLock, releaseLock, generateOperationId } from "../werkstatt/index.ts";
import {
assessmentBundleV1Schema,
computeSourceSha256,
isMissingEnvError,
isNachweisEntitled,
makeSkipResult,
mediaTypeToExt,
resolveAssessmentR2Path,
resolveDefaultLang,
resolveNachweisCachePath,
resolvePbpEntityDir,
uploadToR2,
type AssessmentBundleV1,
type AssessmentIngestResult,
} from "./nachweis-io.ts";
function flagString(input: KernelCommandInput, key: string): string | undefined {
const v = input.flags[key];
return typeof v === "string" ? v : undefined;
}
function flagBool(input: KernelCommandInput, key: string): boolean {
const v = input.flags[key];
return v === true || v === "true";
}
// computeSourceSha256 returns a "sha256:" digest; the PBP evidence-source
// items schema requires bare 64-hex (/^[a-f0-9]{64}$/). Strip the scheme prefix
// for storage and for the already-ingested comparison so both forms match.
function bareSha256(digest: string | undefined): string {
return (digest ?? "").replace(/^sha256:/, "");
}
const CREDENTIAL_PATTERNS: RegExp[] = [
/(?:api[_-]?key|apikey)\s*[:=]\s*["']?[A-Za-z0-9]{20,}["']?/i,
/(?:aws_secret_access_key|private_key|client_secret)\s*[:=]\s*["']?[^\s"']{8,}["']?/i,
/TESTSECRET_[a-zA-Z0-9]{20,}/,
/-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----/,
/eyJ[A-Za-z0-9_-]{10,}\.\.[A-Za-z0-9_-]{10,}/,
/AKIA[0-9A-Z]{16}/,
/Bearer\s+[A-Za-z0-9._-]{20,}/i,
];
function scanForCredentials(json: string): boolean {
return CREDENTIAL_PATTERNS.some((p) => p.test(json));
}
function isPathSafe(segment: string): boolean {
if (segment.includes("..") || segment.includes("/") || segment.includes("\\")) {
return false;
}
return true;
}
function isPathInsideDir(filePath: string, dirPath: string): boolean {
const resolved = path.resolve(filePath);
const dir = path.resolve(dirPath);
const relative = path.relative(dir, resolved);
return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
}
function makeErrorResult(
systemId: string,
bundle: { slug: string; seriesId: string; observationId: string } | null,
dryRun: boolean,
summary: string,
): KernelCommandResult {
return {
data: {
systemId,
slug: bundle?.slug ?? "",
seriesId: bundle?.seriesId ?? "",
observationId: bundle?.observationId ?? "",
verificationLevel: "N1",
artifactHashes: {},
alreadyIngested: false,
bordbuchEventId: null,
dryRun,
},
exitCode: 1,
summary,
nextSteps: [{ action: summary, kind: "required" as const }],
};
}
export async function runNachweisAssessmentIngest(
input: KernelCommandInput,
context: KernelRuntimeContext,
): Promise> {
const { workspaceRoot, logger } = context;
const systemId = flagString(input, "system") ?? context.site?.name;
const bundlePath = flagString(input, "bundle");
const dryRun = context.dryRun === true || flagBool(input, "dry-run");
if (!systemId) throw new Error("[nachweis.assessment.ingest] --system is required");
if (!bundlePath) throw new Error("[nachweis.assessment.ingest] --bundle is required");
const entitled = await isNachweisEntitled(workspaceRoot, systemId);
if (!entitled) {
return makeSkipResult(
"nachweis.assessment.ingest",
systemId,
) as unknown as KernelCommandResult;
}
if (!(await context.io.exists(bundlePath))) {
throw new Error(
`[nachweis.assessment.ingest] NOT_FOUND: bundle '${bundlePath}' does not exist`,
);
}
const rawBundleJson = await context.io.readFile(bundlePath);
if (scanForCredentials(rawBundleJson)) {
return makeErrorResult(
systemId,
null,
dryRun,
`[nachweis.assessment.ingest] CREDENTIAL_DETECTED: bundle '${bundlePath}' contains credential-like patterns`,
);
}
const parsed = assessmentBundleV1Schema.safeParse(JSON.parse(rawBundleJson));
if (!parsed.success) {
return makeErrorResult(
systemId,
null,
dryRun,
`[nachweis.assessment.ingest] ASSESSMENT_BUNDLE_INVALID: ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`,
);
}
const bundle: AssessmentBundleV1 = parsed.data;
if (bundle.systemId !== systemId) {
return makeErrorResult(
systemId,
bundle,
dryRun,
`[nachweis.assessment.ingest] ASSESSMENT_SYSTEM_MISMATCH: bundle systemId '${bundle.systemId}' != --system '${systemId}'`,
);
}
for (const seg of [bundle.slug, bundle.seriesId, bundle.observationId]) {
if (!isPathSafe(seg)) {
return makeErrorResult(
systemId,
bundle,
dryRun,
`[nachweis.assessment.ingest] ASSESSMENT_PATH_UNSAFE: segment '${seg}' contains path traversal characters`,
);
}
}
for (const artifact of bundle.artifacts) {
if (!isPathSafe(artifact.key)) {
return makeErrorResult(
systemId,
bundle,
dryRun,
`[nachweis.assessment.ingest] ASSESSMENT_PATH_UNSAFE: artifact key '${artifact.key}' contains path traversal characters`,
);
}
}
const bundleDir = path.dirname(path.resolve(bundlePath));
for (const artifact of bundle.artifacts) {
const artifactPath = path.resolve(bundleDir, artifact.file);
if (!isPathInsideDir(artifactPath, bundleDir)) {
return makeErrorResult(
systemId,
bundle,
dryRun,
`[nachweis.assessment.ingest] ASSESSMENT_ARTIFACT_PATH_ESCAPE: artifact '${artifact.key}' path '${artifact.file}' escapes bundle directory`,
);
}
if (!(await context.io.exists(artifactPath))) {
return makeErrorResult(
systemId,
bundle,
dryRun,
`[nachweis.assessment.ingest] ASSESSMENT_ARTIFACT_MISSING: artifact '${artifact.key}' file '${artifact.file}' not found`,
);
}
try {
if ((await context.io.exec("test", ["-L", artifactPath])).exitCode === 0) {
return makeErrorResult(
systemId,
bundle,
dryRun,
`[nachweis.assessment.ingest] ASSESSMENT_ARTIFACT_PATH_ESCAPE: artifact '${artifact.key}' is a symlink — symlinks are not allowed`,
);
}
} catch {
return makeErrorResult(
systemId,
bundle,
dryRun,
`[nachweis.assessment.ingest] ASSESSMENT_ARTIFACT_MISSING: artifact '${artifact.key}' file could not be stat'd`,
);
}
}
const hasCanonicalRaw = bundle.artifacts.some((a) => a.role === "raw-result" && a.canonical);
if (!hasCanonicalRaw) {
return makeErrorResult(
systemId,
bundle,
dryRun,
`[nachweis.assessment.ingest] ASSESSMENT_CANONICAL_RAW_REQUIRED: at least one canonical raw-result artifact is required`,
);
}
const artifactHashes: Record = {};
for (const artifact of bundle.artifacts) {
const artifactPath = path.join(bundleDir, artifact.file);
artifactHashes[artifact.key] = await computeSourceSha256(artifactPath);
}
const cachePath = await resolveNachweisCachePath(workspaceRoot, systemId);
const lang = await resolveDefaultLang(cachePath);
const evidenceDir = resolvePbpEntityDir(cachePath, lang, "evidence-source");
const evidenceFile = path.join(evidenceDir, `${bundle.slug}.md`);
const existingEvidence = await context.io.exists(evidenceFile);
let existingData: Record | undefined;
let existingContent = "";
if (existingEvidence) {
const raw = await context.io.readFile(evidenceFile);
const parsed2 = parseMarkdownFrontmatter(raw);
existingData = parsed2.data;
existingContent = parsed2.content;
}
const existingItems =
(existingData?.items as Record> | undefined) ?? {};
const existingAssessment = existingData?.assessment as
{ seriesId?: string; observationId?: string } | undefined;
if (
existingAssessment &&
existingAssessment.seriesId === bundle.seriesId &&
existingAssessment.observationId === bundle.observationId
) {
const existingHashes: Record = {};
for (const artifact of bundle.artifacts) {
const item = existingItems[artifact.key] as { sha256?: string } | undefined;
if (item?.sha256) {
existingHashes[artifact.key] = item.sha256;
}
}
const allMatch = bundle.artifacts.every(
(a) => bareSha256(existingHashes[a.key]) === bareSha256(artifactHashes[a.key]),
);
if (allMatch) {
logger.info(
`[nachweis.assessment.ingest] already ingested for '${bundle.slug}' (${bundle.seriesId}/${bundle.observationId}) — skipping`,
);
return {
data: {
systemId,
slug: bundle.slug,
seriesId: bundle.seriesId,
observationId: bundle.observationId,
verificationLevel: "N1",
artifactHashes,
alreadyIngested: true,
bordbuchEventId: null,
dryRun,
},
exitCode: 0,
summary: `[nachweis.assessment.ingest] ${systemId}: already ingested '${bundle.slug}' (${bundle.seriesId}/${bundle.observationId})`,
};
}
return {
exitCode: 1,
summary: `[nachweis.assessment.ingest] ASSESSMENT_OBSERVATION_CONFLICT: observation (${bundle.seriesId}/${bundle.observationId}) already exists with different artifact hashes for '${bundle.slug}'`,
nextSteps: [
{
action: `[nachweis.assessment.ingest] ASSESSMENT_OBSERVATION_CONFLICT: observation (${bundle.seriesId}/${bundle.observationId}) already exists with different artifact hashes for '${bundle.slug}'`,
kind: "required" as const,
},
],
data: {
systemId,
slug: bundle.slug,
seriesId: bundle.seriesId,
observationId: bundle.observationId,
verificationLevel: "N1",
artifactHashes,
alreadyIngested: false,
bordbuchEventId: null,
dryRun,
},
};
}
if (dryRun) {
return {
data: {
systemId,
slug: bundle.slug,
seriesId: bundle.seriesId,
observationId: bundle.observationId,
verificationLevel: "N1",
artifactHashes,
alreadyIngested: false,
bordbuchEventId: null,
dryRun: true,
},
exitCode: 0,
summary: `[nachweis.assessment.ingest] ${systemId}: DRY RUN — would ingest '${bundle.slug}' (${bundle.seriesId}/${bundle.observationId}) with ${bundle.artifacts.length} artifacts`,
};
}
for (const artifact of bundle.artifacts) {
const artifactPath = path.join(bundleDir, artifact.file);
const ext = mediaTypeToExt(artifact.mediaType);
const r2Path = resolveAssessmentR2Path(
systemId,
bundle.seriesId,
bundle.observationId,
artifact.key,
ext,
);
const fileBuffer = await context.io.readFileBytes(artifactPath);
try {
await uploadToR2(fileBuffer, r2Path, artifact.mediaType);
} catch (err) {
if (isMissingEnvError(err)) {
return {
exitCode: 1,
summary: `[nachweis.assessment.ingest] MISSING_ENV: ${err.message}`,
nextSteps: [
{
action: `[nachweis.assessment.ingest] MISSING_ENV: ${err.message}`,
kind: "required" as const,
},
],
data: {
systemId,
slug: bundle.slug,
seriesId: bundle.seriesId,
observationId: bundle.observationId,
verificationLevel: "N1",
artifactHashes,
alreadyIngested: false,
bordbuchEventId: null,
dryRun: false,
},
};
}
return {
exitCode: 1,
summary: `[nachweis.assessment.ingest] R2_UPLOAD_ERROR: failed to upload artifact '${artifact.key}': ${err}`,
nextSteps: [
{
action: `[nachweis.assessment.ingest] R2_UPLOAD_ERROR: failed to upload artifact '${artifact.key}': ${err}`,
kind: "required" as const,
},
],
data: {
systemId,
slug: bundle.slug,
seriesId: bundle.seriesId,
observationId: bundle.observationId,
verificationLevel: "N1",
artifactHashes,
alreadyIngested: false,
bordbuchEventId: null,
dryRun: false,
},
};
}
}
logger.info(`[nachweis.assessment.ingest] uploaded ${bundle.artifacts.length} artifacts to R2`);
const items: Record> = existingItems;
for (const artifact of bundle.artifacts) {
const ext = mediaTypeToExt(artifact.mediaType);
const r2Path = resolveAssessmentR2Path(
systemId,
bundle.seriesId,
bundle.observationId,
artifact.key,
ext,
);
items[artifact.key] = {
sha256: bareSha256(artifactHashes[artifact.key]),
storage: "private",
mediaType: artifact.mediaType,
qualityStatus: "verified",
role: artifact.role,
canonical: artifact.canonical,
r2Path,
};
}
const evidenceData: Record = existingData ?? {};
evidenceData.kind = "technical-assessment";
evidenceData.slug = bundle.slug;
evidenceData.recordId = `nr_${bundle.slug}`;
evidenceData.version = 1;
evidenceData.items = items;
evidenceData.assessment = {
profile: "technical-assessment",
seriesId: bundle.seriesId,
observationId: bundle.observationId,
subject: bundle.subject,
provider: bundle.provider,
tool: bundle.tool,
executionMode: bundle.execution.mode,
authorizationBasis: bundle.execution.authorizationBasis,
observedAt: bundle.observedAt,
methodology: bundle.methodology,
...(bundle.result.overall ? { overall: bundle.result.overall } : {}),
dimensions: bundle.result.dimensions,
freshness: bundle.freshness,
...(bundle.providerReportUrl ? { providerReportUrl: bundle.providerReportUrl } : {}),
};
await context.io.mkdir(evidenceDir);
const updatedContent = stringifyMarkdownFrontmatter(existingContent, evidenceData);
await context.io.writeFile(evidenceFile, updatedContent);
const operationId = generateOperationId();
await acquireLock(
workspaceRoot,
`system:${systemId}`,
operationId,
"nachweis.assessment.ingest",
"agent",
);
await acquireLock(
workspaceRoot,
`bordbuch:${systemId}`,
operationId,
"nachweis.assessment.ingest",
"agent",
);
let bordbuchEventId: string | null = null;
try {
const { entry } = await appendAndCommitBordbuch(
workspaceRoot,
systemId,
"nachweis-record",
`Assessment ingested for '${bundle.slug}' (${bundle.seriesId})`,
"agent",
{
writerRole: "nachweis",
metadata: {
action: "assessment-ingested",
slug: bundle.slug,
seriesId: bundle.seriesId,
observationId: bundle.observationId,
providerId: bundle.provider.id,
toolId: bundle.tool.id,
observedAt: bundle.observedAt,
artifactHashes,
verificationLevel: "N1",
},
},
`Bordbuch: nachweis-record ${systemId} ${bundle.slug} assessment`,
);
bordbuchEventId = entry.id;
} catch (err) {
logger.warn(
`[nachweis.assessment.ingest] Bordbuch append failed — R2 objects need manual cleanup: ${err}`,
);
return {
exitCode: 1,
summary: `[nachweis.assessment.ingest] BORDBUCH_ERROR: R2 upload succeeded but Bordbuch append failed — manual cleanup needed`,
nextSteps: [
{
action: `[nachweis.assessment.ingest] BORDBUCH_ERROR: R2 upload succeeded but Bordbuch append failed — manual cleanup needed`,
kind: "required" as const,
},
],
data: {
systemId,
slug: bundle.slug,
seriesId: bundle.seriesId,
observationId: bundle.observationId,
verificationLevel: "N1",
artifactHashes,
alreadyIngested: false,
bordbuchEventId: null,
dryRun: false,
},
};
} finally {
await releaseLock(workspaceRoot, `bordbuch:${systemId}`);
await releaseLock(workspaceRoot, `system:${systemId}`);
}
return {
data: {
systemId,
slug: bundle.slug,
seriesId: bundle.seriesId,
observationId: bundle.observationId,
verificationLevel: "N1",
artifactHashes,
alreadyIngested: false,
bordbuchEventId,
dryRun: false,
},
exitCode: 0,
summary: `[nachweis.assessment.ingest] ${systemId}: ingested '${bundle.slug}' (${bundle.seriesId}/${bundle.observationId}, ${bundle.artifacts.length} artifacts, bordbuch: ${bordbuchEventId})`,
nextSteps: [
{
action: `Validate the nachweis: pnpm exec werkstatt run nachweis.validate --site ${systemId}`,
kind: "optional",
},
],
};
}