feat(ql3): verify cluster evidence offline

This commit is contained in:
whyour
2026-08-16 07:40:55 +08:00
parent 4f62a86d15
commit 33b9eb2f5e
16 changed files with 1422 additions and 13 deletions
@@ -0,0 +1,747 @@
/** Offline verifier for browser-local redacted Cluster Console evidence. */
import { createHash } from 'node:crypto';
import {
closeSync,
constants,
fstatSync,
openSync,
readSync,
realpathSync,
} from 'node:fs';
import { isAbsolute, normalize, parse } from 'node:path';
import { TextDecoder } from 'node:util';
export const CLUSTER_CONSOLE_EVIDENCE_BUNDLE_SCHEMA =
'qinglong/cluster-console-redacted-evidence-bundle@v1';
export const CLUSTER_CONSOLE_EVIDENCE_VERIFICATION_SCHEMA =
'qinglong/cluster-console-evidence-verification@v1';
const REQUEST_SCHEMA = 'qinglong/cluster-copilot-console-read-request@v1';
const MAXIMUM_PATH_BYTES = 4 * 1024;
const LIMITS = Object.freeze({
maximumArrayItems: 64,
maximumBundleBytes: 512 * 1024,
maximumDepth: 16,
maximumEntryFactBytes: 2 * 1024 * 1024 + 4 * 1024,
maximumObjectKeys: 256,
maximumRawBytes: 8 * 1024 * 1024,
maximumRecords: 16,
});
const OPERATIONS = Object.freeze([
'inspect',
'output',
'run_list',
'run_read',
'run_event_list',
'run_step_list',
'task_list',
'task_read',
'workflow_list',
'workflow_run_list',
'workflow_run_read',
'workflow_event_list',
'workflow_step_list',
] as const);
type EvidenceOperation = (typeof OPERATIONS)[number];
const OPERATION_SET = new Set<string>(OPERATIONS);
const REQUEST_FIELDS: Readonly<Record<EvidenceOperation, readonly string[]>> =
Object.freeze({
inspect: Object.freeze(['projectId', 'requestId', 'sourceRunId']),
output: Object.freeze(['projectId', 'requestId', 'sourceRunId']),
run_list: Object.freeze([
'afterCreatedAtMs',
'afterRunId',
'limit',
'projectId',
'requestId',
]),
run_read: Object.freeze(['projectId', 'requestId', 'runId']),
run_event_list: Object.freeze([
'afterSequence',
'limit',
'projectId',
'requestId',
'runId',
]),
run_step_list: Object.freeze([
'afterStepKey',
'afterStepRunId',
'limit',
'projectId',
'requestId',
'runId',
]),
task_list: Object.freeze([
'afterTaskId',
'limit',
'projectId',
'requestId',
]),
task_read: Object.freeze(['projectId', 'requestId', 'taskId']),
workflow_list: Object.freeze(['packageName', 'projectId', 'requestId']),
workflow_run_list: Object.freeze([
'afterAdmittedAtMs',
'afterRunId',
'limit',
'packageName',
'projectId',
'requestId',
'workflowId',
]),
workflow_run_read: Object.freeze([
'packageName',
'projectId',
'requestId',
'runId',
'workflowId',
]),
workflow_event_list: Object.freeze([
'afterSequence',
'limit',
'packageName',
'projectId',
'requestId',
'runId',
'workflowId',
]),
workflow_step_list: Object.freeze([
'afterStepKey',
'afterStepRunId',
'limit',
'packageName',
'projectId',
'requestId',
'runId',
'workflowId',
]),
});
const IDENTIFIER_DOMAINS: Readonly<Record<string, string>> = Object.freeze({
afterRunId: 'run',
afterStepKey: 'step',
afterStepRunId: 'step',
afterTaskId: 'task',
artifactId: 'artifact',
contentDigest: 'digest',
diagnosisRunId: 'run',
executionId: 'execution',
id: 'identifier',
modelId: 'model',
outputRef: 'artifact',
packageName: 'package',
projectId: 'project',
providerId: 'provider',
requestId: 'request',
runId: 'run',
sourceRunId: 'run',
stepKey: 'step',
stepRunId: 'step',
taskId: 'task',
triggerId: 'trigger',
workflowId: 'workflow',
workerId: 'worker',
});
const SAFE_CONTAINERS = new Set([
'attempts',
'counts',
'events',
'items',
'metadata',
'next',
'reference',
'run',
'runs',
'source',
'step',
'steps',
'summary',
'target',
'task',
'tasks',
'usage',
'workflow',
'workflows',
]);
const SAFE_BOOLEANS = new Set([
'active',
'archived',
'available',
'cancelRequested',
'enabled',
'hasMore',
'outputAvailable',
'ready',
'replayed',
'tailComplete',
'terminal',
'truncated',
]);
const SAFE_ENUM_KEYS = new Set([
'finishReason',
'kind',
'operation',
'outcome',
'stage',
'status',
]);
const SAFE_ENUM_VALUES = new Set([
'accepted',
'active',
'admission',
'available',
'blocked',
'cancelled',
'completed',
'completion',
'dispatch',
'dispatching',
'disabled',
'enabled',
'execution',
'failed',
'finalization',
'installed',
'local',
'lost',
'missing',
'model',
'not_found',
'pending',
'post_model',
'pre_model',
'prompt',
'quarantined',
'queued',
'ready',
'recovery',
'rejected',
'remote',
'retained',
'retired',
'retry_wait',
'run',
'running',
'skipped',
'staged',
'staging',
'step',
'stop',
'succeeded',
'system',
'task',
'terminal',
'timed_out',
'tool',
'trigger',
'unknown',
'unavailable',
'workflow',
]);
const NUMERIC_KEY =
/^(?:schemaVersion|version|revision|sequence|attempt|priority|limit|offset|size|total|count|[A-Za-z0-9_]*(?:AtMs|TimeMs|DurationMs|Bytes|Tokens|Micros|Sequence|Version|Count|Limit|Offset|Size|Total))$/u;
const SCHEMA_VALUE = /^[a-z0-9][a-z0-9./_-]{0,126}@[a-z0-9._-]{1,16}$/u;
const SHA256 = /^[0-9a-f]{64}$/u;
const CONTROL = /[\0-\x1f\x7f]/u;
export interface ClusterConsoleEvidenceVerification {
readonly schema: typeof CLUSTER_CONSOLE_EVIDENCE_VERIFICATION_SCHEMA;
readonly status: 'verified';
readonly bundle: Readonly<{
schema: typeof CLUSTER_CONSOLE_EVIDENCE_BUNDLE_SCHEMA;
contentDigest: string;
entryCount: number;
totalRawCanonicalBytes: number;
}>;
readonly integrity: Readonly<{
bundleDigest: 'verified';
rawFactDigests: 'not_recomputed_without_raw_facts';
}>;
readonly claims: Readonly<{
serverSignature: 'not_verified';
attestation: 'not_verified';
durableAudit: 'not_verified';
actionAuthority: 'none';
}>;
readonly execution: Readonly<{
networkAccess: false;
mutation: false;
fileWrites: false;
}>;
}
export class ClusterConsoleEvidenceVerificationError extends TypeError {
readonly code = 'QL3_CLUSTER_CONSOLE_EVIDENCE_VERIFICATION_INVALID';
constructor() {
super('Cluster Console evidence bundle verification failed');
this.name = 'ClusterConsoleEvidenceVerificationError';
}
}
function invalid(): never {
throw new ClusterConsoleEvidenceVerificationError();
}
function plainObject(value: unknown): value is Record<string, unknown> {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
return false;
}
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
}
function exactKeys(
value: unknown,
expected: readonly string[],
): value is Record<string, unknown> {
if (!plainObject(value)) return false;
const actual = Object.keys(value).sort();
const canonical = [...expected].sort();
return (
actual.length === canonical.length &&
actual.every((key, index) => key === canonical[index])
);
}
function sortedKeys(value: Readonly<Record<string, unknown>>): boolean {
const actual = Object.keys(value);
const sorted = [...actual].sort();
return actual.every((key, index) => key === sorted[index]);
}
function safeInteger(value: unknown): value is number {
return Number.isSafeInteger(value) && (value as number) >= 0;
}
function canonicalValue(
value: unknown,
depth: number,
stack: WeakSet<object>,
): string {
if (depth > LIMITS.maximumDepth) return invalid();
if (value === null) return 'null';
if (typeof value === 'boolean') return value ? 'true' : 'false';
if (typeof value === 'number') {
if (!Number.isFinite(value)) return invalid();
return JSON.stringify(Object.is(value, -0) ? 0 : value);
}
if (typeof value === 'string') return JSON.stringify(value);
if (value === null || typeof value !== 'object') return invalid();
if (stack.has(value)) return invalid();
stack.add(value);
try {
if (Array.isArray(value)) {
if (value.length > LIMITS.maximumArrayItems) return invalid();
return `[${value
.map((item) => canonicalValue(item, depth + 1, stack))
.join(',')}]`;
}
if (!plainObject(value)) return invalid();
const keys = Object.keys(value).sort();
if (keys.length > LIMITS.maximumObjectKeys) return invalid();
return `{${keys
.map(
(key) =>
`${JSON.stringify(key)}:${canonicalValue(
value[key],
depth + 1,
stack,
)}`,
)
.join(',')}}`;
} finally {
stack.delete(value);
}
}
function canonicalize(value: unknown): string {
return canonicalValue(value, 0, new WeakSet());
}
interface AliasState {
readonly nextByDomain: Map<string, number>;
readonly seen: Set<string>;
}
function validateAlias(
domain: string,
value: unknown,
aliases: AliasState,
): void {
if (value === null) return;
if (typeof value !== 'string') return invalid();
const prefix = `${domain}-`;
if (!value.startsWith(prefix)) return invalid();
const suffix = value.slice(prefix.length);
if (!/^[0-9]{3,}$/u.test(suffix)) return invalid();
const numeric = Number(suffix);
if (
!Number.isSafeInteger(numeric) ||
numeric < 1 ||
String(numeric).padStart(3, '0') !== suffix
) {
return invalid();
}
const identity = `${domain}\0${suffix}`;
if (aliases.seen.has(identity)) return;
const expected = aliases.nextByDomain.get(domain) ?? 1;
if (numeric !== expected) return invalid();
aliases.seen.add(identity);
aliases.nextByDomain.set(domain, expected + 1);
}
function validateSafeValue(
value: unknown,
aliases: AliasState,
depth: number,
): void {
if (depth > LIMITS.maximumDepth) return invalid();
if (Array.isArray(value)) {
if (value.length > LIMITS.maximumArrayItems) return invalid();
for (const item of value) {
if (item === null) continue;
if (!plainObject(item) && !Array.isArray(item)) return invalid();
validateSafeValue(item, aliases, depth + 1);
}
return;
}
if (!plainObject(value) || !sortedKeys(value)) return invalid();
const keys = Object.keys(value);
if (keys.length > LIMITS.maximumObjectKeys) return invalid();
for (const key of keys) {
const candidate = value[key];
const domain = IDENTIFIER_DOMAINS[key];
if (domain !== undefined) {
validateAlias(domain, candidate, aliases);
continue;
}
if (/Digest$/u.test(key)) {
validateAlias('digest', candidate, aliases);
continue;
}
if (key === 'schema') {
if (typeof candidate !== 'string' || !SCHEMA_VALUE.test(candidate)) {
return invalid();
}
continue;
}
if (SAFE_ENUM_KEYS.has(key)) {
if (
candidate !== null &&
(typeof candidate !== 'string' ||
(key === 'operation'
? !OPERATION_SET.has(candidate)
: !SAFE_ENUM_VALUES.has(candidate)))
) {
return invalid();
}
continue;
}
if (SAFE_BOOLEANS.has(key)) {
if (candidate !== null && typeof candidate !== 'boolean') {
return invalid();
}
continue;
}
if (NUMERIC_KEY.test(key)) {
if (candidate !== null && !safeInteger(candidate)) return invalid();
continue;
}
if (SAFE_CONTAINERS.has(key)) {
if (candidate === null) continue;
if (!plainObject(candidate) && !Array.isArray(candidate))
return invalid();
validateSafeValue(candidate, aliases, depth + 1);
continue;
}
return invalid();
}
}
function validateTarget(
value: unknown,
operation: EvidenceOperation,
aliases: AliasState,
): void {
const fields = REQUEST_FIELDS[operation];
if (!exactKeys(value, ['operation', 'schema', ...fields])) return invalid();
if (!sortedKeys(value)) return invalid();
if (value.schema !== REQUEST_SCHEMA || value.operation !== operation) {
return invalid();
}
for (const field of fields) {
const domain = IDENTIFIER_DOMAINS[field];
if (domain !== undefined) {
validateAlias(domain, value[field], aliases);
continue;
}
if (!NUMERIC_KEY.test(field)) return invalid();
const candidate = value[field];
if (candidate !== null && !safeInteger(candidate)) return invalid();
}
}
function validateBundle(value: unknown): ClusterConsoleEvidenceVerification {
if (
!exactKeys(value, [
'actionAuthority',
'attestation',
'classification',
'contentDigest',
'entries',
'generatedAtMs',
'generatedBy',
'integrity',
'redaction',
'schema',
'source',
]) ||
value.schema !== CLUSTER_CONSOLE_EVIDENCE_BUNDLE_SCHEMA ||
value.classification !== 'low_sensitive_redacted' ||
!safeInteger(value.generatedAtMs) ||
value.generatedBy !== 'browser_local' ||
value.actionAuthority !== 'none' ||
value.attestation !== 'none' ||
typeof value.contentDigest !== 'string' ||
!SHA256.test(value.contentDigest)
) {
return invalid();
}
if (
!exactKeys(value.source, [
'collection',
'entryCount',
'surface',
'totalRawCanonicalBytes',
]) ||
value.source.surface !== 'cluster_field_ledger' ||
value.source.collection !== 'explicit_user_reads_only' ||
!safeInteger(value.source.entryCount) ||
!safeInteger(value.source.totalRawCanonicalBytes)
) {
return invalid();
}
if (
!exactKeys(value.redaction, [
'copilotOutputIncluded',
'freeTextIncluded',
'identifiers',
'policy',
'unknownFieldsIncluded',
]) ||
value.redaction.policy !== 'fixed_allowlist_v1' ||
value.redaction.identifiers !== 'per_bundle_typed_alias_without_mapping' ||
value.redaction.freeTextIncluded !== false ||
value.redaction.copilotOutputIncluded !== false ||
value.redaction.unknownFieldsIncluded !== false
) {
return invalid();
}
if (
!exactKeys(value.integrity, [
'algorithm',
'durableAudit',
'scope',
'serverSignature',
]) ||
value.integrity.algorithm !== 'sha256' ||
value.integrity.scope !== 'canonical_bundle_without_contentDigest' ||
value.integrity.serverSignature !== false ||
value.integrity.durableAudit !== false
) {
return invalid();
}
if (
!Array.isArray(value.entries) ||
value.entries.length < 1 ||
value.entries.length > LIMITS.maximumRecords ||
value.source.entryCount !== value.entries.length
) {
return invalid();
}
const aliases: AliasState = {
nextByDomain: new Map(),
seen: new Set(),
};
let totalRawCanonicalBytes = 0;
for (let index = 0; index < value.entries.length; index += 1) {
const entry = value.entries[index];
if (
!exactKeys(entry, [
'fact',
'observedAtMs',
'operation',
'rawFact',
'sanitizer',
'sequence',
'target',
]) ||
entry.sequence !== index + 1 ||
!safeInteger(entry.observedAtMs) ||
typeof entry.operation !== 'string' ||
!OPERATION_SET.has(entry.operation)
) {
return invalid();
}
const operation = entry.operation as EvidenceOperation;
validateTarget(entry.target, operation, aliases);
validateSafeValue(entry.fact, aliases, 0);
if (
!exactKeys(entry.rawFact, ['canonicalBytes', 'sha256']) ||
!safeInteger(entry.rawFact.canonicalBytes) ||
entry.rawFact.canonicalBytes < 2 ||
entry.rawFact.canonicalBytes > LIMITS.maximumEntryFactBytes ||
typeof entry.rawFact.sha256 !== 'string' ||
!SHA256.test(entry.rawFact.sha256)
) {
return invalid();
}
totalRawCanonicalBytes += entry.rawFact.canonicalBytes;
if (totalRawCanonicalBytes > LIMITS.maximumRawBytes) return invalid();
if (
!exactKeys(entry.sanitizer, [
'omittedFieldCount',
'rawContentIncluded',
]) ||
!safeInteger(entry.sanitizer.omittedFieldCount) ||
entry.sanitizer.rawContentIncluded !== false
) {
return invalid();
}
}
if (value.source.totalRawCanonicalBytes !== totalRawCanonicalBytes) {
return invalid();
}
const unsigned: Record<string, unknown> = {};
for (const key of Object.keys(value)) {
if (key !== 'contentDigest') unsigned[key] = value[key];
}
const computed = createHash('sha256')
.update(canonicalize(unsigned), 'utf8')
.digest('hex');
if (computed !== value.contentDigest) return invalid();
return Object.freeze({
schema: CLUSTER_CONSOLE_EVIDENCE_VERIFICATION_SCHEMA,
status: 'verified',
bundle: Object.freeze({
schema: CLUSTER_CONSOLE_EVIDENCE_BUNDLE_SCHEMA,
contentDigest: value.contentDigest,
entryCount: value.entries.length,
totalRawCanonicalBytes,
}),
integrity: Object.freeze({
bundleDigest: 'verified',
rawFactDigests: 'not_recomputed_without_raw_facts',
}),
claims: Object.freeze({
serverSignature: 'not_verified',
attestation: 'not_verified',
durableAudit: 'not_verified',
actionAuthority: 'none',
}),
execution: Object.freeze({
networkAccess: false,
mutation: false,
fileWrites: false,
}),
});
}
function readStableBundle(filePath: string): Buffer {
if (
!isAbsolute(filePath) ||
normalize(filePath) !== filePath ||
parse(filePath).root === filePath ||
Buffer.byteLength(filePath, 'utf8') > MAXIMUM_PATH_BYTES ||
CONTROL.test(filePath)
) {
return invalid();
}
let descriptor = -1;
let bytes: Buffer | undefined;
try {
if (realpathSync(filePath) !== filePath) return invalid();
descriptor = openSync(
filePath,
constants.O_RDONLY |
((constants as unknown as Readonly<Record<string, number>>).O_CLOEXEC ??
0) |
(constants.O_NOFOLLOW ?? 0),
);
const before = fstatSync(descriptor);
if (
!before.isFile() ||
before.size < 2 ||
before.size > LIMITS.maximumBundleBytes
) {
return invalid();
}
bytes = Buffer.alloc(before.size);
let offset = 0;
while (offset < bytes.length) {
const read = readSync(
descriptor,
bytes,
offset,
bytes.length - offset,
offset,
);
if (read < 1) return invalid();
offset += read;
}
const after = fstatSync(descriptor);
if (
after.dev !== before.dev ||
after.ino !== before.ino ||
after.mode !== before.mode ||
after.uid !== before.uid ||
after.gid !== before.gid ||
after.size !== before.size ||
after.mtimeMs !== before.mtimeMs ||
after.ctimeMs !== before.ctimeMs
) {
return invalid();
}
return bytes;
} catch (error) {
bytes?.fill(0);
if (error instanceof ClusterConsoleEvidenceVerificationError) throw error;
return invalid();
} finally {
if (descriptor >= 0) closeSync(descriptor);
}
}
export function verifyClusterConsoleEvidenceBundleFile(
filePath: string,
): ClusterConsoleEvidenceVerification {
const bytes = readStableBundle(filePath);
try {
if (
bytes.length >= 3 &&
bytes[0] === 0xef &&
bytes[1] === 0xbb &&
bytes[2] === 0xbf
) {
return invalid();
}
let text: string;
try {
text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
} catch {
return invalid();
}
let parsed: unknown;
try {
parsed = JSON.parse(text) as unknown;
} catch {
return invalid();
}
if (`${JSON.stringify(parsed, null, 2)}\n` !== text) return invalid();
return validateBundle(parsed);
} finally {
bytes.fill(0);
}
}
@@ -0,0 +1,60 @@
#!/usr/bin/env node
/** Offline, read-only Cluster Console evidence verification entrypoint. */
import {
ClusterConsoleEvidenceVerificationError,
verifyClusterConsoleEvidenceBundleFile,
} from './evidenceVerifier';
const USAGE =
'Usage: ql3-copilot-evidence-verify --bundle=/absolute/evidence.json';
function failure(code: string, message: string): string {
return JSON.stringify({
schemaVersion: 1,
component: 'qinglong3-cluster-console-evidence-verifier',
code,
message,
});
}
function main(argv: readonly string[]): void {
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
process.stdout.write(`${USAGE}\n`);
return;
}
if (
argv.length !== 1 ||
!argv[0]?.startsWith('--bundle=') ||
argv[0] === '--bundle='
) {
process.stderr.write(
`${failure(
'QL3_CLUSTER_CONSOLE_EVIDENCE_VERIFIER_USAGE_INVALID',
USAGE,
)}\n`,
);
process.exitCode = 64;
return;
}
try {
const result = verifyClusterConsoleEvidenceBundleFile(
argv[0].slice('--bundle='.length),
);
process.stdout.write(`${JSON.stringify(result)}\n`);
} catch (error) {
const code =
error instanceof ClusterConsoleEvidenceVerificationError
? error.code
: 'QL3_CLUSTER_CONSOLE_EVIDENCE_VERIFICATION_INVALID';
process.stderr.write(
`${failure(
code,
'Cluster Console evidence bundle verification failed',
)}\n`,
);
process.exitCode = 65;
}
}
main(process.argv.slice(2));
@@ -52,6 +52,12 @@ export const QINGLONG3_CLUSTER_PRODUCT_COMMANDS: readonly QingLong3ClusterProduc
target: 'copilot-console/cli.js',
description: 'open the loopback-only read-only Copilot Console',
}),
Object.freeze({
name: 'evidence-verify',
binary: 'ql3-copilot-evidence-verify',
target: 'copilot-console/evidenceVerifierCli.js',
description: 'verify one redacted Console evidence bundle offline',
}),
Object.freeze({
name: 'package',
binary: 'ql3-plugin-package-client',