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
+13
View File
@@ -27,6 +27,19 @@ outputs, environment, errors, credentials, unknown fields and Copilot model
text are omitted. Per-fact and top-level SHA-256 values provide self-integrity,
not a server signature, durable audit or action authority.
The same package provides an independently implemented offline verifier:
```sh
ql3-cluster-admin evidence-verify \
--bundle=/absolute/qinglong-cluster-evidence.json
```
It performs one read-only, no-follow file read, revalidates the complete fixed
schema/redaction/alias contract and recomputes the top-level digest without a
network request or file write. A verified result does not prove the server
origin, attestation or durable audit, and cannot recompute the per-fact hashes
because the deliberately omitted raw facts are not present.
The reviewed operator-workstation setup, private-file ceremony, release
verification, preflight and session lifecycle are documented in
`deploy/console/ql3-cluster-copilot/README.md`. Native execution binds host
+1
View File
@@ -407,6 +407,7 @@
"ql3-copilot-client": "dist/copilot-client/cli.js",
"ql3-copilot-mcp": "dist/copilot-mcp/cli.js",
"ql3-copilot-console": "dist/copilot-console/cli.js",
"ql3-copilot-evidence-verify": "dist/copilot-console/evidenceVerifierCli.js",
"ql3-plugin-package-recover": "dist/plugin-package/recovery/pluginPackageRecoveryCli.js",
"ql3-plugin-package-manage": "dist/plugin-package/management/pluginPackageManagementCli.js",
"ql3-plugin-package-client": "dist/plugin-package/management/pluginPackageManagementClientCli.js",
@@ -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',
@@ -0,0 +1,342 @@
'use strict';
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const { createHash, webcrypto } = require('node:crypto');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const test = require('node:test');
const {
createClusterConsoleEvidenceBundle,
serializeClusterConsoleEvidenceBundle,
} = require('../assets/copilot-console/evidence-bundle.js');
const {
CLUSTER_CONSOLE_EVIDENCE_BUNDLE_SCHEMA,
CLUSTER_CONSOLE_EVIDENCE_VERIFICATION_SCHEMA,
verifyClusterConsoleEvidenceBundleFile,
} = require('../dist/copilot-console/evidenceVerifier.js');
const cliPath = path.resolve(
__dirname,
'../dist/copilot-console/evidenceVerifierCli.js',
);
const requestSchema = 'qinglong/cluster-copilot-console-read-request@v1';
function canonicalize(value) {
if (value === null || typeof value !== 'object') {
return JSON.stringify(Object.is(value, -0) ? 0 : value);
}
if (Array.isArray(value)) {
return `[${value.map(canonicalize).join(',')}]`;
}
return `{${Object.keys(value)
.sort()
.map((key) => `${JSON.stringify(key)}:${canonicalize(value[key])}`)
.join(',')}}`;
}
function resign(bundle) {
const unsigned = {};
for (const key of Object.keys(bundle)) {
if (key !== 'contentDigest') unsigned[key] = bundle[key];
}
bundle.contentDigest = createHash('sha256')
.update(canonicalize(unsigned), 'utf8')
.digest('hex');
return bundle;
}
async function validBundle() {
return createClusterConsoleEvidenceBundle(
[
{
operation: 'run_read',
observedAtMs: 1_700_000_000_000,
request: {
schema: requestSchema,
operation: 'run_read',
projectId: 'private-project',
requestId: 'private-request',
runId: 'private-run',
},
fact: {
schema: 'qinglong/bounded-run-projection@v1',
schemaVersion: 1,
status: 'succeeded',
projectId: 'private-project',
runId: 'private-run',
createdAtMs: 1_700_000_000_000,
outputAvailable: true,
message: 'must-never-survive-redaction',
},
},
],
1_700_000_001_000,
webcrypto,
);
}
function fixture(t, encoded, name = 'evidence.json') {
const directory = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-evidence-verifier-')),
);
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const filePath = path.join(directory, name);
fs.writeFileSync(filePath, encoded, { mode: 0o644 });
return { directory, filePath };
}
test('independently verifies the browser generator output without authority claims', async (t) => {
const bundle = await validBundle();
const encoded = serializeClusterConsoleEvidenceBundle(bundle);
const { filePath } = fixture(t, encoded);
const result = verifyClusterConsoleEvidenceBundleFile(filePath);
assert.deepEqual(result, {
schema: CLUSTER_CONSOLE_EVIDENCE_VERIFICATION_SCHEMA,
status: 'verified',
bundle: {
schema: CLUSTER_CONSOLE_EVIDENCE_BUNDLE_SCHEMA,
contentDigest: bundle.contentDigest,
entryCount: 1,
totalRawCanonicalBytes: bundle.source.totalRawCanonicalBytes,
},
integrity: {
bundleDigest: 'verified',
rawFactDigests: 'not_recomputed_without_raw_facts',
},
claims: {
serverSignature: 'not_verified',
attestation: 'not_verified',
durableAudit: 'not_verified',
actionAuthority: 'none',
},
execution: { networkAccess: false, mutation: false, fileWrites: false },
});
assert.equal(Object.isFrozen(result), true);
assert.equal(encoded.includes('must-never-survive-redaction'), false);
});
test('cross-verifies every fixed Console read operation', async (t) => {
const requests = {
inspect: { projectId: 'p-1', requestId: 'q-1', sourceRunId: 'r-1' },
output: { projectId: 'p-2', requestId: 'q-2', sourceRunId: 'r-2' },
run_list: {
afterCreatedAtMs: null,
afterRunId: null,
limit: 32,
projectId: 'p-3',
requestId: 'q-3',
},
run_read: { projectId: 'p-4', requestId: 'q-4', runId: 'r-4' },
run_event_list: {
afterSequence: null,
limit: 32,
projectId: 'p-5',
requestId: 'q-5',
runId: 'r-5',
},
run_step_list: {
afterStepKey: null,
afterStepRunId: null,
limit: 32,
projectId: 'p-6',
requestId: 'q-6',
runId: 'r-6',
},
task_list: {
afterTaskId: null,
limit: 32,
projectId: 'p-7',
requestId: 'q-7',
},
task_read: { projectId: 'p-8', requestId: 'q-8', taskId: 't-8' },
workflow_list: {
packageName: 'pkg-9',
projectId: 'p-9',
requestId: 'q-9',
},
workflow_run_list: {
afterAdmittedAtMs: null,
afterRunId: null,
limit: 32,
packageName: 'pkg-10',
projectId: 'p-10',
requestId: 'q-10',
workflowId: 'w-10',
},
workflow_run_read: {
packageName: 'pkg-11',
projectId: 'p-11',
requestId: 'q-11',
runId: 'r-11',
workflowId: 'w-11',
},
workflow_event_list: {
afterSequence: null,
limit: 32,
packageName: 'pkg-12',
projectId: 'p-12',
requestId: 'q-12',
runId: 'r-12',
workflowId: 'w-12',
},
workflow_step_list: {
afterStepKey: null,
afterStepRunId: null,
limit: 32,
packageName: 'pkg-13',
projectId: 'p-13',
requestId: 'q-13',
runId: 'r-13',
workflowId: 'w-13',
},
};
const records = Object.entries(requests).map(
([operation, request], index) => ({
operation,
observedAtMs: 1_700_000_000_000 + index,
request: { schema: requestSchema, operation, ...request },
fact: {
schema: 'qinglong/test-fact@v1',
schemaVersion: 1,
operation,
status: 'succeeded',
},
}),
);
const bundle = await createClusterConsoleEvidenceBundle(
records,
1_700_000_001_000,
webcrypto,
);
const { filePath } = fixture(
t,
serializeClusterConsoleEvidenceBundle(bundle),
);
const result = verifyClusterConsoleEvidenceBundleFile(filePath);
assert.equal(result.status, 'verified');
assert.equal(result.bundle.entryCount, 13);
});
test('CLI is secret-free on success, invalid input and usage errors', async (t) => {
const bundle = await validBundle();
const { filePath } = fixture(
t,
serializeClusterConsoleEvidenceBundle(bundle),
'private-customer-run.json',
);
const success = spawnSync(
process.execPath,
[cliPath, `--bundle=${filePath}`],
{
encoding: 'utf8',
},
);
assert.equal(success.status, 0);
assert.equal(JSON.parse(success.stdout).status, 'verified');
assert.equal(success.stderr, '');
assert.equal(success.stdout.includes(filePath), false);
assert.equal(success.stdout.includes('private-customer-run'), false);
const tampered = JSON.parse(fs.readFileSync(filePath, 'utf8'));
tampered.entries[0].fact.status = 'failed';
fs.writeFileSync(filePath, `${JSON.stringify(tampered, null, 2)}\n`);
const invalid = spawnSync(
process.execPath,
[cliPath, `--bundle=${filePath}`],
{
encoding: 'utf8',
},
);
assert.equal(invalid.status, 65);
assert.equal(invalid.stdout, '');
assert.equal(
JSON.parse(invalid.stderr).code,
'QL3_CLUSTER_CONSOLE_EVIDENCE_VERIFICATION_INVALID',
);
assert.equal(invalid.stderr.includes(filePath), false);
const usage = spawnSync(process.execPath, [cliPath], { encoding: 'utf8' });
assert.equal(usage.status, 64);
assert.equal(
JSON.parse(usage.stderr).code,
'QL3_CLUSTER_CONSOLE_EVIDENCE_VERIFIER_USAGE_INVALID',
);
const help = spawnSync(process.execPath, [cliPath, '--help'], {
encoding: 'utf8',
});
assert.equal(help.status, 0);
assert.match(help.stdout, /^Usage: ql3-copilot-evidence-verify/);
});
test('rejects re-signed structural widening, alias gaps and false proof claims', async (t) => {
const baseline = JSON.parse(
serializeClusterConsoleEvidenceBundle(await validBundle()),
);
const mutations = [
(bundle) => {
bundle.entries[0].fact.message = 'unsafe free text';
},
(bundle) => {
bundle.entries[0].target.runId = 'run-002';
},
(bundle) => {
bundle.integrity.serverSignature = true;
},
(bundle) => {
bundle.source.totalRawCanonicalBytes += 1;
},
(bundle) => {
bundle.entries[0].sequence = 2;
},
];
for (const [index, mutate] of mutations.entries()) {
const candidate = structuredClone(baseline);
mutate(candidate);
resign(candidate);
const { filePath } = fixture(
t,
`${JSON.stringify(candidate, null, 2)}\n`,
`invalid-${index}.json`,
);
assert.throws(() => verifyClusterConsoleEvidenceBundleFile(filePath), {
code: 'QL3_CLUSTER_CONSOLE_EVIDENCE_VERIFICATION_INVALID',
});
}
});
test('rejects non-canonical JSON, links, relative paths and oversized files', async (t) => {
const encoded = serializeClusterConsoleEvidenceBundle(await validBundle());
for (const [name, contents] of [
['minified.json', JSON.stringify(JSON.parse(encoded))],
['bom.json', `\ufeff${encoded}`],
['crlf.json', encoded.replaceAll('\n', '\r\n')],
[
'duplicate.json',
encoded.replace(
'{\n',
`{\n "schema": "${CLUSTER_CONSOLE_EVIDENCE_BUNDLE_SCHEMA}",\n`,
),
],
]) {
const { filePath } = fixture(t, contents, name);
assert.throws(
() => verifyClusterConsoleEvidenceBundleFile(filePath),
undefined,
name,
);
}
const { directory, filePath } = fixture(t, encoded, 'source.json');
const linkPath = path.join(directory, 'link.json');
fs.symlinkSync(filePath, linkPath);
assert.throws(() => verifyClusterConsoleEvidenceBundleFile(linkPath));
assert.throws(() => verifyClusterConsoleEvidenceBundleFile('source.json'));
const oversized = path.join(directory, 'oversized.json');
fs.writeFileSync(oversized, 'x'.repeat(512 * 1024 + 1));
assert.throws(() => verifyClusterConsoleEvidenceBundleFile(oversized));
});
@@ -117,9 +117,7 @@ async function startReadinessServer(status) {
port: server.address().port,
close: () =>
new Promise((resolvePromise, reject) => {
server.close((error) =>
error ? reject(error) : resolvePromise(),
);
server.close((error) => (error ? reject(error) : resolvePromise()));
}),
};
}
@@ -152,9 +150,7 @@ async function startCopilotReadinessServer(status) {
port: server.address().port,
close: () =>
new Promise((resolvePromise, reject) => {
server.close((error) =>
error ? reject(error) : resolvePromise(),
);
server.close((error) => (error ? reject(error) : resolvePromise()));
}),
};
}
@@ -333,7 +329,7 @@ function validContextFixture(t) {
test('catalog exposes only reviewed product entrypoints from the same package', () => {
assert.equal(manifest.bin['ql3-cluster-admin'], 'dist/product-cli/cli.js');
assert.equal(QINGLONG3_CLUSTER_PRODUCT_COMMANDS.length, 10);
assert.equal(QINGLONG3_CLUSTER_PRODUCT_COMMANDS.length, 11);
assert.equal(
new Set(QINGLONG3_CLUSTER_PRODUCT_COMMANDS.map(({ name }) => name)).size,
QINGLONG3_CLUSTER_PRODUCT_COMMANDS.length,
@@ -352,7 +348,8 @@ test('catalog exposes only reviewed product entrypoints from the same package',
assert.equal(
command.binary.includes('-client') ||
command.binary === 'ql3-copilot-mcp' ||
command.binary === 'ql3-copilot-console',
command.binary === 'ql3-copilot-console' ||
command.binary === 'ql3-copilot-evidence-verify',
true,
);
}
@@ -382,6 +379,10 @@ test('help and version are bounded installation-derived product facts', () => {
assert.match(help, /\n copilot\s+diagnose, inspect, read or cancel Runs/);
assert.match(help, /\n copilot-mcp\s+serve the bounded Cluster Copilot MCP/);
assert.match(help, /\n copilot-console\s+open the loopback-only read-only/);
assert.match(
help,
/\n evidence-verify\s+verify one redacted Console evidence/,
);
assert.match(help, /Server, migration, recovery, executor and key-custody/);
assert.equal(help.includes('plugin-package-manage'), false);
assert.equal(