From 2f560a572a7d67f939b219785db55ea9dabbfa56 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:06:14 +0200 Subject: [PATCH 01/33] feat(lab): add CL-10 public evidence projector --- src/lab/index.ts | 1 + src/lab/public/index.ts | 4 + src/lab/public/project.ts | 145 +++++++++++++++++ src/lab/public/registry.ts | 50 ++++++ src/lab/public/types.ts | 106 +++++++++++++ src/lab/public/validate.ts | 308 +++++++++++++++++++++++++++++++++++++ 6 files changed, 614 insertions(+) create mode 100644 src/lab/public/index.ts create mode 100644 src/lab/public/project.ts create mode 100644 src/lab/public/registry.ts create mode 100644 src/lab/public/types.ts create mode 100644 src/lab/public/validate.ts diff --git a/src/lab/index.ts b/src/lab/index.ts index 783eb05526..2110aecff9 100644 --- a/src/lab/index.ts +++ b/src/lab/index.ts @@ -36,3 +36,4 @@ export * from "./subject/installation-salt"; export { CL03_LIVE_SUITES } from "./conformance/types"; export * from "./query"; export * from "./automation"; +export * from "./public"; diff --git a/src/lab/public/index.ts b/src/lab/public/index.ts new file mode 100644 index 0000000000..064bc133c5 --- /dev/null +++ b/src/lab/public/index.ts @@ -0,0 +1,4 @@ +export * from "./types"; +export * from "./registry"; +export * from "./validate"; +export * from "./project"; diff --git a/src/lab/public/project.ts b/src/lab/public/project.ts new file mode 100644 index 0000000000..cf0ad3e988 --- /dev/null +++ b/src/lab/public/project.ts @@ -0,0 +1,145 @@ +import { domainHash, jcsStringify } from "../digest"; +import type { ObservationEvent, ProtocolSubjectV1, RouteSubjectV1 } from "../events/types"; +import { PUBLIC_ROUTE_REGISTRY_V1, findPublicRouteRegistryEntry } from "./registry"; +import type { + PublicAdapterFamilyV1, + PublicEvidenceRecordV1, + PublicEvidenceSubjectV1, + PublicProjectionResult, + PublicProtocolSubjectV1, + PublicRouteAuthorityV1, +} from "./types"; +import { isPublicIncidentRef, validatePublicEvidenceRecord } from "./validate"; + +const PUBLIC_ID_DOMAINS = { + subject: "ocx-lab:public-subject:v1", + record: "ocx-lab:public-record:v1", + bundle: "ocx-lab:public-bundle:v1", + artifact: "ocx-lab:public-artifact:v1", + publisher: "ocx-lab:public-publisher:v1", + revocation: "ocx-lab:public-revocation:v1", +} as const; + +export type PublicEvidenceIdKind = keyof typeof PUBLIC_ID_DOMAINS; + +export function publicEvidenceId(kind: PublicEvidenceIdKind, payload: unknown): string { + return domainHash(PUBLIC_ID_DOMAINS[kind], jcsStringify(payload)); +} + +function publicAdapterFamily(value: string): PublicAdapterFamilyV1 | null { + switch (value) { + case "openai-responses": return "openai-responses"; + case "openai-chat": return "openai-chat"; + case "anthropic": + case "anthropic-messages": return "anthropic-messages"; + case "responses": return "openai-responses"; + case "chat": return "openai-chat"; + default: return null; + } +} + +function publicProtocolSubject(subject: ProtocolSubjectV1): PublicProtocolSubjectV1 | null { + const adapterFamily = publicAdapterFamily(subject.effectiveAdapter); + const inboundProtocol = publicAdapterFamily(subject.inboundProtocol); + const upstreamProtocol = publicAdapterFamily(subject.upstreamProtocol); + if (!adapterFamily || !inboundProtocol || !upstreamProtocol) return null; + return { + subjectKind: "protocol", + adapterFamily, + inboundProtocol, + upstreamProtocol, + surface: subject.surface, + compatibilityVersion: subject.opencodexCompatibilityVersion, + }; +} + +function routeAuthorityMatches( + subject: RouteSubjectV1, + localSubjectId: string, + authority: PublicRouteAuthorityV1 | undefined, +): boolean { + if (!authority || authority.localSubjectId !== localSubjectId) return false; + if (subject.dependencies.length !== 0) return false; + if (subject.clientModelId !== subject.upstreamModelId) return false; + const descriptor = authority.descriptor; + if (descriptor.subjectKind !== "route") return false; + if (descriptor.providerId !== subject.providerId || descriptor.modelId !== subject.upstreamModelId) return false; + if (descriptor.registryDigest !== PUBLIC_ROUTE_REGISTRY_V1.manifestDigest + || descriptor.registryVersion !== PUBLIC_ROUTE_REGISTRY_V1.registryVersion) return false; + const adapterFamily = publicAdapterFamily(subject.effectiveAdapter); + const inboundProtocol = publicAdapterFamily(subject.inboundProtocol); + const upstreamProtocol = publicAdapterFamily(subject.upstreamProtocol); + if (!adapterFamily || !inboundProtocol || !upstreamProtocol) return false; + if (descriptor.adapterFamily !== adapterFamily + || descriptor.inboundProtocol !== inboundProtocol + || descriptor.upstreamProtocol !== upstreamProtocol + || descriptor.surface !== subject.surface + || descriptor.compatibilityVersion !== subject.opencodexCompatibilityVersion) return false; + return findPublicRouteRegistryEntry(descriptor.providerId, descriptor.modelId, descriptor.adapterFamily) !== null; +} + +function observedDayUtc(observation: ObservationEvent): string { + const value = Number.isFinite(observation.completedAt) ? observation.completedAt : observation.recordedAt; + return new Date(value).toISOString().slice(0, 10); +} + +export interface ProjectPublicEvidenceRecordInput { + observation: ObservationEvent; + verdict: PublicEvidenceRecordV1["verdict"]; + incidentRefs?: string[]; + routeAuthority?: PublicRouteAuthorityV1; +} + +export function projectPublicEvidenceRecord(input: ProjectPublicEvidenceRecordInput): PublicProjectionResult { + const observation = input.observation; + let subject: PublicEvidenceSubjectV1; + + if (observation.evidenceLayer === "protocol_conformance") { + if (observation.subject.subjectKind !== "protocol") { + return { status: "not_exportable", reason: "unsupported_public_adapter" }; + } + const projected = publicProtocolSubject(observation.subject); + if (!projected) return { status: "not_exportable", reason: "unsupported_public_adapter" }; + subject = projected; + } else if (observation.evidenceLayer === "live_route_compatibility") { + if (observation.subject.subjectKind !== "route" + || !routeAuthorityMatches(observation.subject, observation.subjectId, input.routeAuthority)) { + return { status: "not_exportable", reason: "private_route_identity" }; + } + subject = input.routeAuthority!.descriptor; + } else { + return { status: "not_exportable", reason: "task_authority_unavailable" }; + } + + let incidentRefs: Array<{ corpusId: string }> | undefined; + if (input.incidentRefs !== undefined) { + if (input.incidentRefs.some((value) => !isPublicIncidentRef(value))) { + return { status: "not_exportable", reason: "invalid_public_incident_ref" }; + } + incidentRefs = [...new Set(input.incidentRefs)].sort().map((corpusId) => ({ corpusId })); + } + + const subjectId = publicEvidenceId("subject", subject); + const recordWithoutId: Omit = { + subjectId, + evidenceLayer: observation.evidenceLayer, + suiteId: observation.suiteId, + suiteVersion: observation.suiteVersion, + scenarioId: observation.scenarioId, + scenarioVersion: observation.scenarioVersion, + verdict: input.verdict, + observedDayUtc: observedDayUtc(observation), + subject, + assertions: observation.assertions.slice(0, 64).map((assertion) => ({ + id: assertion.id, + required: assertion.required, + passed: assertion.passed, + })), + ...(incidentRefs && incidentRefs.length > 0 ? { incidentRefs } : {}), + }; + const record: PublicEvidenceRecordV1 = { + recordId: publicEvidenceId("record", recordWithoutId), + ...recordWithoutId, + }; + return { status: "exportable", record: validatePublicEvidenceRecord(record) }; +} diff --git a/src/lab/public/registry.ts b/src/lab/public/registry.ts new file mode 100644 index 0000000000..cfdf2737cb --- /dev/null +++ b/src/lab/public/registry.ts @@ -0,0 +1,50 @@ +import { domainHash, jcsStringify } from "../digest"; +import type { + PublicAdapterFamilyV1, + PublicRouteRegistryEntryV1, + PublicRouteRegistryManifestV1, +} from "./types"; +import { PUBLIC_ROUTE_REGISTRY_SCHEMA_VERSION } from "./types"; + +const REGISTRY_DOMAIN = "ocx-lab:public-route-registry:v1"; + +const entries: PublicRouteRegistryEntryV1[] = [ + { providerId: "openai-apikey", modelId: "gpt-5.5", adapterFamilies: ["openai-responses"] }, + { providerId: "openai-apikey", modelId: "gpt-5.6-luna", adapterFamilies: ["openai-responses"] }, + { providerId: "openai-apikey", modelId: "gpt-5.6-sol", adapterFamilies: ["openai-responses"] }, + { providerId: "openai-apikey", modelId: "gpt-5.6-terra", adapterFamilies: ["openai-responses"] }, +]; + +const manifestPayload = { + schemaVersion: PUBLIC_ROUTE_REGISTRY_SCHEMA_VERSION, + registryVersion: "2026-08-12.1", + sourceCommit: "4fed8d3fe431ad23be83f3aff2af18ef8b8ecd71", + entries, +}; + +export const PUBLIC_ROUTE_REGISTRY_V1: PublicRouteRegistryManifestV1 = Object.freeze({ + ...manifestPayload, + entries: Object.freeze(entries.map((entry) => Object.freeze({ + ...entry, + adapterFamilies: Object.freeze([...entry.adapterFamilies]) as unknown as PublicAdapterFamilyV1[], + }))) as unknown as PublicRouteRegistryEntryV1[], + manifestDigest: domainHash(REGISTRY_DOMAIN, jcsStringify(manifestPayload)), +}); + +export function publicRouteRegistryDigest( + manifest: Omit, +): string { + return domainHash(REGISTRY_DOMAIN, jcsStringify(manifest)); +} + +export function findPublicRouteRegistryEntry( + providerId: string, + modelId: string, + adapterFamily: PublicAdapterFamilyV1, + manifest: PublicRouteRegistryManifestV1 = PUBLIC_ROUTE_REGISTRY_V1, +): PublicRouteRegistryEntryV1 | null { + return manifest.entries.find((entry) => + entry.providerId === providerId + && entry.modelId === modelId + && entry.adapterFamilies.includes(adapterFamily)) ?? null; +} diff --git a/src/lab/public/types.ts b/src/lab/public/types.ts new file mode 100644 index 0000000000..7985ce7f07 --- /dev/null +++ b/src/lab/public/types.ts @@ -0,0 +1,106 @@ +import type { CompatibilityVerdict, EvidenceLayer } from "../constants"; + +export const PUBLIC_ROUTE_REGISTRY_SCHEMA_VERSION = "public_route_registry_v1" as const; +export const PUBLIC_EXPORT_POLICY_VERSION = "public_export_policy_v1" as const; + +export const PUBLIC_ADAPTER_FAMILIES = [ + "openai-responses", + "openai-chat", + "anthropic-messages", +] as const; +export type PublicAdapterFamilyV1 = (typeof PUBLIC_ADAPTER_FAMILIES)[number]; + +export interface PublicRouteRegistryEntryV1 { + providerId: string; + modelId: string; + adapterFamilies: PublicAdapterFamilyV1[]; +} + +export interface PublicRouteRegistryManifestV1 { + schemaVersion: typeof PUBLIC_ROUTE_REGISTRY_SCHEMA_VERSION; + registryVersion: string; + sourceCommit: string; + entries: PublicRouteRegistryEntryV1[]; + manifestDigest: string; +} + +export interface PublicProtocolSubjectV1 { + subjectKind: "protocol"; + adapterFamily: PublicAdapterFamilyV1; + inboundProtocol: PublicAdapterFamilyV1; + upstreamProtocol: PublicAdapterFamilyV1; + surface: string; + compatibilityVersion: string; +} + +export interface PublicRouteSubjectV1 { + subjectKind: "route"; + providerId: string; + modelId: string; + adapterFamily: PublicAdapterFamilyV1; + inboundProtocol: PublicAdapterFamilyV1; + upstreamProtocol: PublicAdapterFamilyV1; + surface: string; + compatibilityVersion: string; + registryVersion: string; + registryDigest: string; +} + +export interface PublicTaskSubjectV1 { + subjectKind: "task"; + route: PublicRouteSubjectV1; + taskClassId: string; + taskClassVersion: string; + verifierAuthorityId: string; +} + +export type PublicEvidenceSubjectV1 = + | PublicProtocolSubjectV1 + | PublicRouteSubjectV1 + | PublicTaskSubjectV1; + +export interface PublicAssertionSummaryV1 { + id: string; + required: boolean; + passed: boolean; +} + +export interface PublicIncidentRefV1 { + corpusId: string; +} + +export interface PublicEvidenceRecordV1 { + recordId: string; + subjectId: string; + evidenceLayer: EvidenceLayer; + suiteId: string; + suiteVersion: string; + scenarioId: string; + scenarioVersion: string; + verdict: CompatibilityVerdict; + observedDayUtc: string; + subject: PublicEvidenceSubjectV1; + assertions: PublicAssertionSummaryV1[]; + incidentRefs?: PublicIncidentRefV1[]; + artifactRefs?: string[]; +} + +export type PublicProjectionNotExportableReason = + | "private_route_identity" + | "unsupported_public_adapter" + | "task_authority_unavailable" + | "invalid_public_incident_ref"; + +export type PublicProjectionResult = + | { status: "exportable"; record: PublicEvidenceRecordV1 } + | { status: "not_exportable"; reason: PublicProjectionNotExportableReason }; + +/** + * Trusted local proof for an exact route. Callers must derive this from the same + * effective route subject that produced the local observation, never from an + * imported bundle or user-supplied public descriptor. + */ +export interface PublicRouteAuthorityV1 { + localSubjectId: string; + descriptor: PublicRouteSubjectV1; +} diff --git a/src/lab/public/validate.ts b/src/lab/public/validate.ts new file mode 100644 index 0000000000..2c0a3ba963 --- /dev/null +++ b/src/lab/public/validate.ts @@ -0,0 +1,308 @@ +import { EVIDENCE_LAYERS, VERDICTS } from "../constants"; +import { isSha256Hex, jcsStringify } from "../digest"; +import { + PUBLIC_ADAPTER_FAMILIES, + PUBLIC_ROUTE_REGISTRY_SCHEMA_VERSION, + type PublicAdapterFamilyV1, + type PublicEvidenceRecordV1, + type PublicEvidenceSubjectV1, + type PublicIncidentRefV1, + type PublicProtocolSubjectV1, + type PublicRouteRegistryEntryV1, + type PublicRouteRegistryManifestV1, + type PublicRouteSubjectV1, + type PublicTaskSubjectV1, +} from "./types"; +import { publicRouteRegistryDigest } from "./registry"; + +const MAX_STRING_BYTES = 4 * 1024; +const MAX_ASSERTIONS = 64; +const MAX_INCIDENT_REFS = 32; +const MAX_ARTIFACT_REFS = 16; +const MAX_RECORD_BYTES = 64 * 1024; +const MAX_REGISTRY_ENTRIES = 512; +const PUBLIC_INCIDENT_REFS = new Set( + Array.from({ length: 21 }, (_, index) => `IC-${String(index + 1).padStart(3, "0")}`), +); + +export class PublicEvidenceValidationError extends Error { + readonly code: string; + + constructor(code: string, message: string) { + super(message); + this.name = "PublicEvidenceValidationError"; + this.code = code; + } +} + +function isPlainObject(value: unknown): value is Record { + return !!value && typeof value === "object" && !Array.isArray(value); +} + +function assertClosedKeys(raw: Record, allowed: readonly string[], field: string): void { + const set = new Set(allowed); + for (const key of Object.keys(raw)) { + if (!set.has(key)) throw new PublicEvidenceValidationError("unknown_field", `${field}.${key}`); + } +} + +function assertString(value: unknown, field: string, max = MAX_STRING_BYTES): string { + if (typeof value !== "string") { + throw new PublicEvidenceValidationError("invalid_type", `${field} must be string`); + } + if (value.includes("\0")) { + throw new PublicEvidenceValidationError("nul_forbidden", `${field} contains NUL`); + } + if (new TextEncoder().encode(value).byteLength > max) { + throw new PublicEvidenceValidationError("field_too_large", `${field} exceeds ${max} bytes`); + } + return value; +} + +function assertPublicToken(value: unknown, field: string, max = 256): string { + const text = assertString(value, field, max); + if (!/^[A-Za-z0-9][A-Za-z0-9._:/+-]*$/.test(text)) { + throw new PublicEvidenceValidationError("unsafe_string", `${field} contains unsupported characters`); + } + return text; +} + +function assertHex(value: unknown, field: string): string { + const text = assertString(value, field, 64); + if (!isSha256Hex(text)) { + throw new PublicEvidenceValidationError("invalid_id", `${field} must be lowercase sha256 hex`); + } + return text; +} + +function assertAdapter(value: unknown, field: string): PublicAdapterFamilyV1 { + const text = assertString(value, field, 64); + if (!(PUBLIC_ADAPTER_FAMILIES as readonly string[]).includes(text)) { + throw new PublicEvidenceValidationError("closed_set", `${field} is not a public adapter family`); + } + return text as PublicAdapterFamilyV1; +} + +function assertBoolean(value: unknown, field: string): boolean { + if (value !== true && value !== false) { + throw new PublicEvidenceValidationError("invalid_type", `${field} must be boolean`); + } + return value; +} + +function assertDay(value: unknown, field: string): string { + const text = assertString(value, field, 10); + if (!/^\d{4}-\d{2}-\d{2}$/.test(text)) { + throw new PublicEvidenceValidationError("invalid_day", `${field} must be YYYY-MM-DD`); + } + const parsed = new Date(`${text}T00:00:00.000Z`); + if (!Number.isFinite(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== text) { + throw new PublicEvidenceValidationError("invalid_day", `${field} is not a UTC calendar day`); + } + return text; +} + +export function isPublicIncidentRef(value: unknown): value is string { + return typeof value === "string" && PUBLIC_INCIDENT_REFS.has(value); +} + +function validateIncidentRefs(raw: unknown): PublicIncidentRefV1[] { + if (!Array.isArray(raw) || raw.length > MAX_INCIDENT_REFS) { + throw new PublicEvidenceValidationError("invalid_incident_refs", "incidentRefs is invalid or oversized"); + } + const seen = new Set(); + return raw.map((value, index) => { + if (!isPlainObject(value)) { + throw new PublicEvidenceValidationError("invalid_incident_ref", `incidentRefs[${index}]`); + } + assertClosedKeys(value, ["corpusId"], `incidentRefs[${index}]`); + const corpusId = assertString(value.corpusId, `incidentRefs[${index}].corpusId`, 6); + if (!isPublicIncidentRef(corpusId) || seen.has(corpusId)) { + throw new PublicEvidenceValidationError("invalid_incident_ref", `incidentRefs[${index}].corpusId`); + } + seen.add(corpusId); + return { corpusId }; + }); +} + +function validateProtocolSubject(raw: Record): PublicProtocolSubjectV1 { + assertClosedKeys(raw, [ + "subjectKind", "adapterFamily", "inboundProtocol", "upstreamProtocol", "surface", "compatibilityVersion", + ], "subject"); + if (raw.subjectKind !== "protocol") { + throw new PublicEvidenceValidationError("subject_kind", "protocol subjectKind mismatch"); + } + return { + subjectKind: "protocol", + adapterFamily: assertAdapter(raw.adapterFamily, "subject.adapterFamily"), + inboundProtocol: assertAdapter(raw.inboundProtocol, "subject.inboundProtocol"), + upstreamProtocol: assertAdapter(raw.upstreamProtocol, "subject.upstreamProtocol"), + surface: assertPublicToken(raw.surface, "subject.surface", 128), + compatibilityVersion: assertPublicToken(raw.compatibilityVersion, "subject.compatibilityVersion", 128), + }; +} + +function validateRouteSubject(raw: Record): PublicRouteSubjectV1 { + assertClosedKeys(raw, [ + "subjectKind", "providerId", "modelId", "adapterFamily", "inboundProtocol", "upstreamProtocol", "surface", + "compatibilityVersion", "registryVersion", "registryDigest", + ], "subject"); + if (raw.subjectKind !== "route") { + throw new PublicEvidenceValidationError("subject_kind", "route subjectKind mismatch"); + } + return { + subjectKind: "route", + providerId: assertPublicToken(raw.providerId, "subject.providerId", 128), + modelId: assertPublicToken(raw.modelId, "subject.modelId", 256), + adapterFamily: assertAdapter(raw.adapterFamily, "subject.adapterFamily"), + inboundProtocol: assertAdapter(raw.inboundProtocol, "subject.inboundProtocol"), + upstreamProtocol: assertAdapter(raw.upstreamProtocol, "subject.upstreamProtocol"), + surface: assertPublicToken(raw.surface, "subject.surface", 128), + compatibilityVersion: assertPublicToken(raw.compatibilityVersion, "subject.compatibilityVersion", 128), + registryVersion: assertPublicToken(raw.registryVersion, "subject.registryVersion", 128), + registryDigest: assertHex(raw.registryDigest, "subject.registryDigest"), + }; +} + +function validateTaskSubject(raw: Record): PublicTaskSubjectV1 { + assertClosedKeys(raw, ["subjectKind", "route", "taskClassId", "taskClassVersion", "verifierAuthorityId"], "subject"); + if (raw.subjectKind !== "task" || !isPlainObject(raw.route)) { + throw new PublicEvidenceValidationError("subject_kind", "task subject is invalid"); + } + return { + subjectKind: "task", + route: validateRouteSubject(raw.route), + taskClassId: assertPublicToken(raw.taskClassId, "subject.taskClassId", 256), + taskClassVersion: assertPublicToken(raw.taskClassVersion, "subject.taskClassVersion", 128), + verifierAuthorityId: assertPublicToken(raw.verifierAuthorityId, "subject.verifierAuthorityId", 256), + }; +} + +function validateSubject(raw: unknown, layer: string): PublicEvidenceSubjectV1 { + if (!isPlainObject(raw)) { + throw new PublicEvidenceValidationError("invalid_subject", "subject must be object"); + } + if (layer === "protocol_conformance") { + if (raw.subjectKind !== "protocol") throw new PublicEvidenceValidationError("layer_subject_mismatch", "protocol layer requires protocol subject"); + return validateProtocolSubject(raw); + } + if (layer === "live_route_compatibility") { + if (raw.subjectKind !== "route") throw new PublicEvidenceValidationError("layer_subject_mismatch", "live layer requires route subject"); + return validateRouteSubject(raw); + } + if (layer === "task_effectiveness") { + if (raw.subjectKind !== "task") throw new PublicEvidenceValidationError("layer_subject_mismatch", "task layer requires task subject"); + return validateTaskSubject(raw); + } + throw new PublicEvidenceValidationError("unknown_layer", layer); +} + +export function validatePublicEvidenceRecord(raw: unknown): PublicEvidenceRecordV1 { + if (!isPlainObject(raw)) throw new PublicEvidenceValidationError("invalid_record", "record must be object"); + assertClosedKeys(raw, [ + "recordId", "subjectId", "evidenceLayer", "suiteId", "suiteVersion", "scenarioId", "scenarioVersion", "verdict", + "observedDayUtc", "subject", "assertions", "incidentRefs", "artifactRefs", + ], "record"); + + const layer = assertString(raw.evidenceLayer, "evidenceLayer", 64); + if (!(EVIDENCE_LAYERS as readonly string[]).includes(layer)) { + throw new PublicEvidenceValidationError("closed_set", "evidenceLayer"); + } + const verdict = assertString(raw.verdict, "verdict", 32); + if (!(VERDICTS as readonly string[]).includes(verdict)) { + throw new PublicEvidenceValidationError("closed_set", "verdict"); + } + if (!Array.isArray(raw.assertions) || raw.assertions.length > MAX_ASSERTIONS) { + throw new PublicEvidenceValidationError("invalid_assertions", "assertions is invalid or oversized"); + } + const assertions = raw.assertions.map((value, index) => { + if (!isPlainObject(value)) throw new PublicEvidenceValidationError("invalid_assertion", `assertions[${index}]`); + assertClosedKeys(value, ["id", "required", "passed"], `assertions[${index}]`); + return { + id: assertPublicToken(value.id, `assertions[${index}].id`, 256), + required: assertBoolean(value.required, `assertions[${index}].required`), + passed: assertBoolean(value.passed, `assertions[${index}].passed`), + }; + }); + + let artifactRefs: string[] | undefined; + if (raw.artifactRefs !== undefined) { + if (!Array.isArray(raw.artifactRefs) || raw.artifactRefs.length > MAX_ARTIFACT_REFS) { + throw new PublicEvidenceValidationError("invalid_artifact_refs", "artifactRefs is invalid or oversized"); + } + artifactRefs = raw.artifactRefs.map((value, index) => assertHex(value, `artifactRefs[${index}]`)); + if (new Set(artifactRefs).size !== artifactRefs.length) { + throw new PublicEvidenceValidationError("duplicate_artifact_ref", "artifactRefs contains duplicates"); + } + } + + const record: PublicEvidenceRecordV1 = { + recordId: assertHex(raw.recordId, "recordId"), + subjectId: assertHex(raw.subjectId, "subjectId"), + evidenceLayer: layer as PublicEvidenceRecordV1["evidenceLayer"], + suiteId: assertPublicToken(raw.suiteId, "suiteId", 256), + suiteVersion: assertPublicToken(raw.suiteVersion, "suiteVersion", 128), + scenarioId: assertPublicToken(raw.scenarioId, "scenarioId", 256), + scenarioVersion: assertPublicToken(raw.scenarioVersion, "scenarioVersion", 128), + verdict: verdict as PublicEvidenceRecordV1["verdict"], + observedDayUtc: assertDay(raw.observedDayUtc, "observedDayUtc"), + subject: validateSubject(raw.subject, layer), + assertions, + ...(raw.incidentRefs !== undefined ? { incidentRefs: validateIncidentRefs(raw.incidentRefs) } : {}), + ...(artifactRefs !== undefined ? { artifactRefs } : {}), + }; + + if (new TextEncoder().encode(jcsStringify(record)).byteLength > MAX_RECORD_BYTES) { + throw new PublicEvidenceValidationError("record_too_large", `record exceeds ${MAX_RECORD_BYTES} bytes`); + } + return record; +} + +export function validatePublicRouteRegistryManifest(raw: unknown): PublicRouteRegistryManifestV1 { + if (!isPlainObject(raw)) throw new PublicEvidenceValidationError("invalid_registry", "registry must be object"); + assertClosedKeys(raw, ["schemaVersion", "registryVersion", "sourceCommit", "entries", "manifestDigest"], "registry"); + if (raw.schemaVersion !== PUBLIC_ROUTE_REGISTRY_SCHEMA_VERSION) { + throw new PublicEvidenceValidationError("unsupported_version", "public route registry schema version"); + } + const registryVersion = assertPublicToken(raw.registryVersion, "registryVersion", 128); + const sourceCommit = assertHex(raw.sourceCommit, "sourceCommit"); + if (!Array.isArray(raw.entries) || raw.entries.length === 0 || raw.entries.length > MAX_REGISTRY_ENTRIES) { + throw new PublicEvidenceValidationError("invalid_registry", "registry entries are invalid or oversized"); + } + const seen = new Set(); + const entries: PublicRouteRegistryEntryV1[] = raw.entries.map((value, index) => { + if (!isPlainObject(value)) throw new PublicEvidenceValidationError("invalid_registry_entry", `entries[${index}]`); + assertClosedKeys(value, ["providerId", "modelId", "adapterFamilies"], `entries[${index}]`); + const providerId = assertPublicToken(value.providerId, `entries[${index}].providerId`, 128); + const modelId = assertPublicToken(value.modelId, `entries[${index}].modelId`, 256); + if (!Array.isArray(value.adapterFamilies) || value.adapterFamilies.length === 0 || value.adapterFamilies.length > 3) { + throw new PublicEvidenceValidationError("invalid_registry_entry", `entries[${index}].adapterFamilies`); + } + const adapterFamilies = value.adapterFamilies.map((adapter, adapterIndex) => + assertAdapter(adapter, `entries[${index}].adapterFamilies[${adapterIndex}]`)); + if (new Set(adapterFamilies).size !== adapterFamilies.length) { + throw new PublicEvidenceValidationError("invalid_registry_entry", `entries[${index}].adapterFamilies duplicates`); + } + const key = `${providerId}\0${modelId}`; + if (seen.has(key)) throw new PublicEvidenceValidationError("duplicate_registry_entry", `entries[${index}]`); + seen.add(key); + return { providerId, modelId, adapterFamilies }; + }); + const manifestDigest = assertHex(raw.manifestDigest, "manifestDigest"); + const expected = publicRouteRegistryDigest({ + schemaVersion: PUBLIC_ROUTE_REGISTRY_SCHEMA_VERSION, + registryVersion, + sourceCommit, + entries, + }); + if (manifestDigest !== expected) { + throw new PublicEvidenceValidationError("registry_digest_mismatch", "manifestDigest does not match canonical registry bytes"); + } + return { + schemaVersion: PUBLIC_ROUTE_REGISTRY_SCHEMA_VERSION, + registryVersion, + sourceCommit, + entries, + manifestDigest, + }; +} From 60db5806763e24a5a96ecf4d309ba7ffc46b905c Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:11:17 +0200 Subject: [PATCH 02/33] ci: add temporary CL-10 focused validation --- .github/workflows/cl10-focus.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/cl10-focus.yml diff --git a/.github/workflows/cl10-focus.yml b/.github/workflows/cl10-focus.yml new file mode 100644 index 0000000000..b89f30d25b --- /dev/null +++ b/.github/workflows/cl10-focus.yml @@ -0,0 +1,21 @@ +name: CL-10 focused validation + +on: + push: + branches: + - feat/cl-10-public-evidence-runtime + +permissions: + contents: read + +jobs: + focused: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + with: + bun-version: 1.3.14 + - run: bun install --frozen-lockfile + - run: bun test tests/lab-public-evidence.test.ts + - run: bun x tsc --noEmit From 0e15a4661537f9a0291e2d92866557a37d43642b Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:14:14 +0200 Subject: [PATCH 03/33] fix(lab): validate registry source commit correctly --- src/lab/public/validate.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/lab/public/validate.ts b/src/lab/public/validate.ts index 2c0a3ba963..7cf8380093 100644 --- a/src/lab/public/validate.ts +++ b/src/lab/public/validate.ts @@ -75,6 +75,14 @@ function assertHex(value: unknown, field: string): string { return text; } +function assertGitCommit(value: unknown, field: string): string { + const text = assertString(value, field, 40); + if (!/^[0-9a-f]{40}$/.test(text)) { + throw new PublicEvidenceValidationError("invalid_git_commit", `${field} must be a lowercase 40-character git commit id`); + } + return text; +} + function assertAdapter(value: unknown, field: string): PublicAdapterFamilyV1 { const text = assertString(value, field, 64); if (!(PUBLIC_ADAPTER_FAMILIES as readonly string[]).includes(text)) { @@ -265,7 +273,7 @@ export function validatePublicRouteRegistryManifest(raw: unknown): PublicRouteRe throw new PublicEvidenceValidationError("unsupported_version", "public route registry schema version"); } const registryVersion = assertPublicToken(raw.registryVersion, "registryVersion", 128); - const sourceCommit = assertHex(raw.sourceCommit, "sourceCommit"); + const sourceCommit = assertGitCommit(raw.sourceCommit, "sourceCommit"); if (!Array.isArray(raw.entries) || raw.entries.length === 0 || raw.entries.length > MAX_REGISTRY_ENTRIES) { throw new PublicEvidenceValidationError("invalid_registry", "registry entries are invalid or oversized"); } From ef128e86a91cc342d025b36c6dbd935748a919d9 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:18:34 +0200 Subject: [PATCH 04/33] test(lab): extend CL-10 public projection contract --- tests/lab-public-evidence.test.ts | 173 +++++++++++++++++++++++++++++- 1 file changed, 168 insertions(+), 5 deletions(-) diff --git a/tests/lab-public-evidence.test.ts b/tests/lab-public-evidence.test.ts index e668a79655..12225ea8d3 100644 --- a/tests/lab-public-evidence.test.ts +++ b/tests/lab-public-evidence.test.ts @@ -1,19 +1,32 @@ import { describe, expect, test } from "bun:test"; import { + FABRIC_SCENARIO_ID, + FABRIC_SCENARIO_VERSION, + FABRIC_SUITE_ID, + FABRIC_SUITE_VERSION, + FABRIC_VERIFIER_ID, LAB_EVENT_SCHEMA_VERSION, LAB_PRODUCER, assignEventId, + buildTaskSubjectV1, subjectIdForSubject, + taskSubjectId, type ObservationEvent, type ProtocolSubjectV1, type RouteSubjectV1, + type TaskSubjectV1, } from "../src/lab"; import { + PUBLIC_EVIDENCE_BUNDLE_SCHEMA_VERSION, PUBLIC_ROUTE_REGISTRY_V1, PublicEvidenceValidationError, + authorizePublicRouteSubject, + authorizePublicTaskSubject, isPublicIncidentRef, + projectPublicEvidence, projectPublicEvidenceRecord, publicEvidenceId, + validatePublicEvidenceBundleUnsigned, validatePublicEvidenceRecord, validatePublicRouteRegistryManifest, } from "../src/lab/public"; @@ -64,7 +77,13 @@ function protocolObservation(): ObservationEvent { expectedSummary: "CANARY-PRIVATE-EXPECTED", observedSummary: "CANARY-PRIVATE-OBSERVED", }], - environment: { localPath: "C:\\Users\\private\\repo" }, + environment: { + localPath: "C:\\Users\\private\\repo", + authorization: "Bearer sk-private-CANARY", + email: "private@example.com", + endpoint: "https://private.example.test/v1?tenant=acme", + ip: "10.23.45.67", + }, artifactRefs: [], sourceRefs: ["request_1234567890", "decision_1234567890"], }) as ObservationEvent; @@ -74,7 +93,7 @@ function routeObservation(): ObservationEvent { const subject: RouteSubjectV1 = { subjectSchemaVersion: 1, subjectKind: "route", - providerId: "openai", + providerId: "openai-apikey", providerInstanceFingerprint: hex("PRIVATE-provider-instance"), clientModelId: "gpt-5.6-sol", upstreamModelId: "gpt-5.6-sol", @@ -100,6 +119,36 @@ function routeObservation(): ObservationEvent { }) as ObservationEvent; } +function taskObservation(): ObservationEvent { + const route = routeObservation(); + const subject = buildTaskSubjectV1({ routeSubject: route.subject as RouteSubjectV1 }); + const subjectId = taskSubjectId(subject); + return assignEventId({ + ...route, + eventId: undefined, + evidenceLayer: "task_effectiveness" as const, + suiteId: FABRIC_SUITE_ID, + suiteVersion: FABRIC_SUITE_VERSION, + scenarioId: FABRIC_SCENARIO_ID, + scenarioVersion: FABRIC_SCENARIO_VERSION, + executionMode: "sandbox" as const, + subject, + subjectId, + sourceRefs: ["fabric_run_PRIVATE"], + }) as ObservationEvent; +} + +function routeAuthority(event = routeObservation()) { + const authority = authorizePublicRouteSubject({ + subject: event.subject as RouteSubjectV1, + localSubjectId: event.subjectId, + effectiveBaseUrl: "https://api.openai.com/v1", + privateBehaviorDimensions: [], + }); + if (!authority) throw new Error("expected public route authority"); + return authority; +} + describe("CL-10 public authority", () => { test("ships a closed, self-consistent public route registry manifest", () => { const manifest = validatePublicRouteRegistryManifest(PUBLIC_ROUTE_REGISTRY_V1); @@ -107,6 +156,7 @@ describe("CL-10 public authority", () => { expect(manifest.entries.length).toBeGreaterThan(0); expect(manifest.manifestDigest).toMatch(/^[0-9a-f]{64}$/); expect(manifest.entries.every((entry) => entry.providerId && entry.modelId)).toBe(true); + expect(manifest.entries.every((entry) => entry.canonicalBaseUrl.startsWith("https://"))).toBe(true); }); test("public incident references are closed corpus ids only", () => { @@ -116,10 +166,36 @@ describe("CL-10 public authority", () => { expect(isPublicIncidentRef("devlog/_plan/private.md")).toBe(false); expect(isPublicIncidentRef("IC-1")).toBe(false); }); + + test("authorises only an exact canonical route with no private behavior dimensions", () => { + const event = routeObservation(); + const authority = routeAuthority(event); + expect(authority.descriptor.providerId).toBe("openai-apikey"); + expect(authority.descriptor.modelId).toBe("gpt-5.6-sol"); + + expect(authorizePublicRouteSubject({ + subject: event.subject as RouteSubjectV1, + localSubjectId: event.subjectId, + effectiveBaseUrl: "https://proxy.private.example/v1", + privateBehaviorDimensions: [], + })).toBeNull(); + expect(authorizePublicRouteSubject({ + subject: event.subject as RouteSubjectV1, + localSubjectId: event.subjectId, + effectiveBaseUrl: "https://api.openai.com/v1", + privateBehaviorDimensions: ["headers"], + })).toBeNull(); + expect(authorizePublicRouteSubject({ + subject: event.subject as RouteSubjectV1, + localSubjectId: hex("wrong-local-subject"), + effectiveBaseUrl: "https://api.openai.com/v1", + privateBehaviorDimensions: [], + })).toBeNull(); + }); }); describe("CL-10 public projection", () => { - test("projects protocol evidence without leaking local ids, diagnostics, or assertion text", () => { + test("projects protocol evidence without leaking local ids, diagnostics, secrets, or assertion text", () => { const event = protocolObservation(); const result = projectPublicEvidenceRecord({ observation: event, verdict: "VERIFIED" }); expect(result.status).toBe("exportable"); @@ -140,6 +216,10 @@ describe("CL-10 public projection", () => { "CANARY-PRIVATE-EXPECTED", "CANARY-PRIVATE-OBSERVED", "C:\\Users\\private\\repo", + "sk-private-CANARY", + "private@example.com", + "private.example.test", + "10.23.45.67", "request_1234567890", "decision_1234567890", (event.subject as ProtocolSubjectV1).behaviorFingerprint, @@ -154,8 +234,68 @@ describe("CL-10 public projection", () => { expect(result).toEqual({ status: "not_exportable", reason: "private_route_identity" }); }); + test("projects an exact reviewed public route only with a trusted authority", () => { + const event = routeObservation(); + const authority = routeAuthority(event); + const result = projectPublicEvidenceRecord({ observation: event, verdict: "PROBED", routeAuthority: authority }); + expect(result.status).toBe("exportable"); + if (result.status !== "exportable") throw new Error("expected exportable route record"); + expect(result.record.subject).toMatchObject({ + subjectKind: "route", + providerId: "openai-apikey", + modelId: "gpt-5.6-sol", + }); + const serialized = JSON.stringify(result.record); + expect(serialized).not.toContain((event.subject as RouteSubjectV1).providerInstanceFingerprint); + expect(serialized).not.toContain((event.subject as RouteSubjectV1).endpointFingerprint); + expect(serialized).not.toContain((event.subject as RouteSubjectV1).behaviorFingerprint); + + const forged = { ...authority }; + expect(projectPublicEvidenceRecord({ observation: event, verdict: "PROBED", routeAuthority: forged })).toEqual({ + status: "not_exportable", + reason: "private_route_identity", + }); + }); + + test("projects only the frozen public Fabric task authority", () => { + const event = taskObservation(); + const routeSubject = (event.subject as TaskSubjectV1).routeSubject; + const routeEvent = routeObservation(); + const routeAuth = authorizePublicRouteSubject({ + subject: routeSubject, + localSubjectId: subjectIdForSubject(routeSubject), + effectiveBaseUrl: "https://api.openai.com/v1", + privateBehaviorDimensions: [], + }); + if (!routeAuth) throw new Error("expected route authority"); + const taskAuthority = authorizePublicTaskSubject({ + subject: event.subject as TaskSubjectV1, + localSubjectId: event.subjectId, + routeAuthority: routeAuth, + }); + expect(taskAuthority).not.toBeNull(); + const result = projectPublicEvidenceRecord({ + observation: event, + verdict: "VERIFIED", + taskAuthority: taskAuthority ?? undefined, + }); + expect(result.status).toBe("exportable"); + if (result.status !== "exportable") throw new Error("expected exportable task record"); + expect(result.record.subject).toMatchObject({ + subjectKind: "task", + taskClassId: FABRIC_SCENARIO_ID, + taskClassVersion: FABRIC_SCENARIO_VERSION, + verifierAuthorityId: FABRIC_VERIFIER_ID, + }); + const serialized = JSON.stringify(result.record); + expect(serialized).not.toContain(event.subjectId); + expect(serialized).not.toContain((event.subject as TaskSubjectV1).taskFixtureDigest); + expect(serialized).not.toContain((event.subject as TaskSubjectV1).verifierManifestDigest); + expect(routeEvent.subjectId).not.toBe(event.subjectId); + }); + test("uses domain-separated deterministic public ids", () => { - const payload = { providerId: "openai", modelId: "gpt-5.6-sol" }; + const payload = { providerId: "openai-apikey", modelId: "gpt-5.6-sol" }; const a = publicEvidenceId("subject", payload); const b = publicEvidenceId("subject", payload); const c = publicEvidenceId("record", payload); @@ -164,10 +304,33 @@ describe("CL-10 public projection", () => { expect(a).not.toBe(c); }); - test("runtime validation rejects unknown public fields", () => { + test("runtime validation rejects unknown top-level and nested public fields", () => { const result = projectPublicEvidenceRecord({ observation: protocolObservation(), verdict: "VERIFIED" }); if (result.status !== "exportable") throw new Error("expected exportable protocol record"); const withUnknown = { ...result.record, localSubjectId: "PRIVATE" }; expect(() => validatePublicEvidenceRecord(withUnknown)).toThrow(PublicEvidenceValidationError); + const nestedUnknown = { + ...result.record, + subject: { ...result.record.subject, behaviorFingerprint: hex("PRIVATE") }, + }; + expect(() => validatePublicEvidenceRecord(nestedUnknown)).toThrow(PublicEvidenceValidationError); + }); + + test("builds a closed deterministic unsigned bundle and reports exclusions", () => { + const privateRoute = routeObservation(); + const projected = projectPublicEvidence({ + createdDayUtc: "2026-08-12", + records: [ + { observation: protocolObservation(), verdict: "VERIFIED" }, + { observation: privateRoute, verdict: "PROBED" }, + ], + }); + expect(projected.bundle.schemaVersion).toBe(PUBLIC_EVIDENCE_BUNDLE_SCHEMA_VERSION); + expect(projected.bundle.records).toHaveLength(1); + expect(projected.bundle.artifacts).toEqual([]); + expect(projected.excluded).toEqual([{ index: 1, reason: "private_route_identity" }]); + expect(projected.bundle.bundleId).toMatch(/^[0-9a-f]{64}$/); + expect(validatePublicEvidenceBundleUnsigned(projected.bundle)).toEqual(projected.bundle); + expect(JSON.stringify(projected.bundle)).not.toContain(privateRoute.subjectId); }); }); From 7a9b1c54425cc9016db88ab602472de1ae2bdf81 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:22:23 +0200 Subject: [PATCH 05/33] feat(lab): complete CL-10 public projection boundary --- src/lab/public/authority.ts | 129 ++++++++++++++++++++++++++ src/lab/public/bundle.ts | 160 +++++++++++++++++++++++++++++++++ src/lab/public/ids.ts | 20 +++++ src/lab/public/index.ts | 3 + src/lab/public/project.ts | 87 +++++------------- src/lab/public/registry.ts | 11 +-- src/lab/public/types.ts | 31 +++++-- src/lab/public/validate.ts | 174 +++++++++++------------------------- 8 files changed, 421 insertions(+), 194 deletions(-) create mode 100644 src/lab/public/authority.ts create mode 100644 src/lab/public/bundle.ts create mode 100644 src/lab/public/ids.ts diff --git a/src/lab/public/authority.ts b/src/lab/public/authority.ts new file mode 100644 index 0000000000..273e24b6f5 --- /dev/null +++ b/src/lab/public/authority.ts @@ -0,0 +1,129 @@ +import { + FABRIC_COMPATIBILITY_VERSION, + FABRIC_TASK_CLASS_ID, + FABRIC_TASK_CLASS_VERSION, + FABRIC_VERIFIER_ID, +} from "../fabric/constants"; +import { sandboxProfileDigest, taskFixtureDigest, verifierManifestDigest } from "../fabric/subject"; +import { subjectIdForSubject } from "../digest"; +import type { RouteSubjectV1, TaskSubjectV1 } from "../events/types"; +import { PUBLIC_ROUTE_REGISTRY_V1, findPublicRouteRegistryEntry } from "./registry"; +import type { + PublicAdapterFamilyV1, + PublicRouteAuthorityV1, + PublicRouteSubjectV1, + PublicTaskAuthorityV1, + PublicTaskSubjectV1, +} from "./types"; + +const routeAuthorities = new WeakSet(); +const taskAuthorities = new WeakSet(); + +export function toPublicAdapterFamily(value: string): PublicAdapterFamilyV1 | null { + switch (value) { + case "openai-responses": + case "responses": + return "openai-responses"; + case "openai-chat": + case "chat": + return "openai-chat"; + case "anthropic": + case "anthropic-messages": + return "anthropic-messages"; + default: + return null; + } +} + +function canonicalHttpsBaseUrl(value: string): string | null { + try { + const url = new URL(value); + if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash) return null; + url.pathname = url.pathname.replace(/\/+$/, "") || "/"; + return url.toString().replace(/\/$/, url.pathname === "/" ? "/" : ""); + } catch { + return null; + } +} + +export function authorizePublicRouteSubject(input: { + subject: RouteSubjectV1; + localSubjectId: string; + effectiveBaseUrl: string; + /** Closed names derived by the trusted export caller from effective config. Must be empty. */ + privateBehaviorDimensions: readonly string[]; +}): PublicRouteAuthorityV1 | null { + const { subject } = input; + if (subjectIdForSubject(subject) !== input.localSubjectId) return null; + if (subject.dependencies.length !== 0 || subject.clientModelId !== subject.upstreamModelId) return null; + if (input.privateBehaviorDimensions.length !== 0) return null; + + const adapterFamily = toPublicAdapterFamily(subject.effectiveAdapter); + const inboundProtocol = toPublicAdapterFamily(subject.inboundProtocol); + const upstreamProtocol = toPublicAdapterFamily(subject.upstreamProtocol); + if (!adapterFamily || !inboundProtocol || !upstreamProtocol) return null; + + const entry = findPublicRouteRegistryEntry(subject.providerId, subject.upstreamModelId, adapterFamily); + if (!entry) return null; + const effectiveBaseUrl = canonicalHttpsBaseUrl(input.effectiveBaseUrl); + const canonicalBaseUrl = canonicalHttpsBaseUrl(entry.canonicalBaseUrl); + if (!effectiveBaseUrl || !canonicalBaseUrl || effectiveBaseUrl !== canonicalBaseUrl) return null; + + const descriptor: PublicRouteSubjectV1 = Object.freeze({ + subjectKind: "route", + providerId: subject.providerId, + modelId: subject.upstreamModelId, + adapterFamily, + inboundProtocol, + upstreamProtocol, + surface: subject.surface, + compatibilityVersion: subject.opencodexCompatibilityVersion, + registryVersion: PUBLIC_ROUTE_REGISTRY_V1.registryVersion, + registryDigest: PUBLIC_ROUTE_REGISTRY_V1.manifestDigest, + }); + const authority: PublicRouteAuthorityV1 = Object.freeze({ + localSubjectId: input.localSubjectId, + descriptor, + }); + routeAuthorities.add(authority); + return authority; +} + +export function isTrustedPublicRouteAuthority(value: unknown): value is PublicRouteAuthorityV1 { + return !!value && typeof value === "object" && routeAuthorities.has(value); +} + +export function authorizePublicTaskSubject(input: { + subject: TaskSubjectV1; + localSubjectId: string; + routeAuthority: PublicRouteAuthorityV1; +}): PublicTaskAuthorityV1 | null { + const { subject } = input; + if (!isTrustedPublicRouteAuthority(input.routeAuthority)) return null; + if (subjectIdForSubject(subject) !== input.localSubjectId) return null; + if (subjectIdForSubject(subject.routeSubject) !== input.routeAuthority.localSubjectId) return null; + if (subject.taskClassId !== FABRIC_TASK_CLASS_ID + || subject.taskClassVersion !== FABRIC_TASK_CLASS_VERSION + || subject.taskFixtureDigest !== taskFixtureDigest() + || subject.verifierManifestDigest !== verifierManifestDigest() + || subject.fabricCompatibilityVersion !== FABRIC_COMPATIBILITY_VERSION + || subject.sandboxProfileDigest !== sandboxProfileDigest()) return null; + + const descriptor: PublicTaskSubjectV1 = Object.freeze({ + subjectKind: "task", + route: input.routeAuthority.descriptor, + taskClassId: FABRIC_TASK_CLASS_ID, + taskClassVersion: FABRIC_TASK_CLASS_VERSION, + verifierAuthorityId: FABRIC_VERIFIER_ID, + }); + const authority: PublicTaskAuthorityV1 = Object.freeze({ + localSubjectId: input.localSubjectId, + descriptor, + }); + taskAuthorities.add(authority); + return authority; +} + +export function isTrustedPublicTaskAuthority(value: unknown): value is PublicTaskAuthorityV1 { + return !!value && typeof value === "object" && taskAuthorities.has(value); +} diff --git a/src/lab/public/bundle.ts b/src/lab/public/bundle.ts new file mode 100644 index 0000000000..262cbc5223 --- /dev/null +++ b/src/lab/public/bundle.ts @@ -0,0 +1,160 @@ +import { jcsStringify } from "../digest"; +import { publicArtifactId, publicEvidenceId } from "./ids"; +import { projectPublicEvidenceRecord, type ProjectPublicEvidenceRecordInput } from "./project"; +import { + PUBLIC_EVIDENCE_BUNDLE_SCHEMA_VERSION, + PUBLIC_EXPORT_POLICY_VERSION, + type PublicArtifactV1, + type PublicEvidenceBundleUnsignedV1, + type PublicProjectionNotExportableReason, +} from "./types"; +import { PublicEvidenceValidationError, validatePublicEvidenceRecord } from "./validate"; + +const MAX_RECORDS = 256; +const MAX_ARTIFACTS = 16; +const MAX_ARTIFACT_BYTES = 256 * 1024; +const MAX_AGGREGATE_ARTIFACT_BYTES = 1024 * 1024; +const MAX_BUNDLE_BYTES = 2 * 1024 * 1024; +const MAX_STRING_BYTES = 4 * 1024; + +function isPlainObject(value: unknown): value is Record { + return !!value && typeof value === "object" && !Array.isArray(value); +} + +function assertClosedKeys(raw: Record, allowed: readonly string[], field: string): void { + const set = new Set(allowed); + for (const key of Object.keys(raw)) { + if (!set.has(key)) throw new PublicEvidenceValidationError("unknown_field", `${field}.${key}`); + } +} + +function assertDay(value: unknown, field: string): string { + if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value)) { + throw new PublicEvidenceValidationError("invalid_day", `${field} must be YYYY-MM-DD`); + } + const date = new Date(`${value}T00:00:00.000Z`); + if (!Number.isFinite(date.getTime()) || date.toISOString().slice(0, 10) !== value) { + throw new PublicEvidenceValidationError("invalid_day", `${field} is invalid`); + } + return value; +} + +function assertHex(value: unknown, field: string): string { + if (typeof value !== "string" || !/^[0-9a-f]{64}$/.test(value)) { + throw new PublicEvidenceValidationError("invalid_id", `${field} must be lowercase sha256 hex`); + } + return value; +} + +function decodeCanonicalBase64(value: unknown, field: string): Uint8Array { + if (typeof value !== "string" || new TextEncoder().encode(value).byteLength > Math.ceil(MAX_ARTIFACT_BYTES * 4 / 3) + 8) { + throw new PublicEvidenceValidationError("artifact_encoding", `${field} is invalid or oversized`); + } + if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) { + throw new PublicEvidenceValidationError("artifact_encoding", `${field} is not canonical base64`); + } + const bytes = Uint8Array.from(Buffer.from(value, "base64")); + if (Buffer.from(bytes).toString("base64") !== value) { + throw new PublicEvidenceValidationError("artifact_encoding", `${field} is not canonical base64`); + } + return bytes; +} + +function validateArtifact(raw: unknown, index: number): PublicArtifactV1 { + if (!isPlainObject(raw)) throw new PublicEvidenceValidationError("invalid_artifact", `artifacts[${index}]`); + assertClosedKeys(raw, ["artifactId", "mediaType", "byteCount", "contentBase64"], `artifacts[${index}]`); + const artifactId = assertHex(raw.artifactId, `artifacts[${index}].artifactId`); + if (raw.mediaType !== "application/json" && raw.mediaType !== "text/plain; charset=utf-8") { + throw new PublicEvidenceValidationError("artifact_media_type", `artifacts[${index}].mediaType`); + } + if (!Number.isSafeInteger(raw.byteCount) || (raw.byteCount as number) < 0 || (raw.byteCount as number) > MAX_ARTIFACT_BYTES) { + throw new PublicEvidenceValidationError("artifact_size", `artifacts[${index}].byteCount`); + } + const contentBase64 = typeof raw.contentBase64 === "string" ? raw.contentBase64 : ""; + const bytes = decodeCanonicalBase64(contentBase64, `artifacts[${index}].contentBase64`); + if (bytes.byteLength !== raw.byteCount) throw new PublicEvidenceValidationError("artifact_size", `artifacts[${index}] byteCount mismatch`); + if (publicArtifactId(bytes) !== artifactId) throw new PublicEvidenceValidationError("artifact_digest", `artifacts[${index}] id mismatch`); + return { artifactId, mediaType: raw.mediaType, byteCount: raw.byteCount as number, contentBase64 }; +} + +function bundlePayload(bundle: Omit): Omit { + return bundle; +} + +export function validatePublicEvidenceBundleUnsigned(raw: unknown): PublicEvidenceBundleUnsignedV1 { + if (!isPlainObject(raw)) throw new PublicEvidenceValidationError("invalid_bundle", "bundle must be object"); + assertClosedKeys(raw, ["schemaVersion", "exportPolicyVersion", "bundleId", "createdDayUtc", "records", "artifacts"], "bundle"); + if (raw.schemaVersion !== PUBLIC_EVIDENCE_BUNDLE_SCHEMA_VERSION) throw new PublicEvidenceValidationError("unsupported_version", "bundle schemaVersion"); + if (raw.exportPolicyVersion !== PUBLIC_EXPORT_POLICY_VERSION) throw new PublicEvidenceValidationError("unsupported_version", "bundle exportPolicyVersion"); + const bundleId = assertHex(raw.bundleId, "bundleId"); + const createdDayUtc = assertDay(raw.createdDayUtc, "createdDayUtc"); + if (!Array.isArray(raw.records) || raw.records.length > MAX_RECORDS) throw new PublicEvidenceValidationError("record_limit", "bundle records invalid or oversized"); + if (!Array.isArray(raw.artifacts) || raw.artifacts.length > MAX_ARTIFACTS) throw new PublicEvidenceValidationError("artifact_limit", "bundle artifacts invalid or oversized"); + + const records = raw.records.map(validatePublicEvidenceRecord); + const artifacts = raw.artifacts.map(validateArtifact); + if (new Set(records.map((record) => record.recordId)).size !== records.length) throw new PublicEvidenceValidationError("duplicate_record", "bundle record ids must be unique"); + if (new Set(artifacts.map((artifact) => artifact.artifactId)).size !== artifacts.length) throw new PublicEvidenceValidationError("duplicate_artifact", "bundle artifact ids must be unique"); + const artifactIds = new Set(artifacts.map((artifact) => artifact.artifactId)); + for (const record of records) { + for (const ref of record.artifactRefs ?? []) { + if (!artifactIds.has(ref)) throw new PublicEvidenceValidationError("unknown_artifact_ref", `${record.recordId} references absent artifact`); + } + } + const aggregateArtifactBytes = artifacts.reduce((sum, artifact) => sum + artifact.byteCount, 0); + if (aggregateArtifactBytes > MAX_AGGREGATE_ARTIFACT_BYTES) throw new PublicEvidenceValidationError("artifact_limit", "aggregate public artifact bytes exceeded"); + + const normalized: PublicEvidenceBundleUnsignedV1 = { + schemaVersion: PUBLIC_EVIDENCE_BUNDLE_SCHEMA_VERSION, + exportPolicyVersion: PUBLIC_EXPORT_POLICY_VERSION, + bundleId, + createdDayUtc, + records, + artifacts, + }; + const expected = publicEvidenceId("bundle", bundlePayload({ + schemaVersion: normalized.schemaVersion, + exportPolicyVersion: normalized.exportPolicyVersion, + createdDayUtc, + records, + artifacts, + })); + if (expected !== bundleId) throw new PublicEvidenceValidationError("bundle_digest", "bundleId does not match canonical public bytes"); + if (new TextEncoder().encode(jcsStringify(normalized)).byteLength > MAX_BUNDLE_BYTES) throw new PublicEvidenceValidationError("bundle_limit", "serialized public bundle exceeds limit"); + for (const value of [createdDayUtc, ...records.flatMap((record) => [record.suiteId, record.suiteVersion, record.scenarioId, record.scenarioVersion])]) { + if (new TextEncoder().encode(value).byteLength > MAX_STRING_BYTES) throw new PublicEvidenceValidationError("field_too_large", "public bundle string exceeds limit"); + } + return normalized; +} + +export function projectPublicEvidence(input: { + createdDayUtc: string; + records: ProjectPublicEvidenceRecordInput[]; + artifacts?: PublicArtifactV1[]; +}): { + bundle: PublicEvidenceBundleUnsignedV1; + excluded: Array<{ index: number; reason: PublicProjectionNotExportableReason }>; +} { + if (input.records.length > MAX_RECORDS) throw new PublicEvidenceValidationError("record_limit", "export scope exceeds public record limit"); + const records = [] as PublicEvidenceBundleUnsignedV1["records"]; + const excluded: Array<{ index: number; reason: PublicProjectionNotExportableReason }> = []; + input.records.forEach((recordInput, index) => { + const result = projectPublicEvidenceRecord(recordInput); + if (result.status === "exportable") records.push(result.record); + else excluded.push({ index, reason: result.reason }); + }); + records.sort((a, b) => a.recordId.localeCompare(b.recordId)); + const artifacts = [...(input.artifacts ?? [])].sort((a, b) => a.artifactId.localeCompare(b.artifactId)); + const withoutId: Omit = { + schemaVersion: PUBLIC_EVIDENCE_BUNDLE_SCHEMA_VERSION, + exportPolicyVersion: PUBLIC_EXPORT_POLICY_VERSION, + createdDayUtc: assertDay(input.createdDayUtc, "createdDayUtc"), + records, + artifacts, + }; + const bundle: PublicEvidenceBundleUnsignedV1 = { + bundleId: publicEvidenceId("bundle", bundlePayload(withoutId)), + ...withoutId, + }; + return { bundle: validatePublicEvidenceBundleUnsigned(bundle), excluded }; +} diff --git a/src/lab/public/ids.ts b/src/lab/public/ids.ts new file mode 100644 index 0000000000..24d0a6cfde --- /dev/null +++ b/src/lab/public/ids.ts @@ -0,0 +1,20 @@ +import { domainHash, jcsStringify } from "../digest"; + +const PUBLIC_ID_DOMAINS = { + subject: "ocx-lab:public-subject:v1", + record: "ocx-lab:public-record:v1", + bundle: "ocx-lab:public-bundle:v1", + artifact: "ocx-lab:public-artifact:v1", + publisher: "ocx-lab:public-publisher:v1", + revocation: "ocx-lab:public-revocation:v1", +} as const; + +export type PublicEvidenceIdKind = keyof typeof PUBLIC_ID_DOMAINS; + +export function publicEvidenceId(kind: Exclude, payload: unknown): string { + return domainHash(PUBLIC_ID_DOMAINS[kind], jcsStringify(payload)); +} + +export function publicArtifactId(bytes: Uint8Array): string { + return domainHash(PUBLIC_ID_DOMAINS.artifact, bytes); +} diff --git a/src/lab/public/index.ts b/src/lab/public/index.ts index 064bc133c5..8d7ec8d985 100644 --- a/src/lab/public/index.ts +++ b/src/lab/public/index.ts @@ -1,4 +1,7 @@ export * from "./types"; export * from "./registry"; +export * from "./ids"; +export * from "./authority"; export * from "./validate"; export * from "./project"; +export * from "./bundle"; diff --git a/src/lab/public/project.ts b/src/lab/public/project.ts index cf0ad3e988..30c696d0eb 100644 --- a/src/lab/public/project.ts +++ b/src/lab/public/project.ts @@ -1,47 +1,24 @@ -import { domainHash, jcsStringify } from "../digest"; -import type { ObservationEvent, ProtocolSubjectV1, RouteSubjectV1 } from "../events/types"; -import { PUBLIC_ROUTE_REGISTRY_V1, findPublicRouteRegistryEntry } from "./registry"; +import type { ObservationEvent, ProtocolSubjectV1 } from "../events/types"; +import { + isTrustedPublicRouteAuthority, + isTrustedPublicTaskAuthority, + toPublicAdapterFamily, +} from "./authority"; +import { publicEvidenceId } from "./ids"; import type { - PublicAdapterFamilyV1, PublicEvidenceRecordV1, PublicEvidenceSubjectV1, PublicProjectionResult, PublicProtocolSubjectV1, PublicRouteAuthorityV1, + PublicTaskAuthorityV1, } from "./types"; import { isPublicIncidentRef, validatePublicEvidenceRecord } from "./validate"; -const PUBLIC_ID_DOMAINS = { - subject: "ocx-lab:public-subject:v1", - record: "ocx-lab:public-record:v1", - bundle: "ocx-lab:public-bundle:v1", - artifact: "ocx-lab:public-artifact:v1", - publisher: "ocx-lab:public-publisher:v1", - revocation: "ocx-lab:public-revocation:v1", -} as const; - -export type PublicEvidenceIdKind = keyof typeof PUBLIC_ID_DOMAINS; - -export function publicEvidenceId(kind: PublicEvidenceIdKind, payload: unknown): string { - return domainHash(PUBLIC_ID_DOMAINS[kind], jcsStringify(payload)); -} - -function publicAdapterFamily(value: string): PublicAdapterFamilyV1 | null { - switch (value) { - case "openai-responses": return "openai-responses"; - case "openai-chat": return "openai-chat"; - case "anthropic": - case "anthropic-messages": return "anthropic-messages"; - case "responses": return "openai-responses"; - case "chat": return "openai-chat"; - default: return null; - } -} - function publicProtocolSubject(subject: ProtocolSubjectV1): PublicProtocolSubjectV1 | null { - const adapterFamily = publicAdapterFamily(subject.effectiveAdapter); - const inboundProtocol = publicAdapterFamily(subject.inboundProtocol); - const upstreamProtocol = publicAdapterFamily(subject.upstreamProtocol); + const adapterFamily = toPublicAdapterFamily(subject.effectiveAdapter); + const inboundProtocol = toPublicAdapterFamily(subject.inboundProtocol); + const upstreamProtocol = toPublicAdapterFamily(subject.upstreamProtocol); if (!adapterFamily || !inboundProtocol || !upstreamProtocol) return null; return { subjectKind: "protocol", @@ -53,34 +30,11 @@ function publicProtocolSubject(subject: ProtocolSubjectV1): PublicProtocolSubjec }; } -function routeAuthorityMatches( - subject: RouteSubjectV1, - localSubjectId: string, - authority: PublicRouteAuthorityV1 | undefined, -): boolean { - if (!authority || authority.localSubjectId !== localSubjectId) return false; - if (subject.dependencies.length !== 0) return false; - if (subject.clientModelId !== subject.upstreamModelId) return false; - const descriptor = authority.descriptor; - if (descriptor.subjectKind !== "route") return false; - if (descriptor.providerId !== subject.providerId || descriptor.modelId !== subject.upstreamModelId) return false; - if (descriptor.registryDigest !== PUBLIC_ROUTE_REGISTRY_V1.manifestDigest - || descriptor.registryVersion !== PUBLIC_ROUTE_REGISTRY_V1.registryVersion) return false; - const adapterFamily = publicAdapterFamily(subject.effectiveAdapter); - const inboundProtocol = publicAdapterFamily(subject.inboundProtocol); - const upstreamProtocol = publicAdapterFamily(subject.upstreamProtocol); - if (!adapterFamily || !inboundProtocol || !upstreamProtocol) return false; - if (descriptor.adapterFamily !== adapterFamily - || descriptor.inboundProtocol !== inboundProtocol - || descriptor.upstreamProtocol !== upstreamProtocol - || descriptor.surface !== subject.surface - || descriptor.compatibilityVersion !== subject.opencodexCompatibilityVersion) return false; - return findPublicRouteRegistryEntry(descriptor.providerId, descriptor.modelId, descriptor.adapterFamily) !== null; -} - function observedDayUtc(observation: ObservationEvent): string { const value = Number.isFinite(observation.completedAt) ? observation.completedAt : observation.recordedAt; - return new Date(value).toISOString().slice(0, 10); + const date = new Date(value); + if (!Number.isFinite(date.getTime())) throw new Error("invalid observation timestamp"); + return date.toISOString().slice(0, 10); } export interface ProjectPublicEvidenceRecordInput { @@ -88,6 +42,7 @@ export interface ProjectPublicEvidenceRecordInput { verdict: PublicEvidenceRecordV1["verdict"]; incidentRefs?: string[]; routeAuthority?: PublicRouteAuthorityV1; + taskAuthority?: PublicTaskAuthorityV1; } export function projectPublicEvidenceRecord(input: ProjectPublicEvidenceRecordInput): PublicProjectionResult { @@ -103,12 +58,18 @@ export function projectPublicEvidenceRecord(input: ProjectPublicEvidenceRecordIn subject = projected; } else if (observation.evidenceLayer === "live_route_compatibility") { if (observation.subject.subjectKind !== "route" - || !routeAuthorityMatches(observation.subject, observation.subjectId, input.routeAuthority)) { + || !isTrustedPublicRouteAuthority(input.routeAuthority) + || input.routeAuthority.localSubjectId !== observation.subjectId) { return { status: "not_exportable", reason: "private_route_identity" }; } - subject = input.routeAuthority!.descriptor; + subject = input.routeAuthority.descriptor; } else { - return { status: "not_exportable", reason: "task_authority_unavailable" }; + if (observation.subject.subjectKind !== "task" + || !isTrustedPublicTaskAuthority(input.taskAuthority) + || input.taskAuthority.localSubjectId !== observation.subjectId) { + return { status: "not_exportable", reason: "task_authority_unavailable" }; + } + subject = input.taskAuthority.descriptor; } let incidentRefs: Array<{ corpusId: string }> | undefined; diff --git a/src/lab/public/registry.ts b/src/lab/public/registry.ts index cfdf2737cb..ced6d82dc2 100644 --- a/src/lab/public/registry.ts +++ b/src/lab/public/registry.ts @@ -7,17 +7,18 @@ import type { import { PUBLIC_ROUTE_REGISTRY_SCHEMA_VERSION } from "./types"; const REGISTRY_DOMAIN = "ocx-lab:public-route-registry:v1"; +const OPENAI_API_BASE_URL = "https://api.openai.com/v1"; const entries: PublicRouteRegistryEntryV1[] = [ - { providerId: "openai-apikey", modelId: "gpt-5.5", adapterFamilies: ["openai-responses"] }, - { providerId: "openai-apikey", modelId: "gpt-5.6-luna", adapterFamilies: ["openai-responses"] }, - { providerId: "openai-apikey", modelId: "gpt-5.6-sol", adapterFamilies: ["openai-responses"] }, - { providerId: "openai-apikey", modelId: "gpt-5.6-terra", adapterFamilies: ["openai-responses"] }, + { providerId: "openai-apikey", modelId: "gpt-5.5", adapterFamilies: ["openai-responses"], canonicalBaseUrl: OPENAI_API_BASE_URL }, + { providerId: "openai-apikey", modelId: "gpt-5.6-luna", adapterFamilies: ["openai-responses"], canonicalBaseUrl: OPENAI_API_BASE_URL }, + { providerId: "openai-apikey", modelId: "gpt-5.6-sol", adapterFamilies: ["openai-responses"], canonicalBaseUrl: OPENAI_API_BASE_URL }, + { providerId: "openai-apikey", modelId: "gpt-5.6-terra", adapterFamilies: ["openai-responses"], canonicalBaseUrl: OPENAI_API_BASE_URL }, ]; const manifestPayload = { schemaVersion: PUBLIC_ROUTE_REGISTRY_SCHEMA_VERSION, - registryVersion: "2026-08-12.1", + registryVersion: "2026-08-12.2", sourceCommit: "4fed8d3fe431ad23be83f3aff2af18ef8b8ecd71", entries, }; diff --git a/src/lab/public/types.ts b/src/lab/public/types.ts index 7985ce7f07..514a403b47 100644 --- a/src/lab/public/types.ts +++ b/src/lab/public/types.ts @@ -1,6 +1,7 @@ import type { CompatibilityVerdict, EvidenceLayer } from "../constants"; export const PUBLIC_ROUTE_REGISTRY_SCHEMA_VERSION = "public_route_registry_v1" as const; +export const PUBLIC_EVIDENCE_BUNDLE_SCHEMA_VERSION = "public_evidence_bundle_v1" as const; export const PUBLIC_EXPORT_POLICY_VERSION = "public_export_policy_v1" as const; export const PUBLIC_ADAPTER_FAMILIES = [ @@ -14,6 +15,8 @@ export interface PublicRouteRegistryEntryV1 { providerId: string; modelId: string; adapterFamilies: PublicAdapterFamilyV1[]; + /** Reviewed canonical public endpoint used only as local export authority. */ + canonicalBaseUrl: string; } export interface PublicRouteRegistryManifestV1 { @@ -85,6 +88,22 @@ export interface PublicEvidenceRecordV1 { artifactRefs?: string[]; } +export interface PublicArtifactV1 { + artifactId: string; + mediaType: "application/json" | "text/plain; charset=utf-8"; + byteCount: number; + contentBase64: string; +} + +export interface PublicEvidenceBundleUnsignedV1 { + schemaVersion: typeof PUBLIC_EVIDENCE_BUNDLE_SCHEMA_VERSION; + exportPolicyVersion: typeof PUBLIC_EXPORT_POLICY_VERSION; + bundleId: string; + createdDayUtc: string; + records: PublicEvidenceRecordV1[]; + artifacts: PublicArtifactV1[]; +} + export type PublicProjectionNotExportableReason = | "private_route_identity" | "unsupported_public_adapter" @@ -95,12 +114,14 @@ export type PublicProjectionResult = | { status: "exportable"; record: PublicEvidenceRecordV1 } | { status: "not_exportable"; reason: PublicProjectionNotExportableReason }; -/** - * Trusted local proof for an exact route. Callers must derive this from the same - * effective route subject that produced the local observation, never from an - * imported bundle or user-supplied public descriptor. - */ +/** Opaque runtime capability. Plain-object copies are intentionally untrusted. */ export interface PublicRouteAuthorityV1 { localSubjectId: string; descriptor: PublicRouteSubjectV1; } + +/** Opaque runtime capability. Plain-object copies are intentionally untrusted. */ +export interface PublicTaskAuthorityV1 { + localSubjectId: string; + descriptor: PublicTaskSubjectV1; +} diff --git a/src/lab/public/validate.ts b/src/lab/public/validate.ts index 7cf8380093..ad0a4d8df1 100644 --- a/src/lab/public/validate.ts +++ b/src/lab/public/validate.ts @@ -47,99 +47,84 @@ function assertClosedKeys(raw: Record, allowed: readonly string } function assertString(value: unknown, field: string, max = MAX_STRING_BYTES): string { - if (typeof value !== "string") { - throw new PublicEvidenceValidationError("invalid_type", `${field} must be string`); - } - if (value.includes("\0")) { - throw new PublicEvidenceValidationError("nul_forbidden", `${field} contains NUL`); - } - if (new TextEncoder().encode(value).byteLength > max) { - throw new PublicEvidenceValidationError("field_too_large", `${field} exceeds ${max} bytes`); - } + if (typeof value !== "string") throw new PublicEvidenceValidationError("invalid_type", `${field} must be string`); + if (value.includes("\0")) throw new PublicEvidenceValidationError("nul_forbidden", `${field} contains NUL`); + if (new TextEncoder().encode(value).byteLength > max) throw new PublicEvidenceValidationError("field_too_large", `${field} exceeds ${max} bytes`); return value; } function assertPublicToken(value: unknown, field: string, max = 256): string { const text = assertString(value, field, max); - if (!/^[A-Za-z0-9][A-Za-z0-9._:/+-]*$/.test(text)) { - throw new PublicEvidenceValidationError("unsafe_string", `${field} contains unsupported characters`); - } + if (!/^[A-Za-z0-9][A-Za-z0-9._:/+-]*$/.test(text)) throw new PublicEvidenceValidationError("unsafe_string", `${field} contains unsupported characters`); return text; } function assertHex(value: unknown, field: string): string { const text = assertString(value, field, 64); - if (!isSha256Hex(text)) { - throw new PublicEvidenceValidationError("invalid_id", `${field} must be lowercase sha256 hex`); - } + if (!isSha256Hex(text)) throw new PublicEvidenceValidationError("invalid_id", `${field} must be lowercase sha256 hex`); return text; } function assertGitCommit(value: unknown, field: string): string { const text = assertString(value, field, 40); - if (!/^[0-9a-f]{40}$/.test(text)) { - throw new PublicEvidenceValidationError("invalid_git_commit", `${field} must be a lowercase 40-character git commit id`); - } + if (!/^[0-9a-f]{40}$/.test(text)) throw new PublicEvidenceValidationError("invalid_git_commit", `${field} must be a lowercase 40-character git commit id`); return text; } function assertAdapter(value: unknown, field: string): PublicAdapterFamilyV1 { const text = assertString(value, field, 64); - if (!(PUBLIC_ADAPTER_FAMILIES as readonly string[]).includes(text)) { - throw new PublicEvidenceValidationError("closed_set", `${field} is not a public adapter family`); - } + if (!(PUBLIC_ADAPTER_FAMILIES as readonly string[]).includes(text)) throw new PublicEvidenceValidationError("closed_set", `${field} is not a public adapter family`); return text as PublicAdapterFamilyV1; } function assertBoolean(value: unknown, field: string): boolean { - if (value !== true && value !== false) { - throw new PublicEvidenceValidationError("invalid_type", `${field} must be boolean`); - } + if (value !== true && value !== false) throw new PublicEvidenceValidationError("invalid_type", `${field} must be boolean`); return value; } function assertDay(value: unknown, field: string): string { const text = assertString(value, field, 10); - if (!/^\d{4}-\d{2}-\d{2}$/.test(text)) { - throw new PublicEvidenceValidationError("invalid_day", `${field} must be YYYY-MM-DD`); - } + if (!/^\d{4}-\d{2}-\d{2}$/.test(text)) throw new PublicEvidenceValidationError("invalid_day", `${field} must be YYYY-MM-DD`); const parsed = new Date(`${text}T00:00:00.000Z`); - if (!Number.isFinite(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== text) { - throw new PublicEvidenceValidationError("invalid_day", `${field} is not a UTC calendar day`); - } + if (!Number.isFinite(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== text) throw new PublicEvidenceValidationError("invalid_day", `${field} is not a UTC calendar day`); return text; } +function assertCanonicalHttpsBaseUrl(value: unknown, field: string): string { + const text = assertString(value, field, 512); + try { + const url = new URL(text); + if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash) throw new Error("not canonical public https endpoint"); + const normalizedPath = url.pathname.replace(/\/+$/, "") || "/"; + url.pathname = normalizedPath; + const normalized = url.toString().replace(/\/$/, normalizedPath === "/" ? "/" : ""); + if (normalized !== text) throw new Error("not canonical"); + return text; + } catch { + throw new PublicEvidenceValidationError("invalid_registry_endpoint", `${field} must be a canonical https base URL`); + } +} + export function isPublicIncidentRef(value: unknown): value is string { return typeof value === "string" && PUBLIC_INCIDENT_REFS.has(value); } function validateIncidentRefs(raw: unknown): PublicIncidentRefV1[] { - if (!Array.isArray(raw) || raw.length > MAX_INCIDENT_REFS) { - throw new PublicEvidenceValidationError("invalid_incident_refs", "incidentRefs is invalid or oversized"); - } + if (!Array.isArray(raw) || raw.length > MAX_INCIDENT_REFS) throw new PublicEvidenceValidationError("invalid_incident_refs", "incidentRefs is invalid or oversized"); const seen = new Set(); return raw.map((value, index) => { - if (!isPlainObject(value)) { - throw new PublicEvidenceValidationError("invalid_incident_ref", `incidentRefs[${index}]`); - } + if (!isPlainObject(value)) throw new PublicEvidenceValidationError("invalid_incident_ref", `incidentRefs[${index}]`); assertClosedKeys(value, ["corpusId"], `incidentRefs[${index}]`); const corpusId = assertString(value.corpusId, `incidentRefs[${index}].corpusId`, 6); - if (!isPublicIncidentRef(corpusId) || seen.has(corpusId)) { - throw new PublicEvidenceValidationError("invalid_incident_ref", `incidentRefs[${index}].corpusId`); - } + if (!isPublicIncidentRef(corpusId) || seen.has(corpusId)) throw new PublicEvidenceValidationError("invalid_incident_ref", `incidentRefs[${index}].corpusId`); seen.add(corpusId); return { corpusId }; }); } function validateProtocolSubject(raw: Record): PublicProtocolSubjectV1 { - assertClosedKeys(raw, [ - "subjectKind", "adapterFamily", "inboundProtocol", "upstreamProtocol", "surface", "compatibilityVersion", - ], "subject"); - if (raw.subjectKind !== "protocol") { - throw new PublicEvidenceValidationError("subject_kind", "protocol subjectKind mismatch"); - } + assertClosedKeys(raw, ["subjectKind", "adapterFamily", "inboundProtocol", "upstreamProtocol", "surface", "compatibilityVersion"], "subject"); + if (raw.subjectKind !== "protocol") throw new PublicEvidenceValidationError("subject_kind", "protocol subjectKind mismatch"); return { subjectKind: "protocol", adapterFamily: assertAdapter(raw.adapterFamily, "subject.adapterFamily"), @@ -151,13 +136,8 @@ function validateProtocolSubject(raw: Record): PublicProtocolSu } function validateRouteSubject(raw: Record): PublicRouteSubjectV1 { - assertClosedKeys(raw, [ - "subjectKind", "providerId", "modelId", "adapterFamily", "inboundProtocol", "upstreamProtocol", "surface", - "compatibilityVersion", "registryVersion", "registryDigest", - ], "subject"); - if (raw.subjectKind !== "route") { - throw new PublicEvidenceValidationError("subject_kind", "route subjectKind mismatch"); - } + assertClosedKeys(raw, ["subjectKind", "providerId", "modelId", "adapterFamily", "inboundProtocol", "upstreamProtocol", "surface", "compatibilityVersion", "registryVersion", "registryDigest"], "subject"); + if (raw.subjectKind !== "route") throw new PublicEvidenceValidationError("subject_kind", "route subjectKind mismatch"); return { subjectKind: "route", providerId: assertPublicToken(raw.providerId, "subject.providerId", 128), @@ -174,9 +154,7 @@ function validateRouteSubject(raw: Record): PublicRouteSubjectV function validateTaskSubject(raw: Record): PublicTaskSubjectV1 { assertClosedKeys(raw, ["subjectKind", "route", "taskClassId", "taskClassVersion", "verifierAuthorityId"], "subject"); - if (raw.subjectKind !== "task" || !isPlainObject(raw.route)) { - throw new PublicEvidenceValidationError("subject_kind", "task subject is invalid"); - } + if (raw.subjectKind !== "task" || !isPlainObject(raw.route)) throw new PublicEvidenceValidationError("subject_kind", "task subject is invalid"); return { subjectKind: "task", route: validateRouteSubject(raw.route), @@ -187,9 +165,7 @@ function validateTaskSubject(raw: Record): PublicTaskSubjectV1 } function validateSubject(raw: unknown, layer: string): PublicEvidenceSubjectV1 { - if (!isPlainObject(raw)) { - throw new PublicEvidenceValidationError("invalid_subject", "subject must be object"); - } + if (!isPlainObject(raw)) throw new PublicEvidenceValidationError("invalid_subject", "subject must be object"); if (layer === "protocol_conformance") { if (raw.subjectKind !== "protocol") throw new PublicEvidenceValidationError("layer_subject_mismatch", "protocol layer requires protocol subject"); return validateProtocolSubject(raw); @@ -207,43 +183,23 @@ function validateSubject(raw: unknown, layer: string): PublicEvidenceSubjectV1 { export function validatePublicEvidenceRecord(raw: unknown): PublicEvidenceRecordV1 { if (!isPlainObject(raw)) throw new PublicEvidenceValidationError("invalid_record", "record must be object"); - assertClosedKeys(raw, [ - "recordId", "subjectId", "evidenceLayer", "suiteId", "suiteVersion", "scenarioId", "scenarioVersion", "verdict", - "observedDayUtc", "subject", "assertions", "incidentRefs", "artifactRefs", - ], "record"); - + assertClosedKeys(raw, ["recordId", "subjectId", "evidenceLayer", "suiteId", "suiteVersion", "scenarioId", "scenarioVersion", "verdict", "observedDayUtc", "subject", "assertions", "incidentRefs", "artifactRefs"], "record"); const layer = assertString(raw.evidenceLayer, "evidenceLayer", 64); - if (!(EVIDENCE_LAYERS as readonly string[]).includes(layer)) { - throw new PublicEvidenceValidationError("closed_set", "evidenceLayer"); - } + if (!(EVIDENCE_LAYERS as readonly string[]).includes(layer)) throw new PublicEvidenceValidationError("closed_set", "evidenceLayer"); const verdict = assertString(raw.verdict, "verdict", 32); - if (!(VERDICTS as readonly string[]).includes(verdict)) { - throw new PublicEvidenceValidationError("closed_set", "verdict"); - } - if (!Array.isArray(raw.assertions) || raw.assertions.length > MAX_ASSERTIONS) { - throw new PublicEvidenceValidationError("invalid_assertions", "assertions is invalid or oversized"); - } + if (!(VERDICTS as readonly string[]).includes(verdict)) throw new PublicEvidenceValidationError("closed_set", "verdict"); + if (!Array.isArray(raw.assertions) || raw.assertions.length > MAX_ASSERTIONS) throw new PublicEvidenceValidationError("invalid_assertions", "assertions is invalid or oversized"); const assertions = raw.assertions.map((value, index) => { if (!isPlainObject(value)) throw new PublicEvidenceValidationError("invalid_assertion", `assertions[${index}]`); assertClosedKeys(value, ["id", "required", "passed"], `assertions[${index}]`); - return { - id: assertPublicToken(value.id, `assertions[${index}].id`, 256), - required: assertBoolean(value.required, `assertions[${index}].required`), - passed: assertBoolean(value.passed, `assertions[${index}].passed`), - }; + return { id: assertPublicToken(value.id, `assertions[${index}].id`, 256), required: assertBoolean(value.required, `assertions[${index}].required`), passed: assertBoolean(value.passed, `assertions[${index}].passed`) }; }); - let artifactRefs: string[] | undefined; if (raw.artifactRefs !== undefined) { - if (!Array.isArray(raw.artifactRefs) || raw.artifactRefs.length > MAX_ARTIFACT_REFS) { - throw new PublicEvidenceValidationError("invalid_artifact_refs", "artifactRefs is invalid or oversized"); - } + if (!Array.isArray(raw.artifactRefs) || raw.artifactRefs.length > MAX_ARTIFACT_REFS) throw new PublicEvidenceValidationError("invalid_artifact_refs", "artifactRefs is invalid or oversized"); artifactRefs = raw.artifactRefs.map((value, index) => assertHex(value, `artifactRefs[${index}]`)); - if (new Set(artifactRefs).size !== artifactRefs.length) { - throw new PublicEvidenceValidationError("duplicate_artifact_ref", "artifactRefs contains duplicates"); - } + if (new Set(artifactRefs).size !== artifactRefs.length) throw new PublicEvidenceValidationError("duplicate_artifact_ref", "artifactRefs contains duplicates"); } - const record: PublicEvidenceRecordV1 = { recordId: assertHex(raw.recordId, "recordId"), subjectId: assertHex(raw.subjectId, "subjectId"), @@ -259,58 +215,34 @@ export function validatePublicEvidenceRecord(raw: unknown): PublicEvidenceRecord ...(raw.incidentRefs !== undefined ? { incidentRefs: validateIncidentRefs(raw.incidentRefs) } : {}), ...(artifactRefs !== undefined ? { artifactRefs } : {}), }; - - if (new TextEncoder().encode(jcsStringify(record)).byteLength > MAX_RECORD_BYTES) { - throw new PublicEvidenceValidationError("record_too_large", `record exceeds ${MAX_RECORD_BYTES} bytes`); - } + if (new TextEncoder().encode(jcsStringify(record)).byteLength > MAX_RECORD_BYTES) throw new PublicEvidenceValidationError("record_too_large", `record exceeds ${MAX_RECORD_BYTES} bytes`); return record; } export function validatePublicRouteRegistryManifest(raw: unknown): PublicRouteRegistryManifestV1 { if (!isPlainObject(raw)) throw new PublicEvidenceValidationError("invalid_registry", "registry must be object"); assertClosedKeys(raw, ["schemaVersion", "registryVersion", "sourceCommit", "entries", "manifestDigest"], "registry"); - if (raw.schemaVersion !== PUBLIC_ROUTE_REGISTRY_SCHEMA_VERSION) { - throw new PublicEvidenceValidationError("unsupported_version", "public route registry schema version"); - } + if (raw.schemaVersion !== PUBLIC_ROUTE_REGISTRY_SCHEMA_VERSION) throw new PublicEvidenceValidationError("unsupported_version", "public route registry schema version"); const registryVersion = assertPublicToken(raw.registryVersion, "registryVersion", 128); const sourceCommit = assertGitCommit(raw.sourceCommit, "sourceCommit"); - if (!Array.isArray(raw.entries) || raw.entries.length === 0 || raw.entries.length > MAX_REGISTRY_ENTRIES) { - throw new PublicEvidenceValidationError("invalid_registry", "registry entries are invalid or oversized"); - } + if (!Array.isArray(raw.entries) || raw.entries.length === 0 || raw.entries.length > MAX_REGISTRY_ENTRIES) throw new PublicEvidenceValidationError("invalid_registry", "registry entries are invalid or oversized"); const seen = new Set(); const entries: PublicRouteRegistryEntryV1[] = raw.entries.map((value, index) => { if (!isPlainObject(value)) throw new PublicEvidenceValidationError("invalid_registry_entry", `entries[${index}]`); - assertClosedKeys(value, ["providerId", "modelId", "adapterFamilies"], `entries[${index}]`); + assertClosedKeys(value, ["providerId", "modelId", "adapterFamilies", "canonicalBaseUrl"], `entries[${index}]`); const providerId = assertPublicToken(value.providerId, `entries[${index}].providerId`, 128); const modelId = assertPublicToken(value.modelId, `entries[${index}].modelId`, 256); - if (!Array.isArray(value.adapterFamilies) || value.adapterFamilies.length === 0 || value.adapterFamilies.length > 3) { - throw new PublicEvidenceValidationError("invalid_registry_entry", `entries[${index}].adapterFamilies`); - } - const adapterFamilies = value.adapterFamilies.map((adapter, adapterIndex) => - assertAdapter(adapter, `entries[${index}].adapterFamilies[${adapterIndex}]`)); - if (new Set(adapterFamilies).size !== adapterFamilies.length) { - throw new PublicEvidenceValidationError("invalid_registry_entry", `entries[${index}].adapterFamilies duplicates`); - } + if (!Array.isArray(value.adapterFamilies) || value.adapterFamilies.length === 0 || value.adapterFamilies.length > 3) throw new PublicEvidenceValidationError("invalid_registry_entry", `entries[${index}].adapterFamilies`); + const adapterFamilies = value.adapterFamilies.map((adapter, adapterIndex) => assertAdapter(adapter, `entries[${index}].adapterFamilies[${adapterIndex}]`)); + if (new Set(adapterFamilies).size !== adapterFamilies.length) throw new PublicEvidenceValidationError("invalid_registry_entry", `entries[${index}].adapterFamilies duplicates`); + const canonicalBaseUrl = assertCanonicalHttpsBaseUrl(value.canonicalBaseUrl, `entries[${index}].canonicalBaseUrl`); const key = `${providerId}\0${modelId}`; if (seen.has(key)) throw new PublicEvidenceValidationError("duplicate_registry_entry", `entries[${index}]`); seen.add(key); - return { providerId, modelId, adapterFamilies }; + return { providerId, modelId, adapterFamilies, canonicalBaseUrl }; }); const manifestDigest = assertHex(raw.manifestDigest, "manifestDigest"); - const expected = publicRouteRegistryDigest({ - schemaVersion: PUBLIC_ROUTE_REGISTRY_SCHEMA_VERSION, - registryVersion, - sourceCommit, - entries, - }); - if (manifestDigest !== expected) { - throw new PublicEvidenceValidationError("registry_digest_mismatch", "manifestDigest does not match canonical registry bytes"); - } - return { - schemaVersion: PUBLIC_ROUTE_REGISTRY_SCHEMA_VERSION, - registryVersion, - sourceCommit, - entries, - manifestDigest, - }; + const expected = publicRouteRegistryDigest({ schemaVersion: PUBLIC_ROUTE_REGISTRY_SCHEMA_VERSION, registryVersion, sourceCommit, entries }); + if (manifestDigest !== expected) throw new PublicEvidenceValidationError("registry_digest_mismatch", "manifestDigest does not match canonical registry bytes"); + return { schemaVersion: PUBLIC_ROUTE_REGISTRY_SCHEMA_VERSION, registryVersion, sourceCommit, entries, manifestDigest }; } From 4c455d85b3db1c9688aecc1cbf72b3ca95e2ddf1 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:24:29 +0200 Subject: [PATCH 06/33] test(lab): define CL-10 signing and export storage --- .github/workflows/cl10-focus.yml | 2 +- tests/lab-public-evidence-signature.test.ts | 145 ++++++++++++++++++++ 2 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 tests/lab-public-evidence-signature.test.ts diff --git a/.github/workflows/cl10-focus.yml b/.github/workflows/cl10-focus.yml index b89f30d25b..b3d6ccd778 100644 --- a/.github/workflows/cl10-focus.yml +++ b/.github/workflows/cl10-focus.yml @@ -17,5 +17,5 @@ jobs: with: bun-version: 1.3.14 - run: bun install --frozen-lockfile - - run: bun test tests/lab-public-evidence.test.ts + - run: bun test tests/lab-public-evidence.test.ts tests/lab-public-evidence-signature.test.ts - run: bun x tsc --noEmit diff --git a/tests/lab-public-evidence-signature.test.ts b/tests/lab-public-evidence-signature.test.ts new file mode 100644 index 0000000000..75744529bc --- /dev/null +++ b/tests/lab-public-evidence-signature.test.ts @@ -0,0 +1,145 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { mkdtempSync, rmSync, statSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve, sep } from "node:path"; +import { + LAB_EVENT_SCHEMA_VERSION, + LAB_PRODUCER, + assignEventId, + subjectIdForSubject, + type ObservationEvent, + type ProtocolSubjectV1, +} from "../src/lab"; +import { + getOrCreatePublicPublisher, + projectPublicEvidence, + readPublicEvidenceBundle, + signPublicEvidenceBundle, + verifyPublicEvidenceBundle, + writePublicEvidenceBundle, +} from "../src/lab/public"; +import { labPublicPublisherKeyPath, labPublicExportsDir } from "../src/lab/paths"; + +const roots: string[] = []; +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); +}); + +function configDir(): string { + const root = mkdtempSync(join(tmpdir(), "ocx-cl10-sign-")); + roots.push(root); + return root; +} + +function hex(seed: string): string { + return Bun.CryptoHasher.hash("sha256", seed, "hex"); +} + +function observation(): ObservationEvent { + const subject: ProtocolSubjectV1 = { + subjectSchemaVersion: 1, + subjectKind: "protocol", + opencodexCompatibilityVersion: "2.13.0", + effectiveAdapter: "openai-chat", + inboundProtocol: "openai-responses", + upstreamProtocol: "openai-chat", + surface: "responses-http", + behaviorFingerprint: hex("PRIVATE-behavior"), + }; + return assignEventId({ + schemaVersion: LAB_EVENT_SCHEMA_VERSION, + eventKind: "observation" as const, + recordedAt: Date.UTC(2026, 7, 12, 14, 37, 48), + producer: LAB_PRODUCER, + producerVersion: "2.13.0", + evidenceLayer: "protocol_conformance" as const, + scenarioId: "responses-core.protocol.request-shape", + scenarioVersion: "1", + scenarioManifestDigest: hex("scenario"), + suiteId: "responses-core", + suiteVersion: "1", + suiteManifestDigest: hex("suite"), + fixtureDigests: [hex("fixture")], + subject, + subjectId: subjectIdForSubject(subject), + startedAt: Date.UTC(2026, 7, 12, 14, 37, 40), + completedAt: Date.UTC(2026, 7, 12, 14, 37, 41), + executionMode: "fixture" as const, + attempt: 1, + limits: { totalTimeoutMs: 1000 }, + outcome: "pass" as const, + assertions: [{ id: "request-shape", operator: "equals", required: true, passed: true, expectedSummary: "PRIVATE", observedSummary: "PRIVATE" }], + environment: {}, + artifactRefs: [], + }) as ObservationEvent; +} + +function unsignedBundle() { + return projectPublicEvidence({ + createdDayUtc: "2026-08-12", + records: [{ observation: observation(), verdict: "VERIFIED" }], + }).bundle; +} + +describe("CL-10 public publisher", () => { + test("creates a stable opaque Ed25519 publisher with a restricted private key file", () => { + const dir = configDir(); + const a = getOrCreatePublicPublisher(dir); + const b = getOrCreatePublicPublisher(dir); + expect(a.publisher).toEqual(b.publisher); + expect(a.publisher).toMatchObject({ algorithm: "ed25519" }); + expect(a.publisher.keyId).toMatch(/^[0-9a-f]{64}$/); + expect(a.publisher.publicKey.length).toBeGreaterThan(32); + expect(JSON.stringify(a)).not.toContain("PRIVATE KEY"); + expect(JSON.stringify(a)).not.toContain("privateKey"); + if (process.platform !== "win32") { + expect(statSync(labPublicPublisherKeyPath(dir)).mode & 0o777).toBe(0o600); + } + }); + + test("signs deterministic canonical bundle bytes and rejects tampering", () => { + const dir = configDir(); + const signer = getOrCreatePublicPublisher(dir); + const unsigned = unsignedBundle(); + const first = signPublicEvidenceBundle(unsigned, signer); + const second = signPublicEvidenceBundle(unsigned, signer); + expect(first.bundleDigest).toMatch(/^[0-9a-f]{64}$/); + expect(first.signature.signedDigest).toBe(first.bundleDigest); + expect(first.signature.signature).toBe(second.signature.signature); + expect(first.bundleDigest).toBe(second.bundleDigest); + expect(verifyPublicEvidenceBundle(first).status).toBe("cryptographically_valid"); + + const badSignature = { + ...first, + signature: { ...first.signature, signature: Buffer.alloc(64, 1).toString("base64") }, + }; + expect(verifyPublicEvidenceBundle(badSignature).status).toBe("signature_invalid"); + + const tampered = { + ...first, + records: first.records.map((record, index) => index === 0 ? { ...record, scenarioId: "tampered.scenario" } : record), + }; + expect(verifyPublicEvidenceBundle(tampered).status).toBe("digest_invalid"); + }); +}); + +describe("CL-10 public export storage", () => { + test("writes and reads a verified bundle only inside the restricted exports directory", () => { + const dir = configDir(); + const bundle = signPublicEvidenceBundle(unsignedBundle(), getOrCreatePublicPublisher(dir)); + const stored = writePublicEvidenceBundle(bundle, dir); + const exportsDir = resolve(labPublicExportsDir(dir)); + expect(resolve(stored.path).startsWith(exportsDir + sep)).toBe(true); + expect(stored.created).toBe(true); + expect(writePublicEvidenceBundle(bundle, dir).created).toBe(false); + const read = readPublicEvidenceBundle(bundle.bundleId, dir); + expect(read.bundleDigest).toBe(bundle.bundleDigest); + expect(verifyPublicEvidenceBundle(read).status).toBe("cryptographically_valid"); + }); + + test("rejects traversal-like bundle ids before filesystem access", () => { + const dir = configDir(); + expect(() => readPublicEvidenceBundle("../PRIVATE", dir)).toThrow(); + expect(() => readPublicEvidenceBundle("a".repeat(64) + "/PRIVATE", dir)).toThrow(); + }); +}); From 5e1830e467db617a2fdc42532db3504fe493212e Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:27:28 +0200 Subject: [PATCH 07/33] feat(lab): add CL-10 signing and local export storage --- src/lab/paths.ts | 24 ++++- src/lab/public/ids.ts | 13 ++- src/lab/public/index.ts | 2 + src/lab/public/signing.ts | 214 ++++++++++++++++++++++++++++++++++++++ src/lab/public/store.ts | 97 +++++++++++++++++ src/lab/public/types.ts | 73 +++++-------- 6 files changed, 369 insertions(+), 54 deletions(-) create mode 100644 src/lab/public/signing.ts create mode 100644 src/lab/public/store.ts diff --git a/src/lab/paths.ts b/src/lab/paths.ts index f202f4121c..5814110972 100644 --- a/src/lab/paths.ts +++ b/src/lab/paths.ts @@ -81,8 +81,24 @@ export function labScratchDir(configDir = getConfigDir()): string { return join(labRoot(configDir), "scratch"); } +/** CL-10 content-addressed local public export store. */ +export function labPublicExportsDir(configDir = getConfigDir()): string { + return join(labRoot(configDir), "exports"); +} + +/** Backwards-compatible Lab export path helper; CL-10 canonicalizes the directory to `exports/`. */ export function labExportDir(configDir = getConfigDir()): string { - return join(labRoot(configDir), "export"); + return labPublicExportsDir(configDir); +} + +/** CL-10 quarantined imported community evidence cache. */ +export function labCommunityDir(configDir = getConfigDir()): string { + return join(labRoot(configDir), "community"); +} + +/** Installation-local Ed25519 private publisher key. Never exported. */ +export function labPublicPublisherKeyPath(configDir = getConfigDir()): string { + return join(labRoot(configDir), "publisher-ed25519-private.pem"); } /** Opaque per-installation salt for local fingerprinting (never exported as evidence). */ @@ -110,15 +126,18 @@ export function ensureLabDirs(configDir = getConfigDir()): { artifactsDir: string; scratchDir: string; exportDir: string; + communityDir: string; } { const root = labRoot(configDir); const artifactsDir = labArtifactsDir(configDir); const scratchDir = labScratchDir(configDir); - const exportDir = labExportDir(configDir); + const exportDir = labPublicExportsDir(configDir); + const communityDir = labCommunityDir(configDir); ensureRestrictedDir(root, root); ensureRestrictedDir(artifactsDir, root); ensureRestrictedDir(scratchDir, root); ensureRestrictedDir(exportDir, root); + ensureRestrictedDir(communityDir, root); return { root, ledgerPath: labLedgerPath(configDir), @@ -126,5 +145,6 @@ export function ensureLabDirs(configDir = getConfigDir()): { artifactsDir, scratchDir, exportDir, + communityDir, }; } diff --git a/src/lab/public/ids.ts b/src/lab/public/ids.ts index 24d0a6cfde..9c34ff4ea3 100644 --- a/src/lab/public/ids.ts +++ b/src/lab/public/ids.ts @@ -7,14 +7,19 @@ const PUBLIC_ID_DOMAINS = { artifact: "ocx-lab:public-artifact:v1", publisher: "ocx-lab:public-publisher:v1", revocation: "ocx-lab:public-revocation:v1", + bundleDigest: "ocx-lab:public-bundle-digest:v1", } as const; -export type PublicEvidenceIdKind = keyof typeof PUBLIC_ID_DOMAINS; - -export function publicEvidenceId(kind: Exclude, payload: unknown): string { +export type PublicEvidenceIdKind = "subject" | "record" | "bundle" | "publisher" | "revocation"; +export function publicEvidenceId(kind: PublicEvidenceIdKind, payload: unknown): string { return domainHash(PUBLIC_ID_DOMAINS[kind], jcsStringify(payload)); } - export function publicArtifactId(bytes: Uint8Array): string { return domainHash(PUBLIC_ID_DOMAINS.artifact, bytes); } +export function publicPublisherKeyId(publicKeyDer: Uint8Array): string { + return domainHash(PUBLIC_ID_DOMAINS.publisher, publicKeyDer); +} +export function publicBundleDigest(payload: unknown): string { + return domainHash(PUBLIC_ID_DOMAINS.bundleDigest, jcsStringify(payload)); +} diff --git a/src/lab/public/index.ts b/src/lab/public/index.ts index 8d7ec8d985..c2bc113477 100644 --- a/src/lab/public/index.ts +++ b/src/lab/public/index.ts @@ -5,3 +5,5 @@ export * from "./authority"; export * from "./validate"; export * from "./project"; export * from "./bundle"; +export * from "./signing"; +export * from "./store"; diff --git a/src/lab/public/signing.ts b/src/lab/public/signing.ts new file mode 100644 index 0000000000..86bbf0ed0f --- /dev/null +++ b/src/lab/public/signing.ts @@ -0,0 +1,214 @@ +import { + createPrivateKey, + createPublicKey, + generateKeyPairSync, + sign as cryptoSign, + verify as cryptoVerify, + type KeyObject, +} from "node:crypto"; +import { + chmodSync, + closeSync, + constants as fsConstants, + fstatSync, + fsyncSync, + lstatSync, + openSync, + readFileSync, + writeSync, +} from "node:fs"; +import { ensureLabDirs, labPublicPublisherKeyPath } from "../paths"; +import { publicBundleDigest, publicPublisherKeyId } from "./ids"; +import type { + PublicEvidenceBundleUnsignedV1, + PublicEvidenceBundleV1, + PublicPublisherSignerV1, + PublicPublisherV1, + PublicBundleVerificationResult, +} from "./types"; +import { PublicEvidenceValidationError } from "./validate"; +import { validatePublicEvidenceBundleUnsigned } from "./bundle"; + +const signerKeys = new WeakMap(); +const O_NOFOLLOW = (fsConstants as { O_NOFOLLOW?: number }).O_NOFOLLOW ?? 0; +const MAX_PRIVATE_KEY_BYTES = 8 * 1024; + +function writeAll(fd: number, bytes: Uint8Array): void { + let offset = 0; + while (offset < bytes.byteLength) { + const count = writeSync(fd, bytes, offset, bytes.byteLength - offset); + if (count <= 0) throw new PublicEvidenceValidationError("publisher_key_write", "publisher key write made no progress"); + offset += count; + } +} + +function assertPrivateKeyFile(path: string, fd: number): void { + const stats = fstatSync(fd); + if (!stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1 || stats.size > MAX_PRIVATE_KEY_BYTES) { + throw new PublicEvidenceValidationError("publisher_key_unsafe", "publisher key target is not a bounded regular file"); + } + if (process.platform !== "win32") { + const mode = stats.mode & 0o777; + if (mode !== 0o600) chmodSync(path, 0o600); + } +} + +function loadPrivateKey(path: string): KeyObject { + const before = lstatSync(path); + if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1) { + throw new PublicEvidenceValidationError("publisher_key_unsafe", "publisher key path is unsafe"); + } + const fd = openSync(path, fsConstants.O_RDONLY | O_NOFOLLOW); + try { + assertPrivateKeyFile(path, fd); + const pem = readFileSync(fd, { encoding: "utf8" }); + if (Buffer.byteLength(pem) > MAX_PRIVATE_KEY_BYTES || !pem.includes("BEGIN PRIVATE KEY")) { + throw new PublicEvidenceValidationError("publisher_key_invalid", "publisher key encoding invalid"); + } + const key = createPrivateKey(pem); + if (key.asymmetricKeyType !== "ed25519") throw new PublicEvidenceValidationError("publisher_key_invalid", "publisher key must be Ed25519"); + return key; + } finally { + closeSync(fd); + } +} + +function createPrivateKeyFile(path: string): KeyObject { + const { privateKey } = generateKeyPairSync("ed25519"); + const pem = privateKey.export({ type: "pkcs8", format: "pem" }).toString(); + let fd: number | null = null; + try { + fd = openSync(path, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | O_NOFOLLOW, 0o600); + writeAll(fd, Buffer.from(pem, "utf8")); + fsyncSync(fd); + assertPrivateKeyFile(path, fd); + return privateKey; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "EEXIST") return loadPrivateKey(path); + throw error; + } finally { + if (fd !== null) closeSync(fd); + } +} + +function publisherForPrivateKey(privateKey: KeyObject): PublicPublisherV1 { + const publicKey = createPublicKey(privateKey); + const der = publicKey.export({ type: "spki", format: "der" }); + return Object.freeze({ + algorithm: "ed25519" as const, + keyId: publicPublisherKeyId(der), + publicKey: der.toString("base64"), + }); +} + +export function getOrCreatePublicPublisher(configDir?: string): PublicPublisherSignerV1 { + ensureLabDirs(configDir); + const path = labPublicPublisherKeyPath(configDir); + let key: KeyObject; + try { + key = loadPrivateKey(path); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; + key = createPrivateKeyFile(path); + } + const signer: PublicPublisherSignerV1 = Object.freeze({ publisher: publisherForPrivateKey(key) }); + signerKeys.set(signer, key); + return signer; +} + +function signedPayload(unsigned: PublicEvidenceBundleUnsignedV1, publisher: PublicPublisherV1): Record { + return { + schemaVersion: unsigned.schemaVersion, + exportPolicyVersion: unsigned.exportPolicyVersion, + bundleId: unsigned.bundleId, + createdDayUtc: unsigned.createdDayUtc, + publisher, + records: unsigned.records, + artifacts: unsigned.artifacts, + }; +} + +export function signPublicEvidenceBundle( + rawUnsigned: PublicEvidenceBundleUnsignedV1, + signer: PublicPublisherSignerV1, +): PublicEvidenceBundleV1 { + const privateKey = signerKeys.get(signer as object); + if (!privateKey) throw new PublicEvidenceValidationError("publisher_signer_untrusted", "publisher signer is not a live local signer capability"); + const unsigned = validatePublicEvidenceBundleUnsigned(rawUnsigned); + const digest = publicBundleDigest(signedPayload(unsigned, signer.publisher)); + const signature = cryptoSign(null, Buffer.from(digest, "hex"), privateKey).toString("base64"); + return Object.freeze({ + ...unsigned, + publisher: signer.publisher, + bundleDigest: digest, + signature: Object.freeze({ algorithm: "ed25519" as const, signedDigest: digest, signature }), + }); +} + +function isPlainObject(value: unknown): value is Record { + return !!value && typeof value === "object" && !Array.isArray(value); +} +function closedKeys(value: Record, keys: readonly string[]): boolean { + const allowed = new Set(keys); + return Object.keys(value).every((key) => allowed.has(key)) && keys.every((key) => key in value); +} +function canonicalBase64(value: unknown, maxBytes: number): Buffer | null { + if (typeof value !== "string" || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) return null; + const bytes = Buffer.from(value, "base64"); + if (bytes.byteLength > maxBytes || bytes.toString("base64") !== value) return null; + return bytes; +} + +export function verifyPublicEvidenceBundle(raw: unknown): PublicBundleVerificationResult { + try { + if (!isPlainObject(raw) || !closedKeys(raw, ["schemaVersion", "exportPolicyVersion", "bundleId", "createdDayUtc", "publisher", "records", "artifacts", "bundleDigest", "signature"])) { + return { status: "schema_rejected", detail: "closed bundle schema mismatch" }; + } + const unsignedRaw = { + schemaVersion: raw.schemaVersion, + exportPolicyVersion: raw.exportPolicyVersion, + bundleId: raw.bundleId, + createdDayUtc: raw.createdDayUtc, + records: raw.records, + artifacts: raw.artifacts, + }; + let unsigned: PublicEvidenceBundleUnsignedV1; + try { + unsigned = validatePublicEvidenceBundleUnsigned(unsignedRaw); + } catch (error) { + if (error instanceof PublicEvidenceValidationError && error.code === "bundle_digest") return { status: "digest_invalid", detail: error.message }; + return { status: "schema_rejected", detail: error instanceof Error ? error.message : String(error) }; + } + if (!isPlainObject(raw.publisher) || !closedKeys(raw.publisher, ["algorithm", "keyId", "publicKey"]) || raw.publisher.algorithm !== "ed25519") { + return { status: "schema_rejected", detail: "publisher schema mismatch" }; + } + if (typeof raw.publisher.keyId !== "string" || !/^[0-9a-f]{64}$/.test(raw.publisher.keyId)) return { status: "schema_rejected", detail: "publisher key id invalid" }; + const publicKeyDer = canonicalBase64(raw.publisher.publicKey, 1024); + if (!publicKeyDer || publicPublisherKeyId(publicKeyDer) !== raw.publisher.keyId) return { status: "schema_rejected", detail: "publisher public key invalid" }; + const publisher: PublicPublisherV1 = { algorithm: "ed25519", keyId: raw.publisher.keyId, publicKey: raw.publisher.publicKey as string }; + if (typeof raw.bundleDigest !== "string" || !/^[0-9a-f]{64}$/.test(raw.bundleDigest)) return { status: "schema_rejected", detail: "bundle digest invalid" }; + if (!isPlainObject(raw.signature) || !closedKeys(raw.signature, ["algorithm", "signedDigest", "signature"]) || raw.signature.algorithm !== "ed25519") return { status: "schema_rejected", detail: "signature schema mismatch" }; + if (raw.signature.signedDigest !== raw.bundleDigest) return { status: "digest_invalid", detail: "signed digest does not match bundle digest" }; + const expectedDigest = publicBundleDigest(signedPayload(unsigned, publisher)); + if (expectedDigest !== raw.bundleDigest) return { status: "digest_invalid", detail: "canonical bundle digest mismatch" }; + const signatureBytes = canonicalBase64(raw.signature.signature, 128); + if (!signatureBytes || signatureBytes.byteLength !== 64) return { status: "signature_invalid", detail: "signature encoding invalid" }; + let publicKey: KeyObject; + try { + publicKey = createPublicKey({ key: publicKeyDer, format: "der", type: "spki" }); + } catch { + return { status: "schema_rejected", detail: "publisher public key parse failed" }; + } + if (publicKey.asymmetricKeyType !== "ed25519") return { status: "schema_rejected", detail: "publisher key algorithm mismatch" }; + if (!cryptoVerify(null, Buffer.from(raw.bundleDigest, "hex"), publicKey, signatureBytes)) return { status: "signature_invalid", detail: "Ed25519 signature verification failed" }; + const bundle: PublicEvidenceBundleV1 = { + ...unsigned, + publisher, + bundleDigest: raw.bundleDigest, + signature: { algorithm: "ed25519", signedDigest: raw.bundleDigest, signature: raw.signature.signature as string }, + }; + return { status: "cryptographically_valid", bundle }; + } catch (error) { + return { status: "schema_rejected", detail: error instanceof Error ? error.message : String(error) }; + } +} diff --git a/src/lab/public/store.ts b/src/lab/public/store.ts new file mode 100644 index 0000000000..515f68b77c --- /dev/null +++ b/src/lab/public/store.ts @@ -0,0 +1,97 @@ +import { + closeSync, + constants as fsConstants, + fstatSync, + fsyncSync, + lstatSync, + openSync, + readFileSync, + writeSync, +} from "node:fs"; +import { join } from "node:path"; +import { jcsStringify } from "../digest"; +import { ensureLabDirs, labPublicExportsDir } from "../paths"; +import type { PublicEvidenceBundleV1 } from "./types"; +import { PublicEvidenceValidationError } from "./validate"; +import { verifyPublicEvidenceBundle } from "./signing"; + +const MAX_BUNDLE_FILE_BYTES = 2 * 1024 * 1024; +const O_NOFOLLOW = (fsConstants as { O_NOFOLLOW?: number }).O_NOFOLLOW ?? 0; + +function assertId(value: string): string { + if (!/^[0-9a-f]{64}$/.test(value)) throw new PublicEvidenceValidationError("invalid_bundle_id", "bundle id must be lowercase sha256 hex"); + return value; +} + +function assertRegularFile(path: string, fd: number): void { + const stats = fstatSync(fd); + if (!stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1 || stats.size > MAX_BUNDLE_FILE_BYTES) { + throw new PublicEvidenceValidationError("export_unsafe_target", `unsafe public export file: ${path}`); + } +} + +function readBounded(path: string): Buffer { + const before = lstatSync(path); + if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1 || before.size > MAX_BUNDLE_FILE_BYTES) { + throw new PublicEvidenceValidationError("export_unsafe_target", "public export path is unsafe or oversized"); + } + const fd = openSync(path, fsConstants.O_RDONLY | O_NOFOLLOW); + try { + assertRegularFile(path, fd); + const bytes = readFileSync(fd); + if (bytes.byteLength > MAX_BUNDLE_FILE_BYTES) throw new PublicEvidenceValidationError("export_too_large", "public export exceeds read bound"); + return bytes; + } finally { + closeSync(fd); + } +} + +function writeAll(fd: number, bytes: Uint8Array): void { + let offset = 0; + while (offset < bytes.byteLength) { + const count = writeSync(fd, bytes, offset, bytes.byteLength - offset); + if (count <= 0) throw new PublicEvidenceValidationError("export_write_failed", "public export write made no progress"); + offset += count; + } +} + +export function writePublicEvidenceBundle(bundle: PublicEvidenceBundleV1, configDir?: string): { path: string; created: boolean } { + const verified = verifyPublicEvidenceBundle(bundle); + if (verified.status !== "cryptographically_valid") throw new PublicEvidenceValidationError(verified.status, verified.detail ?? "public bundle is not cryptographically valid"); + ensureLabDirs(configDir); + const exportsDir = labPublicExportsDir(configDir); + const path = join(exportsDir, `${assertId(bundle.bundleId)}.json`); + const bytes = Buffer.from(jcsStringify(verified.bundle), "utf8"); + if (bytes.byteLength > MAX_BUNDLE_FILE_BYTES) throw new PublicEvidenceValidationError("export_too_large", "public export exceeds file bound"); + let fd: number | null = null; + try { + fd = openSync(path, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | O_NOFOLLOW, 0o600); + writeAll(fd, bytes); + fsyncSync(fd); + assertRegularFile(path, fd); + closeSync(fd); + fd = null; + return { path, created: true }; + } catch (error) { + if (fd !== null) closeSync(fd); + if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error; + const existing = readBounded(path); + if (!existing.equals(bytes)) throw new PublicEvidenceValidationError("export_conflict", "bundle id already exists with different bytes"); + return { path, created: false }; + } +} + +export function readPublicEvidenceBundle(bundleId: string, configDir?: string): PublicEvidenceBundleV1 { + ensureLabDirs(configDir); + const path = join(labPublicExportsDir(configDir), `${assertId(bundleId)}.json`); + const bytes = readBounded(path); + let raw: unknown; + try { + raw = JSON.parse(bytes.toString("utf8")); + } catch { + throw new PublicEvidenceValidationError("export_invalid_json", "public export is not valid JSON"); + } + const verified = verifyPublicEvidenceBundle(raw); + if (verified.status !== "cryptographically_valid") throw new PublicEvidenceValidationError(verified.status, verified.detail ?? "public export verification failed"); + return verified.bundle; +} diff --git a/src/lab/public/types.ts b/src/lab/public/types.ts index 514a403b47..75593b9c23 100644 --- a/src/lab/public/types.ts +++ b/src/lab/public/types.ts @@ -4,21 +4,15 @@ export const PUBLIC_ROUTE_REGISTRY_SCHEMA_VERSION = "public_route_registry_v1" a export const PUBLIC_EVIDENCE_BUNDLE_SCHEMA_VERSION = "public_evidence_bundle_v1" as const; export const PUBLIC_EXPORT_POLICY_VERSION = "public_export_policy_v1" as const; -export const PUBLIC_ADAPTER_FAMILIES = [ - "openai-responses", - "openai-chat", - "anthropic-messages", -] as const; +export const PUBLIC_ADAPTER_FAMILIES = ["openai-responses", "openai-chat", "anthropic-messages"] as const; export type PublicAdapterFamilyV1 = (typeof PUBLIC_ADAPTER_FAMILIES)[number]; export interface PublicRouteRegistryEntryV1 { providerId: string; modelId: string; adapterFamilies: PublicAdapterFamilyV1[]; - /** Reviewed canonical public endpoint used only as local export authority. */ canonicalBaseUrl: string; } - export interface PublicRouteRegistryManifestV1 { schemaVersion: typeof PUBLIC_ROUTE_REGISTRY_SCHEMA_VERSION; registryVersion: string; @@ -26,7 +20,6 @@ export interface PublicRouteRegistryManifestV1 { entries: PublicRouteRegistryEntryV1[]; manifestDigest: string; } - export interface PublicProtocolSubjectV1 { subjectKind: "protocol"; adapterFamily: PublicAdapterFamilyV1; @@ -35,7 +28,6 @@ export interface PublicProtocolSubjectV1 { surface: string; compatibilityVersion: string; } - export interface PublicRouteSubjectV1 { subjectKind: "route"; providerId: string; @@ -48,7 +40,6 @@ export interface PublicRouteSubjectV1 { registryVersion: string; registryDigest: string; } - export interface PublicTaskSubjectV1 { subjectKind: "task"; route: PublicRouteSubjectV1; @@ -56,22 +47,9 @@ export interface PublicTaskSubjectV1 { taskClassVersion: string; verifierAuthorityId: string; } - -export type PublicEvidenceSubjectV1 = - | PublicProtocolSubjectV1 - | PublicRouteSubjectV1 - | PublicTaskSubjectV1; - -export interface PublicAssertionSummaryV1 { - id: string; - required: boolean; - passed: boolean; -} - -export interface PublicIncidentRefV1 { - corpusId: string; -} - +export type PublicEvidenceSubjectV1 = PublicProtocolSubjectV1 | PublicRouteSubjectV1 | PublicTaskSubjectV1; +export interface PublicAssertionSummaryV1 { id: string; required: boolean; passed: boolean; } +export interface PublicIncidentRefV1 { corpusId: string; } export interface PublicEvidenceRecordV1 { recordId: string; subjectId: string; @@ -87,14 +65,12 @@ export interface PublicEvidenceRecordV1 { incidentRefs?: PublicIncidentRefV1[]; artifactRefs?: string[]; } - export interface PublicArtifactV1 { artifactId: string; mediaType: "application/json" | "text/plain; charset=utf-8"; byteCount: number; contentBase64: string; } - export interface PublicEvidenceBundleUnsignedV1 { schemaVersion: typeof PUBLIC_EVIDENCE_BUNDLE_SCHEMA_VERSION; exportPolicyVersion: typeof PUBLIC_EXPORT_POLICY_VERSION; @@ -103,25 +79,26 @@ export interface PublicEvidenceBundleUnsignedV1 { records: PublicEvidenceRecordV1[]; artifacts: PublicArtifactV1[]; } - -export type PublicProjectionNotExportableReason = - | "private_route_identity" - | "unsupported_public_adapter" - | "task_authority_unavailable" - | "invalid_public_incident_ref"; - -export type PublicProjectionResult = - | { status: "exportable"; record: PublicEvidenceRecordV1 } - | { status: "not_exportable"; reason: PublicProjectionNotExportableReason }; - -/** Opaque runtime capability. Plain-object copies are intentionally untrusted. */ -export interface PublicRouteAuthorityV1 { - localSubjectId: string; - descriptor: PublicRouteSubjectV1; +export interface PublicPublisherV1 { + algorithm: "ed25519"; + keyId: string; + publicKey: string; } - -/** Opaque runtime capability. Plain-object copies are intentionally untrusted. */ -export interface PublicTaskAuthorityV1 { - localSubjectId: string; - descriptor: PublicTaskSubjectV1; +export interface PublicBundleSignatureV1 { + algorithm: "ed25519"; + signedDigest: string; + signature: string; +} +export interface PublicEvidenceBundleV1 extends PublicEvidenceBundleUnsignedV1 { + publisher: PublicPublisherV1; + bundleDigest: string; + signature: PublicBundleSignatureV1; } +export interface PublicPublisherSignerV1 { publisher: PublicPublisherV1; } +export type PublicBundleVerificationResult = + | { status: "cryptographically_valid"; bundle: PublicEvidenceBundleV1 } + | { status: "schema_rejected" | "digest_invalid" | "signature_invalid"; detail?: string }; +export type PublicProjectionNotExportableReason = "private_route_identity" | "unsupported_public_adapter" | "task_authority_unavailable" | "invalid_public_incident_ref"; +export type PublicProjectionResult = { status: "exportable"; record: PublicEvidenceRecordV1 } | { status: "not_exportable"; reason: PublicProjectionNotExportableReason }; +export interface PublicRouteAuthorityV1 { localSubjectId: string; descriptor: PublicRouteSubjectV1; } +export interface PublicTaskAuthorityV1 { localSubjectId: string; descriptor: PublicTaskSubjectV1; } From 7ea7b438ae636596da70b9843fe1936e24ff6f95 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:31:04 +0200 Subject: [PATCH 08/33] fix(lab): derive public key through typed PEM input --- src/lab/public/signing.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lab/public/signing.ts b/src/lab/public/signing.ts index 86bbf0ed0f..6109366d09 100644 --- a/src/lab/public/signing.ts +++ b/src/lab/public/signing.ts @@ -92,7 +92,8 @@ function createPrivateKeyFile(path: string): KeyObject { } function publisherForPrivateKey(privateKey: KeyObject): PublicPublisherV1 { - const publicKey = createPublicKey(privateKey); + const privatePem = privateKey.export({ type: "pkcs8", format: "pem" }); + const publicKey = createPublicKey(privatePem); const der = publicKey.export({ type: "spki", format: "der" }); return Object.freeze({ algorithm: "ed25519" as const, From bf88f7d7d1e7e390fa8765b9062d7efbf13dbcb5 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:34:27 +0200 Subject: [PATCH 09/33] test(lab): define CL-10 community revocation boundary --- .github/workflows/cl10-focus.yml | 2 +- tests/lab-community-evidence.test.ts | 166 +++++++++++++++++++++++++++ 2 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 tests/lab-community-evidence.test.ts diff --git a/.github/workflows/cl10-focus.yml b/.github/workflows/cl10-focus.yml index b3d6ccd778..f6c53f4bc7 100644 --- a/.github/workflows/cl10-focus.yml +++ b/.github/workflows/cl10-focus.yml @@ -17,5 +17,5 @@ jobs: with: bun-version: 1.3.14 - run: bun install --frozen-lockfile - - run: bun test tests/lab-public-evidence.test.ts tests/lab-public-evidence-signature.test.ts + - run: bun test tests/lab-public-evidence.test.ts tests/lab-public-evidence-signature.test.ts tests/lab-community-evidence.test.ts - run: bun x tsc --noEmit diff --git a/tests/lab-community-evidence.test.ts b/tests/lab-community-evidence.test.ts new file mode 100644 index 0000000000..dee1c2b11e --- /dev/null +++ b/tests/lab-community-evidence.test.ts @@ -0,0 +1,166 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { existsSync, mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { + LAB_EVENT_SCHEMA_VERSION, + LAB_PRODUCER, + assignEventId, + labLedgerPath, + labSqlitePath, + subjectIdForSubject, + type ObservationEvent, + type ProtocolSubjectV1, +} from "../src/lab"; +import { + createPublicEvidenceRevocation, + getOrCreatePublicPublisher, + importCommunityEvidenceBundle, + importCommunityEvidenceRevocation, + listCommunityEvidence, + projectPublicEvidence, + signPublicEvidenceBundle, + verifyPublicEvidenceRevocation, +} from "../src/lab/public"; + +const roots: string[] = []; +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); +}); + +function configDir(prefix = "ocx-cl10-community-"): string { + const root = mkdtempSync(join(tmpdir(), prefix)); + roots.push(root); + return root; +} + +function hex(seed: string): string { + return Bun.CryptoHasher.hash("sha256", seed, "hex"); +} + +function protocolObservation(scenarioId = "responses-core.protocol.request-shape"): ObservationEvent { + const subject: ProtocolSubjectV1 = { + subjectSchemaVersion: 1, + subjectKind: "protocol", + opencodexCompatibilityVersion: "2.13.0", + effectiveAdapter: "openai-chat", + inboundProtocol: "openai-responses", + upstreamProtocol: "openai-chat", + surface: "responses-http", + behaviorFingerprint: hex("PRIVATE-community-behavior"), + }; + return assignEventId({ + schemaVersion: LAB_EVENT_SCHEMA_VERSION, + eventKind: "observation" as const, + recordedAt: Date.UTC(2026, 7, 12, 14, 37, 48), + producer: LAB_PRODUCER, + producerVersion: "2.13.0", + evidenceLayer: "protocol_conformance" as const, + scenarioId, + scenarioVersion: "1", + scenarioManifestDigest: hex("scenario"), + suiteId: "responses-core", + suiteVersion: "1", + suiteManifestDigest: hex("suite"), + fixtureDigests: [hex("fixture")], + subject, + subjectId: subjectIdForSubject(subject), + startedAt: Date.UTC(2026, 7, 12, 14, 37, 40), + completedAt: Date.UTC(2026, 7, 12, 14, 37, 41), + executionMode: "fixture" as const, + attempt: 1, + limits: { totalTimeoutMs: 1000 }, + outcome: "pass" as const, + assertions: [{ id: "request-shape", operator: "equals", required: true, passed: true }], + environment: {}, + artifactRefs: [], + }) as ObservationEvent; +} + +function signedBundle(config: string, scenarioId?: string) { + const unsigned = projectPublicEvidence({ + createdDayUtc: "2026-08-12", + records: [{ observation: protocolObservation(scenarioId), verdict: "VERIFIED" }], + }).bundle; + return signPublicEvidenceBundle(unsigned, getOrCreatePublicPublisher(config)); +} + +describe("CL-10 community quarantine", () => { + test("imports valid signed evidence without touching canonical Lab authority", () => { + const publisherDir = configDir("ocx-cl10-publisher-"); + const consumerDir = configDir("ocx-cl10-consumer-"); + const bundle = signedBundle(publisherDir); + const imported = importCommunityEvidenceBundle(bundle, consumerDir); + expect(imported).toMatchObject({ created: true, status: "cryptographically_valid", bundleId: bundle.bundleId }); + expect(existsSync(labLedgerPath(consumerDir))).toBe(false); + expect(existsSync(labSqlitePath(consumerDir))).toBe(false); + expect(listCommunityEvidence(consumerDir)).toEqual([expect.objectContaining({ + bundleId: bundle.bundleId, + status: "cryptographically_valid", + activeRecordCount: 1, + revokedRecordCount: 0, + })]); + expect(importCommunityEvidenceBundle(bundle, consumerDir).created).toBe(false); + }); + + test("rejects cryptographically valid but unknown scenario authority", () => { + const publisherDir = configDir("ocx-cl10-publisher-"); + const consumerDir = configDir("ocx-cl10-consumer-"); + const bundle = signedBundle(publisherDir, "private.unknown.scenario"); + expect(() => importCommunityEvidenceBundle(bundle, consumerDir)).toThrow(/authority/i); + expect(listCommunityEvidence(consumerDir)).toEqual([]); + }); + + test("same-key revocation is verified, idempotent, and removes records from default community context", () => { + const publisherDir = configDir("ocx-cl10-publisher-"); + const consumerDir = configDir("ocx-cl10-consumer-"); + const signer = getOrCreatePublicPublisher(publisherDir); + const bundle = signPublicEvidenceBundle(projectPublicEvidence({ + createdDayUtc: "2026-08-12", + records: [{ observation: protocolObservation(), verdict: "VERIFIED" }], + }).bundle, signer); + importCommunityEvidenceBundle(bundle, consumerDir); + + const revocation = createPublicEvidenceRevocation({ + signer, + targetBundle: bundle, + issuedDayUtc: "2026-08-12", + reason: "evidence_invalidated", + targets: [{ kind: "record", id: bundle.records[0]!.recordId }], + }); + expect(verifyPublicEvidenceRevocation(revocation, bundle).status).toBe("cryptographically_valid"); + expect(importCommunityEvidenceRevocation(revocation, consumerDir).created).toBe(true); + expect(importCommunityEvidenceRevocation(revocation, consumerDir).created).toBe(false); + expect(listCommunityEvidence(consumerDir)[0]).toMatchObject({ activeRecordCount: 0, revokedRecordCount: 1 }); + }); + + test("rejects cross-key revocation and conflicting same-id bytes", () => { + const publisherDir = configDir("ocx-cl10-publisher-"); + const otherDir = configDir("ocx-cl10-other-"); + const consumerDir = configDir("ocx-cl10-consumer-"); + const signer = getOrCreatePublicPublisher(publisherDir); + const bundle = signPublicEvidenceBundle(projectPublicEvidence({ + createdDayUtc: "2026-08-12", + records: [{ observation: protocolObservation(), verdict: "VERIFIED" }], + }).bundle, signer); + importCommunityEvidenceBundle(bundle, consumerDir); + expect(() => createPublicEvidenceRevocation({ + signer: getOrCreatePublicPublisher(otherDir), + targetBundle: bundle, + issuedDayUtc: "2026-08-12", + reason: "publisher_retracted", + targets: [{ kind: "bundle", id: bundle.bundleId }], + })).toThrow(/publisher/i); + + const revocation = createPublicEvidenceRevocation({ + signer, + targetBundle: bundle, + issuedDayUtc: "2026-08-12", + reason: "publisher_retracted", + targets: [{ kind: "bundle", id: bundle.bundleId }], + }); + importCommunityEvidenceRevocation(revocation, consumerDir); + const conflict = { ...revocation, issuedDayUtc: "2026-08-13" }; + expect(() => importCommunityEvidenceRevocation(conflict, consumerDir)).toThrow(); + }); +}); From 6088e412600510d5b2d5670a1e017d5379ba155d Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:37:36 +0200 Subject: [PATCH 10/33] feat(lab): quarantine CL-10 community evidence --- src/lab/public/community-authority.ts | 34 ++++++ src/lab/public/community.ts | 153 ++++++++++++++++++++++++++ src/lab/public/index.ts | 9 +- src/lab/public/revocation.ts | 81 ++++++++++++++ src/lab/public/signing.ts | 152 ++++++++----------------- src/lab/public/types.ts | 26 +++++ 6 files changed, 348 insertions(+), 107 deletions(-) create mode 100644 src/lab/public/community-authority.ts create mode 100644 src/lab/public/community.ts create mode 100644 src/lab/public/revocation.ts diff --git a/src/lab/public/community-authority.ts b/src/lab/public/community-authority.ts new file mode 100644 index 0000000000..0755de7ccf --- /dev/null +++ b/src/lab/public/community-authority.ts @@ -0,0 +1,34 @@ +import { loadCaseAuthority } from "../conformance/manifest"; +import { + FABRIC_SCENARIO_ID, + FABRIC_SCENARIO_VERSION, + FABRIC_SUITE_ID, + FABRIC_SUITE_VERSION, + FABRIC_VERIFIER_ID, +} from "../fabric/constants"; +import { PUBLIC_ROUTE_REGISTRY_V1, findPublicRouteRegistryEntry } from "./registry"; +import type { PublicEvidenceBundleV1, PublicEvidenceRecordV1, PublicRouteSubjectV1 } from "./types"; +import { PublicEvidenceValidationError } from "./validate"; + +function validateRouteAuthority(subject: PublicRouteSubjectV1): void { + if (subject.registryVersion !== PUBLIC_ROUTE_REGISTRY_V1.registryVersion || subject.registryDigest !== PUBLIC_ROUTE_REGISTRY_V1.manifestDigest) throw new PublicEvidenceValidationError("community_authority", "public route registry authority mismatch"); + if (!findPublicRouteRegistryEntry(subject.providerId, subject.modelId, subject.adapterFamily)) throw new PublicEvidenceValidationError("community_authority", "public route is not in reviewed registry authority"); +} +function validateScenarioAuthority(record: PublicEvidenceRecordV1): void { + if (record.evidenceLayer === "task_effectiveness") { + if (record.suiteId !== FABRIC_SUITE_ID || record.suiteVersion !== FABRIC_SUITE_VERSION || record.scenarioId !== FABRIC_SCENARIO_ID || record.scenarioVersion !== FABRIC_SCENARIO_VERSION || record.subject.subjectKind !== "task" || record.subject.taskClassId !== FABRIC_SCENARIO_ID || record.subject.taskClassVersion !== FABRIC_SCENARIO_VERSION || record.subject.verifierAuthorityId !== FABRIC_VERIFIER_ID) throw new PublicEvidenceValidationError("community_authority", "task scenario/verifier authority mismatch"); + validateRouteAuthority(record.subject.route); + return; + } + const authority = loadCaseAuthority(); + const caseRecord = authority.cases.find((candidate) => candidate.id === record.scenarioId); + if (!caseRecord || caseRecord.suite !== record.suiteId || record.scenarioVersion !== String(authority.manifestDefaults.version) || record.suiteVersion !== String(authority.manifestDefaults.suiteVersion)) throw new PublicEvidenceValidationError("community_authority", "scenario/suite authority mismatch"); + if (record.evidenceLayer === "live_route_compatibility") { + if (record.subject.subjectKind !== "route") throw new PublicEvidenceValidationError("community_authority", "live route subject mismatch"); + validateRouteAuthority(record.subject); + } +} +export function validateCommunityEvidenceAuthorities(bundle: PublicEvidenceBundleV1): PublicEvidenceBundleV1 { + for (const record of bundle.records) validateScenarioAuthority(record); + return bundle; +} diff --git a/src/lab/public/community.ts b/src/lab/public/community.ts new file mode 100644 index 0000000000..486a0bbbf7 --- /dev/null +++ b/src/lab/public/community.ts @@ -0,0 +1,153 @@ +import { + closeSync, + constants as fsConstants, + fstatSync, + fsyncSync, + lstatSync, + openSync, + readdirSync, + readFileSync, + writeSync, +} from "node:fs"; +import { join } from "node:path"; +import { jcsStringify } from "../digest"; +import { ensureLabDirs, labCommunityDir } from "../paths"; +import { validateCommunityEvidenceAuthorities } from "./community-authority"; +import { verifyPublicEvidenceRevocation } from "./revocation"; +import { verifyPublicEvidenceBundle } from "./signing"; +import type { + CommunityEvidenceSummaryV1, + PublicEvidenceBundleV1, + PublicEvidenceRevocationV1, +} from "./types"; +import { PublicEvidenceValidationError } from "./validate"; + +const MAX_IMPORT_BYTES = 2 * 1024 * 1024; +const MAX_CACHE_FILES = 4096; +const MAX_DEPTH = 8; +const MAX_OBJECT_KEYS = 64; +const MAX_ARRAY_ELEMENTS = 512; +const MAX_GENERIC_STRING_BYTES = 384 * 1024; +const O_NOFOLLOW = (fsConstants as { O_NOFOLLOW?: number }).O_NOFOLLOW ?? 0; + +function assertId(value: string): string { if (!/^[0-9a-f]{64}$/.test(value)) throw new PublicEvidenceValidationError("community_id", "community object id invalid"); return value; } +function scanStructure(value: unknown, depth = 0): void { + if (depth > MAX_DEPTH) throw new PublicEvidenceValidationError("community_depth", "community JSON nesting depth exceeded"); + if (typeof value === "string") { + if (new TextEncoder().encode(value).byteLength > MAX_GENERIC_STRING_BYTES || value.includes("\0")) throw new PublicEvidenceValidationError("community_string", "community string invalid or oversized"); + return; + } + if (Array.isArray(value)) { + if (value.length > MAX_ARRAY_ELEMENTS) throw new PublicEvidenceValidationError("community_array", "community array bound exceeded"); + for (const item of value) scanStructure(item, depth + 1); + return; + } + if (value && typeof value === "object") { + const keys = Object.keys(value); + if (keys.length > MAX_OBJECT_KEYS) throw new PublicEvidenceValidationError("community_object", "community object key bound exceeded"); + for (const key of keys) { if (new TextEncoder().encode(key).byteLength > 4096) throw new PublicEvidenceValidationError("community_key", "community key oversized"); scanStructure((value as Record)[key], depth + 1); } + } +} +function boundedInput(raw: unknown): unknown { + if (raw instanceof Uint8Array || typeof raw === "string") { + const bytes = typeof raw === "string" ? Buffer.from(raw, "utf8") : Buffer.from(raw); + if (bytes.byteLength > MAX_IMPORT_BYTES) throw new PublicEvidenceValidationError("community_size", "community import exceeds 2 MiB"); + let parsed: unknown; + try { parsed = JSON.parse(bytes.toString("utf8")); } catch { throw new PublicEvidenceValidationError("community_json", "community import is not valid JSON"); } + scanStructure(parsed); + return parsed; + } + scanStructure(raw); + const bytes = Buffer.from(jcsStringify(raw), "utf8"); + if (bytes.byteLength > MAX_IMPORT_BYTES) throw new PublicEvidenceValidationError("community_size", "community import exceeds 2 MiB"); + return raw; +} +function objectPath(kind: "bundle" | "revocation", id: string, configDir?: string): string { return join(labCommunityDir(configDir), `${kind}-${assertId(id)}.json`); } +function assertRegular(path: string, fd: number): void { + const stats = fstatSync(fd); + if (!stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1 || stats.size > MAX_IMPORT_BYTES) throw new PublicEvidenceValidationError("community_unsafe_target", `unsafe community file: ${path}`); +} +function readBounded(path: string): Buffer { + const before = lstatSync(path); + if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1 || before.size > MAX_IMPORT_BYTES) throw new PublicEvidenceValidationError("community_unsafe_target", "unsafe community path"); + const fd = openSync(path, fsConstants.O_RDONLY | O_NOFOLLOW); + try { assertRegular(path, fd); const bytes = readFileSync(fd); if (bytes.byteLength > MAX_IMPORT_BYTES) throw new PublicEvidenceValidationError("community_size", "community file exceeds bound"); return bytes; } + finally { closeSync(fd); } +} +function writeAll(fd: number, bytes: Uint8Array): void { let offset = 0; while (offset < bytes.byteLength) { const count = writeSync(fd, bytes, offset, bytes.byteLength - offset); if (count <= 0) throw new PublicEvidenceValidationError("community_write", "community write made no progress"); offset += count; } } +function persist(kind: "bundle" | "revocation", id: string, value: unknown, configDir?: string): { path: string; created: boolean } { + ensureLabDirs(configDir); + const path = objectPath(kind, id, configDir); + const bytes = Buffer.from(jcsStringify(value), "utf8"); + if (bytes.byteLength > MAX_IMPORT_BYTES) throw new PublicEvidenceValidationError("community_size", "community object exceeds bound"); + let fd: number | null = null; + try { + fd = openSync(path, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | O_NOFOLLOW, 0o600); + writeAll(fd, bytes); fsyncSync(fd); assertRegular(path, fd); closeSync(fd); fd = null; + return { path, created: true }; + } catch (error) { + if (fd !== null) closeSync(fd); + if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error; + if (!readBounded(path).equals(bytes)) throw new PublicEvidenceValidationError("community_conflict", `${kind} id already exists with different bytes`); + return { path, created: false }; + } +} +function readJson(path: string): unknown { try { return JSON.parse(readBounded(path).toString("utf8")); } catch (error) { if (error instanceof PublicEvidenceValidationError) throw error; throw new PublicEvidenceValidationError("community_json", "stored community object is invalid JSON"); } } +function files(configDir?: string): string[] { + ensureLabDirs(configDir); + const names = readdirSync(labCommunityDir(configDir)); + if (names.length > MAX_CACHE_FILES) throw new PublicEvidenceValidationError("community_cache_bound", "community cache file bound exceeded"); + return names.sort(); +} +export function importCommunityEvidenceBundle(raw: unknown, configDir?: string): { created: boolean; status: "cryptographically_valid"; bundleId: string; path: string } { + const parsed = boundedInput(raw); + const verified = verifyPublicEvidenceBundle(parsed); + if (verified.status !== "cryptographically_valid") throw new PublicEvidenceValidationError(verified.status, verified.detail ?? "community bundle verification failed"); + validateCommunityEvidenceAuthorities(verified.bundle); + const stored = persist("bundle", verified.bundle.bundleId, verified.bundle, configDir); + return { ...stored, status: "cryptographically_valid", bundleId: verified.bundle.bundleId }; +} +export function readCommunityEvidenceBundle(bundleId: string, configDir?: string): PublicEvidenceBundleV1 { + const raw = readJson(objectPath("bundle", bundleId, configDir)); + const verified = verifyPublicEvidenceBundle(raw); + if (verified.status !== "cryptographically_valid") throw new PublicEvidenceValidationError(verified.status, verified.detail ?? "stored community bundle invalid"); + return validateCommunityEvidenceAuthorities(verified.bundle); +} +function allCommunityBundles(configDir?: string): PublicEvidenceBundleV1[] { + return files(configDir).filter((name) => /^bundle-[0-9a-f]{64}\.json$/.test(name)).map((name) => readCommunityEvidenceBundle(name.slice(7, 71), configDir)); +} +function findTargetBundle(revocation: unknown, configDir?: string): PublicEvidenceBundleV1 { + if (!revocation || typeof revocation !== "object" || !Array.isArray((revocation as { targets?: unknown }).targets)) throw new PublicEvidenceValidationError("revocation_target", "revocation targets unavailable"); + const ids = new Set((revocation as { targets: Array<{ id?: unknown }> }).targets.map((target) => typeof target?.id === "string" ? target.id : "")); + const candidates = allCommunityBundles(configDir).filter((bundle) => ids.has(bundle.bundleId) || bundle.records.some((record) => ids.has(record.recordId))); + const fullyMatching = candidates.filter((bundle) => (revocation as { targets: Array<{ kind?: unknown; id?: unknown }> }).targets.every((target) => target.kind === "bundle" ? target.id === bundle.bundleId : target.kind === "record" && bundle.records.some((record) => record.recordId === target.id))); + if (fullyMatching.length !== 1) throw new PublicEvidenceValidationError("revocation_target", "revocation targets must resolve to one verified community bundle"); + return fullyMatching[0]!; +} +export function importCommunityEvidenceRevocation(raw: unknown, configDir?: string): { created: boolean; status: "cryptographically_valid"; revocationId: string; path: string } { + const parsed = boundedInput(raw); + const targetBundle = findTargetBundle(parsed, configDir); + const verified = verifyPublicEvidenceRevocation(parsed, targetBundle); + if (verified.status !== "cryptographically_valid") throw new PublicEvidenceValidationError(verified.status, verified.detail ?? "community revocation verification failed"); + const stored = persist("revocation", verified.revocation.revocationId, verified.revocation, configDir); + return { ...stored, status: "cryptographically_valid", revocationId: verified.revocation.revocationId }; +} +function verifiedRevocationsForBundle(bundle: PublicEvidenceBundleV1, configDir?: string): PublicEvidenceRevocationV1[] { + const result: PublicEvidenceRevocationV1[] = []; + for (const name of files(configDir).filter((value) => /^revocation-[0-9a-f]{64}\.json$/.test(value))) { + const raw = readJson(join(labCommunityDir(configDir), name)); + const verified = verifyPublicEvidenceRevocation(raw, bundle); + if (verified.status === "cryptographically_valid") result.push(verified.revocation); + } + return result; +} +export function listCommunityEvidence(configDir?: string): CommunityEvidenceSummaryV1[] { + return allCommunityBundles(configDir).map((bundle) => { + const revoked = new Set(); + for (const revocation of verifiedRevocationsForBundle(bundle, configDir)) { + if (revocation.targets.some((target) => target.kind === "bundle" && target.id === bundle.bundleId)) for (const record of bundle.records) revoked.add(record.recordId); + for (const target of revocation.targets) if (target.kind === "record") revoked.add(target.id); + } + return { trustClass: "community_untrusted_v1" as const, status: "cryptographically_valid" as const, bundleId: bundle.bundleId, publisherKeyId: bundle.publisher.keyId, activeRecordCount: bundle.records.filter((record) => !revoked.has(record.recordId)).length, revokedRecordCount: bundle.records.filter((record) => revoked.has(record.recordId)).length }; + }).sort((a, b) => a.bundleId.localeCompare(b.bundleId)); +} diff --git a/src/lab/public/index.ts b/src/lab/public/index.ts index c2bc113477..d5a2e9817c 100644 --- a/src/lab/public/index.ts +++ b/src/lab/public/index.ts @@ -5,5 +5,12 @@ export * from "./authority"; export * from "./validate"; export * from "./project"; export * from "./bundle"; -export * from "./signing"; +export { + getOrCreatePublicPublisher, + signPublicEvidenceBundle, + verifyPublicEvidenceBundle, +} from "./signing"; export * from "./store"; +export * from "./revocation"; +export * from "./community-authority"; +export * from "./community"; diff --git a/src/lab/public/revocation.ts b/src/lab/public/revocation.ts new file mode 100644 index 0000000000..ea846cf149 --- /dev/null +++ b/src/lab/public/revocation.ts @@ -0,0 +1,81 @@ +import { publicEvidenceId } from "./ids"; +import { decodeCanonicalPublicBase64, signDigestWithPublicPublisherCapability, verifyPublicPublisherSignature } from "./signing"; +import { + PUBLIC_EVIDENCE_REVOCATION_SCHEMA_VERSION, + type PublicEvidenceBundleV1, + type PublicEvidenceRevocationV1, + type PublicPublisherSignerV1, + type PublicPublisherV1, + type PublicRevocationReasonV1, + type PublicRevocationTargetV1, + type PublicRevocationVerificationResult, +} from "./types"; +import { PublicEvidenceValidationError } from "./validate"; + +const REASONS = new Set(["publisher_retracted", "privacy_retraction", "evidence_invalidated", "superseded"]); +const MAX_TARGETS = 256; +function isPlainObject(value: unknown): value is Record { return !!value && typeof value === "object" && !Array.isArray(value); } +function closedKeys(value: Record, keys: readonly string[]): boolean { const allowed = new Set(keys); return Object.keys(value).every((key) => allowed.has(key)) && keys.every((key) => key in value); } +function validId(value: unknown): value is string { return typeof value === "string" && /^[0-9a-f]{64}$/.test(value); } +function validDay(value: unknown): value is string { + if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return false; + const date = new Date(`${value}T00:00:00.000Z`); + return Number.isFinite(date.getTime()) && date.toISOString().slice(0, 10) === value; +} +function targetKey(target: PublicRevocationTargetV1): string { return `${target.kind}:${target.id}`; } +function canonicalTargets(targets: readonly PublicRevocationTargetV1[]): PublicRevocationTargetV1[] { + if (targets.length === 0 || targets.length > MAX_TARGETS) throw new PublicEvidenceValidationError("revocation_targets", "revocation must contain 1..256 targets"); + const normalized = targets.map((target) => { + if ((target.kind !== "bundle" && target.kind !== "record") || !validId(target.id)) throw new PublicEvidenceValidationError("revocation_target", "invalid revocation target"); + return { kind: target.kind, id: target.id } as PublicRevocationTargetV1; + }).sort((a, b) => targetKey(a).localeCompare(targetKey(b))); + if (new Set(normalized.map(targetKey)).size !== normalized.length) throw new PublicEvidenceValidationError("revocation_target_duplicate", "revocation targets must be unique"); + return normalized; +} +function samePublisher(a: PublicPublisherV1, b: PublicPublisherV1): boolean { return a.algorithm === b.algorithm && a.keyId === b.keyId && a.publicKey === b.publicKey; } +function validateTargetsAgainstBundle(targets: readonly PublicRevocationTargetV1[], bundle: PublicEvidenceBundleV1): boolean { + const records = new Set(bundle.records.map((record) => record.recordId)); + return targets.every((target) => target.kind === "bundle" ? target.id === bundle.bundleId : records.has(target.id)); +} +function revocationPayload(issuedDayUtc: string, publisher: PublicPublisherV1, targets: PublicRevocationTargetV1[], reason: PublicRevocationReasonV1): Record { + return { schemaVersion: PUBLIC_EVIDENCE_REVOCATION_SCHEMA_VERSION, issuedDayUtc, publisher, targets, reason }; +} +export function createPublicEvidenceRevocation(input: { + signer: PublicPublisherSignerV1; + targetBundle: PublicEvidenceBundleV1; + issuedDayUtc: string; + targets: PublicRevocationTargetV1[]; + reason: PublicRevocationReasonV1; +}): PublicEvidenceRevocationV1 { + if (!samePublisher(input.signer.publisher, input.targetBundle.publisher)) throw new PublicEvidenceValidationError("revocation_publisher", "revocation publisher must exactly match target bundle publisher"); + if (!validDay(input.issuedDayUtc)) throw new PublicEvidenceValidationError("revocation_day", "issuedDayUtc is invalid"); + if (!REASONS.has(input.reason)) throw new PublicEvidenceValidationError("revocation_reason", "unsupported revocation reason"); + const targets = canonicalTargets(input.targets); + if (!validateTargetsAgainstBundle(targets, input.targetBundle)) throw new PublicEvidenceValidationError("revocation_target", "revocation target is unknown to target bundle"); + const revocationId = publicEvidenceId("revocation", revocationPayload(input.issuedDayUtc, input.signer.publisher, targets, input.reason)); + const signature = signDigestWithPublicPublisherCapability(revocationId, input.signer); + return Object.freeze({ schemaVersion: PUBLIC_EVIDENCE_REVOCATION_SCHEMA_VERSION, revocationId, issuedDayUtc: input.issuedDayUtc, publisher: input.signer.publisher, targets, reason: input.reason, signature: Object.freeze({ algorithm: "ed25519" as const, signedDigest: revocationId, signature }) }); +} +export function verifyPublicEvidenceRevocation(raw: unknown, targetBundle: PublicEvidenceBundleV1): PublicRevocationVerificationResult { + try { + if (!isPlainObject(raw) || !closedKeys(raw, ["schemaVersion", "revocationId", "issuedDayUtc", "publisher", "targets", "reason", "signature"])) return { status: "schema_rejected", detail: "closed revocation schema mismatch" }; + if (raw.schemaVersion !== PUBLIC_EVIDENCE_REVOCATION_SCHEMA_VERSION || !validId(raw.revocationId) || !validDay(raw.issuedDayUtc) || !REASONS.has(raw.reason as PublicRevocationReasonV1)) return { status: "schema_rejected", detail: "revocation version/id/day/reason invalid" }; + if (!isPlainObject(raw.publisher) || !closedKeys(raw.publisher, ["algorithm", "keyId", "publicKey"]) || raw.publisher.algorithm !== "ed25519" || !validId(raw.publisher.keyId) || typeof raw.publisher.publicKey !== "string" || !decodeCanonicalPublicBase64(raw.publisher.publicKey, 1024)) return { status: "schema_rejected", detail: "revocation publisher invalid" }; + const publisher: PublicPublisherV1 = { algorithm: "ed25519", keyId: raw.publisher.keyId, publicKey: raw.publisher.publicKey }; + if (!samePublisher(publisher, targetBundle.publisher)) return { status: "publisher_mismatch", detail: "revocation publisher does not match target bundle" }; + if (!Array.isArray(raw.targets) || raw.targets.length === 0 || raw.targets.length > MAX_TARGETS) return { status: "schema_rejected", detail: "revocation targets invalid" }; + const targets: PublicRevocationTargetV1[] = []; + for (const [index, value] of raw.targets.entries()) { + if (!isPlainObject(value) || !closedKeys(value, ["kind", "id"]) || (value.kind !== "bundle" && value.kind !== "record") || !validId(value.id)) return { status: "schema_rejected", detail: `revocation target ${index} invalid` }; + targets.push({ kind: value.kind, id: value.id }); + } + const canonical = canonicalTargets(targets); + if (canonical.some((target, index) => target.kind !== targets[index]!.kind || target.id !== targets[index]!.id)) return { status: "schema_rejected", detail: "revocation targets must be sorted" }; + if (!validateTargetsAgainstBundle(targets, targetBundle)) return { status: "unknown_target", detail: "revocation target not present in target bundle" }; + if (!isPlainObject(raw.signature) || !closedKeys(raw.signature, ["algorithm", "signedDigest", "signature"]) || raw.signature.algorithm !== "ed25519" || raw.signature.signedDigest !== raw.revocationId || typeof raw.signature.signature !== "string") return { status: "schema_rejected", detail: "revocation signature schema invalid" }; + const expected = publicEvidenceId("revocation", revocationPayload(raw.issuedDayUtc, publisher, targets, raw.reason as PublicRevocationReasonV1)); + if (expected !== raw.revocationId) return { status: "digest_invalid", detail: "revocation id does not match canonical bytes" }; + if (!verifyPublicPublisherSignature(raw.revocationId, publisher, raw.signature.signature)) return { status: "signature_invalid", detail: "revocation signature invalid" }; + return { status: "cryptographically_valid", revocation: { schemaVersion: PUBLIC_EVIDENCE_REVOCATION_SCHEMA_VERSION, revocationId: raw.revocationId, issuedDayUtc: raw.issuedDayUtc, publisher, targets, reason: raw.reason as PublicRevocationReasonV1, signature: { algorithm: "ed25519", signedDigest: raw.revocationId, signature: raw.signature.signature } } }; + } catch (error) { return { status: "schema_rejected", detail: error instanceof Error ? error.message : String(error) }; } +} diff --git a/src/lab/public/signing.ts b/src/lab/public/signing.ts index 6109366d09..0b17dcc0b5 100644 --- a/src/lab/public/signing.ts +++ b/src/lab/public/signing.ts @@ -41,38 +41,24 @@ function writeAll(fd: number, bytes: Uint8Array): void { offset += count; } } - function assertPrivateKeyFile(path: string, fd: number): void { const stats = fstatSync(fd); - if (!stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1 || stats.size > MAX_PRIVATE_KEY_BYTES) { - throw new PublicEvidenceValidationError("publisher_key_unsafe", "publisher key target is not a bounded regular file"); - } - if (process.platform !== "win32") { - const mode = stats.mode & 0o777; - if (mode !== 0o600) chmodSync(path, 0o600); - } + if (!stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1 || stats.size > MAX_PRIVATE_KEY_BYTES) throw new PublicEvidenceValidationError("publisher_key_unsafe", "publisher key target is not a bounded regular file"); + if (process.platform !== "win32" && (stats.mode & 0o777) !== 0o600) chmodSync(path, 0o600); } - function loadPrivateKey(path: string): KeyObject { const before = lstatSync(path); - if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1) { - throw new PublicEvidenceValidationError("publisher_key_unsafe", "publisher key path is unsafe"); - } + if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1) throw new PublicEvidenceValidationError("publisher_key_unsafe", "publisher key path is unsafe"); const fd = openSync(path, fsConstants.O_RDONLY | O_NOFOLLOW); try { assertPrivateKeyFile(path, fd); const pem = readFileSync(fd, { encoding: "utf8" }); - if (Buffer.byteLength(pem) > MAX_PRIVATE_KEY_BYTES || !pem.includes("BEGIN PRIVATE KEY")) { - throw new PublicEvidenceValidationError("publisher_key_invalid", "publisher key encoding invalid"); - } + if (Buffer.byteLength(pem) > MAX_PRIVATE_KEY_BYTES || !pem.includes("BEGIN PRIVATE KEY")) throw new PublicEvidenceValidationError("publisher_key_invalid", "publisher key encoding invalid"); const key = createPrivateKey(pem); if (key.asymmetricKeyType !== "ed25519") throw new PublicEvidenceValidationError("publisher_key_invalid", "publisher key must be Ed25519"); return key; - } finally { - closeSync(fd); - } + } finally { closeSync(fd); } } - function createPrivateKeyFile(path: string): KeyObject { const { privateKey } = generateKeyPairSync("ed25519"); const pem = privateKey.export({ type: "pkcs8", format: "pem" }).toString(); @@ -86,29 +72,20 @@ function createPrivateKeyFile(path: string): KeyObject { } catch (error) { if ((error as NodeJS.ErrnoException).code === "EEXIST") return loadPrivateKey(path); throw error; - } finally { - if (fd !== null) closeSync(fd); - } + } finally { if (fd !== null) closeSync(fd); } } - function publisherForPrivateKey(privateKey: KeyObject): PublicPublisherV1 { const privatePem = privateKey.export({ type: "pkcs8", format: "pem" }); const publicKey = createPublicKey(privatePem); const der = publicKey.export({ type: "spki", format: "der" }); - return Object.freeze({ - algorithm: "ed25519" as const, - keyId: publicPublisherKeyId(der), - publicKey: der.toString("base64"), - }); + return Object.freeze({ algorithm: "ed25519" as const, keyId: publicPublisherKeyId(der), publicKey: der.toString("base64") }); } - export function getOrCreatePublicPublisher(configDir?: string): PublicPublisherSignerV1 { ensureLabDirs(configDir); const path = labPublicPublisherKeyPath(configDir); let key: KeyObject; - try { - key = loadPrivateKey(path); - } catch (error) { + try { key = loadPrivateKey(path); } + catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; key = createPrivateKeyFile(path); } @@ -117,99 +94,62 @@ export function getOrCreatePublicPublisher(configDir?: string): PublicPublisherS return signer; } -function signedPayload(unsigned: PublicEvidenceBundleUnsignedV1, publisher: PublicPublisherV1): Record { - return { - schemaVersion: unsigned.schemaVersion, - exportPolicyVersion: unsigned.exportPolicyVersion, - bundleId: unsigned.bundleId, - createdDayUtc: unsigned.createdDayUtc, - publisher, - records: unsigned.records, - artifacts: unsigned.artifacts, - }; -} - -export function signPublicEvidenceBundle( - rawUnsigned: PublicEvidenceBundleUnsignedV1, - signer: PublicPublisherSignerV1, -): PublicEvidenceBundleV1 { +/** Internal CL-10 primitive. Not re-exported by the public barrel. */ +export function signDigestWithPublicPublisherCapability(digest: string, signer: PublicPublisherSignerV1): string { + if (!/^[0-9a-f]{64}$/.test(digest)) throw new PublicEvidenceValidationError("invalid_signing_digest", "signing digest must be lowercase sha256 hex"); const privateKey = signerKeys.get(signer as object); if (!privateKey) throw new PublicEvidenceValidationError("publisher_signer_untrusted", "publisher signer is not a live local signer capability"); - const unsigned = validatePublicEvidenceBundleUnsigned(rawUnsigned); - const digest = publicBundleDigest(signedPayload(unsigned, signer.publisher)); - const signature = cryptoSign(null, Buffer.from(digest, "hex"), privateKey).toString("base64"); - return Object.freeze({ - ...unsigned, - publisher: signer.publisher, - bundleDigest: digest, - signature: Object.freeze({ algorithm: "ed25519" as const, signedDigest: digest, signature }), - }); + return cryptoSign(null, Buffer.from(digest, "hex"), privateKey).toString("base64"); } -function isPlainObject(value: unknown): value is Record { - return !!value && typeof value === "object" && !Array.isArray(value); +function signedPayload(unsigned: PublicEvidenceBundleUnsignedV1, publisher: PublicPublisherV1): Record { + return { schemaVersion: unsigned.schemaVersion, exportPolicyVersion: unsigned.exportPolicyVersion, bundleId: unsigned.bundleId, createdDayUtc: unsigned.createdDayUtc, publisher, records: unsigned.records, artifacts: unsigned.artifacts }; } -function closedKeys(value: Record, keys: readonly string[]): boolean { - const allowed = new Set(keys); - return Object.keys(value).every((key) => allowed.has(key)) && keys.every((key) => key in value); +export function signPublicEvidenceBundle(rawUnsigned: PublicEvidenceBundleUnsignedV1, signer: PublicPublisherSignerV1): PublicEvidenceBundleV1 { + const unsigned = validatePublicEvidenceBundleUnsigned(rawUnsigned); + const digest = publicBundleDigest(signedPayload(unsigned, signer.publisher)); + const signature = signDigestWithPublicPublisherCapability(digest, signer); + return Object.freeze({ ...unsigned, publisher: signer.publisher, bundleDigest: digest, signature: Object.freeze({ algorithm: "ed25519" as const, signedDigest: digest, signature }) }); } -function canonicalBase64(value: unknown, maxBytes: number): Buffer | null { +function isPlainObject(value: unknown): value is Record { return !!value && typeof value === "object" && !Array.isArray(value); } +function closedKeys(value: Record, keys: readonly string[]): boolean { const allowed = new Set(keys); return Object.keys(value).every((key) => allowed.has(key)) && keys.every((key) => key in value); } +export function decodeCanonicalPublicBase64(value: unknown, maxBytes: number): Buffer | null { if (typeof value !== "string" || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) return null; const bytes = Buffer.from(value, "base64"); if (bytes.byteLength > maxBytes || bytes.toString("base64") !== value) return null; return bytes; } - +export function verifyPublicPublisherSignature(digest: string, publisher: PublicPublisherV1, signature: string): boolean { + if (!/^[0-9a-f]{64}$/.test(digest)) return false; + const publicKeyDer = decodeCanonicalPublicBase64(publisher.publicKey, 1024); + const signatureBytes = decodeCanonicalPublicBase64(signature, 128); + if (!publicKeyDer || publicPublisherKeyId(publicKeyDer) !== publisher.keyId || !signatureBytes || signatureBytes.byteLength !== 64) return false; + try { + const publicKey = createPublicKey({ key: publicKeyDer, format: "der", type: "spki" }); + return publicKey.asymmetricKeyType === "ed25519" && cryptoVerify(null, Buffer.from(digest, "hex"), publicKey, signatureBytes); + } catch { return false; } +} export function verifyPublicEvidenceBundle(raw: unknown): PublicBundleVerificationResult { try { - if (!isPlainObject(raw) || !closedKeys(raw, ["schemaVersion", "exportPolicyVersion", "bundleId", "createdDayUtc", "publisher", "records", "artifacts", "bundleDigest", "signature"])) { - return { status: "schema_rejected", detail: "closed bundle schema mismatch" }; - } - const unsignedRaw = { - schemaVersion: raw.schemaVersion, - exportPolicyVersion: raw.exportPolicyVersion, - bundleId: raw.bundleId, - createdDayUtc: raw.createdDayUtc, - records: raw.records, - artifacts: raw.artifacts, - }; + if (!isPlainObject(raw) || !closedKeys(raw, ["schemaVersion", "exportPolicyVersion", "bundleId", "createdDayUtc", "publisher", "records", "artifacts", "bundleDigest", "signature"])) return { status: "schema_rejected", detail: "closed bundle schema mismatch" }; + const unsignedRaw = { schemaVersion: raw.schemaVersion, exportPolicyVersion: raw.exportPolicyVersion, bundleId: raw.bundleId, createdDayUtc: raw.createdDayUtc, records: raw.records, artifacts: raw.artifacts }; let unsigned: PublicEvidenceBundleUnsignedV1; - try { - unsigned = validatePublicEvidenceBundleUnsigned(unsignedRaw); - } catch (error) { + try { unsigned = validatePublicEvidenceBundleUnsigned(unsignedRaw); } + catch (error) { if (error instanceof PublicEvidenceValidationError && error.code === "bundle_digest") return { status: "digest_invalid", detail: error.message }; return { status: "schema_rejected", detail: error instanceof Error ? error.message : String(error) }; } - if (!isPlainObject(raw.publisher) || !closedKeys(raw.publisher, ["algorithm", "keyId", "publicKey"]) || raw.publisher.algorithm !== "ed25519") { - return { status: "schema_rejected", detail: "publisher schema mismatch" }; - } - if (typeof raw.publisher.keyId !== "string" || !/^[0-9a-f]{64}$/.test(raw.publisher.keyId)) return { status: "schema_rejected", detail: "publisher key id invalid" }; - const publicKeyDer = canonicalBase64(raw.publisher.publicKey, 1024); - if (!publicKeyDer || publicPublisherKeyId(publicKeyDer) !== raw.publisher.keyId) return { status: "schema_rejected", detail: "publisher public key invalid" }; - const publisher: PublicPublisherV1 = { algorithm: "ed25519", keyId: raw.publisher.keyId, publicKey: raw.publisher.publicKey as string }; + if (!isPlainObject(raw.publisher) || !closedKeys(raw.publisher, ["algorithm", "keyId", "publicKey"]) || raw.publisher.algorithm !== "ed25519") return { status: "schema_rejected", detail: "publisher schema mismatch" }; + if (typeof raw.publisher.keyId !== "string" || !/^[0-9a-f]{64}$/.test(raw.publisher.keyId) || typeof raw.publisher.publicKey !== "string") return { status: "schema_rejected", detail: "publisher key invalid" }; + const publisher: PublicPublisherV1 = { algorithm: "ed25519", keyId: raw.publisher.keyId, publicKey: raw.publisher.publicKey }; + const publicKeyDer = decodeCanonicalPublicBase64(publisher.publicKey, 1024); + if (!publicKeyDer || publicPublisherKeyId(publicKeyDer) !== publisher.keyId) return { status: "schema_rejected", detail: "publisher public key invalid" }; if (typeof raw.bundleDigest !== "string" || !/^[0-9a-f]{64}$/.test(raw.bundleDigest)) return { status: "schema_rejected", detail: "bundle digest invalid" }; - if (!isPlainObject(raw.signature) || !closedKeys(raw.signature, ["algorithm", "signedDigest", "signature"]) || raw.signature.algorithm !== "ed25519") return { status: "schema_rejected", detail: "signature schema mismatch" }; + if (!isPlainObject(raw.signature) || !closedKeys(raw.signature, ["algorithm", "signedDigest", "signature"]) || raw.signature.algorithm !== "ed25519" || typeof raw.signature.signature !== "string") return { status: "schema_rejected", detail: "signature schema mismatch" }; if (raw.signature.signedDigest !== raw.bundleDigest) return { status: "digest_invalid", detail: "signed digest does not match bundle digest" }; const expectedDigest = publicBundleDigest(signedPayload(unsigned, publisher)); if (expectedDigest !== raw.bundleDigest) return { status: "digest_invalid", detail: "canonical bundle digest mismatch" }; - const signatureBytes = canonicalBase64(raw.signature.signature, 128); - if (!signatureBytes || signatureBytes.byteLength !== 64) return { status: "signature_invalid", detail: "signature encoding invalid" }; - let publicKey: KeyObject; - try { - publicKey = createPublicKey({ key: publicKeyDer, format: "der", type: "spki" }); - } catch { - return { status: "schema_rejected", detail: "publisher public key parse failed" }; - } - if (publicKey.asymmetricKeyType !== "ed25519") return { status: "schema_rejected", detail: "publisher key algorithm mismatch" }; - if (!cryptoVerify(null, Buffer.from(raw.bundleDigest, "hex"), publicKey, signatureBytes)) return { status: "signature_invalid", detail: "Ed25519 signature verification failed" }; - const bundle: PublicEvidenceBundleV1 = { - ...unsigned, - publisher, - bundleDigest: raw.bundleDigest, - signature: { algorithm: "ed25519", signedDigest: raw.bundleDigest, signature: raw.signature.signature as string }, - }; - return { status: "cryptographically_valid", bundle }; - } catch (error) { - return { status: "schema_rejected", detail: error instanceof Error ? error.message : String(error) }; - } + if (!verifyPublicPublisherSignature(raw.bundleDigest, publisher, raw.signature.signature)) return { status: "signature_invalid", detail: "Ed25519 signature verification failed" }; + return { status: "cryptographically_valid", bundle: { ...unsigned, publisher, bundleDigest: raw.bundleDigest, signature: { algorithm: "ed25519", signedDigest: raw.bundleDigest, signature: raw.signature.signature } } }; + } catch (error) { return { status: "schema_rejected", detail: error instanceof Error ? error.message : String(error) }; } } diff --git a/src/lab/public/types.ts b/src/lab/public/types.ts index 75593b9c23..2b4f0c12de 100644 --- a/src/lab/public/types.ts +++ b/src/lab/public/types.ts @@ -2,6 +2,7 @@ import type { CompatibilityVerdict, EvidenceLayer } from "../constants"; export const PUBLIC_ROUTE_REGISTRY_SCHEMA_VERSION = "public_route_registry_v1" as const; export const PUBLIC_EVIDENCE_BUNDLE_SCHEMA_VERSION = "public_evidence_bundle_v1" as const; +export const PUBLIC_EVIDENCE_REVOCATION_SCHEMA_VERSION = "public_evidence_revocation_v1" as const; export const PUBLIC_EXPORT_POLICY_VERSION = "public_export_policy_v1" as const; export const PUBLIC_ADAPTER_FAMILIES = ["openai-responses", "openai-chat", "anthropic-messages"] as const; @@ -98,6 +99,31 @@ export interface PublicPublisherSignerV1 { publisher: PublicPublisherV1; } export type PublicBundleVerificationResult = | { status: "cryptographically_valid"; bundle: PublicEvidenceBundleV1 } | { status: "schema_rejected" | "digest_invalid" | "signature_invalid"; detail?: string }; + +export type PublicRevocationReasonV1 = "publisher_retracted" | "privacy_retraction" | "evidence_invalidated" | "superseded"; +export interface PublicRevocationTargetV1 { kind: "bundle" | "record"; id: string; } +export interface PublicEvidenceRevocationV1 { + schemaVersion: typeof PUBLIC_EVIDENCE_REVOCATION_SCHEMA_VERSION; + revocationId: string; + issuedDayUtc: string; + publisher: PublicPublisherV1; + targets: PublicRevocationTargetV1[]; + reason: PublicRevocationReasonV1; + signature: PublicBundleSignatureV1; +} +export type PublicRevocationVerificationResult = + | { status: "cryptographically_valid"; revocation: PublicEvidenceRevocationV1 } + | { status: "schema_rejected" | "digest_invalid" | "signature_invalid" | "publisher_mismatch" | "unknown_target"; detail?: string }; + +export interface CommunityEvidenceSummaryV1 { + trustClass: "community_untrusted_v1"; + status: "cryptographically_valid"; + bundleId: string; + publisherKeyId: string; + activeRecordCount: number; + revokedRecordCount: number; +} + export type PublicProjectionNotExportableReason = "private_route_identity" | "unsupported_public_adapter" | "task_authority_unavailable" | "invalid_public_incident_ref"; export type PublicProjectionResult = { status: "exportable"; record: PublicEvidenceRecordV1 } | { status: "not_exportable"; reason: PublicProjectionNotExportableReason }; export interface PublicRouteAuthorityV1 { localSubjectId: string; descriptor: PublicRouteSubjectV1; } From 812a3a84b269ea16dd6522499c4a35495d2a008b Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:39:02 +0200 Subject: [PATCH 11/33] test(lab): use canonical public scenario versions --- tests/lab-community-evidence.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/lab-community-evidence.test.ts b/tests/lab-community-evidence.test.ts index dee1c2b11e..43a89e31be 100644 --- a/tests/lab-community-evidence.test.ts +++ b/tests/lab-community-evidence.test.ts @@ -57,10 +57,10 @@ function protocolObservation(scenarioId = "responses-core.protocol.request-shape producerVersion: "2.13.0", evidenceLayer: "protocol_conformance" as const, scenarioId, - scenarioVersion: "1", + scenarioVersion: "1.0.0", scenarioManifestDigest: hex("scenario"), suiteId: "responses-core", - suiteVersion: "1", + suiteVersion: "1.0.0", suiteManifestDigest: hex("suite"), fixtureDigests: [hex("fixture")], subject, From 15351fa3dc2c1337be3822b6bee2b53364f08a5b Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:41:41 +0200 Subject: [PATCH 12/33] test(lab): define CL-10 purge export interaction --- tests/lab-community-evidence.test.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/lab-community-evidence.test.ts b/tests/lab-community-evidence.test.ts index 43a89e31be..36f1af1898 100644 --- a/tests/lab-community-evidence.test.ts +++ b/tests/lab-community-evidence.test.ts @@ -8,6 +8,7 @@ import { assignEventId, labLedgerPath, labSqlitePath, + purgeSensitiveEvidence, subjectIdForSubject, type ObservationEvent, type ProtocolSubjectV1, @@ -21,6 +22,7 @@ import { projectPublicEvidence, signPublicEvidenceBundle, verifyPublicEvidenceRevocation, + writePublicEvidenceBundle, } from "../src/lab/public"; const roots: string[] = []; @@ -163,4 +165,26 @@ describe("CL-10 community quarantine", () => { const conflict = { ...revocation, issuedDayUtc: "2026-08-13" }; expect(() => importCommunityEvidenceRevocation(conflict, consumerDir)).toThrow(); }); + + test("sensitive export purge removes local exports and local community copies but preserves third-party bundles", () => { + const consumerDir = configDir("ocx-cl10-consumer-"); + const thirdPartyDir = configDir("ocx-cl10-third-party-"); + const localBundle = signedBundle(consumerDir); + const localStored = writePublicEvidenceBundle(localBundle, consumerDir); + importCommunityEvidenceBundle(localBundle, consumerDir); + + const thirdPartyBundle = signedBundle(thirdPartyDir); + importCommunityEvidenceBundle(thirdPartyBundle, consumerDir); + expect(listCommunityEvidence(consumerDir)).toHaveLength(2); + + purgeSensitiveEvidence({ + configDir: consumerDir, + targetArtifactDigests: [hex("sensitive-purge-target")], + purgeActions: ["export"], + recordedAt: Date.UTC(2026, 7, 12, 18, 0, 0), + }); + + expect(existsSync(localStored.path)).toBe(false); + expect(listCommunityEvidence(consumerDir).map((row) => row.bundleId)).toEqual([thirdPartyBundle.bundleId]); + }); }); From 84e2d5a71ffb5078107705cef77adf7d3c0946cb Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:43:55 +0200 Subject: [PATCH 13/33] test(lab): isolate CL-10 purge provenance case --- tests/lab-community-evidence.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lab-community-evidence.test.ts b/tests/lab-community-evidence.test.ts index 36f1af1898..f237b62a5e 100644 --- a/tests/lab-community-evidence.test.ts +++ b/tests/lab-community-evidence.test.ts @@ -173,7 +173,7 @@ describe("CL-10 community quarantine", () => { const localStored = writePublicEvidenceBundle(localBundle, consumerDir); importCommunityEvidenceBundle(localBundle, consumerDir); - const thirdPartyBundle = signedBundle(thirdPartyDir); + const thirdPartyBundle = signedBundle(thirdPartyDir, "responses-core.protocol.sse-framing"); importCommunityEvidenceBundle(thirdPartyBundle, consumerDir); expect(listCommunityEvidence(consumerDir)).toHaveLength(2); From 74d79b57e860f8aadb81c9ab6245b14f2f9b21e5 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:45:57 +0200 Subject: [PATCH 14/33] feat(lab): purge locally-originated public evidence copies --- src/lab/public/purge.ts | 144 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 src/lab/public/purge.ts diff --git a/src/lab/public/purge.ts b/src/lab/public/purge.ts new file mode 100644 index 0000000000..f8cb1eb0d1 --- /dev/null +++ b/src/lab/public/purge.ts @@ -0,0 +1,144 @@ +import { createPrivateKey, createPublicKey } from "node:crypto"; +import { + closeSync, + constants as fsConstants, + existsSync, + fstatSync, + lstatSync, + openSync, + readFileSync, + readdirSync, + rmSync, + unlinkSync, +} from "node:fs"; +import { join } from "node:path"; +import { + ensureLabDirs, + labCommunityDir, + labPublicExportsDir, + labPublicPublisherKeyPath, +} from "../paths"; +import { readCommunityEvidenceBundle } from "./community"; +import { publicPublisherKeyId } from "./ids"; +import { PublicEvidenceValidationError } from "./validate"; + +const O_NOFOLLOW = (fsConstants as { O_NOFOLLOW?: number }).O_NOFOLLOW ?? 0; +const MAX_PRIVATE_KEY_BYTES = 8 * 1024; +const EXPORT_FILE_RE = /^([0-9a-f]{64})\.json$/; +const COMMUNITY_BUNDLE_RE = /^bundle-([0-9a-f]{64})\.json$/; + +function readExistingPublisherKeyId(configDir?: string): string | null { + const path = labPublicPublisherKeyPath(configDir); + if (!existsSync(path)) return null; + + const before = lstatSync(path); + if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1 || before.size > MAX_PRIVATE_KEY_BYTES) { + throw new PublicEvidenceValidationError( + "publisher_key_unsafe", + "cannot establish local publisher provenance from an unsafe publisher key file", + ); + } + if (process.platform !== "win32" && (before.mode & 0o777) !== 0o600) { + throw new PublicEvidenceValidationError( + "publisher_key_unsafe", + "cannot establish local publisher provenance from an incorrectly-permissioned publisher key file", + ); + } + + const fd = openSync(path, fsConstants.O_RDONLY | O_NOFOLLOW); + try { + const stats = fstatSync(fd); + if (!stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1 || stats.size > MAX_PRIVATE_KEY_BYTES) { + throw new PublicEvidenceValidationError( + "publisher_key_unsafe", + "publisher key changed while establishing local public-evidence provenance", + ); + } + const pem = readFileSync(fd, { encoding: "utf8" }); + if (Buffer.byteLength(pem) > MAX_PRIVATE_KEY_BYTES || !pem.includes("BEGIN PRIVATE KEY")) { + throw new PublicEvidenceValidationError("publisher_key_invalid", "local publisher key encoding is invalid"); + } + const privateKey = createPrivateKey(pem); + if (privateKey.asymmetricKeyType !== "ed25519") { + throw new PublicEvidenceValidationError("publisher_key_invalid", "local publisher key is not Ed25519"); + } + const privatePem = privateKey.export({ type: "pkcs8", format: "pem" }); + const publicKey = createPublicKey(privatePem); + const publicKeyDer = publicKey.export({ type: "spki", format: "der" }); + return publicPublisherKeyId(publicKeyDer); + } finally { + closeSync(fd); + } +} + +function localExportBundleIds(configDir?: string): Set { + const ids = new Set(); + for (const entry of readdirSync(labPublicExportsDir(configDir), { withFileTypes: true })) { + const match = EXPORT_FILE_RE.exec(entry.name); + if (match) ids.add(match[1]!); + } + return ids; +} + +function purgeAllExports(configDir?: string): number { + const exportsDir = labPublicExportsDir(configDir); + let deleted = 0; + for (const entry of readdirSync(exportsDir, { withFileTypes: true })) { + rmSync(join(exportsDir, entry.name), { recursive: entry.isDirectory(), force: true }); + deleted++; + } + return deleted; +} + +/** + * CL-10 sensitive-purge bridge. + * + * The pre-CL-10 purge contract already removes every local export when the + * `export` action is selected. CL-10 additionally removes quarantined community + * bundle copies that can be proven locally-originated, either because their + * content id is currently present in the local export store or because their + * publisher key is this installation's existing publisher key. + * + * Third-party community evidence is deliberately preserved. When local + * provenance cannot be inspected safely, this function fails closed rather than + * pretending the purge completed. + */ +export function purgeLocalPublicEvidenceCopies(configDir?: string): { + deletedExports: number; + deletedCommunityBundles: number; +} { + ensureLabDirs(configDir); + const exportedBundleIds = localExportBundleIds(configDir); + const localPublisherKeyId = readExistingPublisherKeyId(configDir); + const communityDir = labCommunityDir(configDir); + + let deletedCommunityBundles = 0; + for (const entry of readdirSync(communityDir, { withFileTypes: true })) { + const match = COMMUNITY_BUNDLE_RE.exec(entry.name); + if (!match) continue; + const bundleId = match[1]!; + + let locallyOriginated = exportedBundleIds.has(bundleId); + if (!locallyOriginated && localPublisherKeyId) { + const bundle = readCommunityEvidenceBundle(bundleId, configDir); + locallyOriginated = bundle.publisher.keyId === localPublisherKeyId; + } + if (!locallyOriginated) continue; + + const path = join(communityDir, entry.name); + const before = lstatSync(path); + if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1) { + throw new PublicEvidenceValidationError( + "community_unsafe_target", + `refusing to purge unsafe locally-originated community bundle path: ${entry.name}`, + ); + } + unlinkSync(path); + deletedCommunityBundles++; + } + + return { + deletedExports: purgeAllExports(configDir), + deletedCommunityBundles, + }; +} From f11d6000e2fb9ec6c231482cc2da7ac218c9b15c Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:46:28 +0200 Subject: [PATCH 15/33] feat(lab): include CL-10 copies in sensitive export purge --- src/lab/ledger/purge.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lab/ledger/purge.ts b/src/lab/ledger/purge.ts index dd6b95853f..0f5d89bc91 100644 --- a/src/lab/ledger/purge.ts +++ b/src/lab/ledger/purge.ts @@ -13,6 +13,7 @@ import { } from "../constants"; import type { LabEvent, PurgeTombstoneEvent } from "../events/types"; import { assignEventId, validateLabEvent } from "../events/validate"; +import { purgeLocalPublicEvidenceCopies } from "../public/purge"; import { artifactDeletionPlan, expandSensitiveArtifactEventTargets, @@ -195,7 +196,7 @@ export function purgeSensitiveEvidence(req: SensitivePurgeRequest): PurgeTombsto completed.push("scratch"); } if (purgeActions.includes("export")) { - purgeBoundedDirectory(paths.exportDir); + purgeLocalPublicEvidenceCopies(req.configDir); completed.push("export"); } From f86b89cc2370b0421f7eab2f4f8983448b609532 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:47:45 +0200 Subject: [PATCH 16/33] test(lab): define cross-publisher community continuity --- ...lab-community-publisher-continuity.test.ts | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 tests/lab-community-publisher-continuity.test.ts diff --git a/tests/lab-community-publisher-continuity.test.ts b/tests/lab-community-publisher-continuity.test.ts new file mode 100644 index 0000000000..8a51937f2d --- /dev/null +++ b/tests/lab-community-publisher-continuity.test.ts @@ -0,0 +1,119 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { + LAB_EVENT_SCHEMA_VERSION, + LAB_PRODUCER, + assignEventId, + subjectIdForSubject, + type ObservationEvent, + type ProtocolSubjectV1, +} from "../src/lab"; +import { + createPublicEvidenceRevocation, + getOrCreatePublicPublisher, + importCommunityEvidenceBundle, + importCommunityEvidenceRevocation, + listCommunityEvidence, + projectPublicEvidence, + signPublicEvidenceBundle, +} from "../src/lab/public"; + +const roots: string[] = []; +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); +}); + +function configDir(prefix: string): string { + const root = mkdtempSync(join(tmpdir(), prefix)); + roots.push(root); + return root; +} + +function hex(seed: string): string { + return Bun.CryptoHasher.hash("sha256", seed, "hex"); +} + +function observation(): ObservationEvent { + const subject: ProtocolSubjectV1 = { + subjectSchemaVersion: 1, + subjectKind: "protocol", + opencodexCompatibilityVersion: "2.13.0", + effectiveAdapter: "openai-chat", + inboundProtocol: "openai-responses", + upstreamProtocol: "openai-chat", + surface: "responses-http", + behaviorFingerprint: hex("PRIVATE-publisher-continuity"), + }; + return assignEventId({ + schemaVersion: LAB_EVENT_SCHEMA_VERSION, + eventKind: "observation" as const, + recordedAt: Date.UTC(2026, 7, 12, 14, 37, 48), + producer: LAB_PRODUCER, + producerVersion: "2.13.0", + evidenceLayer: "protocol_conformance" as const, + scenarioId: "responses-core.protocol.request-shape", + scenarioVersion: "1.0.0", + scenarioManifestDigest: hex("scenario"), + suiteId: "responses-core", + suiteVersion: "1.0.0", + suiteManifestDigest: hex("suite"), + fixtureDigests: [hex("fixture")], + subject, + subjectId: subjectIdForSubject(subject), + startedAt: Date.UTC(2026, 7, 12, 14, 37, 40), + completedAt: Date.UTC(2026, 7, 12, 14, 37, 41), + executionMode: "fixture" as const, + attempt: 1, + limits: { totalTimeoutMs: 1000 }, + outcome: "pass" as const, + assertions: [{ id: "request-shape", operator: "equals", required: true, passed: true }], + environment: {}, + artifactRefs: [], + }) as ObservationEvent; +} + +function unsignedBundle() { + return projectPublicEvidence({ + createdDayUtc: "2026-08-12", + records: [{ observation: observation(), verdict: "VERIFIED" }], + }).bundle; +} + +describe("CL-10 publisher continuity", () => { + test("same content from two publishers coexists and revokes independently", () => { + const publisherA = configDir("ocx-cl10-publisher-a-"); + const publisherB = configDir("ocx-cl10-publisher-b-"); + const consumer = configDir("ocx-cl10-consumer-"); + const signerA = getOrCreatePublicPublisher(publisherA); + const signerB = getOrCreatePublicPublisher(publisherB); + const unsigned = unsignedBundle(); + const bundleA = signPublicEvidenceBundle(unsigned, signerA); + const bundleB = signPublicEvidenceBundle(unsigned, signerB); + + expect(bundleA.bundleId).toBe(bundleB.bundleId); + expect(bundleA.publisher.keyId).not.toBe(bundleB.publisher.keyId); + expect(importCommunityEvidenceBundle(bundleA, consumer).created).toBe(true); + expect(importCommunityEvidenceBundle(bundleB, consumer).created).toBe(true); + + let summaries = listCommunityEvidence(consumer); + expect(summaries).toHaveLength(2); + expect(new Set(summaries.map((row) => row.publisherKeyId)).size).toBe(2); + + const revocationA = createPublicEvidenceRevocation({ + signer: signerA, + targetBundle: bundleA, + issuedDayUtc: "2026-08-12", + reason: "publisher_retracted", + targets: [{ kind: "bundle", id: bundleA.bundleId }], + }); + expect(importCommunityEvidenceRevocation(revocationA, consumer).created).toBe(true); + + summaries = listCommunityEvidence(consumer); + const rowA = summaries.find((row) => row.publisherKeyId === bundleA.publisher.keyId)!; + const rowB = summaries.find((row) => row.publisherKeyId === bundleB.publisher.keyId)!; + expect(rowA).toMatchObject({ activeRecordCount: 0, revokedRecordCount: 1 }); + expect(rowB).toMatchObject({ activeRecordCount: 1, revokedRecordCount: 0 }); + }); +}); From a4cb6399292659f20f2316cb98226f5a1d896408 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:48:10 +0200 Subject: [PATCH 17/33] ci: cover CL-10 publisher continuity --- .github/workflows/cl10-focus.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cl10-focus.yml b/.github/workflows/cl10-focus.yml index f6c53f4bc7..c68413b2b7 100644 --- a/.github/workflows/cl10-focus.yml +++ b/.github/workflows/cl10-focus.yml @@ -17,5 +17,5 @@ jobs: with: bun-version: 1.3.14 - run: bun install --frozen-lockfile - - run: bun test tests/lab-public-evidence.test.ts tests/lab-public-evidence-signature.test.ts tests/lab-community-evidence.test.ts + - run: bun test tests/lab-public-evidence.test.ts tests/lab-public-evidence-signature.test.ts tests/lab-community-evidence.test.ts tests/lab-community-publisher-continuity.test.ts - run: bun x tsc --noEmit From 489e4da75e265bfdc0b1bf9906c797d7fcd4b9d1 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:51:15 +0200 Subject: [PATCH 18/33] fix(lab): scope community bundles by publisher continuity --- src/lab/public/community.ts | 308 ++++++++++++++++++++++++++++++------ 1 file changed, 261 insertions(+), 47 deletions(-) diff --git a/src/lab/public/community.ts b/src/lab/public/community.ts index 486a0bbbf7..cccee24339 100644 --- a/src/lab/public/community.ts +++ b/src/lab/public/community.ts @@ -29,125 +29,339 @@ const MAX_OBJECT_KEYS = 64; const MAX_ARRAY_ELEMENTS = 512; const MAX_GENERIC_STRING_BYTES = 384 * 1024; const O_NOFOLLOW = (fsConstants as { O_NOFOLLOW?: number }).O_NOFOLLOW ?? 0; +const COMMUNITY_BUNDLE_FILE_RE = /^bundle-([0-9a-f]{64})-([0-9a-f]{64})\.json$/; +const COMMUNITY_REVOCATION_FILE_RE = /^revocation-([0-9a-f]{64})\.json$/; + +function assertId(value: string): string { + if (!/^[0-9a-f]{64}$/.test(value)) { + throw new PublicEvidenceValidationError("community_id", "community object id invalid"); + } + return value; +} -function assertId(value: string): string { if (!/^[0-9a-f]{64}$/.test(value)) throw new PublicEvidenceValidationError("community_id", "community object id invalid"); return value; } function scanStructure(value: unknown, depth = 0): void { - if (depth > MAX_DEPTH) throw new PublicEvidenceValidationError("community_depth", "community JSON nesting depth exceeded"); + if (depth > MAX_DEPTH) { + throw new PublicEvidenceValidationError("community_depth", "community JSON nesting depth exceeded"); + } if (typeof value === "string") { - if (new TextEncoder().encode(value).byteLength > MAX_GENERIC_STRING_BYTES || value.includes("\0")) throw new PublicEvidenceValidationError("community_string", "community string invalid or oversized"); + if (new TextEncoder().encode(value).byteLength > MAX_GENERIC_STRING_BYTES || value.includes("\0")) { + throw new PublicEvidenceValidationError("community_string", "community string invalid or oversized"); + } return; } if (Array.isArray(value)) { - if (value.length > MAX_ARRAY_ELEMENTS) throw new PublicEvidenceValidationError("community_array", "community array bound exceeded"); + if (value.length > MAX_ARRAY_ELEMENTS) { + throw new PublicEvidenceValidationError("community_array", "community array bound exceeded"); + } for (const item of value) scanStructure(item, depth + 1); return; } if (value && typeof value === "object") { const keys = Object.keys(value); - if (keys.length > MAX_OBJECT_KEYS) throw new PublicEvidenceValidationError("community_object", "community object key bound exceeded"); - for (const key of keys) { if (new TextEncoder().encode(key).byteLength > 4096) throw new PublicEvidenceValidationError("community_key", "community key oversized"); scanStructure((value as Record)[key], depth + 1); } + if (keys.length > MAX_OBJECT_KEYS) { + throw new PublicEvidenceValidationError("community_object", "community object key bound exceeded"); + } + for (const key of keys) { + if (new TextEncoder().encode(key).byteLength > 4096) { + throw new PublicEvidenceValidationError("community_key", "community key oversized"); + } + scanStructure((value as Record)[key], depth + 1); + } } } + function boundedInput(raw: unknown): unknown { if (raw instanceof Uint8Array || typeof raw === "string") { const bytes = typeof raw === "string" ? Buffer.from(raw, "utf8") : Buffer.from(raw); - if (bytes.byteLength > MAX_IMPORT_BYTES) throw new PublicEvidenceValidationError("community_size", "community import exceeds 2 MiB"); + if (bytes.byteLength > MAX_IMPORT_BYTES) { + throw new PublicEvidenceValidationError("community_size", "community import exceeds 2 MiB"); + } let parsed: unknown; - try { parsed = JSON.parse(bytes.toString("utf8")); } catch { throw new PublicEvidenceValidationError("community_json", "community import is not valid JSON"); } + try { + parsed = JSON.parse(bytes.toString("utf8")); + } catch { + throw new PublicEvidenceValidationError("community_json", "community import is not valid JSON"); + } scanStructure(parsed); return parsed; } scanStructure(raw); const bytes = Buffer.from(jcsStringify(raw), "utf8"); - if (bytes.byteLength > MAX_IMPORT_BYTES) throw new PublicEvidenceValidationError("community_size", "community import exceeds 2 MiB"); + if (bytes.byteLength > MAX_IMPORT_BYTES) { + throw new PublicEvidenceValidationError("community_size", "community import exceeds 2 MiB"); + } return raw; } -function objectPath(kind: "bundle" | "revocation", id: string, configDir?: string): string { return join(labCommunityDir(configDir), `${kind}-${assertId(id)}.json`); } + +function bundleObjectPath(publisherKeyId: string, bundleId: string, configDir?: string): string { + return join( + labCommunityDir(configDir), + `bundle-${assertId(publisherKeyId)}-${assertId(bundleId)}.json`, + ); +} + +function revocationObjectPath(revocationId: string, configDir?: string): string { + return join(labCommunityDir(configDir), `revocation-${assertId(revocationId)}.json`); +} + function assertRegular(path: string, fd: number): void { const stats = fstatSync(fd); - if (!stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1 || stats.size > MAX_IMPORT_BYTES) throw new PublicEvidenceValidationError("community_unsafe_target", `unsafe community file: ${path}`); + if (!stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1 || stats.size > MAX_IMPORT_BYTES) { + throw new PublicEvidenceValidationError("community_unsafe_target", `unsafe community file: ${path}`); + } } + function readBounded(path: string): Buffer { const before = lstatSync(path); - if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1 || before.size > MAX_IMPORT_BYTES) throw new PublicEvidenceValidationError("community_unsafe_target", "unsafe community path"); + if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1 || before.size > MAX_IMPORT_BYTES) { + throw new PublicEvidenceValidationError("community_unsafe_target", "unsafe community path"); + } const fd = openSync(path, fsConstants.O_RDONLY | O_NOFOLLOW); - try { assertRegular(path, fd); const bytes = readFileSync(fd); if (bytes.byteLength > MAX_IMPORT_BYTES) throw new PublicEvidenceValidationError("community_size", "community file exceeds bound"); return bytes; } - finally { closeSync(fd); } + try { + assertRegular(path, fd); + const bytes = readFileSync(fd); + if (bytes.byteLength > MAX_IMPORT_BYTES) { + throw new PublicEvidenceValidationError("community_size", "community file exceeds bound"); + } + return bytes; + } finally { + closeSync(fd); + } } -function writeAll(fd: number, bytes: Uint8Array): void { let offset = 0; while (offset < bytes.byteLength) { const count = writeSync(fd, bytes, offset, bytes.byteLength - offset); if (count <= 0) throw new PublicEvidenceValidationError("community_write", "community write made no progress"); offset += count; } } -function persist(kind: "bundle" | "revocation", id: string, value: unknown, configDir?: string): { path: string; created: boolean } { - ensureLabDirs(configDir); - const path = objectPath(kind, id, configDir); + +function writeAll(fd: number, bytes: Uint8Array): void { + let offset = 0; + while (offset < bytes.byteLength) { + const count = writeSync(fd, bytes, offset, bytes.byteLength - offset); + if (count <= 0) { + throw new PublicEvidenceValidationError("community_write", "community write made no progress"); + } + offset += count; + } +} + +function persistAt(path: string, kind: "bundle" | "revocation", value: unknown): { path: string; created: boolean } { const bytes = Buffer.from(jcsStringify(value), "utf8"); - if (bytes.byteLength > MAX_IMPORT_BYTES) throw new PublicEvidenceValidationError("community_size", "community object exceeds bound"); + if (bytes.byteLength > MAX_IMPORT_BYTES) { + throw new PublicEvidenceValidationError("community_size", "community object exceeds bound"); + } let fd: number | null = null; try { fd = openSync(path, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | O_NOFOLLOW, 0o600); - writeAll(fd, bytes); fsyncSync(fd); assertRegular(path, fd); closeSync(fd); fd = null; + writeAll(fd, bytes); + fsyncSync(fd); + assertRegular(path, fd); + closeSync(fd); + fd = null; return { path, created: true }; } catch (error) { if (fd !== null) closeSync(fd); if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error; - if (!readBounded(path).equals(bytes)) throw new PublicEvidenceValidationError("community_conflict", `${kind} id already exists with different bytes`); + if (!readBounded(path).equals(bytes)) { + throw new PublicEvidenceValidationError( + "community_conflict", + `${kind} identity already exists with different bytes`, + ); + } return { path, created: false }; } } -function readJson(path: string): unknown { try { return JSON.parse(readBounded(path).toString("utf8")); } catch (error) { if (error instanceof PublicEvidenceValidationError) throw error; throw new PublicEvidenceValidationError("community_json", "stored community object is invalid JSON"); } } + +function readJson(path: string): unknown { + try { + return JSON.parse(readBounded(path).toString("utf8")); + } catch (error) { + if (error instanceof PublicEvidenceValidationError) throw error; + throw new PublicEvidenceValidationError("community_json", "stored community object is invalid JSON"); + } +} + function files(configDir?: string): string[] { ensureLabDirs(configDir); const names = readdirSync(labCommunityDir(configDir)); - if (names.length > MAX_CACHE_FILES) throw new PublicEvidenceValidationError("community_cache_bound", "community cache file bound exceeded"); + if (names.length > MAX_CACHE_FILES) { + throw new PublicEvidenceValidationError("community_cache_bound", "community cache file bound exceeded"); + } return names.sort(); } -export function importCommunityEvidenceBundle(raw: unknown, configDir?: string): { created: boolean; status: "cryptographically_valid"; bundleId: string; path: string } { + +function readVerifiedBundleAt(path: string): PublicEvidenceBundleV1 { + const raw = readJson(path); + const verified = verifyPublicEvidenceBundle(raw); + if (verified.status !== "cryptographically_valid") { + throw new PublicEvidenceValidationError( + verified.status, + verified.detail ?? "stored community bundle invalid", + ); + } + return validateCommunityEvidenceAuthorities(verified.bundle); +} + +export function importCommunityEvidenceBundle( + raw: unknown, + configDir?: string, +): { + created: boolean; + status: "cryptographically_valid"; + bundleId: string; + publisherKeyId: string; + path: string; +} { const parsed = boundedInput(raw); const verified = verifyPublicEvidenceBundle(parsed); - if (verified.status !== "cryptographically_valid") throw new PublicEvidenceValidationError(verified.status, verified.detail ?? "community bundle verification failed"); + if (verified.status !== "cryptographically_valid") { + throw new PublicEvidenceValidationError( + verified.status, + verified.detail ?? "community bundle verification failed", + ); + } validateCommunityEvidenceAuthorities(verified.bundle); - const stored = persist("bundle", verified.bundle.bundleId, verified.bundle, configDir); - return { ...stored, status: "cryptographically_valid", bundleId: verified.bundle.bundleId }; + ensureLabDirs(configDir); + const stored = persistAt( + bundleObjectPath(verified.bundle.publisher.keyId, verified.bundle.bundleId, configDir), + "bundle", + verified.bundle, + ); + return { + ...stored, + status: "cryptographically_valid", + bundleId: verified.bundle.bundleId, + publisherKeyId: verified.bundle.publisher.keyId, + }; +} + +export function readCommunityEvidenceBundleForPublisher( + bundleId: string, + publisherKeyId: string, + configDir?: string, +): PublicEvidenceBundleV1 { + const bundle = readVerifiedBundleAt(bundleObjectPath(publisherKeyId, bundleId, configDir)); + if (bundle.bundleId !== bundleId || bundle.publisher.keyId !== publisherKeyId) { + throw new PublicEvidenceValidationError( + "community_identity_mismatch", + "stored community bundle does not match filename identity", + ); + } + return bundle; } + export function readCommunityEvidenceBundle(bundleId: string, configDir?: string): PublicEvidenceBundleV1 { - const raw = readJson(objectPath("bundle", bundleId, configDir)); - const verified = verifyPublicEvidenceBundle(raw); - if (verified.status !== "cryptographically_valid") throw new PublicEvidenceValidationError(verified.status, verified.detail ?? "stored community bundle invalid"); - return validateCommunityEvidenceAuthorities(verified.bundle); + assertId(bundleId); + const matches = files(configDir).flatMap((name) => { + const match = COMMUNITY_BUNDLE_FILE_RE.exec(name); + return match && match[2] === bundleId ? [{ name, publisherKeyId: match[1]! }] : []; + }); + if (matches.length === 0) { + throw new PublicEvidenceValidationError("community_missing", "community bundle not found"); + } + if (matches.length !== 1) { + throw new PublicEvidenceValidationError( + "community_ambiguous_bundle", + "bundleId is shared by multiple publishers; publisherKeyId is required", + ); + } + return readCommunityEvidenceBundleForPublisher(bundleId, matches[0]!.publisherKeyId, configDir); } + function allCommunityBundles(configDir?: string): PublicEvidenceBundleV1[] { - return files(configDir).filter((name) => /^bundle-[0-9a-f]{64}\.json$/.test(name)).map((name) => readCommunityEvidenceBundle(name.slice(7, 71), configDir)); + return files(configDir).flatMap((name) => { + const match = COMMUNITY_BUNDLE_FILE_RE.exec(name); + if (!match) return []; + return [readCommunityEvidenceBundleForPublisher(match[2]!, match[1]!, configDir)]; + }); } + function findTargetBundle(revocation: unknown, configDir?: string): PublicEvidenceBundleV1 { - if (!revocation || typeof revocation !== "object" || !Array.isArray((revocation as { targets?: unknown }).targets)) throw new PublicEvidenceValidationError("revocation_target", "revocation targets unavailable"); - const ids = new Set((revocation as { targets: Array<{ id?: unknown }> }).targets.map((target) => typeof target?.id === "string" ? target.id : "")); - const candidates = allCommunityBundles(configDir).filter((bundle) => ids.has(bundle.bundleId) || bundle.records.some((record) => ids.has(record.recordId))); - const fullyMatching = candidates.filter((bundle) => (revocation as { targets: Array<{ kind?: unknown; id?: unknown }> }).targets.every((target) => target.kind === "bundle" ? target.id === bundle.bundleId : target.kind === "record" && bundle.records.some((record) => record.recordId === target.id))); - if (fullyMatching.length !== 1) throw new PublicEvidenceValidationError("revocation_target", "revocation targets must resolve to one verified community bundle"); + if (!revocation || typeof revocation !== "object") { + throw new PublicEvidenceValidationError("revocation_target", "revocation target metadata unavailable"); + } + const raw = revocation as { + publisher?: { keyId?: unknown }; + targets?: Array<{ kind?: unknown; id?: unknown }>; + }; + if (!Array.isArray(raw.targets) || typeof raw.publisher?.keyId !== "string") { + throw new PublicEvidenceValidationError("revocation_target", "revocation targets or publisher unavailable"); + } + const publisherKeyId = assertId(raw.publisher.keyId); + const candidates = allCommunityBundles(configDir).filter( + (bundle) => bundle.publisher.keyId === publisherKeyId, + ); + const fullyMatching = candidates.filter((bundle) => raw.targets!.every((target) => + target.kind === "bundle" + ? target.id === bundle.bundleId + : target.kind === "record" + && bundle.records.some((record) => record.recordId === target.id), + )); + if (fullyMatching.length !== 1) { + throw new PublicEvidenceValidationError( + "revocation_target", + "revocation targets must resolve to one verified bundle for the same publisher", + ); + } return fullyMatching[0]!; } -export function importCommunityEvidenceRevocation(raw: unknown, configDir?: string): { created: boolean; status: "cryptographically_valid"; revocationId: string; path: string } { + +export function importCommunityEvidenceRevocation( + raw: unknown, + configDir?: string, +): { created: boolean; status: "cryptographically_valid"; revocationId: string; path: string } { const parsed = boundedInput(raw); const targetBundle = findTargetBundle(parsed, configDir); const verified = verifyPublicEvidenceRevocation(parsed, targetBundle); - if (verified.status !== "cryptographically_valid") throw new PublicEvidenceValidationError(verified.status, verified.detail ?? "community revocation verification failed"); - const stored = persist("revocation", verified.revocation.revocationId, verified.revocation, configDir); - return { ...stored, status: "cryptographically_valid", revocationId: verified.revocation.revocationId }; + if (verified.status !== "cryptographically_valid") { + throw new PublicEvidenceValidationError( + verified.status, + verified.detail ?? "community revocation verification failed", + ); + } + ensureLabDirs(configDir); + const stored = persistAt( + revocationObjectPath(verified.revocation.revocationId, configDir), + "revocation", + verified.revocation, + ); + return { + ...stored, + status: "cryptographically_valid", + revocationId: verified.revocation.revocationId, + }; } -function verifiedRevocationsForBundle(bundle: PublicEvidenceBundleV1, configDir?: string): PublicEvidenceRevocationV1[] { + +function verifiedRevocationsForBundle( + bundle: PublicEvidenceBundleV1, + configDir?: string, +): PublicEvidenceRevocationV1[] { const result: PublicEvidenceRevocationV1[] = []; - for (const name of files(configDir).filter((value) => /^revocation-[0-9a-f]{64}\.json$/.test(value))) { + for (const name of files(configDir)) { + if (!COMMUNITY_REVOCATION_FILE_RE.test(name)) continue; const raw = readJson(join(labCommunityDir(configDir), name)); const verified = verifyPublicEvidenceRevocation(raw, bundle); if (verified.status === "cryptographically_valid") result.push(verified.revocation); } return result; } + export function listCommunityEvidence(configDir?: string): CommunityEvidenceSummaryV1[] { return allCommunityBundles(configDir).map((bundle) => { const revoked = new Set(); for (const revocation of verifiedRevocationsForBundle(bundle, configDir)) { - if (revocation.targets.some((target) => target.kind === "bundle" && target.id === bundle.bundleId)) for (const record of bundle.records) revoked.add(record.recordId); - for (const target of revocation.targets) if (target.kind === "record") revoked.add(target.id); + if (revocation.targets.some( + (target) => target.kind === "bundle" && target.id === bundle.bundleId, + )) { + for (const record of bundle.records) revoked.add(record.recordId); + } + for (const target of revocation.targets) { + if (target.kind === "record") revoked.add(target.id); + } } - return { trustClass: "community_untrusted_v1" as const, status: "cryptographically_valid" as const, bundleId: bundle.bundleId, publisherKeyId: bundle.publisher.keyId, activeRecordCount: bundle.records.filter((record) => !revoked.has(record.recordId)).length, revokedRecordCount: bundle.records.filter((record) => revoked.has(record.recordId)).length }; - }).sort((a, b) => a.bundleId.localeCompare(b.bundleId)); + return { + trustClass: "community_untrusted_v1" as const, + status: "cryptographically_valid" as const, + bundleId: bundle.bundleId, + publisherKeyId: bundle.publisher.keyId, + activeRecordCount: bundle.records.filter((record) => !revoked.has(record.recordId)).length, + revokedRecordCount: bundle.records.filter((record) => revoked.has(record.recordId)).length, + }; + }).sort((a, b) => + a.bundleId.localeCompare(b.bundleId) || a.publisherKeyId.localeCompare(b.publisherKeyId)); } From 8fb10e3b10f134594af0aa301cb4f23b33957b5d Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:51:46 +0200 Subject: [PATCH 19/33] fix(lab): preserve third-party publisher continuity during purge --- src/lab/public/purge.ts | 48 ++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/src/lab/public/purge.ts b/src/lab/public/purge.ts index f8cb1eb0d1..e4d845a589 100644 --- a/src/lab/public/purge.ts +++ b/src/lab/public/purge.ts @@ -18,14 +18,15 @@ import { labPublicExportsDir, labPublicPublisherKeyPath, } from "../paths"; -import { readCommunityEvidenceBundle } from "./community"; +import { readCommunityEvidenceBundleForPublisher } from "./community"; import { publicPublisherKeyId } from "./ids"; +import { readPublicEvidenceBundle } from "./store"; import { PublicEvidenceValidationError } from "./validate"; const O_NOFOLLOW = (fsConstants as { O_NOFOLLOW?: number }).O_NOFOLLOW ?? 0; const MAX_PRIVATE_KEY_BYTES = 8 * 1024; const EXPORT_FILE_RE = /^([0-9a-f]{64})\.json$/; -const COMMUNITY_BUNDLE_RE = /^bundle-([0-9a-f]{64})\.json$/; +const COMMUNITY_BUNDLE_RE = /^bundle-([0-9a-f]{64})-([0-9a-f]{64})\.json$/; function readExistingPublisherKeyId(configDir?: string): string | null { const path = labPublicPublisherKeyPath(configDir); @@ -71,13 +72,20 @@ function readExistingPublisherKeyId(configDir?: string): string | null { } } -function localExportBundleIds(configDir?: string): Set { - const ids = new Set(); +function publicIdentity(publisherKeyId: string, bundleId: string): string { + return `${publisherKeyId}:${bundleId}`; +} + +function localExportIdentities(configDir?: string): Set { + const identities = new Set(); for (const entry of readdirSync(labPublicExportsDir(configDir), { withFileTypes: true })) { const match = EXPORT_FILE_RE.exec(entry.name); - if (match) ids.add(match[1]!); + if (!match) continue; + const bundleId = match[1]!; + const bundle = readPublicEvidenceBundle(bundleId, configDir); + identities.add(publicIdentity(bundle.publisher.keyId, bundle.bundleId)); } - return ids; + return identities; } function purgeAllExports(configDir?: string): number { @@ -95,11 +103,12 @@ function purgeAllExports(configDir?: string): number { * * The pre-CL-10 purge contract already removes every local export when the * `export` action is selected. CL-10 additionally removes quarantined community - * bundle copies that can be proven locally-originated, either because their - * content id is currently present in the local export store or because their - * publisher key is this installation's existing publisher key. + * bundle copies that can be proven locally-originated by the exact + * `(publisherKeyId, bundleId)` pair or by this installation's existing + * publisher key. * - * Third-party community evidence is deliberately preserved. When local + * Third-party community evidence is deliberately preserved, including an + * independently signed copy with the same content `bundleId`. When local * provenance cannot be inspected safely, this function fails closed rather than * pretending the purge completed. */ @@ -108,7 +117,7 @@ export function purgeLocalPublicEvidenceCopies(configDir?: string): { deletedCommunityBundles: number; } { ensureLabDirs(configDir); - const exportedBundleIds = localExportBundleIds(configDir); + const exportedIdentities = localExportIdentities(configDir); const localPublisherKeyId = readExistingPublisherKeyId(configDir); const communityDir = labCommunityDir(configDir); @@ -116,14 +125,19 @@ export function purgeLocalPublicEvidenceCopies(configDir?: string): { for (const entry of readdirSync(communityDir, { withFileTypes: true })) { const match = COMMUNITY_BUNDLE_RE.exec(entry.name); if (!match) continue; - const bundleId = match[1]!; + const publisherKeyId = match[1]!; + const bundleId = match[2]!; + const locallyOriginated = exportedIdentities.has(publicIdentity(publisherKeyId, bundleId)) + || publisherKeyId === localPublisherKeyId; + if (!locallyOriginated) continue; - let locallyOriginated = exportedBundleIds.has(bundleId); - if (!locallyOriginated && localPublisherKeyId) { - const bundle = readCommunityEvidenceBundle(bundleId, configDir); - locallyOriginated = bundle.publisher.keyId === localPublisherKeyId; + const bundle = readCommunityEvidenceBundleForPublisher(bundleId, publisherKeyId, configDir); + if (bundle.publisher.keyId !== publisherKeyId || bundle.bundleId !== bundleId) { + throw new PublicEvidenceValidationError( + "community_identity_mismatch", + `community bundle identity changed while purging: ${entry.name}`, + ); } - if (!locallyOriginated) continue; const path = join(communityDir, entry.name); const before = lstatSync(path); From 345877c10602ca90402327555355af96443a610b Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:58:36 +0200 Subject: [PATCH 20/33] test(lab): define CL-10 local operator surfaces --- tests/lab-public-surfaces.test.ts | 274 ++++++++++++++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 tests/lab-public-surfaces.test.ts diff --git a/tests/lab-public-surfaces.test.ts b/tests/lab-public-surfaces.test.ts new file mode 100644 index 0000000000..379d1fa776 --- /dev/null +++ b/tests/lab-public-surfaces.test.ts @@ -0,0 +1,274 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { handleLabCommand } from "../src/cli/lab"; +import { + labPublicExportsDir, + labPublicPublisherKeyPath, + persistConformanceResult, + rebuildLabProjection, +} from "../src/lab"; +import { createArtifactStore } from "../src/lab/artifacts/store"; +import { resolveProtocolExecutionContext } from "../src/lab/conformance/executor"; +import { discoverScenarios, loadCaseAuthority } from "../src/lab/conformance/manifest"; +import type { CaseRecord } from "../src/lab/conformance/types"; +import { queryLabObservations } from "../src/lab/query"; +import { handleManagementAPI } from "../src/server/management-api"; +import type { OcxConfig } from "../src/types"; +import { ManagementRequest } from "./helpers/management-auth"; + +const HOMES: string[] = []; + +afterEach(() => { + for (const home of HOMES.splice(0)) rmSync(home, { recursive: true, force: true }); + delete process.env.OPENCODEX_HOME; +}); + +function tempHome(): string { + const home = join(tmpdir(), `ocx-cl10-surfaces-${process.pid}-${Math.random().toString(16).slice(2)}`); + mkdirSync(home, { recursive: true, mode: 0o700 }); + HOMES.push(home); + return home; +} + +function syntheticPassResult(caseRecord: CaseRecord) { + return { + scenarioId: caseRecord.id, + suite: caseRecord.suite, + passed: true, + classification: "inconclusive" as const, + assertionResults: caseRecord.assertions.map((assertion) => ({ + id: assertion.id, + operator: assertion.operator, + required: assertion.required, + passed: true, + observedSummary: "PRIVATE-CANARY-OBSERVED", + })), + diagnostics: ["PRIVATE-CANARY-DIAGNOSTIC"], + executionContext: resolveProtocolExecutionContext(caseRecord), + startedAt: 1_700_000_000_000, + completedAt: 1_700_000_001_000, + }; +} + +function seedProtocolProjection(home: string): string { + const authority = loadCaseAuthority(); + const scenario = discoverScenarios(authority, ["responses-core"]) + .find((candidate) => candidate.id === "responses-core.protocol.request-shape") + ?? discoverScenarios(authority, ["responses-core"])[0]; + if (!scenario) throw new Error("no responses-core protocol scenario available"); + const store = createArtifactStore(join(home, "lab", "artifacts")); + try { + persistConformanceResult(syntheticPassResult(scenario), scenario, authority, { + configDir: home, + recordedAt: 1_700_000_001_100, + artifactStore: store, + }); + } finally { + store.close(); + } + rebuildLabProjection(home); + const rows = queryLabObservations( + { layer: "protocol_conformance", scenarioId: scenario.id }, + undefined, + 10, + home, + ); + const eventId = rows.items[0]?.eventId; + if (!eventId) throw new Error("seeded observation missing"); + return eventId; +} + +function config(home: string): OcxConfig { + void home; + return { port: 0, defaultProvider: "openai-apikey", providers: {} } as OcxConfig; +} + +async function api( + home: string, + path: string, + init: { method?: string; body?: unknown } = {}, +): Promise { + process.env.OPENCODEX_HOME = home; + const req = new ManagementRequest(`http://127.0.0.1${path}`, { + method: init.method ?? "GET", + ...(init.body !== undefined + ? { headers: { "content-type": "application/json" }, body: JSON.stringify(init.body) } + : {}), + }); + const response = await handleManagementAPI(req, new URL(req.url), config(home), { + refreshCodexCatalog: async () => {}, + }); + expect(response).not.toBeNull(); + return response!; +} + +async function captureCli(argv: string[], home: string): Promise<{ code: number; stdout: string; stderr: string }> { + const stdout: string[] = []; + const stderr: string[] = []; + const originalLog = console.log; + const originalError = console.error; + console.log = (...args: unknown[]) => { stdout.push(args.join(" ")); }; + console.error = (...args: unknown[]) => { stderr.push(args.join(" ")); }; + try { + return { + code: await handleLabCommand(argv, { configDir: home }), + stdout: stdout.join("\n"), + stderr: stderr.join("\n"), + }; + } finally { + console.log = originalLog; + console.error = originalError; + } +} + +function installNetworkCanary(): () => void { + const original = globalThis.fetch; + globalThis.fetch = (async () => { + throw new Error("CL10-NETWORK-CANARY"); + }) as typeof fetch; + return () => { globalThis.fetch = original; }; +} + +describe("CL-10 CLI local public evidence", () => { + test("preview is network-free and does not create publisher or export state", async () => { + const home = tempHome(); + const eventId = seedProtocolProjection(home); + const restoreFetch = installNetworkCanary(); + try { + const result = await captureCli(["public", "preview", "--event", eventId, "--json"], home); + expect(result.code).toBe(0); + const body = JSON.parse(result.stdout) as { bundle: { records: unknown[]; publisher?: unknown }; excluded: unknown[] }; + expect(body.bundle.records).toHaveLength(1); + expect(body.bundle).not.toHaveProperty("publisher"); + expect(body.excluded).toEqual([]); + expect(existsSync(labPublicPublisherKeyPath(home))).toBe(false); + expect(existsSync(labPublicExportsDir(home)) ? readdirSync(labPublicExportsDir(home)) : []).toEqual([]); + expect(result.stdout).not.toContain("PRIVATE-CANARY"); + } finally { + restoreFetch(); + } + }); + + test("explicit export signs and stores, then verify/import/community remain local", async () => { + const home = tempHome(); + const eventId = seedProtocolProjection(home); + const restoreFetch = installNetworkCanary(); + try { + const exported = await captureCli(["public", "export", "--event", eventId, "--json"], home); + expect(exported.code).toBe(0); + const exportBody = JSON.parse(exported.stdout) as { + bundle: { bundleId: string; publisher: { keyId: string } }; + stored: { path: string; created: boolean }; + }; + expect(exportBody.bundle.publisher.keyId).toMatch(/^[0-9a-f]{64}$/); + expect(exportBody.stored.created).toBe(true); + expect(existsSync(exportBody.stored.path)).toBe(true); + + const verified = await captureCli(["public", "verify", "--file", exportBody.stored.path, "--json"], home); + expect(verified.code).toBe(0); + expect(JSON.parse(verified.stdout)).toMatchObject({ + status: "cryptographically_valid", + bundleId: exportBody.bundle.bundleId, + publisherKeyId: exportBody.bundle.publisher.keyId, + locallyVerified: false, + }); + + const ledgerBefore = readFileSync(join(home, "lab", "compatibility.jsonl")); + const sqliteBefore = readFileSync(join(home, "lab", "compatibility.sqlite")); + const imported = await captureCli(["public", "import", "--file", exportBody.stored.path, "--json"], home); + expect(imported.code).toBe(0); + expect(JSON.parse(imported.stdout)).toMatchObject({ + status: "cryptographically_valid", + trustClass: "community_untrusted_v1", + bundleId: exportBody.bundle.bundleId, + }); + expect(readFileSync(join(home, "lab", "compatibility.jsonl")).equals(ledgerBefore)).toBe(true); + expect(readFileSync(join(home, "lab", "compatibility.sqlite")).equals(sqliteBefore)).toBe(true); + + const community = await captureCli(["public", "community", "--json"], home); + expect(community.code).toBe(0); + const communityBody = JSON.parse(community.stdout) as { evidence: Array<{ bundleId: string; trustClass: string }> }; + expect(communityBody.evidence).toEqual([ + expect.objectContaining({ bundleId: exportBody.bundle.bundleId, trustClass: "community_untrusted_v1" }), + ]); + } finally { + restoreFetch(); + } + }); + + test("has no publish command", async () => { + const home = tempHome(); + const result = await captureCli(["public", "publish", "--json"], home); + expect(result.code).toBe(2); + expect(result.stderr).toMatch(/unknown public subcommand|usage/i); + }); +}); + +describe("CL-10 management local public evidence", () => { + test("preview/export/verify/import/community are explicit authenticated local actions", async () => { + const home = tempHome(); + const eventId = seedProtocolProjection(home); + const restoreFetch = installNetworkCanary(); + try { + const preview = await api(home, "/api/lab/public/preview", { + method: "POST", + body: { eventIds: [eventId] }, + }); + expect(preview.status).toBe(200); + const previewBody = await preview.json() as { bundle: { records: unknown[]; publisher?: unknown } }; + expect(previewBody.bundle.records).toHaveLength(1); + expect(previewBody.bundle).not.toHaveProperty("publisher"); + expect(existsSync(labPublicPublisherKeyPath(home))).toBe(false); + + const exported = await api(home, "/api/lab/public/export", { + method: "POST", + body: { eventIds: [eventId] }, + }); + expect(exported.status).toBe(200); + const exportBody = await exported.json() as { + bundle: { bundleId: string; publisher: { keyId: string } }; + stored: { path: string; created: boolean }; + }; + expect(exportBody.stored.created).toBe(true); + + const verified = await api(home, "/api/lab/public/verify", { + method: "POST", + body: { bundle: exportBody.bundle }, + }); + expect(verified.status).toBe(200); + expect(await verified.json()).toMatchObject({ + status: "cryptographically_valid", + bundleId: exportBody.bundle.bundleId, + locallyVerified: false, + }); + + const ledgerBefore = readFileSync(join(home, "lab", "compatibility.jsonl")); + const imported = await api(home, "/api/lab/public/community/import", { + method: "POST", + body: { bundle: exportBody.bundle }, + }); + expect(imported.status).toBe(200); + expect(await imported.json()).toMatchObject({ + status: "cryptographically_valid", + trustClass: "community_untrusted_v1", + }); + expect(readFileSync(join(home, "lab", "compatibility.jsonl")).equals(ledgerBefore)).toBe(true); + + const community = await api(home, "/api/lab/public/community"); + expect(community.status).toBe(200); + expect(await community.json()).toMatchObject({ + evidence: [expect.objectContaining({ bundleId: exportBody.bundle.bundleId })], + }); + } finally { + restoreFetch(); + } + }); + + test("does not expose a remote publish endpoint", async () => { + const home = tempHome(); + const res = await api(home, "/api/lab/public/publish", { method: "POST", body: {} }); + expect(res.status).toBe(404); + }); +}); From 7dce9c71b261e9538d4a16835be59a21abd27531 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:59:00 +0200 Subject: [PATCH 21/33] ci: cover CL-10 local operator surfaces --- .github/workflows/cl10-focus.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cl10-focus.yml b/.github/workflows/cl10-focus.yml index c68413b2b7..b1181bc457 100644 --- a/.github/workflows/cl10-focus.yml +++ b/.github/workflows/cl10-focus.yml @@ -17,5 +17,5 @@ jobs: with: bun-version: 1.3.14 - run: bun install --frozen-lockfile - - run: bun test tests/lab-public-evidence.test.ts tests/lab-public-evidence-signature.test.ts tests/lab-community-evidence.test.ts tests/lab-community-publisher-continuity.test.ts + - run: bun test tests/lab-public-evidence.test.ts tests/lab-public-evidence-signature.test.ts tests/lab-community-evidence.test.ts tests/lab-community-publisher-continuity.test.ts tests/lab-public-surfaces.test.ts - run: bun x tsc --noEmit From 0215df7a73410cf2177c61864624da445aa788b4 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 20:03:04 +0200 Subject: [PATCH 22/33] feat(lab): add CL-10 local public evidence operator --- src/lab/public/operator.ts | 308 +++++++++++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 src/lab/public/operator.ts diff --git a/src/lab/public/operator.ts b/src/lab/public/operator.ts new file mode 100644 index 0000000000..8cefd90498 --- /dev/null +++ b/src/lab/public/operator.ts @@ -0,0 +1,308 @@ +import { lstatSync, readFileSync } from "node:fs"; +import { replayLabLedger } from "../ledger/store"; +import { labLedgerPath } from "../paths"; +import { + queryLabEventById, + queryLabVerdicts, +} from "../query"; +import type { ObservationEvent } from "../events/types"; +import { + projectPublicEvidence, +} from "./bundle"; +import type { ProjectPublicEvidenceRecordInput } from "./project"; +import { + getOrCreatePublicPublisher, + signPublicEvidenceBundle, + verifyPublicEvidenceBundle, +} from "./signing"; +import { writePublicEvidenceBundle } from "./store"; +import { + importCommunityEvidenceBundle, + listCommunityEvidence, +} from "./community"; +import type { + PublicBundleVerificationResult, + PublicEvidenceBundleUnsignedV1, + PublicEvidenceBundleV1, + PublicProjectionNotExportableReason, +} from "./types"; +import { PublicEvidenceValidationError } from "./validate"; + +const MAX_OPERATOR_EVENTS = 256; +const MAX_PUBLIC_FILE_BYTES = 2 * 1024 * 1024; + +export type PublicOperatorExclusionReason = + | PublicProjectionNotExportableReason + | "event_not_found" + | "not_observation" + | "event_excluded" + | "no_canonical_verdict"; + +export interface PublicOperatorExclusionV1 { + eventId: string; + reason: PublicOperatorExclusionReason; +} + +export interface LocalPublicPreviewV1 { + bundle: PublicEvidenceBundleUnsignedV1; + excluded: PublicOperatorExclusionV1[]; +} + +export interface LocalPublicExportV1 { + bundle: PublicEvidenceBundleV1; + stored: { + path: string; + created: boolean; + }; + excluded: PublicOperatorExclusionV1[]; +} + +export type PublicVerificationSummaryV1 = + | { + status: "cryptographically_valid"; + bundleId: string; + publisherKeyId: string; + locallyVerified: false; + } + | { + status: Exclude; + locallyVerified: false; + detail?: string; + }; + +function assertOperatorEventIds(eventIds: readonly string[]): string[] { + if (eventIds.length === 0 || eventIds.length > MAX_OPERATOR_EVENTS) { + throw new PublicEvidenceValidationError( + "public_selection_limit", + `public evidence selection must contain 1..${MAX_OPERATOR_EVENTS} event ids`, + ); + } + const unique: string[] = []; + const seen = new Set(); + for (const eventId of eventIds) { + if (!/^[0-9a-f]{64}$/.test(eventId)) { + throw new PublicEvidenceValidationError( + "public_selection_event_id", + "public evidence event ids must be lowercase sha256 hex", + ); + } + if (seen.has(eventId)) continue; + seen.add(eventId); + unique.push(eventId); + } + return unique; +} + +function utcDay(timestamp: number): string { + const date = new Date(timestamp); + if (!Number.isFinite(date.getTime())) { + throw new PublicEvidenceValidationError( + "public_selection_time", + "selected observation has an invalid completion timestamp", + ); + } + return date.toISOString().slice(0, 10); +} + +function canonicalVerdictForObservation( + observation: ObservationEvent, + configDir?: string, +): ProjectPublicEvidenceRecordInput["verdict"] | null { + const page = queryLabVerdicts( + { + subjectId: observation.subjectId, + layer: observation.evidenceLayer, + suiteId: observation.suiteId, + }, + undefined, + 200, + configDir, + ); + const verdict = page.items.find((row) => + row.suiteVersion === observation.suiteVersion + && row.contributingEventIds.includes(observation.eventId), + ); + return verdict?.verdict ?? null; +} + +/** + * Build a public-safe unsigned bundle from explicitly selected local Lab events. + * + * This function is read-only. It does not create a publisher key, write an + * export, import community evidence, contact a provider, or perform network IO. + * Live-route/task records remain fail-closed unless a future trusted caller can + * provide an internally-derived public authority; this operator currently never + * accepts authority assertions from user input. + */ +export function previewLocalPublicEvidence( + input: { eventIds: readonly string[] }, + configDir?: string, +): LocalPublicPreviewV1 { + const eventIds = assertOperatorEventIds(input.eventIds); + const replay = replayLabLedger(labLedgerPath(configDir)); + const byId = new Map(replay.events.map((event) => [event.eventId, event] as const)); + const projectInputs: ProjectPublicEvidenceRecordInput[] = []; + const projectEventIds: string[] = []; + const excluded: PublicOperatorExclusionV1[] = []; + let latestObservationCompletedAt: number | null = null; + + for (const eventId of eventIds) { + const event = byId.get(eventId); + if (!event) { + excluded.push({ eventId, reason: "event_not_found" }); + continue; + } + if (event.eventKind !== "observation") { + excluded.push({ eventId, reason: "not_observation" }); + continue; + } + + latestObservationCompletedAt = Math.max( + latestObservationCompletedAt ?? event.completedAt, + event.completedAt, + ); + + const projectedEvent = queryLabEventById(eventId, configDir); + if (!projectedEvent) { + excluded.push({ eventId, reason: "event_not_found" }); + continue; + } + if (projectedEvent.excluded) { + excluded.push({ eventId, reason: "event_excluded" }); + continue; + } + + const verdict = canonicalVerdictForObservation(event, configDir); + if (!verdict) { + excluded.push({ eventId, reason: "no_canonical_verdict" }); + continue; + } + + projectInputs.push({ observation: event, verdict }); + projectEventIds.push(eventId); + } + + if (latestObservationCompletedAt === null) { + throw new PublicEvidenceValidationError( + "public_selection_empty", + "public evidence selection contains no observation events", + ); + } + + const projected = projectPublicEvidence({ + createdDayUtc: utcDay(latestObservationCompletedAt), + records: projectInputs, + }); + for (const row of projected.excluded) { + excluded.push({ + eventId: projectEventIds[row.index]!, + reason: row.reason, + }); + } + + return { + bundle: projected.bundle, + excluded, + }; +} + +/** Explicit local export. No remote publish transport exists in CL-10.1..10.4. */ +export function exportLocalPublicEvidence( + input: { eventIds: readonly string[] }, + configDir?: string, +): LocalPublicExportV1 { + const preview = previewLocalPublicEvidence(input, configDir); + if (preview.bundle.records.length === 0) { + throw new PublicEvidenceValidationError( + "public_export_empty", + "selected events produced no exportable public evidence records", + ); + } + const signer = getOrCreatePublicPublisher(configDir); + const bundle = signPublicEvidenceBundle(preview.bundle, signer); + const stored = writePublicEvidenceBundle(bundle, configDir); + return { bundle, stored, excluded: preview.excluded }; +} + +export function summarizePublicEvidenceVerification(raw: unknown): PublicVerificationSummaryV1 { + const verified = verifyPublicEvidenceBundle(raw); + if (verified.status !== "cryptographically_valid") { + return { + status: verified.status, + locallyVerified: false, + ...(verified.detail ? { detail: verified.detail } : {}), + }; + } + return { + status: "cryptographically_valid", + bundleId: verified.bundle.bundleId, + publisherKeyId: verified.bundle.publisher.keyId, + locallyVerified: false, + }; +} + +function readBoundedPublicFile(path: string): Buffer { + const stats = lstatSync(path); + if (!stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1) { + throw new PublicEvidenceValidationError( + "public_file_unsafe", + "public evidence input must be a regular non-symlink file", + ); + } + if (stats.size > MAX_PUBLIC_FILE_BYTES) { + throw new PublicEvidenceValidationError( + "public_file_too_large", + "public evidence input exceeds 2 MiB", + ); + } + const bytes = readFileSync(path); + if (bytes.byteLength > MAX_PUBLIC_FILE_BYTES) { + throw new PublicEvidenceValidationError( + "public_file_too_large", + "public evidence input exceeds 2 MiB", + ); + } + return bytes; +} + +function parsePublicFile(path: string): unknown { + const bytes = readBoundedPublicFile(path); + try { + return JSON.parse(bytes.toString("utf8")); + } catch { + throw new PublicEvidenceValidationError( + "public_file_json", + "public evidence input is not valid JSON", + ); + } +} + +export function verifyPublicEvidenceFile(path: string): PublicVerificationSummaryV1 { + return summarizePublicEvidenceVerification(parsePublicFile(path)); +} + +export function importCommunityEvidenceFile(path: string, configDir?: string) { + const imported = importCommunityEvidenceBundle(readBoundedPublicFile(path), configDir); + return { + ...imported, + trustClass: "community_untrusted_v1" as const, + locallyVerified: false as const, + }; +} + +export function importCommunityEvidenceValue(raw: unknown, configDir?: string) { + const imported = importCommunityEvidenceBundle(raw, configDir); + return { + ...imported, + trustClass: "community_untrusted_v1" as const, + locallyVerified: false as const, + }; +} + +export function listCommunityEvidenceContext(configDir?: string) { + return { + evidence: listCommunityEvidence(configDir), + trustClass: "community_untrusted_v1" as const, + locallyVerified: false as const, + }; +} From aee7fd75bdf34bcab2b201de0a535d71f21e281a Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 20:03:25 +0200 Subject: [PATCH 23/33] feat(lab): export CL-10 local operator API --- src/lab/public/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lab/public/index.ts b/src/lab/public/index.ts index d5a2e9817c..3033898f01 100644 --- a/src/lab/public/index.ts +++ b/src/lab/public/index.ts @@ -14,3 +14,4 @@ export * from "./store"; export * from "./revocation"; export * from "./community-authority"; export * from "./community"; +export * from "./operator"; From 4ffd5520e1fd02d82d18514389fdf30e1614202b Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 20:04:59 +0200 Subject: [PATCH 24/33] chore: apply CL-10 Task 6 --- .github/workflows/cl10-task6-apply.yml | 278 +++++++++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 .github/workflows/cl10-task6-apply.yml diff --git a/.github/workflows/cl10-task6-apply.yml b/.github/workflows/cl10-task6-apply.yml new file mode 100644 index 0000000000..725af79b16 --- /dev/null +++ b/.github/workflows/cl10-task6-apply.yml @@ -0,0 +1,278 @@ +name: CL-10 Task 6 patch + +on: + push: + branches: + - feat/cl-10-public-evidence-runtime + +permissions: + contents: write + +jobs: + apply: + if: contains(github.event.head_commit.message, 'chore: apply CL-10 Task 6') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + with: + fetch-depth: 1 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + with: + bun-version: 1.3.14 + - name: Apply operator surfaces + shell: bash + run: | + python3 <<'PY' + from pathlib import Path + + cli = Path('src/cli/lab.ts') + s = cli.read_text() + needle = 'import { createProductionLabRouteExecutor } from "../lib/lab-live-route-production";\n' + insert = needle + '''import {\n exportLocalPublicEvidence,\n importCommunityEvidenceFile,\n listCommunityEvidenceContext,\n previewLocalPublicEvidence,\n verifyPublicEvidenceFile,\n type PublicVerificationSummaryV1,\n} from "../lab/public";\n''' + assert needle in s + s = s.replace(needle, insert, 1) + + usage_needle = ' ocx lab catalog [--layer ] [--suite ] [--json]\n' + usage_insert = usage_needle + ''' ocx lab public preview --event [--event ...] [--json]\n ocx lab public export --event [--event ...] [--json]\n ocx lab public verify --file [--json]\n ocx lab public import --file [--json]\n ocx lab public community [--json]\n''' + assert usage_needle in s + s = s.replace(usage_needle, usage_insert, 1) + + handle_needle = 'export async function handleLabCommand(argv: string[], deps: LabCliDeps = {}): Promise {\n' + helpers = r'''function takeRepeatedOptions(args: string[], flag: string): string[] { + const values: string[] = []; + while (true) { + const index = args.indexOf(flag); + if (index < 0) break; + const value = args[index + 1]; + if (!value || value.startsWith("--")) { + throw new CliUsageError(`${flag} requires a value`, USAGE); + } + values.push(value); + args.splice(index, 2); + } + return values; + } + + function publicPreviewLines(result: ReturnType): string[] { + return [ + `Public evidence preview: ${result.bundle.records.length} exportable record(s)`, + `Excluded: ${result.excluded.length}`, + "Unsigned local preview; no publisher key or remote publish is created.", + ]; + } + + function publicExportLines(result: ReturnType): string[] { + return [ + "Public evidence exported locally", + `Bundle: ${result.bundle.bundleId}`, + `Publisher: ${result.bundle.publisher.keyId}`, + `Path: ${result.stored.path}`, + `Excluded: ${result.excluded.length}`, + "No remote publish occurred.", + ]; + } + + function publicVerificationLines(result: PublicVerificationSummaryV1): string[] { + if (result.status !== "cryptographically_valid") { + return [ + `Public evidence verification: ${result.status}`, + "Not locally verified.", + ...(result.detail ? [result.detail] : []), + ]; + } + return [ + "Public evidence verification: cryptographically valid", + `Bundle: ${result.bundleId}`, + `Publisher: ${result.publisherKeyId}`, + "Not locally verified. Signature validity proves integrity/continuity only.", + ]; + } + + function handlePublicLabCommand( + argv: string[], + wantsJson: boolean, + configDir: string, + ): void { + const [action, ...restInput] = argv; + const rest = [...restInput]; + switch (action) { + case "preview": { + const eventIds = takeRepeatedOptions(rest, "--event"); + rejectArgs(rest, USAGE); + const result = previewLocalPublicEvidence({ eventIds }, configDir); + printData(result, wantsJson, publicPreviewLines(result)); + return; + } + case "export": { + const eventIds = takeRepeatedOptions(rest, "--event"); + rejectArgs(rest, USAGE); + const result = exportLocalPublicEvidence({ eventIds }, configDir); + printData(result, wantsJson, publicExportLines(result)); + return; + } + case "verify": { + const path = takeOption(rest, "--file"); + if (!path) throw new CliUsageError("public verify requires --file", USAGE); + rejectArgs(rest, USAGE); + const result = verifyPublicEvidenceFile(path); + printData(result, wantsJson, publicVerificationLines(result)); + return; + } + case "import": { + const path = takeOption(rest, "--file"); + if (!path) throw new CliUsageError("public import requires --file", USAGE); + rejectArgs(rest, USAGE); + const result = importCommunityEvidenceFile(path, configDir); + printData(result, wantsJson, [ + `Community evidence imported: ${result.bundleId}`, + `Publisher: ${result.publisherKeyId}`, + "Trust: community_untrusted_v1; not locally verified.", + ]); + return; + } + case "community": { + rejectArgs(rest, USAGE); + const result = listCommunityEvidenceContext(configDir); + const lines = result.evidence.length > 0 + ? result.evidence.map((row) => + `${row.bundleId} publisher=${row.publisherKeyId} active=${row.activeRecordCount} revoked=${row.revokedRecordCount}`, + ) + : ["No community evidence"]; + printData(result, wantsJson, [ + "Community evidence (untrusted, read-only context; not locally verified)", + ...lines, + ]); + return; + } + default: + throw new CliUsageError("unknown public subcommand", USAGE); + } + } + + ''' + assert handle_needle in s + s = s.replace(handle_needle, helpers + handle_needle, 1) + + switch_needle = ' switch (sub) {\n' + switch_insert = ''' switch (sub) {\n case "public": {\n handlePublicLabCommand(rest, wantsJson, configDir);\n return;\n }\n''' + assert switch_needle in s + s = s.replace(switch_needle, switch_insert, 1) + cli.write_text(s) + + routes = Path('src/server/management/lab-routes.ts') + s = routes.read_text() + import_needle = 'import { jsonResponse } from "../auth-cors";\n' + import_insert = '''import {\n exportLocalPublicEvidence,\n importCommunityEvidenceValue,\n listCommunityEvidenceContext,\n previewLocalPublicEvidence,\n summarizePublicEvidenceVerification,\n PublicEvidenceValidationError,\n} from "../../lab/public";\n''' + import_needle + assert import_needle in s + s = s.replace(import_needle, import_insert, 1) + + handler_needle = 'export async function handleLabRoutes(ctx: ManagementContext): Promise {\n' + handler_helpers = r'''const MAX_PUBLIC_REQUEST_BYTES = 2 * 1024 * 1024; + + async function readBoundedPublicJson(req: Request): Promise { + const lengthRaw = req.headers.get("content-length"); + if (lengthRaw) { + const length = Number(lengthRaw); + if (!Number.isFinite(length) || length < 0 || length > MAX_PUBLIC_REQUEST_BYTES) { + throw new PublicEvidenceValidationError( + "public_request_too_large", + "public evidence request exceeds 2 MiB", + ); + } + } + if (!req.body) { + throw new PublicEvidenceValidationError("public_request_body", "JSON body is required"); + } + const reader = req.body.getReader(); + const chunks: Uint8Array[] = []; + let total = 0; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + total += value.byteLength; + if (total > MAX_PUBLIC_REQUEST_BYTES) { + await reader.cancel(); + throw new PublicEvidenceValidationError( + "public_request_too_large", + "public evidence request exceeds 2 MiB", + ); + } + chunks.push(value); + } + const bytes = new Uint8Array(total); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; + } + try { + return JSON.parse(new TextDecoder().decode(bytes)); + } catch { + throw new PublicEvidenceValidationError("public_request_json", "request body is not valid JSON"); + } + } + + function publicEventIds(raw: unknown): string[] { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) { + throw new PublicEvidenceValidationError("public_request_body", "request body must be an object"); + } + const keys = Object.keys(raw); + if (keys.length !== 1 || keys[0] !== "eventIds") { + throw new PublicEvidenceValidationError("public_request_body", "only eventIds is accepted"); + } + const eventIds = (raw as { eventIds?: unknown }).eventIds; + if (!Array.isArray(eventIds) || !eventIds.every((value) => typeof value === "string")) { + throw new PublicEvidenceValidationError("public_request_body", "eventIds must be a string array"); + } + return eventIds as string[]; + } + + function publicBundleValue(raw: unknown): unknown { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) { + throw new PublicEvidenceValidationError("public_request_body", "request body must be an object"); + } + const keys = Object.keys(raw); + if (keys.length !== 1 || keys[0] !== "bundle") { + throw new PublicEvidenceValidationError("public_request_body", "only bundle is accepted"); + } + return (raw as { bundle?: unknown }).bundle; + } + + function publicErrorResponse(err: unknown, ctx: ManagementContext): Response { + const message = err instanceof Error ? err.message : "public evidence operation failed"; + const code = err instanceof PublicEvidenceValidationError + ? err.code + : "public_evidence_error"; + return errorResponse(code, message, 400, ctx); + } + + ''' + assert handler_needle in s + s = s.replace(handler_needle, handler_helpers + handler_needle, 1) + + method_needle = ''' if (!url.pathname.startsWith("/api/lab")) return null;\n if (req.method !== "GET") return null;\n\n if (url.pathname === "/api/lab/status") {\n''' + method_insert = ''' if (!url.pathname.startsWith("/api/lab")) return null;\n\n if (req.method === "GET" && url.pathname === "/api/lab/public/community") {\n try {\n return jsonResponse(listCommunityEvidenceContext(), 200, req, config);\n } catch (err) {\n return publicErrorResponse(err, ctx);\n }\n }\n\n if (req.method === "POST") {\n if (url.pathname === "/api/lab/public/preview") {\n try {\n const body = await readBoundedPublicJson(req);\n return jsonResponse(\n previewLocalPublicEvidence({ eventIds: publicEventIds(body) }),\n 200,\n req,\n config,\n );\n } catch (err) {\n return publicErrorResponse(err, ctx);\n }\n }\n if (url.pathname === "/api/lab/public/export") {\n try {\n const body = await readBoundedPublicJson(req);\n return jsonResponse(\n exportLocalPublicEvidence({ eventIds: publicEventIds(body) }),\n 200,\n req,\n config,\n );\n } catch (err) {\n return publicErrorResponse(err, ctx);\n }\n }\n if (url.pathname === "/api/lab/public/verify") {\n try {\n const body = await readBoundedPublicJson(req);\n const result = summarizePublicEvidenceVerification(publicBundleValue(body));\n return jsonResponse(\n result,\n result.status === "cryptographically_valid" ? 200 : 400,\n req,\n config,\n );\n } catch (err) {\n return publicErrorResponse(err, ctx);\n }\n }\n if (url.pathname === "/api/lab/public/community/import") {\n try {\n const body = await readBoundedPublicJson(req);\n return jsonResponse(\n importCommunityEvidenceValue(publicBundleValue(body)),\n 200,\n req,\n config,\n );\n } catch (err) {\n return publicErrorResponse(err, ctx);\n }\n }\n return null;\n }\n\n if (req.method !== "GET") return null;\n\n if (url.pathname === "/api/lab/status") {\n''' + assert method_needle in s + s = s.replace(method_needle, method_insert, 1) + routes.write_text(s) + + test = Path('tests/lab-public-surfaces.test.ts') + s = test.read_text() + old = ''' test("does not expose a remote publish endpoint", async () => {\n const home = tempHome();\n const res = await api(home, "/api/lab/public/publish", { method: "POST", body: {} });\n expect(res.status).toBe(404);\n });''' + new = ''' test("does not expose a remote publish endpoint", async () => {\n const home = tempHome();\n process.env.OPENCODEX_HOME = home;\n const req = new ManagementRequest("http://127.0.0.1/api/lab/public/publish", {\n method: "POST",\n headers: { "content-type": "application/json" },\n body: "{}",\n });\n const res = await handleManagementAPI(req, new URL(req.url), config(home), {\n refreshCodexCatalog: async () => {},\n });\n expect(res).toBeNull();\n });''' + assert old in s + s = s.replace(old, new, 1) + test.write_text(s) + PY + - run: bun install --frozen-lockfile + - run: bun test tests/lab-public-evidence.test.ts tests/lab-public-evidence-signature.test.ts tests/lab-community-evidence.test.ts tests/lab-community-publisher-continuity.test.ts tests/lab-public-surfaces.test.ts + - run: bun x tsc --noEmit + - name: Commit validated Task 6 patch + shell: bash + run: | + rm .github/workflows/cl10-task6-apply.yml + git config user.name Wibias + git config user.email 37517432+Wibias@users.noreply.github.com + git add src/cli/lab.ts src/server/management/lab-routes.ts tests/lab-public-surfaces.test.ts .github/workflows/cl10-task6-apply.yml + git commit -m "feat(lab): add CL-10 local public evidence surfaces" + git push origin HEAD:feat/cl-10-public-evidence-runtime From 63b724f387cab208e05753b87d5bcbf582d3c226 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 20:07:02 +0200 Subject: [PATCH 25/33] chore: retry CL-10 Task 6 patch --- .github/workflows/cl10-task6-apply2.yml | 277 ++++++++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 .github/workflows/cl10-task6-apply2.yml diff --git a/.github/workflows/cl10-task6-apply2.yml b/.github/workflows/cl10-task6-apply2.yml new file mode 100644 index 0000000000..bd865435c6 --- /dev/null +++ b/.github/workflows/cl10-task6-apply2.yml @@ -0,0 +1,277 @@ +name: CL-10 Task 6 patch retry + +on: + push: + branches: + - feat/cl-10-public-evidence-runtime + +permissions: + contents: write + +jobs: + apply: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + with: + fetch-depth: 1 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + with: + bun-version: 1.3.14 + - name: Apply operator surfaces + shell: bash + run: | + python3 <<'PY' + from pathlib import Path + + cli = Path('src/cli/lab.ts') + s = cli.read_text() + needle = 'import { createProductionLabRouteExecutor } from "../lib/lab-live-route-production";\n' + insert = needle + '''import {\n exportLocalPublicEvidence,\n importCommunityEvidenceFile,\n listCommunityEvidenceContext,\n previewLocalPublicEvidence,\n verifyPublicEvidenceFile,\n type PublicVerificationSummaryV1,\n} from "../lab/public";\n''' + assert needle in s + s = s.replace(needle, insert, 1) + + usage_needle = ' ocx lab catalog [--layer ] [--suite ] [--json]\n' + usage_insert = usage_needle + ''' ocx lab public preview --event [--event ...] [--json]\n ocx lab public export --event [--event ...] [--json]\n ocx lab public verify --file [--json]\n ocx lab public import --file [--json]\n ocx lab public community [--json]\n''' + assert usage_needle in s + s = s.replace(usage_needle, usage_insert, 1) + + handle_needle = 'export async function handleLabCommand(argv: string[], deps: LabCliDeps = {}): Promise {\n' + helpers = r'''function takeRepeatedOptions(args: string[], flag: string): string[] { + const values: string[] = []; + while (true) { + const index = args.indexOf(flag); + if (index < 0) break; + const value = args[index + 1]; + if (!value || value.startsWith("--")) { + throw new CliUsageError(`${flag} requires a value`, USAGE); + } + values.push(value); + args.splice(index, 2); + } + return values; + } + + function publicPreviewLines(result: ReturnType): string[] { + return [ + `Public evidence preview: ${result.bundle.records.length} exportable record(s)`, + `Excluded: ${result.excluded.length}`, + "Unsigned local preview; no publisher key or remote publish is created.", + ]; + } + + function publicExportLines(result: ReturnType): string[] { + return [ + "Public evidence exported locally", + `Bundle: ${result.bundle.bundleId}`, + `Publisher: ${result.bundle.publisher.keyId}`, + `Path: ${result.stored.path}`, + `Excluded: ${result.excluded.length}`, + "No remote publish occurred.", + ]; + } + + function publicVerificationLines(result: PublicVerificationSummaryV1): string[] { + if (result.status !== "cryptographically_valid") { + return [ + `Public evidence verification: ${result.status}`, + "Not locally verified.", + ...(result.detail ? [result.detail] : []), + ]; + } + return [ + "Public evidence verification: cryptographically valid", + `Bundle: ${result.bundleId}`, + `Publisher: ${result.publisherKeyId}`, + "Not locally verified. Signature validity proves integrity/continuity only.", + ]; + } + + function handlePublicLabCommand( + argv: string[], + wantsJson: boolean, + configDir: string, + ): void { + const [action, ...restInput] = argv; + const rest = [...restInput]; + switch (action) { + case "preview": { + const eventIds = takeRepeatedOptions(rest, "--event"); + rejectArgs(rest, USAGE); + const result = previewLocalPublicEvidence({ eventIds }, configDir); + printData(result, wantsJson, publicPreviewLines(result)); + return; + } + case "export": { + const eventIds = takeRepeatedOptions(rest, "--event"); + rejectArgs(rest, USAGE); + const result = exportLocalPublicEvidence({ eventIds }, configDir); + printData(result, wantsJson, publicExportLines(result)); + return; + } + case "verify": { + const path = takeOption(rest, "--file"); + if (!path) throw new CliUsageError("public verify requires --file", USAGE); + rejectArgs(rest, USAGE); + const result = verifyPublicEvidenceFile(path); + printData(result, wantsJson, publicVerificationLines(result)); + return; + } + case "import": { + const path = takeOption(rest, "--file"); + if (!path) throw new CliUsageError("public import requires --file", USAGE); + rejectArgs(rest, USAGE); + const result = importCommunityEvidenceFile(path, configDir); + printData(result, wantsJson, [ + `Community evidence imported: ${result.bundleId}`, + `Publisher: ${result.publisherKeyId}`, + "Trust: community_untrusted_v1; not locally verified.", + ]); + return; + } + case "community": { + rejectArgs(rest, USAGE); + const result = listCommunityEvidenceContext(configDir); + const lines = result.evidence.length > 0 + ? result.evidence.map((row) => + `${row.bundleId} publisher=${row.publisherKeyId} active=${row.activeRecordCount} revoked=${row.revokedRecordCount}`, + ) + : ["No community evidence"]; + printData(result, wantsJson, [ + "Community evidence (untrusted, read-only context; not locally verified)", + ...lines, + ]); + return; + } + default: + throw new CliUsageError("unknown public subcommand", USAGE); + } + } + + ''' + assert handle_needle in s + s = s.replace(handle_needle, helpers + handle_needle, 1) + + switch_needle = ' switch (sub) {\n' + switch_insert = ''' switch (sub) {\n case "public": {\n handlePublicLabCommand(rest, wantsJson, configDir);\n return;\n }\n''' + assert switch_needle in s + s = s.replace(switch_needle, switch_insert, 1) + cli.write_text(s) + + routes = Path('src/server/management/lab-routes.ts') + s = routes.read_text() + import_needle = 'import { jsonResponse } from "../auth-cors";\n' + import_insert = '''import {\n exportLocalPublicEvidence,\n importCommunityEvidenceValue,\n listCommunityEvidenceContext,\n previewLocalPublicEvidence,\n summarizePublicEvidenceVerification,\n PublicEvidenceValidationError,\n} from "../../lab/public";\n''' + import_needle + assert import_needle in s + s = s.replace(import_needle, import_insert, 1) + + handler_needle = 'export async function handleLabRoutes(ctx: ManagementContext): Promise {\n' + handler_helpers = r'''const MAX_PUBLIC_REQUEST_BYTES = 2 * 1024 * 1024; + + async function readBoundedPublicJson(req: Request): Promise { + const lengthRaw = req.headers.get("content-length"); + if (lengthRaw) { + const length = Number(lengthRaw); + if (!Number.isFinite(length) || length < 0 || length > MAX_PUBLIC_REQUEST_BYTES) { + throw new PublicEvidenceValidationError( + "public_request_too_large", + "public evidence request exceeds 2 MiB", + ); + } + } + if (!req.body) { + throw new PublicEvidenceValidationError("public_request_body", "JSON body is required"); + } + const reader = req.body.getReader(); + const chunks: Uint8Array[] = []; + let total = 0; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + total += value.byteLength; + if (total > MAX_PUBLIC_REQUEST_BYTES) { + await reader.cancel(); + throw new PublicEvidenceValidationError( + "public_request_too_large", + "public evidence request exceeds 2 MiB", + ); + } + chunks.push(value); + } + const bytes = new Uint8Array(total); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; + } + try { + return JSON.parse(new TextDecoder().decode(bytes)); + } catch { + throw new PublicEvidenceValidationError("public_request_json", "request body is not valid JSON"); + } + } + + function publicEventIds(raw: unknown): string[] { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) { + throw new PublicEvidenceValidationError("public_request_body", "request body must be an object"); + } + const keys = Object.keys(raw); + if (keys.length !== 1 || keys[0] !== "eventIds") { + throw new PublicEvidenceValidationError("public_request_body", "only eventIds is accepted"); + } + const eventIds = (raw as { eventIds?: unknown }).eventIds; + if (!Array.isArray(eventIds) || !eventIds.every((value) => typeof value === "string")) { + throw new PublicEvidenceValidationError("public_request_body", "eventIds must be a string array"); + } + return eventIds as string[]; + } + + function publicBundleValue(raw: unknown): unknown { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) { + throw new PublicEvidenceValidationError("public_request_body", "request body must be an object"); + } + const keys = Object.keys(raw); + if (keys.length !== 1 || keys[0] !== "bundle") { + throw new PublicEvidenceValidationError("public_request_body", "only bundle is accepted"); + } + return (raw as { bundle?: unknown }).bundle; + } + + function publicErrorResponse(err: unknown, ctx: ManagementContext): Response { + const message = err instanceof Error ? err.message : "public evidence operation failed"; + const code = err instanceof PublicEvidenceValidationError + ? err.code + : "public_evidence_error"; + return errorResponse(code, message, 400, ctx); + } + + ''' + assert handler_needle in s + s = s.replace(handler_needle, handler_helpers + handler_needle, 1) + + method_needle = ''' if (!url.pathname.startsWith("/api/lab")) return null;\n if (req.method !== "GET") return null;\n\n if (url.pathname === "/api/lab/status") {\n''' + method_insert = ''' if (!url.pathname.startsWith("/api/lab")) return null;\n\n if (req.method === "GET" && url.pathname === "/api/lab/public/community") {\n try {\n return jsonResponse(listCommunityEvidenceContext(), 200, req, config);\n } catch (err) {\n return publicErrorResponse(err, ctx);\n }\n }\n\n if (req.method === "POST") {\n if (url.pathname === "/api/lab/public/preview") {\n try {\n const body = await readBoundedPublicJson(req);\n return jsonResponse(\n previewLocalPublicEvidence({ eventIds: publicEventIds(body) }),\n 200,\n req,\n config,\n );\n } catch (err) {\n return publicErrorResponse(err, ctx);\n }\n }\n if (url.pathname === "/api/lab/public/export") {\n try {\n const body = await readBoundedPublicJson(req);\n return jsonResponse(\n exportLocalPublicEvidence({ eventIds: publicEventIds(body) }),\n 200,\n req,\n config,\n );\n } catch (err) {\n return publicErrorResponse(err, ctx);\n }\n }\n if (url.pathname === "/api/lab/public/verify") {\n try {\n const body = await readBoundedPublicJson(req);\n const result = summarizePublicEvidenceVerification(publicBundleValue(body));\n return jsonResponse(\n result,\n result.status === "cryptographically_valid" ? 200 : 400,\n req,\n config,\n );\n } catch (err) {\n return publicErrorResponse(err, ctx);\n }\n }\n if (url.pathname === "/api/lab/public/community/import") {\n try {\n const body = await readBoundedPublicJson(req);\n return jsonResponse(\n importCommunityEvidenceValue(publicBundleValue(body)),\n 200,\n req,\n config,\n );\n } catch (err) {\n return publicErrorResponse(err, ctx);\n }\n }\n return null;\n }\n\n if (req.method !== "GET") return null;\n\n if (url.pathname === "/api/lab/status") {\n''' + assert method_needle in s + s = s.replace(method_needle, method_insert, 1) + routes.write_text(s) + + test = Path('tests/lab-public-surfaces.test.ts') + s = test.read_text() + old = ''' test("does not expose a remote publish endpoint", async () => {\n const home = tempHome();\n const res = await api(home, "/api/lab/public/publish", { method: "POST", body: {} });\n expect(res.status).toBe(404);\n });''' + new = ''' test("does not expose a remote publish endpoint", async () => {\n const home = tempHome();\n process.env.OPENCODEX_HOME = home;\n const req = new ManagementRequest("http://127.0.0.1/api/lab/public/publish", {\n method: "POST",\n headers: { "content-type": "application/json" },\n body: "{}",\n });\n const res = await handleManagementAPI(req, new URL(req.url), config(home), {\n refreshCodexCatalog: async () => {},\n });\n expect(res).toBeNull();\n });''' + assert old in s + s = s.replace(old, new, 1) + test.write_text(s) + PY + - run: bun install --frozen-lockfile + - run: bun test tests/lab-public-evidence.test.ts tests/lab-public-evidence-signature.test.ts tests/lab-community-evidence.test.ts tests/lab-community-publisher-continuity.test.ts tests/lab-public-surfaces.test.ts + - run: bun x tsc --noEmit + - name: Commit validated Task 6 patch + shell: bash + run: | + rm .github/workflows/cl10-task6-apply.yml .github/workflows/cl10-task6-apply2.yml + git config user.name Wibias + git config user.email 37517432+Wibias@users.noreply.github.com + git add src/cli/lab.ts src/server/management/lab-routes.ts tests/lab-public-surfaces.test.ts .github/workflows/cl10-task6-apply.yml .github/workflows/cl10-task6-apply2.yml + git commit -m "feat(lab): add CL-10 local public evidence surfaces" + git push origin HEAD:feat/cl-10-public-evidence-runtime From fd7f8040d98bde4190025d6aa06d82fd2708a262 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 18:07:20 +0000 Subject: [PATCH 26/33] feat(lab): add CL-10 local public evidence surfaces --- .github/workflows/cl10-task6-apply.yml | 278 ------------------------ .github/workflows/cl10-task6-apply2.yml | 277 ----------------------- src/cli/lab.ts | 128 +++++++++++ src/server/management/lab-routes.ts | 153 +++++++++++++ tests/lab-public-surfaces.test.ts | 12 +- 5 files changed, 291 insertions(+), 557 deletions(-) delete mode 100644 .github/workflows/cl10-task6-apply.yml delete mode 100644 .github/workflows/cl10-task6-apply2.yml diff --git a/.github/workflows/cl10-task6-apply.yml b/.github/workflows/cl10-task6-apply.yml deleted file mode 100644 index 725af79b16..0000000000 --- a/.github/workflows/cl10-task6-apply.yml +++ /dev/null @@ -1,278 +0,0 @@ -name: CL-10 Task 6 patch - -on: - push: - branches: - - feat/cl-10-public-evidence-runtime - -permissions: - contents: write - -jobs: - apply: - if: contains(github.event.head_commit.message, 'chore: apply CL-10 Task 6') - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 - with: - fetch-depth: 1 - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 - with: - bun-version: 1.3.14 - - name: Apply operator surfaces - shell: bash - run: | - python3 <<'PY' - from pathlib import Path - - cli = Path('src/cli/lab.ts') - s = cli.read_text() - needle = 'import { createProductionLabRouteExecutor } from "../lib/lab-live-route-production";\n' - insert = needle + '''import {\n exportLocalPublicEvidence,\n importCommunityEvidenceFile,\n listCommunityEvidenceContext,\n previewLocalPublicEvidence,\n verifyPublicEvidenceFile,\n type PublicVerificationSummaryV1,\n} from "../lab/public";\n''' - assert needle in s - s = s.replace(needle, insert, 1) - - usage_needle = ' ocx lab catalog [--layer ] [--suite ] [--json]\n' - usage_insert = usage_needle + ''' ocx lab public preview --event [--event ...] [--json]\n ocx lab public export --event [--event ...] [--json]\n ocx lab public verify --file [--json]\n ocx lab public import --file [--json]\n ocx lab public community [--json]\n''' - assert usage_needle in s - s = s.replace(usage_needle, usage_insert, 1) - - handle_needle = 'export async function handleLabCommand(argv: string[], deps: LabCliDeps = {}): Promise {\n' - helpers = r'''function takeRepeatedOptions(args: string[], flag: string): string[] { - const values: string[] = []; - while (true) { - const index = args.indexOf(flag); - if (index < 0) break; - const value = args[index + 1]; - if (!value || value.startsWith("--")) { - throw new CliUsageError(`${flag} requires a value`, USAGE); - } - values.push(value); - args.splice(index, 2); - } - return values; - } - - function publicPreviewLines(result: ReturnType): string[] { - return [ - `Public evidence preview: ${result.bundle.records.length} exportable record(s)`, - `Excluded: ${result.excluded.length}`, - "Unsigned local preview; no publisher key or remote publish is created.", - ]; - } - - function publicExportLines(result: ReturnType): string[] { - return [ - "Public evidence exported locally", - `Bundle: ${result.bundle.bundleId}`, - `Publisher: ${result.bundle.publisher.keyId}`, - `Path: ${result.stored.path}`, - `Excluded: ${result.excluded.length}`, - "No remote publish occurred.", - ]; - } - - function publicVerificationLines(result: PublicVerificationSummaryV1): string[] { - if (result.status !== "cryptographically_valid") { - return [ - `Public evidence verification: ${result.status}`, - "Not locally verified.", - ...(result.detail ? [result.detail] : []), - ]; - } - return [ - "Public evidence verification: cryptographically valid", - `Bundle: ${result.bundleId}`, - `Publisher: ${result.publisherKeyId}`, - "Not locally verified. Signature validity proves integrity/continuity only.", - ]; - } - - function handlePublicLabCommand( - argv: string[], - wantsJson: boolean, - configDir: string, - ): void { - const [action, ...restInput] = argv; - const rest = [...restInput]; - switch (action) { - case "preview": { - const eventIds = takeRepeatedOptions(rest, "--event"); - rejectArgs(rest, USAGE); - const result = previewLocalPublicEvidence({ eventIds }, configDir); - printData(result, wantsJson, publicPreviewLines(result)); - return; - } - case "export": { - const eventIds = takeRepeatedOptions(rest, "--event"); - rejectArgs(rest, USAGE); - const result = exportLocalPublicEvidence({ eventIds }, configDir); - printData(result, wantsJson, publicExportLines(result)); - return; - } - case "verify": { - const path = takeOption(rest, "--file"); - if (!path) throw new CliUsageError("public verify requires --file", USAGE); - rejectArgs(rest, USAGE); - const result = verifyPublicEvidenceFile(path); - printData(result, wantsJson, publicVerificationLines(result)); - return; - } - case "import": { - const path = takeOption(rest, "--file"); - if (!path) throw new CliUsageError("public import requires --file", USAGE); - rejectArgs(rest, USAGE); - const result = importCommunityEvidenceFile(path, configDir); - printData(result, wantsJson, [ - `Community evidence imported: ${result.bundleId}`, - `Publisher: ${result.publisherKeyId}`, - "Trust: community_untrusted_v1; not locally verified.", - ]); - return; - } - case "community": { - rejectArgs(rest, USAGE); - const result = listCommunityEvidenceContext(configDir); - const lines = result.evidence.length > 0 - ? result.evidence.map((row) => - `${row.bundleId} publisher=${row.publisherKeyId} active=${row.activeRecordCount} revoked=${row.revokedRecordCount}`, - ) - : ["No community evidence"]; - printData(result, wantsJson, [ - "Community evidence (untrusted, read-only context; not locally verified)", - ...lines, - ]); - return; - } - default: - throw new CliUsageError("unknown public subcommand", USAGE); - } - } - - ''' - assert handle_needle in s - s = s.replace(handle_needle, helpers + handle_needle, 1) - - switch_needle = ' switch (sub) {\n' - switch_insert = ''' switch (sub) {\n case "public": {\n handlePublicLabCommand(rest, wantsJson, configDir);\n return;\n }\n''' - assert switch_needle in s - s = s.replace(switch_needle, switch_insert, 1) - cli.write_text(s) - - routes = Path('src/server/management/lab-routes.ts') - s = routes.read_text() - import_needle = 'import { jsonResponse } from "../auth-cors";\n' - import_insert = '''import {\n exportLocalPublicEvidence,\n importCommunityEvidenceValue,\n listCommunityEvidenceContext,\n previewLocalPublicEvidence,\n summarizePublicEvidenceVerification,\n PublicEvidenceValidationError,\n} from "../../lab/public";\n''' + import_needle - assert import_needle in s - s = s.replace(import_needle, import_insert, 1) - - handler_needle = 'export async function handleLabRoutes(ctx: ManagementContext): Promise {\n' - handler_helpers = r'''const MAX_PUBLIC_REQUEST_BYTES = 2 * 1024 * 1024; - - async function readBoundedPublicJson(req: Request): Promise { - const lengthRaw = req.headers.get("content-length"); - if (lengthRaw) { - const length = Number(lengthRaw); - if (!Number.isFinite(length) || length < 0 || length > MAX_PUBLIC_REQUEST_BYTES) { - throw new PublicEvidenceValidationError( - "public_request_too_large", - "public evidence request exceeds 2 MiB", - ); - } - } - if (!req.body) { - throw new PublicEvidenceValidationError("public_request_body", "JSON body is required"); - } - const reader = req.body.getReader(); - const chunks: Uint8Array[] = []; - let total = 0; - while (true) { - const { done, value } = await reader.read(); - if (done) break; - total += value.byteLength; - if (total > MAX_PUBLIC_REQUEST_BYTES) { - await reader.cancel(); - throw new PublicEvidenceValidationError( - "public_request_too_large", - "public evidence request exceeds 2 MiB", - ); - } - chunks.push(value); - } - const bytes = new Uint8Array(total); - let offset = 0; - for (const chunk of chunks) { - bytes.set(chunk, offset); - offset += chunk.byteLength; - } - try { - return JSON.parse(new TextDecoder().decode(bytes)); - } catch { - throw new PublicEvidenceValidationError("public_request_json", "request body is not valid JSON"); - } - } - - function publicEventIds(raw: unknown): string[] { - if (!raw || typeof raw !== "object" || Array.isArray(raw)) { - throw new PublicEvidenceValidationError("public_request_body", "request body must be an object"); - } - const keys = Object.keys(raw); - if (keys.length !== 1 || keys[0] !== "eventIds") { - throw new PublicEvidenceValidationError("public_request_body", "only eventIds is accepted"); - } - const eventIds = (raw as { eventIds?: unknown }).eventIds; - if (!Array.isArray(eventIds) || !eventIds.every((value) => typeof value === "string")) { - throw new PublicEvidenceValidationError("public_request_body", "eventIds must be a string array"); - } - return eventIds as string[]; - } - - function publicBundleValue(raw: unknown): unknown { - if (!raw || typeof raw !== "object" || Array.isArray(raw)) { - throw new PublicEvidenceValidationError("public_request_body", "request body must be an object"); - } - const keys = Object.keys(raw); - if (keys.length !== 1 || keys[0] !== "bundle") { - throw new PublicEvidenceValidationError("public_request_body", "only bundle is accepted"); - } - return (raw as { bundle?: unknown }).bundle; - } - - function publicErrorResponse(err: unknown, ctx: ManagementContext): Response { - const message = err instanceof Error ? err.message : "public evidence operation failed"; - const code = err instanceof PublicEvidenceValidationError - ? err.code - : "public_evidence_error"; - return errorResponse(code, message, 400, ctx); - } - - ''' - assert handler_needle in s - s = s.replace(handler_needle, handler_helpers + handler_needle, 1) - - method_needle = ''' if (!url.pathname.startsWith("/api/lab")) return null;\n if (req.method !== "GET") return null;\n\n if (url.pathname === "/api/lab/status") {\n''' - method_insert = ''' if (!url.pathname.startsWith("/api/lab")) return null;\n\n if (req.method === "GET" && url.pathname === "/api/lab/public/community") {\n try {\n return jsonResponse(listCommunityEvidenceContext(), 200, req, config);\n } catch (err) {\n return publicErrorResponse(err, ctx);\n }\n }\n\n if (req.method === "POST") {\n if (url.pathname === "/api/lab/public/preview") {\n try {\n const body = await readBoundedPublicJson(req);\n return jsonResponse(\n previewLocalPublicEvidence({ eventIds: publicEventIds(body) }),\n 200,\n req,\n config,\n );\n } catch (err) {\n return publicErrorResponse(err, ctx);\n }\n }\n if (url.pathname === "/api/lab/public/export") {\n try {\n const body = await readBoundedPublicJson(req);\n return jsonResponse(\n exportLocalPublicEvidence({ eventIds: publicEventIds(body) }),\n 200,\n req,\n config,\n );\n } catch (err) {\n return publicErrorResponse(err, ctx);\n }\n }\n if (url.pathname === "/api/lab/public/verify") {\n try {\n const body = await readBoundedPublicJson(req);\n const result = summarizePublicEvidenceVerification(publicBundleValue(body));\n return jsonResponse(\n result,\n result.status === "cryptographically_valid" ? 200 : 400,\n req,\n config,\n );\n } catch (err) {\n return publicErrorResponse(err, ctx);\n }\n }\n if (url.pathname === "/api/lab/public/community/import") {\n try {\n const body = await readBoundedPublicJson(req);\n return jsonResponse(\n importCommunityEvidenceValue(publicBundleValue(body)),\n 200,\n req,\n config,\n );\n } catch (err) {\n return publicErrorResponse(err, ctx);\n }\n }\n return null;\n }\n\n if (req.method !== "GET") return null;\n\n if (url.pathname === "/api/lab/status") {\n''' - assert method_needle in s - s = s.replace(method_needle, method_insert, 1) - routes.write_text(s) - - test = Path('tests/lab-public-surfaces.test.ts') - s = test.read_text() - old = ''' test("does not expose a remote publish endpoint", async () => {\n const home = tempHome();\n const res = await api(home, "/api/lab/public/publish", { method: "POST", body: {} });\n expect(res.status).toBe(404);\n });''' - new = ''' test("does not expose a remote publish endpoint", async () => {\n const home = tempHome();\n process.env.OPENCODEX_HOME = home;\n const req = new ManagementRequest("http://127.0.0.1/api/lab/public/publish", {\n method: "POST",\n headers: { "content-type": "application/json" },\n body: "{}",\n });\n const res = await handleManagementAPI(req, new URL(req.url), config(home), {\n refreshCodexCatalog: async () => {},\n });\n expect(res).toBeNull();\n });''' - assert old in s - s = s.replace(old, new, 1) - test.write_text(s) - PY - - run: bun install --frozen-lockfile - - run: bun test tests/lab-public-evidence.test.ts tests/lab-public-evidence-signature.test.ts tests/lab-community-evidence.test.ts tests/lab-community-publisher-continuity.test.ts tests/lab-public-surfaces.test.ts - - run: bun x tsc --noEmit - - name: Commit validated Task 6 patch - shell: bash - run: | - rm .github/workflows/cl10-task6-apply.yml - git config user.name Wibias - git config user.email 37517432+Wibias@users.noreply.github.com - git add src/cli/lab.ts src/server/management/lab-routes.ts tests/lab-public-surfaces.test.ts .github/workflows/cl10-task6-apply.yml - git commit -m "feat(lab): add CL-10 local public evidence surfaces" - git push origin HEAD:feat/cl-10-public-evidence-runtime diff --git a/.github/workflows/cl10-task6-apply2.yml b/.github/workflows/cl10-task6-apply2.yml deleted file mode 100644 index bd865435c6..0000000000 --- a/.github/workflows/cl10-task6-apply2.yml +++ /dev/null @@ -1,277 +0,0 @@ -name: CL-10 Task 6 patch retry - -on: - push: - branches: - - feat/cl-10-public-evidence-runtime - -permissions: - contents: write - -jobs: - apply: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 - with: - fetch-depth: 1 - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 - with: - bun-version: 1.3.14 - - name: Apply operator surfaces - shell: bash - run: | - python3 <<'PY' - from pathlib import Path - - cli = Path('src/cli/lab.ts') - s = cli.read_text() - needle = 'import { createProductionLabRouteExecutor } from "../lib/lab-live-route-production";\n' - insert = needle + '''import {\n exportLocalPublicEvidence,\n importCommunityEvidenceFile,\n listCommunityEvidenceContext,\n previewLocalPublicEvidence,\n verifyPublicEvidenceFile,\n type PublicVerificationSummaryV1,\n} from "../lab/public";\n''' - assert needle in s - s = s.replace(needle, insert, 1) - - usage_needle = ' ocx lab catalog [--layer ] [--suite ] [--json]\n' - usage_insert = usage_needle + ''' ocx lab public preview --event [--event ...] [--json]\n ocx lab public export --event [--event ...] [--json]\n ocx lab public verify --file [--json]\n ocx lab public import --file [--json]\n ocx lab public community [--json]\n''' - assert usage_needle in s - s = s.replace(usage_needle, usage_insert, 1) - - handle_needle = 'export async function handleLabCommand(argv: string[], deps: LabCliDeps = {}): Promise {\n' - helpers = r'''function takeRepeatedOptions(args: string[], flag: string): string[] { - const values: string[] = []; - while (true) { - const index = args.indexOf(flag); - if (index < 0) break; - const value = args[index + 1]; - if (!value || value.startsWith("--")) { - throw new CliUsageError(`${flag} requires a value`, USAGE); - } - values.push(value); - args.splice(index, 2); - } - return values; - } - - function publicPreviewLines(result: ReturnType): string[] { - return [ - `Public evidence preview: ${result.bundle.records.length} exportable record(s)`, - `Excluded: ${result.excluded.length}`, - "Unsigned local preview; no publisher key or remote publish is created.", - ]; - } - - function publicExportLines(result: ReturnType): string[] { - return [ - "Public evidence exported locally", - `Bundle: ${result.bundle.bundleId}`, - `Publisher: ${result.bundle.publisher.keyId}`, - `Path: ${result.stored.path}`, - `Excluded: ${result.excluded.length}`, - "No remote publish occurred.", - ]; - } - - function publicVerificationLines(result: PublicVerificationSummaryV1): string[] { - if (result.status !== "cryptographically_valid") { - return [ - `Public evidence verification: ${result.status}`, - "Not locally verified.", - ...(result.detail ? [result.detail] : []), - ]; - } - return [ - "Public evidence verification: cryptographically valid", - `Bundle: ${result.bundleId}`, - `Publisher: ${result.publisherKeyId}`, - "Not locally verified. Signature validity proves integrity/continuity only.", - ]; - } - - function handlePublicLabCommand( - argv: string[], - wantsJson: boolean, - configDir: string, - ): void { - const [action, ...restInput] = argv; - const rest = [...restInput]; - switch (action) { - case "preview": { - const eventIds = takeRepeatedOptions(rest, "--event"); - rejectArgs(rest, USAGE); - const result = previewLocalPublicEvidence({ eventIds }, configDir); - printData(result, wantsJson, publicPreviewLines(result)); - return; - } - case "export": { - const eventIds = takeRepeatedOptions(rest, "--event"); - rejectArgs(rest, USAGE); - const result = exportLocalPublicEvidence({ eventIds }, configDir); - printData(result, wantsJson, publicExportLines(result)); - return; - } - case "verify": { - const path = takeOption(rest, "--file"); - if (!path) throw new CliUsageError("public verify requires --file", USAGE); - rejectArgs(rest, USAGE); - const result = verifyPublicEvidenceFile(path); - printData(result, wantsJson, publicVerificationLines(result)); - return; - } - case "import": { - const path = takeOption(rest, "--file"); - if (!path) throw new CliUsageError("public import requires --file", USAGE); - rejectArgs(rest, USAGE); - const result = importCommunityEvidenceFile(path, configDir); - printData(result, wantsJson, [ - `Community evidence imported: ${result.bundleId}`, - `Publisher: ${result.publisherKeyId}`, - "Trust: community_untrusted_v1; not locally verified.", - ]); - return; - } - case "community": { - rejectArgs(rest, USAGE); - const result = listCommunityEvidenceContext(configDir); - const lines = result.evidence.length > 0 - ? result.evidence.map((row) => - `${row.bundleId} publisher=${row.publisherKeyId} active=${row.activeRecordCount} revoked=${row.revokedRecordCount}`, - ) - : ["No community evidence"]; - printData(result, wantsJson, [ - "Community evidence (untrusted, read-only context; not locally verified)", - ...lines, - ]); - return; - } - default: - throw new CliUsageError("unknown public subcommand", USAGE); - } - } - - ''' - assert handle_needle in s - s = s.replace(handle_needle, helpers + handle_needle, 1) - - switch_needle = ' switch (sub) {\n' - switch_insert = ''' switch (sub) {\n case "public": {\n handlePublicLabCommand(rest, wantsJson, configDir);\n return;\n }\n''' - assert switch_needle in s - s = s.replace(switch_needle, switch_insert, 1) - cli.write_text(s) - - routes = Path('src/server/management/lab-routes.ts') - s = routes.read_text() - import_needle = 'import { jsonResponse } from "../auth-cors";\n' - import_insert = '''import {\n exportLocalPublicEvidence,\n importCommunityEvidenceValue,\n listCommunityEvidenceContext,\n previewLocalPublicEvidence,\n summarizePublicEvidenceVerification,\n PublicEvidenceValidationError,\n} from "../../lab/public";\n''' + import_needle - assert import_needle in s - s = s.replace(import_needle, import_insert, 1) - - handler_needle = 'export async function handleLabRoutes(ctx: ManagementContext): Promise {\n' - handler_helpers = r'''const MAX_PUBLIC_REQUEST_BYTES = 2 * 1024 * 1024; - - async function readBoundedPublicJson(req: Request): Promise { - const lengthRaw = req.headers.get("content-length"); - if (lengthRaw) { - const length = Number(lengthRaw); - if (!Number.isFinite(length) || length < 0 || length > MAX_PUBLIC_REQUEST_BYTES) { - throw new PublicEvidenceValidationError( - "public_request_too_large", - "public evidence request exceeds 2 MiB", - ); - } - } - if (!req.body) { - throw new PublicEvidenceValidationError("public_request_body", "JSON body is required"); - } - const reader = req.body.getReader(); - const chunks: Uint8Array[] = []; - let total = 0; - while (true) { - const { done, value } = await reader.read(); - if (done) break; - total += value.byteLength; - if (total > MAX_PUBLIC_REQUEST_BYTES) { - await reader.cancel(); - throw new PublicEvidenceValidationError( - "public_request_too_large", - "public evidence request exceeds 2 MiB", - ); - } - chunks.push(value); - } - const bytes = new Uint8Array(total); - let offset = 0; - for (const chunk of chunks) { - bytes.set(chunk, offset); - offset += chunk.byteLength; - } - try { - return JSON.parse(new TextDecoder().decode(bytes)); - } catch { - throw new PublicEvidenceValidationError("public_request_json", "request body is not valid JSON"); - } - } - - function publicEventIds(raw: unknown): string[] { - if (!raw || typeof raw !== "object" || Array.isArray(raw)) { - throw new PublicEvidenceValidationError("public_request_body", "request body must be an object"); - } - const keys = Object.keys(raw); - if (keys.length !== 1 || keys[0] !== "eventIds") { - throw new PublicEvidenceValidationError("public_request_body", "only eventIds is accepted"); - } - const eventIds = (raw as { eventIds?: unknown }).eventIds; - if (!Array.isArray(eventIds) || !eventIds.every((value) => typeof value === "string")) { - throw new PublicEvidenceValidationError("public_request_body", "eventIds must be a string array"); - } - return eventIds as string[]; - } - - function publicBundleValue(raw: unknown): unknown { - if (!raw || typeof raw !== "object" || Array.isArray(raw)) { - throw new PublicEvidenceValidationError("public_request_body", "request body must be an object"); - } - const keys = Object.keys(raw); - if (keys.length !== 1 || keys[0] !== "bundle") { - throw new PublicEvidenceValidationError("public_request_body", "only bundle is accepted"); - } - return (raw as { bundle?: unknown }).bundle; - } - - function publicErrorResponse(err: unknown, ctx: ManagementContext): Response { - const message = err instanceof Error ? err.message : "public evidence operation failed"; - const code = err instanceof PublicEvidenceValidationError - ? err.code - : "public_evidence_error"; - return errorResponse(code, message, 400, ctx); - } - - ''' - assert handler_needle in s - s = s.replace(handler_needle, handler_helpers + handler_needle, 1) - - method_needle = ''' if (!url.pathname.startsWith("/api/lab")) return null;\n if (req.method !== "GET") return null;\n\n if (url.pathname === "/api/lab/status") {\n''' - method_insert = ''' if (!url.pathname.startsWith("/api/lab")) return null;\n\n if (req.method === "GET" && url.pathname === "/api/lab/public/community") {\n try {\n return jsonResponse(listCommunityEvidenceContext(), 200, req, config);\n } catch (err) {\n return publicErrorResponse(err, ctx);\n }\n }\n\n if (req.method === "POST") {\n if (url.pathname === "/api/lab/public/preview") {\n try {\n const body = await readBoundedPublicJson(req);\n return jsonResponse(\n previewLocalPublicEvidence({ eventIds: publicEventIds(body) }),\n 200,\n req,\n config,\n );\n } catch (err) {\n return publicErrorResponse(err, ctx);\n }\n }\n if (url.pathname === "/api/lab/public/export") {\n try {\n const body = await readBoundedPublicJson(req);\n return jsonResponse(\n exportLocalPublicEvidence({ eventIds: publicEventIds(body) }),\n 200,\n req,\n config,\n );\n } catch (err) {\n return publicErrorResponse(err, ctx);\n }\n }\n if (url.pathname === "/api/lab/public/verify") {\n try {\n const body = await readBoundedPublicJson(req);\n const result = summarizePublicEvidenceVerification(publicBundleValue(body));\n return jsonResponse(\n result,\n result.status === "cryptographically_valid" ? 200 : 400,\n req,\n config,\n );\n } catch (err) {\n return publicErrorResponse(err, ctx);\n }\n }\n if (url.pathname === "/api/lab/public/community/import") {\n try {\n const body = await readBoundedPublicJson(req);\n return jsonResponse(\n importCommunityEvidenceValue(publicBundleValue(body)),\n 200,\n req,\n config,\n );\n } catch (err) {\n return publicErrorResponse(err, ctx);\n }\n }\n return null;\n }\n\n if (req.method !== "GET") return null;\n\n if (url.pathname === "/api/lab/status") {\n''' - assert method_needle in s - s = s.replace(method_needle, method_insert, 1) - routes.write_text(s) - - test = Path('tests/lab-public-surfaces.test.ts') - s = test.read_text() - old = ''' test("does not expose a remote publish endpoint", async () => {\n const home = tempHome();\n const res = await api(home, "/api/lab/public/publish", { method: "POST", body: {} });\n expect(res.status).toBe(404);\n });''' - new = ''' test("does not expose a remote publish endpoint", async () => {\n const home = tempHome();\n process.env.OPENCODEX_HOME = home;\n const req = new ManagementRequest("http://127.0.0.1/api/lab/public/publish", {\n method: "POST",\n headers: { "content-type": "application/json" },\n body: "{}",\n });\n const res = await handleManagementAPI(req, new URL(req.url), config(home), {\n refreshCodexCatalog: async () => {},\n });\n expect(res).toBeNull();\n });''' - assert old in s - s = s.replace(old, new, 1) - test.write_text(s) - PY - - run: bun install --frozen-lockfile - - run: bun test tests/lab-public-evidence.test.ts tests/lab-public-evidence-signature.test.ts tests/lab-community-evidence.test.ts tests/lab-community-publisher-continuity.test.ts tests/lab-public-surfaces.test.ts - - run: bun x tsc --noEmit - - name: Commit validated Task 6 patch - shell: bash - run: | - rm .github/workflows/cl10-task6-apply.yml .github/workflows/cl10-task6-apply2.yml - git config user.name Wibias - git config user.email 37517432+Wibias@users.noreply.github.com - git add src/cli/lab.ts src/server/management/lab-routes.ts tests/lab-public-surfaces.test.ts .github/workflows/cl10-task6-apply.yml .github/workflows/cl10-task6-apply2.yml - git commit -m "feat(lab): add CL-10 local public evidence surfaces" - git push origin HEAD:feat/cl-10-public-evidence-runtime diff --git a/src/cli/lab.ts b/src/cli/lab.ts index 013b96327a..deac35b94c 100644 --- a/src/cli/lab.ts +++ b/src/cli/lab.ts @@ -63,6 +63,14 @@ import { planManualLabRun } from "../lab/automation/planner"; import { listLabAutomationRuns } from "../lab/automation/runs-query"; import { LabAutomationError, type LabAutomationLayer } from "../lab/automation/types"; import { createProductionLabRouteExecutor } from "../lib/lab-live-route-production"; +import { + exportLocalPublicEvidence, + importCommunityEvidenceFile, + listCommunityEvidenceContext, + previewLocalPublicEvidence, + verifyPublicEvidenceFile, + type PublicVerificationSummaryV1, +} from "../lab/public"; const USAGE = `Usage: ocx lab status [--json] @@ -76,6 +84,11 @@ const USAGE = `Usage: ocx lab artifacts [--status ] [--artifact-class ] [--limit ] [--cursor ] [--json] ocx lab artifact [--json] ocx lab catalog [--layer ] [--suite ] [--json] + ocx lab public preview --event [--event ...] [--json] + ocx lab public export --event [--event ...] [--json] + ocx lab public verify --file [--json] + ocx lab public import --file [--json] + ocx lab public community [--json] ocx lab automation status [--json] ocx lab automation enable [--protocol] [--live] [--json] ocx lab automation disable [--json] @@ -223,6 +236,117 @@ function runListLines(page: ReturnType): string[] return lines.length > 0 ? lines : ["No automation runs"]; } +function takeRepeatedOptions(args: string[], flag: string): string[] { + const values: string[] = []; + while (true) { + const index = args.indexOf(flag); + if (index < 0) break; + const value = args[index + 1]; + if (!value || value.startsWith("--")) { + throw new CliUsageError(`${flag} requires a value`, USAGE); + } + values.push(value); + args.splice(index, 2); + } + return values; +} + +function publicPreviewLines(result: ReturnType): string[] { + return [ + `Public evidence preview: ${result.bundle.records.length} exportable record(s)`, + `Excluded: ${result.excluded.length}`, + "Unsigned local preview; no publisher key or remote publish is created.", + ]; +} + +function publicExportLines(result: ReturnType): string[] { + return [ + "Public evidence exported locally", + `Bundle: ${result.bundle.bundleId}`, + `Publisher: ${result.bundle.publisher.keyId}`, + `Path: ${result.stored.path}`, + `Excluded: ${result.excluded.length}`, + "No remote publish occurred.", + ]; +} + +function publicVerificationLines(result: PublicVerificationSummaryV1): string[] { + if (result.status !== "cryptographically_valid") { + return [ + `Public evidence verification: ${result.status}`, + "Not locally verified.", + ...(result.detail ? [result.detail] : []), + ]; + } + return [ + "Public evidence verification: cryptographically valid", + `Bundle: ${result.bundleId}`, + `Publisher: ${result.publisherKeyId}`, + "Not locally verified. Signature validity proves integrity/continuity only.", + ]; +} + +function handlePublicLabCommand( + argv: string[], + wantsJson: boolean, + configDir: string, +): void { + const [action, ...restInput] = argv; + const rest = [...restInput]; + switch (action) { + case "preview": { + const eventIds = takeRepeatedOptions(rest, "--event"); + rejectArgs(rest, USAGE); + const result = previewLocalPublicEvidence({ eventIds }, configDir); + printData(result, wantsJson, publicPreviewLines(result)); + return; + } + case "export": { + const eventIds = takeRepeatedOptions(rest, "--event"); + rejectArgs(rest, USAGE); + const result = exportLocalPublicEvidence({ eventIds }, configDir); + printData(result, wantsJson, publicExportLines(result)); + return; + } + case "verify": { + const path = takeOption(rest, "--file"); + if (!path) throw new CliUsageError("public verify requires --file", USAGE); + rejectArgs(rest, USAGE); + const result = verifyPublicEvidenceFile(path); + printData(result, wantsJson, publicVerificationLines(result)); + return; + } + case "import": { + const path = takeOption(rest, "--file"); + if (!path) throw new CliUsageError("public import requires --file", USAGE); + rejectArgs(rest, USAGE); + const result = importCommunityEvidenceFile(path, configDir); + printData(result, wantsJson, [ + `Community evidence imported: ${result.bundleId}`, + `Publisher: ${result.publisherKeyId}`, + "Trust: community_untrusted_v1; not locally verified.", + ]); + return; + } + case "community": { + rejectArgs(rest, USAGE); + const result = listCommunityEvidenceContext(configDir); + const lines = result.evidence.length > 0 + ? result.evidence.map((row) => + `${row.bundleId} publisher=${row.publisherKeyId} active=${row.activeRecordCount} revoked=${row.revokedRecordCount}`, + ) + : ["No community evidence"]; + printData(result, wantsJson, [ + "Community evidence (untrusted, read-only context; not locally verified)", + ...lines, + ]); + return; + } + default: + throw new CliUsageError("unknown public subcommand", USAGE); + } +} + export async function handleLabCommand(argv: string[], deps: LabCliDeps = {}): Promise { return runCliAction(async () => { const configDir = deps.configDir ?? getConfigDir(); @@ -232,6 +356,10 @@ export async function handleLabCommand(argv: string[], deps: LabCliDeps = {}): P try { switch (sub) { + case "public": { + handlePublicLabCommand(rest, wantsJson, configDir); + return; + } case "status": { rejectArgs(rest, USAGE); const status = queryLabStatus(configDir); diff --git a/src/server/management/lab-routes.ts b/src/server/management/lab-routes.ts index 5c0929e47f..1a7c732529 100644 --- a/src/server/management/lab-routes.ts +++ b/src/server/management/lab-routes.ts @@ -42,6 +42,14 @@ import { queryLabVerdicts, queryPassiveProductionSignals, } from "../../lab/query"; +import { + exportLocalPublicEvidence, + importCommunityEvidenceValue, + listCommunityEvidenceContext, + previewLocalPublicEvidence, + summarizePublicEvidenceVerification, + PublicEvidenceValidationError, +} from "../../lab/public"; import { jsonResponse } from "../auth-cors"; import type { ManagementContext } from "./context"; @@ -186,9 +194,154 @@ function paginatedEnvelope(page: { items: T[]; nextCursor?: string; hasMore: }; } +const MAX_PUBLIC_REQUEST_BYTES = 2 * 1024 * 1024; + +async function readBoundedPublicJson(req: Request): Promise { + const lengthRaw = req.headers.get("content-length"); + if (lengthRaw) { + const length = Number(lengthRaw); + if (!Number.isFinite(length) || length < 0 || length > MAX_PUBLIC_REQUEST_BYTES) { + throw new PublicEvidenceValidationError( + "public_request_too_large", + "public evidence request exceeds 2 MiB", + ); + } + } + if (!req.body) { + throw new PublicEvidenceValidationError("public_request_body", "JSON body is required"); + } + const reader = req.body.getReader(); + const chunks: Uint8Array[] = []; + let total = 0; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + total += value.byteLength; + if (total > MAX_PUBLIC_REQUEST_BYTES) { + await reader.cancel(); + throw new PublicEvidenceValidationError( + "public_request_too_large", + "public evidence request exceeds 2 MiB", + ); + } + chunks.push(value); + } + const bytes = new Uint8Array(total); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; + } + try { + return JSON.parse(new TextDecoder().decode(bytes)); + } catch { + throw new PublicEvidenceValidationError("public_request_json", "request body is not valid JSON"); + } +} + +function publicEventIds(raw: unknown): string[] { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) { + throw new PublicEvidenceValidationError("public_request_body", "request body must be an object"); + } + const keys = Object.keys(raw); + if (keys.length !== 1 || keys[0] !== "eventIds") { + throw new PublicEvidenceValidationError("public_request_body", "only eventIds is accepted"); + } + const eventIds = (raw as { eventIds?: unknown }).eventIds; + if (!Array.isArray(eventIds) || !eventIds.every((value) => typeof value === "string")) { + throw new PublicEvidenceValidationError("public_request_body", "eventIds must be a string array"); + } + return eventIds as string[]; +} + +function publicBundleValue(raw: unknown): unknown { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) { + throw new PublicEvidenceValidationError("public_request_body", "request body must be an object"); + } + const keys = Object.keys(raw); + if (keys.length !== 1 || keys[0] !== "bundle") { + throw new PublicEvidenceValidationError("public_request_body", "only bundle is accepted"); + } + return (raw as { bundle?: unknown }).bundle; +} + +function publicErrorResponse(err: unknown, ctx: ManagementContext): Response { + const message = err instanceof Error ? err.message : "public evidence operation failed"; + const code = err instanceof PublicEvidenceValidationError + ? err.code + : "public_evidence_error"; + return errorResponse(code, message, 400, ctx); +} + export async function handleLabRoutes(ctx: ManagementContext): Promise { const { url, req, config } = ctx; if (!url.pathname.startsWith("/api/lab")) return null; + + if (req.method === "GET" && url.pathname === "/api/lab/public/community") { + try { + return jsonResponse(listCommunityEvidenceContext(), 200, req, config); + } catch (err) { + return publicErrorResponse(err, ctx); + } + } + + if (req.method === "POST") { + if (url.pathname === "/api/lab/public/preview") { + try { + const body = await readBoundedPublicJson(req); + return jsonResponse( + previewLocalPublicEvidence({ eventIds: publicEventIds(body) }), + 200, + req, + config, + ); + } catch (err) { + return publicErrorResponse(err, ctx); + } + } + if (url.pathname === "/api/lab/public/export") { + try { + const body = await readBoundedPublicJson(req); + return jsonResponse( + exportLocalPublicEvidence({ eventIds: publicEventIds(body) }), + 200, + req, + config, + ); + } catch (err) { + return publicErrorResponse(err, ctx); + } + } + if (url.pathname === "/api/lab/public/verify") { + try { + const body = await readBoundedPublicJson(req); + const result = summarizePublicEvidenceVerification(publicBundleValue(body)); + return jsonResponse( + result, + result.status === "cryptographically_valid" ? 200 : 400, + req, + config, + ); + } catch (err) { + return publicErrorResponse(err, ctx); + } + } + if (url.pathname === "/api/lab/public/community/import") { + try { + const body = await readBoundedPublicJson(req); + return jsonResponse( + importCommunityEvidenceValue(publicBundleValue(body)), + 200, + req, + config, + ); + } catch (err) { + return publicErrorResponse(err, ctx); + } + } + return null; + } + if (req.method !== "GET") return null; if (url.pathname === "/api/lab/status") { diff --git a/tests/lab-public-surfaces.test.ts b/tests/lab-public-surfaces.test.ts index 379d1fa776..e970bc2761 100644 --- a/tests/lab-public-surfaces.test.ts +++ b/tests/lab-public-surfaces.test.ts @@ -268,7 +268,15 @@ describe("CL-10 management local public evidence", () => { test("does not expose a remote publish endpoint", async () => { const home = tempHome(); - const res = await api(home, "/api/lab/public/publish", { method: "POST", body: {} }); - expect(res.status).toBe(404); + process.env.OPENCODEX_HOME = home; + const req = new ManagementRequest("http://127.0.0.1/api/lab/public/publish", { + method: "POST", + headers: { "content-type": "application/json" }, + body: "{}", + }); + const res = await handleManagementAPI(req, new URL(req.url), config(home), { + refreshCodexCatalog: async () => {}, + }); + expect(res).toBeNull(); }); }); From 90ad98099443f93a9a0afcbd2a8a1a505bef3cd2 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 20:12:45 +0200 Subject: [PATCH 27/33] test(gui): define CL-10 community evidence context --- .../compatibility-community-evidence.test.ts | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 gui/tests/compatibility-community-evidence.test.ts diff --git a/gui/tests/compatibility-community-evidence.test.ts b/gui/tests/compatibility-community-evidence.test.ts new file mode 100644 index 0000000000..cf76d149ab --- /dev/null +++ b/gui/tests/compatibility-community-evidence.test.ts @@ -0,0 +1,58 @@ +import { expect, test } from "bun:test"; +import { + parseCommunityEvidenceContext, + type CommunityEvidenceContextDto, +} from "../src/pages/compatibility-matrix-api"; +import { labSupplement, type LabLocale } from "../src/i18n/lab-translations"; + +const LOCALES: LabLocale[] = ["en", "de", "ja", "ko", "ru", "tr", "zh", "zh-TW"]; + +function validContext(): CommunityEvidenceContextDto { + return { + trustClass: "community_untrusted_v1", + locallyVerified: false, + evidence: [ + { + trustClass: "community_untrusted_v1", + status: "cryptographically_valid", + bundleId: "a".repeat(64), + publisherKeyId: "b".repeat(64), + activeRecordCount: 3, + revokedRecordCount: 1, + }, + ], + }; +} + +test("Compatibility Matrix parses only quarantined community evidence context", () => { + expect(parseCommunityEvidenceContext(validContext())).toEqual(validContext()); + expect(parseCommunityEvidenceContext({ ...validContext(), locallyVerified: true })).toBeNull(); + expect(parseCommunityEvidenceContext({ ...validContext(), trustClass: "local" })).toBeNull(); + expect(parseCommunityEvidenceContext({ + ...validContext(), + evidence: [{ ...validContext().evidence[0]!, activeRecordCount: -1 }], + })).toBeNull(); + expect(parseCommunityEvidenceContext({ + ...validContext(), + evidence: [{ ...validContext().evidence[0]!, status: "locally_verified" }], + })).toBeNull(); +}); + +test("Compatibility Matrix community copy is localized and explicitly non-authoritative", () => { + for (const locale of LOCALES) { + expect(labSupplement(locale, "community.title")).toBeTruthy(); + expect(labSupplement(locale, "community.notLocalVerdict")).toBeTruthy(); + expect(labSupplement(locale, "community.bundles")).toBeTruthy(); + expect(labSupplement(locale, "community.activeRecords")).toBeTruthy(); + expect(labSupplement(locale, "community.revokedRecords")).toBeTruthy(); + } + expect(labSupplement("en", "community.notLocalVerdict")).toMatch(/untrusted|not included|local verdict/i); +}); + +test("Compatibility Matrix renders community evidence as separate context, never a combined score", async () => { + const source = await Bun.file(new URL("../src/pages/CompatibilityMatrix.tsx", import.meta.url)).text(); + expect(source).toContain('data-testid="lab-community-evidence"'); + expect(source).toContain('labSupplement(locale, "community.notLocalVerdict")'); + expect(source).not.toMatch(/combined.?score/i); + expect(source).not.toMatch(/community.*verdict\s*=|verdict\s*=.*community/i); +}); From edf2d836257adf9d24b1027978722cceed49c935 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 20:13:04 +0200 Subject: [PATCH 28/33] ci: cover CL-10 Matrix community context --- .github/workflows/cl10-focus.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/cl10-focus.yml b/.github/workflows/cl10-focus.yml index b1181bc457..63b24c2075 100644 --- a/.github/workflows/cl10-focus.yml +++ b/.github/workflows/cl10-focus.yml @@ -19,3 +19,6 @@ jobs: - run: bun install --frozen-lockfile - run: bun test tests/lab-public-evidence.test.ts tests/lab-public-evidence-signature.test.ts tests/lab-community-evidence.test.ts tests/lab-community-publisher-continuity.test.ts tests/lab-public-surfaces.test.ts - run: bun x tsc --noEmit + - run: cd gui && bun install --frozen-lockfile + - run: cd gui && bun test tests/compatibility-community-evidence.test.ts tests/compatibility-lab-i18n.test.ts + - run: cd gui && bun x tsc -b --pretty false From 3c4eae264263d058f5aaf24acf04dd3f6cf20f20 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 20:16:35 +0200 Subject: [PATCH 29/33] chore: apply CL-10 Task 7 --- .github/workflows/cl10-task7-apply.yml | 179 +++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 .github/workflows/cl10-task7-apply.yml diff --git a/.github/workflows/cl10-task7-apply.yml b/.github/workflows/cl10-task7-apply.yml new file mode 100644 index 0000000000..c86682217e --- /dev/null +++ b/.github/workflows/cl10-task7-apply.yml @@ -0,0 +1,179 @@ +name: CL-10 Task 7 patch + +on: + push: + branches: + - feat/cl-10-public-evidence-runtime + +permissions: + contents: write + +jobs: + apply: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + with: + fetch-depth: 1 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + with: + bun-version: 1.3.14 + - name: Apply Matrix community context + shell: bash + run: | + python3 <<'PY' + from pathlib import Path + + api = Path('gui/src/pages/compatibility-matrix-api.ts') + s = api.read_text() + marker = 'export type LabPageData = {\n' + community = r'''export type CommunityEvidenceSummaryRowDto = { + trustClass: "community_untrusted_v1"; + status: "cryptographically_valid"; + bundleId: string; + publisherKeyId: string; + activeRecordCount: number; + revokedRecordCount: number; + }; + + export type CommunityEvidenceContextDto = { + evidence: CommunityEvidenceSummaryRowDto[]; + trustClass: "community_untrusted_v1"; + locallyVerified: false; + }; + + function hasOnlyKeys(raw: Record, allowed: readonly string[]): boolean { + const allowedSet = new Set(allowed); + return Object.keys(raw).every(key => allowedSet.has(key)); + } + + function isSha256Hex(value: unknown): value is string { + return typeof value === "string" && /^[0-9a-f]{64}$/.test(value); + } + + function isNonNegativeInteger(value: unknown): value is number { + return typeof value === "number" && Number.isSafeInteger(value) && value >= 0; + } + + export function parseCommunityEvidenceContext(raw: unknown): CommunityEvidenceContextDto | null { + if (!isPlainObject(raw) + || !hasOnlyKeys(raw, ["evidence", "trustClass", "locallyVerified"]) + || raw.trustClass !== "community_untrusted_v1" + || raw.locallyVerified !== false + || !Array.isArray(raw.evidence) + || raw.evidence.length > 4096) { + return null; + } + const evidence: CommunityEvidenceSummaryRowDto[] = []; + for (const value of raw.evidence) { + if (!isPlainObject(value) + || !hasOnlyKeys(value, [ + "trustClass", + "status", + "bundleId", + "publisherKeyId", + "activeRecordCount", + "revokedRecordCount", + ]) + || value.trustClass !== "community_untrusted_v1" + || value.status !== "cryptographically_valid" + || !isSha256Hex(value.bundleId) + || !isSha256Hex(value.publisherKeyId) + || !isNonNegativeInteger(value.activeRecordCount) + || !isNonNegativeInteger(value.revokedRecordCount)) { + return null; + } + evidence.push({ + trustClass: "community_untrusted_v1", + status: "cryptographically_valid", + bundleId: value.bundleId, + publisherKeyId: value.publisherKeyId, + activeRecordCount: value.activeRecordCount, + revokedRecordCount: value.revokedRecordCount, + }); + } + return { + evidence, + trustClass: "community_untrusted_v1", + locallyVerified: false, + }; + } + + export async function fetchCommunityEvidenceContext( + apiBase: string, + signal: AbortSignal, + ): Promise { + const raw = await fetchLabJson(apiBase, "/api/lab/public/community", signal); + const context = parseCommunityEvidenceContext(raw); + if (!context) throw invalidResponse(); + return context; + } + + ''' + assert marker in s + s = s.replace(marker, community + marker, 1) + + old_detail = ''' production: PassiveProductionSummaryDto | null;\n};''' + new_detail = ''' production: PassiveProductionSummaryDto | null;\n community: CommunityEvidenceContextDto | null;\n};''' + assert old_detail in s + s = s.replace(old_detail, new_detail, 1) + + old_promise = ''' const [subject, observations, events, artifacts, production] = await Promise.all([\n fetchSubjectDetail(apiBase, verdict.subjectId, signal),\n fetchAllObservations(apiBase, observationFilters, signal),\n mapSettledBounded(eventIds, DETAIL_CONCURRENCY, signal, id => fetchEventById(apiBase, id, signal)),\n mapSettledBounded(digests, DETAIL_CONCURRENCY, signal, digest => fetchArtifactByDigest(apiBase, digest, signal)),\n fetchPassiveProductionSummary(apiBase, verdict.subjectId, signal).catch(error => {\n if (signal.aborted) throw error;\n return null;\n }),\n ]);''' + new_promise = ''' const [subject, observations, events, artifacts, production, community] = await Promise.all([\n fetchSubjectDetail(apiBase, verdict.subjectId, signal),\n fetchAllObservations(apiBase, observationFilters, signal),\n mapSettledBounded(eventIds, DETAIL_CONCURRENCY, signal, id => fetchEventById(apiBase, id, signal)),\n mapSettledBounded(digests, DETAIL_CONCURRENCY, signal, digest => fetchArtifactByDigest(apiBase, digest, signal)),\n fetchPassiveProductionSummary(apiBase, verdict.subjectId, signal).catch(error => {\n if (signal.aborted) throw error;\n return null;\n }),\n fetchCommunityEvidenceContext(apiBase, signal).catch(error => {\n if (signal.aborted) throw error;\n return null;\n }),\n ]);''' + assert old_promise in s + s = s.replace(old_promise, new_promise, 1) + old_return = ''' artifacts,\n production,\n };''' + new_return = ''' artifacts,\n production,\n community,\n };''' + assert old_return in s + s = s.replace(old_return, new_return, 1) + api.write_text(s) + + translations = Path('gui/src/i18n/lab-translations.ts') + s = translations.read_text() + old_keys = ''' | "artifact.purged_unavailable"\n | "selectVerdict";''' + new_keys = ''' | "artifact.purged_unavailable"\n | "selectVerdict"\n | "community.title"\n | "community.notLocalVerdict"\n | "community.bundles"\n | "community.activeRecords"\n | "community.revokedRecords";''' + assert old_keys in s + s = s.replace(old_keys, new_keys, 1) + + additions = { + ' selectVerdict: "View verdict for {subject}",': ''' selectVerdict: "View verdict for {subject}",\n "community.title": "Community evidence",\n "community.notLocalVerdict": "Untrusted read-only context. Not included in this local verdict.",\n "community.bundles": "Bundles",\n "community.activeRecords": "Active records",\n "community.revokedRecords": "Revoked records",''', + ' selectVerdict: "Urteil für {subject} anzeigen",': ''' selectVerdict: "Urteil für {subject} anzeigen",\n "community.title": "Community-Evidenz",\n "community.notLocalVerdict": "Nicht vertrauenswürdiger Nur-Lese-Kontext. Nicht Teil dieses lokalen Urteils.",\n "community.bundles": "Pakete",\n "community.activeRecords": "Aktive Einträge",\n "community.revokedRecords": "Widerrufene Einträge",''', + ' selectVerdict: "{subject}의 판정 보기",': ''' selectVerdict: "{subject}의 판정 보기",\n "community.title": "커뮤니티 증거",\n "community.notLocalVerdict": "신뢰되지 않는 읽기 전용 컨텍스트입니다. 이 로컬 판정에는 포함되지 않습니다.",\n "community.bundles": "번들",\n "community.activeRecords": "활성 레코드",\n "community.revokedRecords": "폐기된 레코드",''', + ' selectVerdict: "查看 {subject} 的判定",': ''' selectVerdict: "查看 {subject} 的判定",\n "community.title": "社区证据",\n "community.notLocalVerdict": "不受信任的只读上下文。不计入此本地判定。",\n "community.bundles": "证据包",\n "community.activeRecords": "有效记录",\n "community.revokedRecords": "已撤销记录",''', + ' selectVerdict: "Открыть вердикт для {subject}",': ''' selectVerdict: "Открыть вердикт для {subject}",\n "community.title": "Данные сообщества",\n "community.notLocalVerdict": "Недоверенный контекст только для чтения. Не входит в этот локальный вердикт.",\n "community.bundles": "Пакеты",\n "community.activeRecords": "Активные записи",\n "community.revokedRecords": "Отозванные записи",''', + ' selectVerdict: "{subject} の判定を表示",': ''' selectVerdict: "{subject} の判定を表示",\n "community.title": "コミュニティ証拠",\n "community.notLocalVerdict": "信頼されていない読み取り専用コンテキストです。このローカル判定には含まれません。",\n "community.bundles": "バンドル",\n "community.activeRecords": "有効なレコード",\n "community.revokedRecords": "取り消されたレコード",''', + ' selectVerdict: "{subject} için kararı görüntüle",': ''' selectVerdict: "{subject} için kararı görüntüle",\n "community.title": "Topluluk kanıtı",\n "community.notLocalVerdict": "Güvenilmeyen salt okunur bağlam. Bu yerel karara dahil değildir.",\n "community.bundles": "Paketler",\n "community.activeRecords": "Etkin kayıtlar",\n "community.revokedRecords": "Geri çekilen kayıtlar",''', + } + # Simplified and Traditional Chinese share the same selectVerdict line; replace sequentially. + zh_key = ' selectVerdict: "查看 {subject} 的判定",' + zh_replacement = additions.pop(zh_key) + assert s.count(zh_key) == 2 + s = s.replace(zh_key, zh_replacement, 1) + zh_tw = ''' selectVerdict: "查看 {subject} 的判定",\n "community.title": "社群證據",\n "community.notLocalVerdict": "不受信任的唯讀脈絡。不計入此本地判定。",\n "community.bundles": "證據包",\n "community.activeRecords": "有效記錄",\n "community.revokedRecords": "已撤銷記錄",''' + s = s.replace(zh_key, zh_tw, 1) + for old, new in additions.items(): + assert old in s, old + s = s.replace(old, new, 1) + translations.write_text(s) + + component = Path('gui/src/pages/CompatibilityMatrix.tsx') + s = component.read_text() + production_end = ''' {detail.production && (\n
\n

{t("lab.production.title")}

\n

{t("lab.production.notVerification")}

\n
\n
{t("lab.production.attempts")}
{detail.production.summary.recentProductionAttempts}
\n
{t("lab.production.successes")}
{detail.production.summary.recentSuccessfulAttempts}
\n
{t("lab.production.routeErrors")}
{detail.production.summary.recentRouteErrorSignals}
\n {detail.production.summary.lastObservedProductionAttempt !== undefined && (\n
{t("lab.production.lastObserved")}
{formatAsOf(detail.production.summary.lastObservedProductionAttempt, locale)}
\n )}\n
\n
\n )}\n''' + community_block = production_end + ''' {detail.community && detail.community.evidence.length > 0 && (\n
\n

{labSupplement(locale, "community.title")}

\n

{labSupplement(locale, "community.notLocalVerdict")}

\n
\n
{labSupplement(locale, "community.bundles")}
{detail.community.evidence.length}
\n
{labSupplement(locale, "community.activeRecords")}
{detail.community.evidence.reduce((total, row) => total + row.activeRecordCount, 0)}
\n
{labSupplement(locale, "community.revokedRecords")}
{detail.community.evidence.reduce((total, row) => total + row.revokedRecordCount, 0)}
\n
\n
\n )}\n''' + assert production_end in s + s = s.replace(production_end, community_block, 1) + component.write_text(s) + PY + - run: cd gui && bun install --frozen-lockfile + - run: cd gui && bun test tests/compatibility-community-evidence.test.ts tests/compatibility-lab-i18n.test.ts + - run: cd gui && bun x tsc -b --pretty false + - run: cd gui && bun run lint + - name: Commit validated Task 7 patch + shell: bash + run: | + rm .github/workflows/cl10-task7-apply.yml + git config user.name Wibias + git config user.email 37517432+Wibias@users.noreply.github.com + git add gui/src/pages/compatibility-matrix-api.ts gui/src/pages/CompatibilityMatrix.tsx gui/src/i18n/lab-translations.ts .github/workflows/cl10-task7-apply.yml + git commit -m "feat(gui): show quarantined CL-10 community evidence" + git push origin HEAD:feat/cl-10-public-evidence-runtime From 0b563d6f577f6754db00b0de4a6562548ec46196 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 20:17:53 +0200 Subject: [PATCH 30/33] chore: retry CL-10 Task 7 --- .github/workflows/cl10-task7-apply2.yml | 163 ++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 .github/workflows/cl10-task7-apply2.yml diff --git a/.github/workflows/cl10-task7-apply2.yml b/.github/workflows/cl10-task7-apply2.yml new file mode 100644 index 0000000000..72d35d5faf --- /dev/null +++ b/.github/workflows/cl10-task7-apply2.yml @@ -0,0 +1,163 @@ +name: CL-10 Task 7 patch retry + +on: + push: + branches: + - feat/cl-10-public-evidence-runtime + +permissions: + contents: write + +jobs: + apply: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + with: + fetch-depth: 1 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + with: + bun-version: 1.3.14 + - name: Apply Matrix community context + shell: bash + run: | + python3 <<'PY' + from pathlib import Path + + api = Path('gui/src/pages/compatibility-matrix-api.ts') + s = api.read_text() + marker = 'export type LabPageData = {\n' + community = r'''export type CommunityEvidenceSummaryRowDto = { + trustClass: "community_untrusted_v1"; + status: "cryptographically_valid"; + bundleId: string; + publisherKeyId: string; + activeRecordCount: number; + revokedRecordCount: number; + }; + + export type CommunityEvidenceContextDto = { + evidence: CommunityEvidenceSummaryRowDto[]; + trustClass: "community_untrusted_v1"; + locallyVerified: false; + }; + + function hasOnlyKeys(raw: Record, allowed: readonly string[]): boolean { + const allowedSet = new Set(allowed); + return Object.keys(raw).every(key => allowedSet.has(key)); + } + + function isSha256Hex(value: unknown): value is string { + return typeof value === "string" && /^[0-9a-f]{64}$/.test(value); + } + + function isNonNegativeInteger(value: unknown): value is number { + return typeof value === "number" && Number.isSafeInteger(value) && value >= 0; + } + + export function parseCommunityEvidenceContext(raw: unknown): CommunityEvidenceContextDto | null { + if (!isPlainObject(raw) + || !hasOnlyKeys(raw, ["evidence", "trustClass", "locallyVerified"]) + || raw.trustClass !== "community_untrusted_v1" + || raw.locallyVerified !== false + || !Array.isArray(raw.evidence) + || raw.evidence.length > 4096) { + return null; + } + const evidence: CommunityEvidenceSummaryRowDto[] = []; + for (const value of raw.evidence) { + if (!isPlainObject(value) + || !hasOnlyKeys(value, [ + "trustClass", "status", "bundleId", "publisherKeyId", + "activeRecordCount", "revokedRecordCount", + ]) + || value.trustClass !== "community_untrusted_v1" + || value.status !== "cryptographically_valid" + || !isSha256Hex(value.bundleId) + || !isSha256Hex(value.publisherKeyId) + || !isNonNegativeInteger(value.activeRecordCount) + || !isNonNegativeInteger(value.revokedRecordCount)) { + return null; + } + evidence.push({ + trustClass: "community_untrusted_v1", + status: "cryptographically_valid", + bundleId: value.bundleId, + publisherKeyId: value.publisherKeyId, + activeRecordCount: value.activeRecordCount, + revokedRecordCount: value.revokedRecordCount, + }); + } + return { evidence, trustClass: "community_untrusted_v1", locallyVerified: false }; + } + + export async function fetchCommunityEvidenceContext( + apiBase: string, + signal: AbortSignal, + ): Promise { + const raw = await fetchLabJson(apiBase, "/api/lab/public/community", signal); + const context = parseCommunityEvidenceContext(raw); + if (!context) throw invalidResponse(); + return context; + } + + ''' + assert marker in s + s = s.replace(marker, community + marker, 1) + old = ' production: PassiveProductionSummaryDto | null;\n};' + assert old in s + s = s.replace(old, ' production: PassiveProductionSummaryDto | null;\n community: CommunityEvidenceContextDto | null;\n};', 1) + old = ''' const [subject, observations, events, artifacts, production] = await Promise.all([\n fetchSubjectDetail(apiBase, verdict.subjectId, signal),\n fetchAllObservations(apiBase, observationFilters, signal),\n mapSettledBounded(eventIds, DETAIL_CONCURRENCY, signal, id => fetchEventById(apiBase, id, signal)),\n mapSettledBounded(digests, DETAIL_CONCURRENCY, signal, digest => fetchArtifactByDigest(apiBase, digest, signal)),\n fetchPassiveProductionSummary(apiBase, verdict.subjectId, signal).catch(error => {\n if (signal.aborted) throw error;\n return null;\n }),\n ]);''' + new = ''' const [subject, observations, events, artifacts, production, community] = await Promise.all([\n fetchSubjectDetail(apiBase, verdict.subjectId, signal),\n fetchAllObservations(apiBase, observationFilters, signal),\n mapSettledBounded(eventIds, DETAIL_CONCURRENCY, signal, id => fetchEventById(apiBase, id, signal)),\n mapSettledBounded(digests, DETAIL_CONCURRENCY, signal, digest => fetchArtifactByDigest(apiBase, digest, signal)),\n fetchPassiveProductionSummary(apiBase, verdict.subjectId, signal).catch(error => {\n if (signal.aborted) throw error;\n return null;\n }),\n fetchCommunityEvidenceContext(apiBase, signal).catch(error => {\n if (signal.aborted) throw error;\n return null;\n }),\n ]);''' + assert old in s + s = s.replace(old, new, 1) + old = ' artifacts,\n production,\n };' + assert old in s + s = s.replace(old, ' artifacts,\n production,\n community,\n };', 1) + api.write_text(s) + + translations = Path('gui/src/i18n/lab-translations.ts') + s = translations.read_text() + old = ' | "artifact.purged_unavailable"\n | "selectVerdict";' + assert old in s + s = s.replace(old, ''' | "artifact.purged_unavailable"\n | "selectVerdict"\n | "community.title"\n | "community.notLocalVerdict"\n | "community.bundles"\n | "community.activeRecords"\n | "community.revokedRecords";''', 1) + supplement_marker = 'const LAB_SUPPLEMENTS: Record> = {' + pos = s.index(supplement_marker) + prefix, tail = s[:pos], s[pos:] + replacements = [ + (' selectVerdict: "View verdict for {subject}",', ''' selectVerdict: "View verdict for {subject}",\n "community.title": "Community evidence",\n "community.notLocalVerdict": "Untrusted read-only context. Not included in this local verdict.",\n "community.bundles": "Bundles",\n "community.activeRecords": "Active records",\n "community.revokedRecords": "Revoked records",'''), + (' selectVerdict: "Urteil für {subject} anzeigen",', ''' selectVerdict: "Urteil für {subject} anzeigen",\n "community.title": "Community-Evidenz",\n "community.notLocalVerdict": "Nicht vertrauenswürdiger Nur-Lese-Kontext. Nicht Teil dieses lokalen Urteils.",\n "community.bundles": "Pakete",\n "community.activeRecords": "Aktive Einträge",\n "community.revokedRecords": "Widerrufene Einträge",'''), + (' selectVerdict: "{subject}의 판정 보기",', ''' selectVerdict: "{subject}의 판정 보기",\n "community.title": "커뮤니티 증거",\n "community.notLocalVerdict": "신뢰되지 않는 읽기 전용 컨텍스트입니다. 이 로컬 판정에는 포함되지 않습니다.",\n "community.bundles": "번들",\n "community.activeRecords": "활성 레코드",\n "community.revokedRecords": "폐기된 레코드",'''), + (' selectVerdict: "Открыть вердикт для {subject}",', ''' selectVerdict: "Открыть вердикт для {subject}",\n "community.title": "Данные сообщества",\n "community.notLocalVerdict": "Недоверенный контекст только для чтения. Не входит в этот локальный вердикт.",\n "community.bundles": "Пакеты",\n "community.activeRecords": "Активные записи",\n "community.revokedRecords": "Отозванные записи",'''), + (' selectVerdict: "{subject} の判定を表示",', ''' selectVerdict: "{subject} の判定を表示",\n "community.title": "コミュニティ証拠",\n "community.notLocalVerdict": "信頼されていない読み取り専用コンテキストです。このローカル判定には含まれません。",\n "community.bundles": "バンドル",\n "community.activeRecords": "有効なレコード",\n "community.revokedRecords": "取り消されたレコード",'''), + (' selectVerdict: "{subject} için kararı görüntüle",', ''' selectVerdict: "{subject} için kararı görüntüle",\n "community.title": "Topluluk kanıtı",\n "community.notLocalVerdict": "Güvenilmeyen salt okunur bağlam. Bu yerel karara dahil değildir.",\n "community.bundles": "Paketler",\n "community.activeRecords": "Etkin kayıtlar",\n "community.revokedRecords": "Geri çekilen kayıtlar",'''), + ] + for old_value, new_value in replacements: + assert old_value in tail, old_value + tail = tail.replace(old_value, new_value, 1) + zh = ' selectVerdict: "查看 {subject} 的判定",' + assert tail.count(zh) == 2 + tail = tail.replace(zh, ''' selectVerdict: "查看 {subject} 的判定",\n "community.title": "社区证据",\n "community.notLocalVerdict": "不受信任的只读上下文。不计入此本地判定。",\n "community.bundles": "证据包",\n "community.activeRecords": "有效记录",\n "community.revokedRecords": "已撤销记录",''', 1) + tail = tail.replace(zh, ''' selectVerdict: "查看 {subject} 的判定",\n "community.title": "社群證據",\n "community.notLocalVerdict": "不受信任的唯讀脈絡。不計入此本地判定。",\n "community.bundles": "證據包",\n "community.activeRecords": "有效記錄",\n "community.revokedRecords": "已撤銷記錄",''', 1) + translations.write_text(prefix + tail) + + component = Path('gui/src/pages/CompatibilityMatrix.tsx') + s = component.read_text() + marker = ''' {detail.production && (\n
\n

{t("lab.production.title")}

\n

{t("lab.production.notVerification")}

\n
\n
{t("lab.production.attempts")}
{detail.production.summary.recentProductionAttempts}
\n
{t("lab.production.successes")}
{detail.production.summary.recentSuccessfulAttempts}
\n
{t("lab.production.routeErrors")}
{detail.production.summary.recentRouteErrorSignals}
\n {detail.production.summary.lastObservedProductionAttempt !== undefined && (\n
{t("lab.production.lastObserved")}
{formatAsOf(detail.production.summary.lastObservedProductionAttempt, locale)}
\n )}\n
\n
\n )}\n''' + assert marker in s + addition = marker + ''' {detail.community && detail.community.evidence.length > 0 && (\n
\n

{labSupplement(locale, "community.title")}

\n

{labSupplement(locale, "community.notLocalVerdict")}

\n
\n
{labSupplement(locale, "community.bundles")}
{detail.community.evidence.length}
\n
{labSupplement(locale, "community.activeRecords")}
{detail.community.evidence.reduce((total, row) => total + row.activeRecordCount, 0)}
\n
{labSupplement(locale, "community.revokedRecords")}
{detail.community.evidence.reduce((total, row) => total + row.revokedRecordCount, 0)}
\n
\n
\n )}\n''' + component.write_text(s.replace(marker, addition, 1)) + PY + - run: cd gui && bun install --frozen-lockfile + - run: cd gui && bun test tests/compatibility-community-evidence.test.ts tests/compatibility-lab-i18n.test.ts + - run: cd gui && bun x tsc -b --pretty false + - run: cd gui && bun run lint + - name: Commit validated Task 7 patch + shell: bash + run: | + rm .github/workflows/cl10-task7-apply.yml .github/workflows/cl10-task7-apply2.yml + git config user.name Wibias + git config user.email 37517432+Wibias@users.noreply.github.com + git add gui/src/pages/compatibility-matrix-api.ts gui/src/pages/CompatibilityMatrix.tsx gui/src/i18n/lab-translations.ts .github/workflows/cl10-task7-apply.yml .github/workflows/cl10-task7-apply2.yml + git commit -m "feat(gui): show quarantined CL-10 community evidence" + git push origin HEAD:feat/cl-10-public-evidence-runtime From 47b11fd964f66be6703458da0a4e07535632ef50 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 20:23:07 +0200 Subject: [PATCH 31/33] chore: finalize CL-10 Task 7 patch --- .github/workflows/cl10-task7-apply3.yml | 89 +++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 .github/workflows/cl10-task7-apply3.yml diff --git a/.github/workflows/cl10-task7-apply3.yml b/.github/workflows/cl10-task7-apply3.yml new file mode 100644 index 0000000000..180bbf7daa --- /dev/null +++ b/.github/workflows/cl10-task7-apply3.yml @@ -0,0 +1,89 @@ +name: CL-10 Task 7 patch final + +on: + push: + branches: + - feat/cl-10-public-evidence-runtime + +permissions: + contents: write + +jobs: + apply: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + with: + fetch-depth: 1 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + with: + bun-version: 1.3.14 + - name: Apply Matrix community context + shell: bash + run: | + python3 <<'PY' + from pathlib import Path + + api = Path('gui/src/pages/compatibility-matrix-api.ts') + s = api.read_text() + marker = 'export type LabPageData = {\n' + assert marker in s + community = '''export type CommunityEvidenceSummaryRowDto = {\n trustClass: "community_untrusted_v1";\n status: "cryptographically_valid";\n bundleId: string;\n publisherKeyId: string;\n activeRecordCount: number;\n revokedRecordCount: number;\n};\n\nexport type CommunityEvidenceContextDto = {\n evidence: CommunityEvidenceSummaryRowDto[];\n trustClass: "community_untrusted_v1";\n locallyVerified: false;\n};\n\nfunction hasOnlyKeys(raw: Record, allowed: readonly string[]): boolean {\n const allowedSet = new Set(allowed);\n return Object.keys(raw).every(key => allowedSet.has(key));\n}\n\nfunction isSha256Hex(value: unknown): value is string {\n return typeof value === "string" && /^[0-9a-f]{64}$/.test(value);\n}\n\nfunction isNonNegativeInteger(value: unknown): value is number {\n return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;\n}\n\nexport function parseCommunityEvidenceContext(raw: unknown): CommunityEvidenceContextDto | null {\n if (!isPlainObject(raw)\n || !hasOnlyKeys(raw, ["evidence", "trustClass", "locallyVerified"])\n || raw.trustClass !== "community_untrusted_v1"\n || raw.locallyVerified !== false\n || !Array.isArray(raw.evidence)\n || raw.evidence.length > 4096) {\n return null;\n }\n const evidence: CommunityEvidenceSummaryRowDto[] = [];\n for (const value of raw.evidence) {\n if (!isPlainObject(value)\n || !hasOnlyKeys(value, [\n "trustClass", "status", "bundleId", "publisherKeyId",\n "activeRecordCount", "revokedRecordCount",\n ])\n || value.trustClass !== "community_untrusted_v1"\n || value.status !== "cryptographically_valid"\n || !isSha256Hex(value.bundleId)\n || !isSha256Hex(value.publisherKeyId)\n || !isNonNegativeInteger(value.activeRecordCount)\n || !isNonNegativeInteger(value.revokedRecordCount)) {\n return null;\n }\n evidence.push({\n trustClass: "community_untrusted_v1",\n status: "cryptographically_valid",\n bundleId: value.bundleId,\n publisherKeyId: value.publisherKeyId,\n activeRecordCount: value.activeRecordCount,\n revokedRecordCount: value.revokedRecordCount,\n });\n }\n return { evidence, trustClass: "community_untrusted_v1", locallyVerified: false };\n}\n\nexport async function fetchCommunityEvidenceContext(\n apiBase: string,\n signal: AbortSignal,\n): Promise {\n const raw = await fetchLabJson(apiBase, "/api/lab/public/community", signal);\n const context = parseCommunityEvidenceContext(raw);\n if (!context) throw invalidResponse();\n return context;\n}\n\n''' + s = s.replace(marker, community + marker, 1) + old = ' production: PassiveProductionSummaryDto | null;\n};' + assert old in s + s = s.replace(old, ' production: PassiveProductionSummaryDto | null;\n community: CommunityEvidenceContextDto | null;\n};', 1) + old = ''' const [subject, observations, events, artifacts, production] = await Promise.all([\n fetchSubjectDetail(apiBase, verdict.subjectId, signal),\n fetchAllObservations(apiBase, observationFilters, signal),\n mapSettledBounded(eventIds, DETAIL_CONCURRENCY, signal, id => fetchEventById(apiBase, id, signal)),\n mapSettledBounded(digests, DETAIL_CONCURRENCY, signal, digest => fetchArtifactByDigest(apiBase, digest, signal)),\n fetchPassiveProductionSummary(apiBase, verdict.subjectId, signal).catch(error => {\n if (signal.aborted) throw error;\n return null;\n }),\n ]);''' + new = ''' const [subject, observations, events, artifacts, production, community] = await Promise.all([\n fetchSubjectDetail(apiBase, verdict.subjectId, signal),\n fetchAllObservations(apiBase, observationFilters, signal),\n mapSettledBounded(eventIds, DETAIL_CONCURRENCY, signal, id => fetchEventById(apiBase, id, signal)),\n mapSettledBounded(digests, DETAIL_CONCURRENCY, signal, digest => fetchArtifactByDigest(apiBase, digest, signal)),\n fetchPassiveProductionSummary(apiBase, verdict.subjectId, signal).catch(error => {\n if (signal.aborted) throw error;\n return null;\n }),\n fetchCommunityEvidenceContext(apiBase, signal).catch(error => {\n if (signal.aborted) throw error;\n return null;\n }),\n ]);''' + assert old in s + s = s.replace(old, new, 1) + old = ' artifacts,\n production,\n };' + assert old in s + s = s.replace(old, ' artifacts,\n production,\n community,\n };', 1) + api.write_text(s) + + translations = Path('gui/src/i18n/lab-translations.ts') + s = translations.read_text() + old = '''export type LabSupplementKey =\n | "subjectKindUnknown"\n | "artifact.present"\n | "artifact.corrupt"\n | "artifact.purged_unavailable"\n | "selectVerdict";''' + new = '''export type LabSupplementKey =\n | "subjectKindUnknown"\n | "artifact.present"\n | "artifact.corrupt"\n | "artifact.purged_unavailable"\n | "selectVerdict"\n | "community.title"\n | "community.notLocalVerdict"\n | "community.bundles"\n | "community.activeRecords"\n | "community.revokedRecords";''' + assert old in s + s = s.replace(old, new, 1) + supplement_replacements = { + ' selectVerdict: "View verdict for {subject}",\n },': ''' selectVerdict: "View verdict for {subject}",\n "community.title": "Community evidence",\n "community.notLocalVerdict": "Untrusted read-only context. Not included in this local verdict.",\n "community.bundles": "Bundles",\n "community.activeRecords": "Active records",\n "community.revokedRecords": "Revoked records",\n },''', + ' selectVerdict: "Urteil für {subject} anzeigen",\n },': ''' selectVerdict: "Urteil für {subject} anzeigen",\n "community.title": "Community-Evidenz",\n "community.notLocalVerdict": "Nicht vertrauenswürdiger Nur-Lese-Kontext. Nicht Teil dieses lokalen Urteils.",\n "community.bundles": "Pakete",\n "community.activeRecords": "Aktive Einträge",\n "community.revokedRecords": "Widerrufene Einträge",\n },''', + ' selectVerdict: "{subject}의 판정 보기",\n },': ''' selectVerdict: "{subject}의 판정 보기",\n "community.title": "커뮤니티 증거",\n "community.notLocalVerdict": "신뢰되지 않는 읽기 전용 컨텍스트입니다. 이 로컬 판정에는 포함되지 않습니다.",\n "community.bundles": "번들",\n "community.activeRecords": "활성 레코드",\n "community.revokedRecords": "폐기된 레코드",\n },''', + ' selectVerdict: "Открыть вердикт для {subject}",\n },': ''' selectVerdict: "Открыть вердикт для {subject}",\n "community.title": "Данные сообщества",\n "community.notLocalVerdict": "Недоверенный контекст только для чтения. Не входит в этот локальный вердикт.",\n "community.bundles": "Пакеты",\n "community.activeRecords": "Активные записи",\n "community.revokedRecords": "Отозванные записи",\n },''', + ' selectVerdict: "{subject} の判定を表示",\n },': ''' selectVerdict: "{subject} の判定を表示",\n "community.title": "コミュニティ証拠",\n "community.notLocalVerdict": "信頼されていない読み取り専用コンテキストです。このローカル判定には含まれません。",\n "community.bundles": "バンドル",\n "community.activeRecords": "有効なレコード",\n "community.revokedRecords": "取り消されたレコード",\n },''', + ' selectVerdict: "{subject} için kararı görüntüle",\n },': ''' selectVerdict: "{subject} için kararı görüntüle",\n "community.title": "Topluluk kanıtı",\n "community.notLocalVerdict": "Güvenilmeyen salt okunur bağlam. Bu yerel karara dahil değildir.",\n "community.bundles": "Paketler",\n "community.activeRecords": "Etkin kayıtlar",\n "community.revokedRecords": "Geri çekilen kayıtlar",\n },''', + } + supplements_start = s.index('const supplements: Record> = {') + prefix, tail = s[:supplements_start], s[supplements_start:] + for old_value, new_value in supplement_replacements.items(): + assert old_value in tail, old_value + tail = tail.replace(old_value, new_value, 1) + zh_old = ' selectVerdict: "查看 {subject} 的判定",\n },' + assert tail.count(zh_old) == 2 + tail = tail.replace(zh_old, ''' selectVerdict: "查看 {subject} 的判定",\n "community.title": "社区证据",\n "community.notLocalVerdict": "不受信任的只读上下文。不计入此本地判定。",\n "community.bundles": "证据包",\n "community.activeRecords": "有效记录",\n "community.revokedRecords": "已撤销记录",\n },''', 1) + tail = tail.replace(zh_old, ''' selectVerdict: "查看 {subject} 的判定",\n "community.title": "社群證據",\n "community.notLocalVerdict": "不受信任的唯讀脈絡。不計入此本地判定。",\n "community.bundles": "證據包",\n "community.activeRecords": "有效記錄",\n "community.revokedRecords": "已撤銷記錄",\n },''', 1) + translations.write_text(prefix + tail) + + component = Path('gui/src/pages/CompatibilityMatrix.tsx') + s = component.read_text() + marker = ''' {detail.production && (\n
\n

{t("lab.production.title")}

\n

{t("lab.production.notVerification")}

\n
\n
{t("lab.production.attempts")}
{detail.production.summary.recentProductionAttempts}
\n
{t("lab.production.successes")}
{detail.production.summary.recentSuccessfulAttempts}
\n
{t("lab.production.routeErrors")}
{detail.production.summary.recentRouteErrorSignals}
\n {detail.production.summary.lastObservedProductionAttempt !== undefined && (\n
{t("lab.production.lastObserved")}
{formatAsOf(detail.production.summary.lastObservedProductionAttempt, locale)}
\n )}\n
\n
\n )}\n''' + assert marker in s + addition = marker + ''' {detail.community && detail.community.evidence.length > 0 && (\n
\n

{labSupplement(locale, "community.title")}

\n

{labSupplement(locale, "community.notLocalVerdict")}

\n
\n
{labSupplement(locale, "community.bundles")}
{detail.community.evidence.length}
\n
{labSupplement(locale, "community.activeRecords")}
{detail.community.evidence.reduce((total, row) => total + row.activeRecordCount, 0)}
\n
{labSupplement(locale, "community.revokedRecords")}
{detail.community.evidence.reduce((total, row) => total + row.revokedRecordCount, 0)}
\n
\n
\n )}\n''' + component.write_text(s.replace(marker, addition, 1)) + PY + - run: cd gui && bun install --frozen-lockfile + - run: cd gui && bun test tests/compatibility-community-evidence.test.ts tests/compatibility-lab-i18n.test.ts + - run: cd gui && bun x tsc -b --pretty false + - run: cd gui && bun run lint + - name: Commit validated Task 7 patch + shell: bash + run: | + rm -f .github/workflows/cl10-task7-apply.yml .github/workflows/cl10-task7-apply2.yml .github/workflows/cl10-task7-apply3.yml + git config user.name Wibias + git config user.email 37517432+Wibias@users.noreply.github.com + git add gui/src/pages/compatibility-matrix-api.ts gui/src/pages/CompatibilityMatrix.tsx gui/src/i18n/lab-translations.ts .github/workflows/cl10-task7-apply.yml .github/workflows/cl10-task7-apply2.yml .github/workflows/cl10-task7-apply3.yml + git commit -m "feat(gui): show quarantined CL-10 community evidence" + git push origin HEAD:feat/cl-10-public-evidence-runtime From 4d4ee47d05a7fd6c84b5ceb4c59ad9f841bc371b Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 18:23:49 +0000 Subject: [PATCH 32/33] feat(gui): show quarantined CL-10 community evidence --- .github/workflows/cl10-task7-apply.yml | 179 ---------------------- .github/workflows/cl10-task7-apply2.yml | 163 -------------------- .github/workflows/cl10-task7-apply3.yml | 89 ----------- gui/src/i18n/lab-translations.ts | 47 +++++- gui/src/pages/CompatibilityMatrix.tsx | 11 ++ gui/src/pages/compatibility-matrix-api.ts | 82 +++++++++- 6 files changed, 138 insertions(+), 433 deletions(-) delete mode 100644 .github/workflows/cl10-task7-apply.yml delete mode 100644 .github/workflows/cl10-task7-apply2.yml delete mode 100644 .github/workflows/cl10-task7-apply3.yml diff --git a/.github/workflows/cl10-task7-apply.yml b/.github/workflows/cl10-task7-apply.yml deleted file mode 100644 index c86682217e..0000000000 --- a/.github/workflows/cl10-task7-apply.yml +++ /dev/null @@ -1,179 +0,0 @@ -name: CL-10 Task 7 patch - -on: - push: - branches: - - feat/cl-10-public-evidence-runtime - -permissions: - contents: write - -jobs: - apply: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 - with: - fetch-depth: 1 - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 - with: - bun-version: 1.3.14 - - name: Apply Matrix community context - shell: bash - run: | - python3 <<'PY' - from pathlib import Path - - api = Path('gui/src/pages/compatibility-matrix-api.ts') - s = api.read_text() - marker = 'export type LabPageData = {\n' - community = r'''export type CommunityEvidenceSummaryRowDto = { - trustClass: "community_untrusted_v1"; - status: "cryptographically_valid"; - bundleId: string; - publisherKeyId: string; - activeRecordCount: number; - revokedRecordCount: number; - }; - - export type CommunityEvidenceContextDto = { - evidence: CommunityEvidenceSummaryRowDto[]; - trustClass: "community_untrusted_v1"; - locallyVerified: false; - }; - - function hasOnlyKeys(raw: Record, allowed: readonly string[]): boolean { - const allowedSet = new Set(allowed); - return Object.keys(raw).every(key => allowedSet.has(key)); - } - - function isSha256Hex(value: unknown): value is string { - return typeof value === "string" && /^[0-9a-f]{64}$/.test(value); - } - - function isNonNegativeInteger(value: unknown): value is number { - return typeof value === "number" && Number.isSafeInteger(value) && value >= 0; - } - - export function parseCommunityEvidenceContext(raw: unknown): CommunityEvidenceContextDto | null { - if (!isPlainObject(raw) - || !hasOnlyKeys(raw, ["evidence", "trustClass", "locallyVerified"]) - || raw.trustClass !== "community_untrusted_v1" - || raw.locallyVerified !== false - || !Array.isArray(raw.evidence) - || raw.evidence.length > 4096) { - return null; - } - const evidence: CommunityEvidenceSummaryRowDto[] = []; - for (const value of raw.evidence) { - if (!isPlainObject(value) - || !hasOnlyKeys(value, [ - "trustClass", - "status", - "bundleId", - "publisherKeyId", - "activeRecordCount", - "revokedRecordCount", - ]) - || value.trustClass !== "community_untrusted_v1" - || value.status !== "cryptographically_valid" - || !isSha256Hex(value.bundleId) - || !isSha256Hex(value.publisherKeyId) - || !isNonNegativeInteger(value.activeRecordCount) - || !isNonNegativeInteger(value.revokedRecordCount)) { - return null; - } - evidence.push({ - trustClass: "community_untrusted_v1", - status: "cryptographically_valid", - bundleId: value.bundleId, - publisherKeyId: value.publisherKeyId, - activeRecordCount: value.activeRecordCount, - revokedRecordCount: value.revokedRecordCount, - }); - } - return { - evidence, - trustClass: "community_untrusted_v1", - locallyVerified: false, - }; - } - - export async function fetchCommunityEvidenceContext( - apiBase: string, - signal: AbortSignal, - ): Promise { - const raw = await fetchLabJson(apiBase, "/api/lab/public/community", signal); - const context = parseCommunityEvidenceContext(raw); - if (!context) throw invalidResponse(); - return context; - } - - ''' - assert marker in s - s = s.replace(marker, community + marker, 1) - - old_detail = ''' production: PassiveProductionSummaryDto | null;\n};''' - new_detail = ''' production: PassiveProductionSummaryDto | null;\n community: CommunityEvidenceContextDto | null;\n};''' - assert old_detail in s - s = s.replace(old_detail, new_detail, 1) - - old_promise = ''' const [subject, observations, events, artifacts, production] = await Promise.all([\n fetchSubjectDetail(apiBase, verdict.subjectId, signal),\n fetchAllObservations(apiBase, observationFilters, signal),\n mapSettledBounded(eventIds, DETAIL_CONCURRENCY, signal, id => fetchEventById(apiBase, id, signal)),\n mapSettledBounded(digests, DETAIL_CONCURRENCY, signal, digest => fetchArtifactByDigest(apiBase, digest, signal)),\n fetchPassiveProductionSummary(apiBase, verdict.subjectId, signal).catch(error => {\n if (signal.aborted) throw error;\n return null;\n }),\n ]);''' - new_promise = ''' const [subject, observations, events, artifacts, production, community] = await Promise.all([\n fetchSubjectDetail(apiBase, verdict.subjectId, signal),\n fetchAllObservations(apiBase, observationFilters, signal),\n mapSettledBounded(eventIds, DETAIL_CONCURRENCY, signal, id => fetchEventById(apiBase, id, signal)),\n mapSettledBounded(digests, DETAIL_CONCURRENCY, signal, digest => fetchArtifactByDigest(apiBase, digest, signal)),\n fetchPassiveProductionSummary(apiBase, verdict.subjectId, signal).catch(error => {\n if (signal.aborted) throw error;\n return null;\n }),\n fetchCommunityEvidenceContext(apiBase, signal).catch(error => {\n if (signal.aborted) throw error;\n return null;\n }),\n ]);''' - assert old_promise in s - s = s.replace(old_promise, new_promise, 1) - old_return = ''' artifacts,\n production,\n };''' - new_return = ''' artifacts,\n production,\n community,\n };''' - assert old_return in s - s = s.replace(old_return, new_return, 1) - api.write_text(s) - - translations = Path('gui/src/i18n/lab-translations.ts') - s = translations.read_text() - old_keys = ''' | "artifact.purged_unavailable"\n | "selectVerdict";''' - new_keys = ''' | "artifact.purged_unavailable"\n | "selectVerdict"\n | "community.title"\n | "community.notLocalVerdict"\n | "community.bundles"\n | "community.activeRecords"\n | "community.revokedRecords";''' - assert old_keys in s - s = s.replace(old_keys, new_keys, 1) - - additions = { - ' selectVerdict: "View verdict for {subject}",': ''' selectVerdict: "View verdict for {subject}",\n "community.title": "Community evidence",\n "community.notLocalVerdict": "Untrusted read-only context. Not included in this local verdict.",\n "community.bundles": "Bundles",\n "community.activeRecords": "Active records",\n "community.revokedRecords": "Revoked records",''', - ' selectVerdict: "Urteil für {subject} anzeigen",': ''' selectVerdict: "Urteil für {subject} anzeigen",\n "community.title": "Community-Evidenz",\n "community.notLocalVerdict": "Nicht vertrauenswürdiger Nur-Lese-Kontext. Nicht Teil dieses lokalen Urteils.",\n "community.bundles": "Pakete",\n "community.activeRecords": "Aktive Einträge",\n "community.revokedRecords": "Widerrufene Einträge",''', - ' selectVerdict: "{subject}의 판정 보기",': ''' selectVerdict: "{subject}의 판정 보기",\n "community.title": "커뮤니티 증거",\n "community.notLocalVerdict": "신뢰되지 않는 읽기 전용 컨텍스트입니다. 이 로컬 판정에는 포함되지 않습니다.",\n "community.bundles": "번들",\n "community.activeRecords": "활성 레코드",\n "community.revokedRecords": "폐기된 레코드",''', - ' selectVerdict: "查看 {subject} 的判定",': ''' selectVerdict: "查看 {subject} 的判定",\n "community.title": "社区证据",\n "community.notLocalVerdict": "不受信任的只读上下文。不计入此本地判定。",\n "community.bundles": "证据包",\n "community.activeRecords": "有效记录",\n "community.revokedRecords": "已撤销记录",''', - ' selectVerdict: "Открыть вердикт для {subject}",': ''' selectVerdict: "Открыть вердикт для {subject}",\n "community.title": "Данные сообщества",\n "community.notLocalVerdict": "Недоверенный контекст только для чтения. Не входит в этот локальный вердикт.",\n "community.bundles": "Пакеты",\n "community.activeRecords": "Активные записи",\n "community.revokedRecords": "Отозванные записи",''', - ' selectVerdict: "{subject} の判定を表示",': ''' selectVerdict: "{subject} の判定を表示",\n "community.title": "コミュニティ証拠",\n "community.notLocalVerdict": "信頼されていない読み取り専用コンテキストです。このローカル判定には含まれません。",\n "community.bundles": "バンドル",\n "community.activeRecords": "有効なレコード",\n "community.revokedRecords": "取り消されたレコード",''', - ' selectVerdict: "{subject} için kararı görüntüle",': ''' selectVerdict: "{subject} için kararı görüntüle",\n "community.title": "Topluluk kanıtı",\n "community.notLocalVerdict": "Güvenilmeyen salt okunur bağlam. Bu yerel karara dahil değildir.",\n "community.bundles": "Paketler",\n "community.activeRecords": "Etkin kayıtlar",\n "community.revokedRecords": "Geri çekilen kayıtlar",''', - } - # Simplified and Traditional Chinese share the same selectVerdict line; replace sequentially. - zh_key = ' selectVerdict: "查看 {subject} 的判定",' - zh_replacement = additions.pop(zh_key) - assert s.count(zh_key) == 2 - s = s.replace(zh_key, zh_replacement, 1) - zh_tw = ''' selectVerdict: "查看 {subject} 的判定",\n "community.title": "社群證據",\n "community.notLocalVerdict": "不受信任的唯讀脈絡。不計入此本地判定。",\n "community.bundles": "證據包",\n "community.activeRecords": "有效記錄",\n "community.revokedRecords": "已撤銷記錄",''' - s = s.replace(zh_key, zh_tw, 1) - for old, new in additions.items(): - assert old in s, old - s = s.replace(old, new, 1) - translations.write_text(s) - - component = Path('gui/src/pages/CompatibilityMatrix.tsx') - s = component.read_text() - production_end = ''' {detail.production && (\n
\n

{t("lab.production.title")}

\n

{t("lab.production.notVerification")}

\n
\n
{t("lab.production.attempts")}
{detail.production.summary.recentProductionAttempts}
\n
{t("lab.production.successes")}
{detail.production.summary.recentSuccessfulAttempts}
\n
{t("lab.production.routeErrors")}
{detail.production.summary.recentRouteErrorSignals}
\n {detail.production.summary.lastObservedProductionAttempt !== undefined && (\n
{t("lab.production.lastObserved")}
{formatAsOf(detail.production.summary.lastObservedProductionAttempt, locale)}
\n )}\n
\n
\n )}\n''' - community_block = production_end + ''' {detail.community && detail.community.evidence.length > 0 && (\n
\n

{labSupplement(locale, "community.title")}

\n

{labSupplement(locale, "community.notLocalVerdict")}

\n
\n
{labSupplement(locale, "community.bundles")}
{detail.community.evidence.length}
\n
{labSupplement(locale, "community.activeRecords")}
{detail.community.evidence.reduce((total, row) => total + row.activeRecordCount, 0)}
\n
{labSupplement(locale, "community.revokedRecords")}
{detail.community.evidence.reduce((total, row) => total + row.revokedRecordCount, 0)}
\n
\n
\n )}\n''' - assert production_end in s - s = s.replace(production_end, community_block, 1) - component.write_text(s) - PY - - run: cd gui && bun install --frozen-lockfile - - run: cd gui && bun test tests/compatibility-community-evidence.test.ts tests/compatibility-lab-i18n.test.ts - - run: cd gui && bun x tsc -b --pretty false - - run: cd gui && bun run lint - - name: Commit validated Task 7 patch - shell: bash - run: | - rm .github/workflows/cl10-task7-apply.yml - git config user.name Wibias - git config user.email 37517432+Wibias@users.noreply.github.com - git add gui/src/pages/compatibility-matrix-api.ts gui/src/pages/CompatibilityMatrix.tsx gui/src/i18n/lab-translations.ts .github/workflows/cl10-task7-apply.yml - git commit -m "feat(gui): show quarantined CL-10 community evidence" - git push origin HEAD:feat/cl-10-public-evidence-runtime diff --git a/.github/workflows/cl10-task7-apply2.yml b/.github/workflows/cl10-task7-apply2.yml deleted file mode 100644 index 72d35d5faf..0000000000 --- a/.github/workflows/cl10-task7-apply2.yml +++ /dev/null @@ -1,163 +0,0 @@ -name: CL-10 Task 7 patch retry - -on: - push: - branches: - - feat/cl-10-public-evidence-runtime - -permissions: - contents: write - -jobs: - apply: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 - with: - fetch-depth: 1 - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 - with: - bun-version: 1.3.14 - - name: Apply Matrix community context - shell: bash - run: | - python3 <<'PY' - from pathlib import Path - - api = Path('gui/src/pages/compatibility-matrix-api.ts') - s = api.read_text() - marker = 'export type LabPageData = {\n' - community = r'''export type CommunityEvidenceSummaryRowDto = { - trustClass: "community_untrusted_v1"; - status: "cryptographically_valid"; - bundleId: string; - publisherKeyId: string; - activeRecordCount: number; - revokedRecordCount: number; - }; - - export type CommunityEvidenceContextDto = { - evidence: CommunityEvidenceSummaryRowDto[]; - trustClass: "community_untrusted_v1"; - locallyVerified: false; - }; - - function hasOnlyKeys(raw: Record, allowed: readonly string[]): boolean { - const allowedSet = new Set(allowed); - return Object.keys(raw).every(key => allowedSet.has(key)); - } - - function isSha256Hex(value: unknown): value is string { - return typeof value === "string" && /^[0-9a-f]{64}$/.test(value); - } - - function isNonNegativeInteger(value: unknown): value is number { - return typeof value === "number" && Number.isSafeInteger(value) && value >= 0; - } - - export function parseCommunityEvidenceContext(raw: unknown): CommunityEvidenceContextDto | null { - if (!isPlainObject(raw) - || !hasOnlyKeys(raw, ["evidence", "trustClass", "locallyVerified"]) - || raw.trustClass !== "community_untrusted_v1" - || raw.locallyVerified !== false - || !Array.isArray(raw.evidence) - || raw.evidence.length > 4096) { - return null; - } - const evidence: CommunityEvidenceSummaryRowDto[] = []; - for (const value of raw.evidence) { - if (!isPlainObject(value) - || !hasOnlyKeys(value, [ - "trustClass", "status", "bundleId", "publisherKeyId", - "activeRecordCount", "revokedRecordCount", - ]) - || value.trustClass !== "community_untrusted_v1" - || value.status !== "cryptographically_valid" - || !isSha256Hex(value.bundleId) - || !isSha256Hex(value.publisherKeyId) - || !isNonNegativeInteger(value.activeRecordCount) - || !isNonNegativeInteger(value.revokedRecordCount)) { - return null; - } - evidence.push({ - trustClass: "community_untrusted_v1", - status: "cryptographically_valid", - bundleId: value.bundleId, - publisherKeyId: value.publisherKeyId, - activeRecordCount: value.activeRecordCount, - revokedRecordCount: value.revokedRecordCount, - }); - } - return { evidence, trustClass: "community_untrusted_v1", locallyVerified: false }; - } - - export async function fetchCommunityEvidenceContext( - apiBase: string, - signal: AbortSignal, - ): Promise { - const raw = await fetchLabJson(apiBase, "/api/lab/public/community", signal); - const context = parseCommunityEvidenceContext(raw); - if (!context) throw invalidResponse(); - return context; - } - - ''' - assert marker in s - s = s.replace(marker, community + marker, 1) - old = ' production: PassiveProductionSummaryDto | null;\n};' - assert old in s - s = s.replace(old, ' production: PassiveProductionSummaryDto | null;\n community: CommunityEvidenceContextDto | null;\n};', 1) - old = ''' const [subject, observations, events, artifacts, production] = await Promise.all([\n fetchSubjectDetail(apiBase, verdict.subjectId, signal),\n fetchAllObservations(apiBase, observationFilters, signal),\n mapSettledBounded(eventIds, DETAIL_CONCURRENCY, signal, id => fetchEventById(apiBase, id, signal)),\n mapSettledBounded(digests, DETAIL_CONCURRENCY, signal, digest => fetchArtifactByDigest(apiBase, digest, signal)),\n fetchPassiveProductionSummary(apiBase, verdict.subjectId, signal).catch(error => {\n if (signal.aborted) throw error;\n return null;\n }),\n ]);''' - new = ''' const [subject, observations, events, artifacts, production, community] = await Promise.all([\n fetchSubjectDetail(apiBase, verdict.subjectId, signal),\n fetchAllObservations(apiBase, observationFilters, signal),\n mapSettledBounded(eventIds, DETAIL_CONCURRENCY, signal, id => fetchEventById(apiBase, id, signal)),\n mapSettledBounded(digests, DETAIL_CONCURRENCY, signal, digest => fetchArtifactByDigest(apiBase, digest, signal)),\n fetchPassiveProductionSummary(apiBase, verdict.subjectId, signal).catch(error => {\n if (signal.aborted) throw error;\n return null;\n }),\n fetchCommunityEvidenceContext(apiBase, signal).catch(error => {\n if (signal.aborted) throw error;\n return null;\n }),\n ]);''' - assert old in s - s = s.replace(old, new, 1) - old = ' artifacts,\n production,\n };' - assert old in s - s = s.replace(old, ' artifacts,\n production,\n community,\n };', 1) - api.write_text(s) - - translations = Path('gui/src/i18n/lab-translations.ts') - s = translations.read_text() - old = ' | "artifact.purged_unavailable"\n | "selectVerdict";' - assert old in s - s = s.replace(old, ''' | "artifact.purged_unavailable"\n | "selectVerdict"\n | "community.title"\n | "community.notLocalVerdict"\n | "community.bundles"\n | "community.activeRecords"\n | "community.revokedRecords";''', 1) - supplement_marker = 'const LAB_SUPPLEMENTS: Record> = {' - pos = s.index(supplement_marker) - prefix, tail = s[:pos], s[pos:] - replacements = [ - (' selectVerdict: "View verdict for {subject}",', ''' selectVerdict: "View verdict for {subject}",\n "community.title": "Community evidence",\n "community.notLocalVerdict": "Untrusted read-only context. Not included in this local verdict.",\n "community.bundles": "Bundles",\n "community.activeRecords": "Active records",\n "community.revokedRecords": "Revoked records",'''), - (' selectVerdict: "Urteil für {subject} anzeigen",', ''' selectVerdict: "Urteil für {subject} anzeigen",\n "community.title": "Community-Evidenz",\n "community.notLocalVerdict": "Nicht vertrauenswürdiger Nur-Lese-Kontext. Nicht Teil dieses lokalen Urteils.",\n "community.bundles": "Pakete",\n "community.activeRecords": "Aktive Einträge",\n "community.revokedRecords": "Widerrufene Einträge",'''), - (' selectVerdict: "{subject}의 판정 보기",', ''' selectVerdict: "{subject}의 판정 보기",\n "community.title": "커뮤니티 증거",\n "community.notLocalVerdict": "신뢰되지 않는 읽기 전용 컨텍스트입니다. 이 로컬 판정에는 포함되지 않습니다.",\n "community.bundles": "번들",\n "community.activeRecords": "활성 레코드",\n "community.revokedRecords": "폐기된 레코드",'''), - (' selectVerdict: "Открыть вердикт для {subject}",', ''' selectVerdict: "Открыть вердикт для {subject}",\n "community.title": "Данные сообщества",\n "community.notLocalVerdict": "Недоверенный контекст только для чтения. Не входит в этот локальный вердикт.",\n "community.bundles": "Пакеты",\n "community.activeRecords": "Активные записи",\n "community.revokedRecords": "Отозванные записи",'''), - (' selectVerdict: "{subject} の判定を表示",', ''' selectVerdict: "{subject} の判定を表示",\n "community.title": "コミュニティ証拠",\n "community.notLocalVerdict": "信頼されていない読み取り専用コンテキストです。このローカル判定には含まれません。",\n "community.bundles": "バンドル",\n "community.activeRecords": "有効なレコード",\n "community.revokedRecords": "取り消されたレコード",'''), - (' selectVerdict: "{subject} için kararı görüntüle",', ''' selectVerdict: "{subject} için kararı görüntüle",\n "community.title": "Topluluk kanıtı",\n "community.notLocalVerdict": "Güvenilmeyen salt okunur bağlam. Bu yerel karara dahil değildir.",\n "community.bundles": "Paketler",\n "community.activeRecords": "Etkin kayıtlar",\n "community.revokedRecords": "Geri çekilen kayıtlar",'''), - ] - for old_value, new_value in replacements: - assert old_value in tail, old_value - tail = tail.replace(old_value, new_value, 1) - zh = ' selectVerdict: "查看 {subject} 的判定",' - assert tail.count(zh) == 2 - tail = tail.replace(zh, ''' selectVerdict: "查看 {subject} 的判定",\n "community.title": "社区证据",\n "community.notLocalVerdict": "不受信任的只读上下文。不计入此本地判定。",\n "community.bundles": "证据包",\n "community.activeRecords": "有效记录",\n "community.revokedRecords": "已撤销记录",''', 1) - tail = tail.replace(zh, ''' selectVerdict: "查看 {subject} 的判定",\n "community.title": "社群證據",\n "community.notLocalVerdict": "不受信任的唯讀脈絡。不計入此本地判定。",\n "community.bundles": "證據包",\n "community.activeRecords": "有效記錄",\n "community.revokedRecords": "已撤銷記錄",''', 1) - translations.write_text(prefix + tail) - - component = Path('gui/src/pages/CompatibilityMatrix.tsx') - s = component.read_text() - marker = ''' {detail.production && (\n
\n

{t("lab.production.title")}

\n

{t("lab.production.notVerification")}

\n
\n
{t("lab.production.attempts")}
{detail.production.summary.recentProductionAttempts}
\n
{t("lab.production.successes")}
{detail.production.summary.recentSuccessfulAttempts}
\n
{t("lab.production.routeErrors")}
{detail.production.summary.recentRouteErrorSignals}
\n {detail.production.summary.lastObservedProductionAttempt !== undefined && (\n
{t("lab.production.lastObserved")}
{formatAsOf(detail.production.summary.lastObservedProductionAttempt, locale)}
\n )}\n
\n
\n )}\n''' - assert marker in s - addition = marker + ''' {detail.community && detail.community.evidence.length > 0 && (\n
\n

{labSupplement(locale, "community.title")}

\n

{labSupplement(locale, "community.notLocalVerdict")}

\n
\n
{labSupplement(locale, "community.bundles")}
{detail.community.evidence.length}
\n
{labSupplement(locale, "community.activeRecords")}
{detail.community.evidence.reduce((total, row) => total + row.activeRecordCount, 0)}
\n
{labSupplement(locale, "community.revokedRecords")}
{detail.community.evidence.reduce((total, row) => total + row.revokedRecordCount, 0)}
\n
\n
\n )}\n''' - component.write_text(s.replace(marker, addition, 1)) - PY - - run: cd gui && bun install --frozen-lockfile - - run: cd gui && bun test tests/compatibility-community-evidence.test.ts tests/compatibility-lab-i18n.test.ts - - run: cd gui && bun x tsc -b --pretty false - - run: cd gui && bun run lint - - name: Commit validated Task 7 patch - shell: bash - run: | - rm .github/workflows/cl10-task7-apply.yml .github/workflows/cl10-task7-apply2.yml - git config user.name Wibias - git config user.email 37517432+Wibias@users.noreply.github.com - git add gui/src/pages/compatibility-matrix-api.ts gui/src/pages/CompatibilityMatrix.tsx gui/src/i18n/lab-translations.ts .github/workflows/cl10-task7-apply.yml .github/workflows/cl10-task7-apply2.yml - git commit -m "feat(gui): show quarantined CL-10 community evidence" - git push origin HEAD:feat/cl-10-public-evidence-runtime diff --git a/.github/workflows/cl10-task7-apply3.yml b/.github/workflows/cl10-task7-apply3.yml deleted file mode 100644 index 180bbf7daa..0000000000 --- a/.github/workflows/cl10-task7-apply3.yml +++ /dev/null @@ -1,89 +0,0 @@ -name: CL-10 Task 7 patch final - -on: - push: - branches: - - feat/cl-10-public-evidence-runtime - -permissions: - contents: write - -jobs: - apply: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 - with: - fetch-depth: 1 - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 - with: - bun-version: 1.3.14 - - name: Apply Matrix community context - shell: bash - run: | - python3 <<'PY' - from pathlib import Path - - api = Path('gui/src/pages/compatibility-matrix-api.ts') - s = api.read_text() - marker = 'export type LabPageData = {\n' - assert marker in s - community = '''export type CommunityEvidenceSummaryRowDto = {\n trustClass: "community_untrusted_v1";\n status: "cryptographically_valid";\n bundleId: string;\n publisherKeyId: string;\n activeRecordCount: number;\n revokedRecordCount: number;\n};\n\nexport type CommunityEvidenceContextDto = {\n evidence: CommunityEvidenceSummaryRowDto[];\n trustClass: "community_untrusted_v1";\n locallyVerified: false;\n};\n\nfunction hasOnlyKeys(raw: Record, allowed: readonly string[]): boolean {\n const allowedSet = new Set(allowed);\n return Object.keys(raw).every(key => allowedSet.has(key));\n}\n\nfunction isSha256Hex(value: unknown): value is string {\n return typeof value === "string" && /^[0-9a-f]{64}$/.test(value);\n}\n\nfunction isNonNegativeInteger(value: unknown): value is number {\n return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;\n}\n\nexport function parseCommunityEvidenceContext(raw: unknown): CommunityEvidenceContextDto | null {\n if (!isPlainObject(raw)\n || !hasOnlyKeys(raw, ["evidence", "trustClass", "locallyVerified"])\n || raw.trustClass !== "community_untrusted_v1"\n || raw.locallyVerified !== false\n || !Array.isArray(raw.evidence)\n || raw.evidence.length > 4096) {\n return null;\n }\n const evidence: CommunityEvidenceSummaryRowDto[] = [];\n for (const value of raw.evidence) {\n if (!isPlainObject(value)\n || !hasOnlyKeys(value, [\n "trustClass", "status", "bundleId", "publisherKeyId",\n "activeRecordCount", "revokedRecordCount",\n ])\n || value.trustClass !== "community_untrusted_v1"\n || value.status !== "cryptographically_valid"\n || !isSha256Hex(value.bundleId)\n || !isSha256Hex(value.publisherKeyId)\n || !isNonNegativeInteger(value.activeRecordCount)\n || !isNonNegativeInteger(value.revokedRecordCount)) {\n return null;\n }\n evidence.push({\n trustClass: "community_untrusted_v1",\n status: "cryptographically_valid",\n bundleId: value.bundleId,\n publisherKeyId: value.publisherKeyId,\n activeRecordCount: value.activeRecordCount,\n revokedRecordCount: value.revokedRecordCount,\n });\n }\n return { evidence, trustClass: "community_untrusted_v1", locallyVerified: false };\n}\n\nexport async function fetchCommunityEvidenceContext(\n apiBase: string,\n signal: AbortSignal,\n): Promise {\n const raw = await fetchLabJson(apiBase, "/api/lab/public/community", signal);\n const context = parseCommunityEvidenceContext(raw);\n if (!context) throw invalidResponse();\n return context;\n}\n\n''' - s = s.replace(marker, community + marker, 1) - old = ' production: PassiveProductionSummaryDto | null;\n};' - assert old in s - s = s.replace(old, ' production: PassiveProductionSummaryDto | null;\n community: CommunityEvidenceContextDto | null;\n};', 1) - old = ''' const [subject, observations, events, artifacts, production] = await Promise.all([\n fetchSubjectDetail(apiBase, verdict.subjectId, signal),\n fetchAllObservations(apiBase, observationFilters, signal),\n mapSettledBounded(eventIds, DETAIL_CONCURRENCY, signal, id => fetchEventById(apiBase, id, signal)),\n mapSettledBounded(digests, DETAIL_CONCURRENCY, signal, digest => fetchArtifactByDigest(apiBase, digest, signal)),\n fetchPassiveProductionSummary(apiBase, verdict.subjectId, signal).catch(error => {\n if (signal.aborted) throw error;\n return null;\n }),\n ]);''' - new = ''' const [subject, observations, events, artifacts, production, community] = await Promise.all([\n fetchSubjectDetail(apiBase, verdict.subjectId, signal),\n fetchAllObservations(apiBase, observationFilters, signal),\n mapSettledBounded(eventIds, DETAIL_CONCURRENCY, signal, id => fetchEventById(apiBase, id, signal)),\n mapSettledBounded(digests, DETAIL_CONCURRENCY, signal, digest => fetchArtifactByDigest(apiBase, digest, signal)),\n fetchPassiveProductionSummary(apiBase, verdict.subjectId, signal).catch(error => {\n if (signal.aborted) throw error;\n return null;\n }),\n fetchCommunityEvidenceContext(apiBase, signal).catch(error => {\n if (signal.aborted) throw error;\n return null;\n }),\n ]);''' - assert old in s - s = s.replace(old, new, 1) - old = ' artifacts,\n production,\n };' - assert old in s - s = s.replace(old, ' artifacts,\n production,\n community,\n };', 1) - api.write_text(s) - - translations = Path('gui/src/i18n/lab-translations.ts') - s = translations.read_text() - old = '''export type LabSupplementKey =\n | "subjectKindUnknown"\n | "artifact.present"\n | "artifact.corrupt"\n | "artifact.purged_unavailable"\n | "selectVerdict";''' - new = '''export type LabSupplementKey =\n | "subjectKindUnknown"\n | "artifact.present"\n | "artifact.corrupt"\n | "artifact.purged_unavailable"\n | "selectVerdict"\n | "community.title"\n | "community.notLocalVerdict"\n | "community.bundles"\n | "community.activeRecords"\n | "community.revokedRecords";''' - assert old in s - s = s.replace(old, new, 1) - supplement_replacements = { - ' selectVerdict: "View verdict for {subject}",\n },': ''' selectVerdict: "View verdict for {subject}",\n "community.title": "Community evidence",\n "community.notLocalVerdict": "Untrusted read-only context. Not included in this local verdict.",\n "community.bundles": "Bundles",\n "community.activeRecords": "Active records",\n "community.revokedRecords": "Revoked records",\n },''', - ' selectVerdict: "Urteil für {subject} anzeigen",\n },': ''' selectVerdict: "Urteil für {subject} anzeigen",\n "community.title": "Community-Evidenz",\n "community.notLocalVerdict": "Nicht vertrauenswürdiger Nur-Lese-Kontext. Nicht Teil dieses lokalen Urteils.",\n "community.bundles": "Pakete",\n "community.activeRecords": "Aktive Einträge",\n "community.revokedRecords": "Widerrufene Einträge",\n },''', - ' selectVerdict: "{subject}의 판정 보기",\n },': ''' selectVerdict: "{subject}의 판정 보기",\n "community.title": "커뮤니티 증거",\n "community.notLocalVerdict": "신뢰되지 않는 읽기 전용 컨텍스트입니다. 이 로컬 판정에는 포함되지 않습니다.",\n "community.bundles": "번들",\n "community.activeRecords": "활성 레코드",\n "community.revokedRecords": "폐기된 레코드",\n },''', - ' selectVerdict: "Открыть вердикт для {subject}",\n },': ''' selectVerdict: "Открыть вердикт для {subject}",\n "community.title": "Данные сообщества",\n "community.notLocalVerdict": "Недоверенный контекст только для чтения. Не входит в этот локальный вердикт.",\n "community.bundles": "Пакеты",\n "community.activeRecords": "Активные записи",\n "community.revokedRecords": "Отозванные записи",\n },''', - ' selectVerdict: "{subject} の判定を表示",\n },': ''' selectVerdict: "{subject} の判定を表示",\n "community.title": "コミュニティ証拠",\n "community.notLocalVerdict": "信頼されていない読み取り専用コンテキストです。このローカル判定には含まれません。",\n "community.bundles": "バンドル",\n "community.activeRecords": "有効なレコード",\n "community.revokedRecords": "取り消されたレコード",\n },''', - ' selectVerdict: "{subject} için kararı görüntüle",\n },': ''' selectVerdict: "{subject} için kararı görüntüle",\n "community.title": "Topluluk kanıtı",\n "community.notLocalVerdict": "Güvenilmeyen salt okunur bağlam. Bu yerel karara dahil değildir.",\n "community.bundles": "Paketler",\n "community.activeRecords": "Etkin kayıtlar",\n "community.revokedRecords": "Geri çekilen kayıtlar",\n },''', - } - supplements_start = s.index('const supplements: Record> = {') - prefix, tail = s[:supplements_start], s[supplements_start:] - for old_value, new_value in supplement_replacements.items(): - assert old_value in tail, old_value - tail = tail.replace(old_value, new_value, 1) - zh_old = ' selectVerdict: "查看 {subject} 的判定",\n },' - assert tail.count(zh_old) == 2 - tail = tail.replace(zh_old, ''' selectVerdict: "查看 {subject} 的判定",\n "community.title": "社区证据",\n "community.notLocalVerdict": "不受信任的只读上下文。不计入此本地判定。",\n "community.bundles": "证据包",\n "community.activeRecords": "有效记录",\n "community.revokedRecords": "已撤销记录",\n },''', 1) - tail = tail.replace(zh_old, ''' selectVerdict: "查看 {subject} 的判定",\n "community.title": "社群證據",\n "community.notLocalVerdict": "不受信任的唯讀脈絡。不計入此本地判定。",\n "community.bundles": "證據包",\n "community.activeRecords": "有效記錄",\n "community.revokedRecords": "已撤銷記錄",\n },''', 1) - translations.write_text(prefix + tail) - - component = Path('gui/src/pages/CompatibilityMatrix.tsx') - s = component.read_text() - marker = ''' {detail.production && (\n
\n

{t("lab.production.title")}

\n

{t("lab.production.notVerification")}

\n
\n
{t("lab.production.attempts")}
{detail.production.summary.recentProductionAttempts}
\n
{t("lab.production.successes")}
{detail.production.summary.recentSuccessfulAttempts}
\n
{t("lab.production.routeErrors")}
{detail.production.summary.recentRouteErrorSignals}
\n {detail.production.summary.lastObservedProductionAttempt !== undefined && (\n
{t("lab.production.lastObserved")}
{formatAsOf(detail.production.summary.lastObservedProductionAttempt, locale)}
\n )}\n
\n
\n )}\n''' - assert marker in s - addition = marker + ''' {detail.community && detail.community.evidence.length > 0 && (\n
\n

{labSupplement(locale, "community.title")}

\n

{labSupplement(locale, "community.notLocalVerdict")}

\n
\n
{labSupplement(locale, "community.bundles")}
{detail.community.evidence.length}
\n
{labSupplement(locale, "community.activeRecords")}
{detail.community.evidence.reduce((total, row) => total + row.activeRecordCount, 0)}
\n
{labSupplement(locale, "community.revokedRecords")}
{detail.community.evidence.reduce((total, row) => total + row.revokedRecordCount, 0)}
\n
\n
\n )}\n''' - component.write_text(s.replace(marker, addition, 1)) - PY - - run: cd gui && bun install --frozen-lockfile - - run: cd gui && bun test tests/compatibility-community-evidence.test.ts tests/compatibility-lab-i18n.test.ts - - run: cd gui && bun x tsc -b --pretty false - - run: cd gui && bun run lint - - name: Commit validated Task 7 patch - shell: bash - run: | - rm -f .github/workflows/cl10-task7-apply.yml .github/workflows/cl10-task7-apply2.yml .github/workflows/cl10-task7-apply3.yml - git config user.name Wibias - git config user.email 37517432+Wibias@users.noreply.github.com - git add gui/src/pages/compatibility-matrix-api.ts gui/src/pages/CompatibilityMatrix.tsx gui/src/i18n/lab-translations.ts .github/workflows/cl10-task7-apply.yml .github/workflows/cl10-task7-apply2.yml .github/workflows/cl10-task7-apply3.yml - git commit -m "feat(gui): show quarantined CL-10 community evidence" - git push origin HEAD:feat/cl-10-public-evidence-runtime diff --git a/gui/src/i18n/lab-translations.ts b/gui/src/i18n/lab-translations.ts index 3428a847dd..75a21feb4e 100644 --- a/gui/src/i18n/lab-translations.ts +++ b/gui/src/i18n/lab-translations.ts @@ -7,7 +7,12 @@ export type LabSupplementKey = | "artifact.present" | "artifact.corrupt" | "artifact.purged_unavailable" - | "selectVerdict"; + | "selectVerdict" + | "community.title" + | "community.notLocalVerdict" + | "community.bundles" + | "community.activeRecords" + | "community.revokedRecords"; const en: Record = { "lab.title": "Compatibility Lab", @@ -427,6 +432,11 @@ const supplements: Record> = { "artifact.corrupt": "Corrupt", "artifact.purged_unavailable": "Purged / unavailable", selectVerdict: "View verdict for {subject}", + "community.title": "Community evidence", + "community.notLocalVerdict": "Untrusted read-only context. Not included in this local verdict.", + "community.bundles": "Bundles", + "community.activeRecords": "Active records", + "community.revokedRecords": "Revoked records", }, de: { subjectKindUnknown: "Unbekannt", @@ -434,6 +444,11 @@ const supplements: Record> = { "artifact.corrupt": "Beschädigt", "artifact.purged_unavailable": "Gelöscht / nicht verfügbar", selectVerdict: "Urteil für {subject} anzeigen", + "community.title": "Community-Evidenz", + "community.notLocalVerdict": "Nicht vertrauenswürdiger Nur-Lese-Kontext. Nicht Teil dieses lokalen Urteils.", + "community.bundles": "Pakete", + "community.activeRecords": "Aktive Einträge", + "community.revokedRecords": "Widerrufene Einträge", }, ko: { subjectKindUnknown: "알 수 없음", @@ -441,6 +456,11 @@ const supplements: Record> = { "artifact.corrupt": "손상됨", "artifact.purged_unavailable": "삭제됨 / 사용할 수 없음", selectVerdict: "{subject}의 판정 보기", + "community.title": "커뮤니티 증거", + "community.notLocalVerdict": "신뢰되지 않는 읽기 전용 컨텍스트입니다. 이 로컬 판정에는 포함되지 않습니다.", + "community.bundles": "번들", + "community.activeRecords": "활성 레코드", + "community.revokedRecords": "폐기된 레코드", }, zh: { subjectKindUnknown: "未知", @@ -448,6 +468,11 @@ const supplements: Record> = { "artifact.corrupt": "已损坏", "artifact.purged_unavailable": "已清除 / 不可用", selectVerdict: "查看 {subject} 的判定", + "community.title": "社区证据", + "community.notLocalVerdict": "不受信任的只读上下文。不计入此本地判定。", + "community.bundles": "证据包", + "community.activeRecords": "有效记录", + "community.revokedRecords": "已撤销记录", }, "zh-TW": { subjectKindUnknown: "未知", @@ -455,6 +480,11 @@ const supplements: Record> = { "artifact.corrupt": "已損壞", "artifact.purged_unavailable": "已清除 / 不可用", selectVerdict: "查看 {subject} 的判定", + "community.title": "社群證據", + "community.notLocalVerdict": "不受信任的唯讀脈絡。不計入此本地判定。", + "community.bundles": "證據包", + "community.activeRecords": "有效記錄", + "community.revokedRecords": "已撤銷記錄", }, ru: { subjectKindUnknown: "Неизвестно", @@ -462,6 +492,11 @@ const supplements: Record> = { "artifact.corrupt": "Повреждён", "artifact.purged_unavailable": "Удалён / недоступен", selectVerdict: "Открыть вердикт для {subject}", + "community.title": "Данные сообщества", + "community.notLocalVerdict": "Недоверенный контекст только для чтения. Не входит в этот локальный вердикт.", + "community.bundles": "Пакеты", + "community.activeRecords": "Активные записи", + "community.revokedRecords": "Отозванные записи", }, ja: { subjectKindUnknown: "不明", @@ -469,6 +504,11 @@ const supplements: Record> = { "artifact.corrupt": "破損", "artifact.purged_unavailable": "削除済み / 利用不可", selectVerdict: "{subject} の判定を表示", + "community.title": "コミュニティ証拠", + "community.notLocalVerdict": "信頼されていない読み取り専用コンテキストです。このローカル判定には含まれません。", + "community.bundles": "バンドル", + "community.activeRecords": "有効なレコード", + "community.revokedRecords": "取り消されたレコード", }, tr: { subjectKindUnknown: "Bilinmiyor", @@ -476,6 +516,11 @@ const supplements: Record> = { "artifact.corrupt": "Bozuk", "artifact.purged_unavailable": "Temizlenmiş / kullanılamıyor", selectVerdict: "{subject} için kararı görüntüle", + "community.title": "Topluluk kanıtı", + "community.notLocalVerdict": "Güvenilmeyen salt okunur bağlam. Bu yerel karara dahil değildir.", + "community.bundles": "Paketler", + "community.activeRecords": "Etkin kayıtlar", + "community.revokedRecords": "Geri çekilen kayıtlar", }, }; diff --git a/gui/src/pages/CompatibilityMatrix.tsx b/gui/src/pages/CompatibilityMatrix.tsx index 9fcac9b129..632d30fe05 100644 --- a/gui/src/pages/CompatibilityMatrix.tsx +++ b/gui/src/pages/CompatibilityMatrix.tsx @@ -205,6 +205,17 @@ function DetailPane({ )} + {detail.community && detail.community.evidence.length > 0 && ( +
+

{labSupplement(locale, "community.title")}

+

{labSupplement(locale, "community.notLocalVerdict")}

+
+
{labSupplement(locale, "community.bundles")}
{detail.community.evidence.length}
+
{labSupplement(locale, "community.activeRecords")}
{detail.community.evidence.reduce((total, row) => total + row.activeRecordCount, 0)}
+
{labSupplement(locale, "community.revokedRecords")}
{detail.community.evidence.reduce((total, row) => total + row.revokedRecordCount, 0)}
+
+
+ )} {detail.observations.length > 0 && (

{t("lab.detailObservations")}

diff --git a/gui/src/pages/compatibility-matrix-api.ts b/gui/src/pages/compatibility-matrix-api.ts index 139ba8594a..de3452299f 100644 --- a/gui/src/pages/compatibility-matrix-api.ts +++ b/gui/src/pages/compatibility-matrix-api.ts @@ -248,6 +248,80 @@ export async function fetchPassiveProductionSummary( return parsePassiveProductionSummary(raw); } +export type CommunityEvidenceSummaryRowDto = { + trustClass: "community_untrusted_v1"; + status: "cryptographically_valid"; + bundleId: string; + publisherKeyId: string; + activeRecordCount: number; + revokedRecordCount: number; +}; + +export type CommunityEvidenceContextDto = { + evidence: CommunityEvidenceSummaryRowDto[]; + trustClass: "community_untrusted_v1"; + locallyVerified: false; +}; + +function hasOnlyKeys(raw: Record, allowed: readonly string[]): boolean { + const allowedSet = new Set(allowed); + return Object.keys(raw).every(key => allowedSet.has(key)); +} + +function isSha256Hex(value: unknown): value is string { + return typeof value === "string" && /^[0-9a-f]{64}$/.test(value); +} + +function isNonNegativeInteger(value: unknown): value is number { + return typeof value === "number" && Number.isSafeInteger(value) && value >= 0; +} + +export function parseCommunityEvidenceContext(raw: unknown): CommunityEvidenceContextDto | null { + if (!isPlainObject(raw) + || !hasOnlyKeys(raw, ["evidence", "trustClass", "locallyVerified"]) + || raw.trustClass !== "community_untrusted_v1" + || raw.locallyVerified !== false + || !Array.isArray(raw.evidence) + || raw.evidence.length > 4096) { + return null; + } + const evidence: CommunityEvidenceSummaryRowDto[] = []; + for (const value of raw.evidence) { + if (!isPlainObject(value) + || !hasOnlyKeys(value, [ + "trustClass", "status", "bundleId", "publisherKeyId", + "activeRecordCount", "revokedRecordCount", + ]) + || value.trustClass !== "community_untrusted_v1" + || value.status !== "cryptographically_valid" + || !isSha256Hex(value.bundleId) + || !isSha256Hex(value.publisherKeyId) + || !isNonNegativeInteger(value.activeRecordCount) + || !isNonNegativeInteger(value.revokedRecordCount)) { + return null; + } + evidence.push({ + trustClass: "community_untrusted_v1", + status: "cryptographically_valid", + bundleId: value.bundleId, + publisherKeyId: value.publisherKeyId, + activeRecordCount: value.activeRecordCount, + revokedRecordCount: value.revokedRecordCount, + }); + } + return { evidence, trustClass: "community_untrusted_v1", locallyVerified: false }; +} + +export async function fetchCommunityEvidenceContext( + apiBase: string, + signal: AbortSignal, +): Promise { + const raw = await fetchLabJson(apiBase, "/api/lab/public/community", signal); + const context = parseCommunityEvidenceContext(raw); + if (!context) throw invalidResponse(); + return context; +} + export type LabPageData = { status: LabStatusDto; verdicts: VerdictDto[]; @@ -296,6 +370,7 @@ export type VerdictDetailData = { events: LabEventDto[]; artifacts: ArtifactMetadataDto[]; production: PassiveProductionSummaryDto | null; + community: CommunityEvidenceContextDto | null; }; async function mapSettledBounded( @@ -337,7 +412,7 @@ export async function fetchVerdictDetail( layer: verdict.evidenceLayer, suiteId: verdict.suiteId, }; - const [subject, observations, events, artifacts, production] = await Promise.all([ + const [subject, observations, events, artifacts, production, community] = await Promise.all([ fetchSubjectDetail(apiBase, verdict.subjectId, signal), fetchAllObservations(apiBase, observationFilters, signal), mapSettledBounded(eventIds, DETAIL_CONCURRENCY, signal, id => fetchEventById(apiBase, id, signal)), @@ -346,6 +421,10 @@ export async function fetchVerdictDetail( if (signal.aborted) throw error; return null; }), + fetchCommunityEvidenceContext(apiBase, signal).catch(error => { + if (signal.aborted) throw error; + return null; + }), ]); return { subject, @@ -354,5 +433,6 @@ export async function fetchVerdictDetail( events, artifacts, production, + community, }; } From e8080b4b3e6f7727943176acf4080adb843c6f32 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 12 Aug 2026 20:25:15 +0200 Subject: [PATCH 33/33] docs(lab): record CL-10 implementation validation state --- ...6-08-12-cl10-public-evidence-validation.md | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-12-cl10-public-evidence-validation.md diff --git a/docs/superpowers/plans/2026-08-12-cl10-public-evidence-validation.md b/docs/superpowers/plans/2026-08-12-cl10-public-evidence-validation.md new file mode 100644 index 0000000000..562580e8bb --- /dev/null +++ b/docs/superpowers/plans/2026-08-12-cl10-public-evidence-validation.md @@ -0,0 +1,47 @@ +# CL-10 Public Evidence Validation State + +Validated implementation scope: CL-10.1 through CL-10.4 only. CL-10.5 remote publishing remains blocked by contract. + +## Implemented + +- Closed, independently versioned public evidence DTOs and runtime validators. +- Domain-separated public subject, record, bundle, artifact, publisher, and revocation identities. +- Repo-owned, versioned, content-addressed public route registry authority. +- Fail-closed protocol/route/task privacy projection. Private route dimensions are never dropped to broaden a public claim. +- Ed25519 publisher continuity with an installation-local restricted private key and public-only bundle identity. +- Deterministic signed local bundles and content-addressed local export storage. +- Same-publisher revocation with bounded targets and idempotent identical replay. +- Quarantined `community_untrusted_v1` import/cache with cryptographic validation followed by repository authority validation. +- Publisher-scoped community identity so identical content signed by different publishers can coexist and revoke independently. +- Sensitive `export` purge integration for generated exports and provably locally-originated community copies while preserving third-party evidence. +- Explicit local CLI/API preview, export, verify, import, and community-list surfaces. +- Compatibility Matrix read-only community context, labelled non-authoritative and kept separate from the canonical local verdict. + +## Hard stops preserved + +- No remote publish command or management endpoint. +- No arbitrary upload URL or remote transport implementation. +- No automatic telemetry or background public-evidence upload. +- No imported community write to `compatibility.jsonl` or `compatibility.sqlite`. +- No community evidence input to canonical local verdicts, routing, Router Intelligence, or CL-08. +- A valid signature yields `cryptographically_valid`, never `locally_verified`. + +## Focused TDD evidence before closure run + +- Public projection/registry/privacy tests: GREEN. +- Signing/local export tests: GREEN. +- Community/revocation/publisher-continuity tests: GREEN. +- Purge interaction tests: GREEN. +- Local CLI/API surface tests with network canaries: GREEN. +- Compatibility Matrix community parser/render/i18n tests: GREEN. +- Root TypeScript: GREEN on the implemented core/operator slices. +- GUI TypeScript and GUI lint: GREEN on the Matrix slice. + +## Closure still required on exact final head + +- Focused CL-10 tests including existing Lab purge regressions. +- Root TypeScript and privacy scan. +- Relevant Lab query/ledger/CLI/management tests. +- GUI targeted tests, lint, build, and React Doctor. +- Full Cross-platform CI. +- Final changed-file/static audit confirming the CL-10.5 remote-publish hard stop and zero feedback into routing/local verdict/CL-08.