feat(ql3): add optional console run drilldown

This commit is contained in:
whyour
2026-08-20 09:23:19 +08:00
parent cf21e984cb
commit 0a5f1448f1
29 changed files with 1193 additions and 68 deletions
@@ -234,7 +234,7 @@ h3 {
}
.mode-tabs {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(4, 1fr);
margin: 1.8rem 0 1.2rem;
border-bottom: 1px solid var(--line);
}
@@ -5,6 +5,11 @@
const routes = Object.freeze({
inspect: '/api/v1/copilot/inspect',
output: '/api/v1/copilot/output',
run_cancellation_status: '/api/v1/run-management/cancellation-status',
run_cancellation_blocked_list:
'/api/v1/run-management/blocked-cancellations',
run_cancellation_inspect:
'/api/v1/run-management/cancellation-inspect',
run_list: '/api/v1/observe/run-list',
run_read: '/api/v1/observe/run',
run_event_list: '/api/v1/observe/run-events',
@@ -20,6 +25,9 @@
const labels = Object.freeze({
inspect: 'Copilot 诊断状态',
output: 'Copilot 诊断内容',
run_cancellation_status: '取消可用性',
run_cancellation_blocked_list: 'Blocked Cancellations',
run_cancellation_inspect: '取消诊断',
run_list: 'Run 目录',
run_read: 'Run 详情',
run_event_list: 'Run Events',
@@ -110,6 +118,12 @@
if (operation === 'inspect' || operation === 'output') {
result.sourceRunId = value('source-run-id');
result.requestId = value('diagnosis-request-id');
} else if (operation === 'run_cancellation_status') {
return result;
} else if (operation === 'run_cancellation_blocked_list') {
result.cursor = null;
} else if (operation === 'run_cancellation_inspect') {
result.runId = value('cancellation-run-id');
} else if (operation === 'run_list') {
result.afterCreatedAtMs = null;
result.afterRunId = null;
@@ -156,7 +170,13 @@
const nextPage = function (operation, prior, fact) {
const next = Object.assign({}, prior, { requestId: requestId() });
if (operation === 'run_list' && fact.hasMore === true && fact.next) {
if (
operation === 'run_cancellation_blocked_list' &&
fact.truncated === true &&
typeof fact.nextCursor === 'string'
) {
next.cursor = fact.nextCursor;
} else if (operation === 'run_list' && fact.hasMore === true && fact.next) {
next.afterCreatedAtMs = fact.next.createdAtMs;
next.afterRunId = fact.next.runId;
} else if (
@@ -200,6 +220,39 @@
return next;
};
const appendDrilldownControls = function (entry, operation, fact) {
if (
operation === 'run_cancellation_status' &&
fact.operatorAction === 'inspect'
) {
const button = document.createElement('button');
button.type = 'button';
button.textContent = '显式读取 Blocked Runs';
button.addEventListener('click', function () {
void execute('run_cancellation_blocked_list');
});
entry.append(button);
return;
}
if (
operation !== 'run_cancellation_blocked_list' ||
!Array.isArray(fact.items)
) {
return;
}
fact.items.forEach(function (item) {
if (!item || typeof item.runId !== 'string') return;
const button = document.createElement('button');
button.type = 'button';
button.textContent = '显式检查 ' + item.runId;
button.addEventListener('click', function () {
document.getElementById('cancellation-run-id').value = item.runId;
void execute('run_cancellation_inspect');
});
entry.append(button);
});
};
const appendEvidence = function (operation, request, response) {
const fact = response.result.result;
const observedAtMs = Date.now();
@@ -235,6 +288,7 @@
});
entry.append(button);
}
appendDrilldownControls(entry, operation, fact);
ledger.prepend(entry);
evidenceRecords.push({ record: record, bytes: recordBytes, entry: entry });
evidenceBytes += recordBytes;
@@ -28,6 +28,9 @@
const operations = Object.freeze([
'inspect',
'output',
'run_cancellation_status',
'run_cancellation_blocked_list',
'run_cancellation_inspect',
'run_list',
'run_read',
'run_event_list',
@@ -44,6 +47,9 @@
const requestFields = Object.freeze({
inspect: ['projectId', 'requestId', 'sourceRunId'],
output: ['projectId', 'requestId', 'sourceRunId'],
run_cancellation_status: ['projectId', 'requestId'],
run_cancellation_blocked_list: ['cursor', 'projectId', 'requestId'],
run_cancellation_inspect: ['projectId', 'requestId', 'runId'],
run_list: [
'afterCreatedAtMs',
'afterRunId',
@@ -112,7 +118,9 @@
afterStepRunId: 'step',
afterTaskId: 'task',
artifactId: 'artifact',
attemptId: 'attempt',
contentDigest: 'digest',
cursor: 'cursor',
diagnosisRunId: 'run',
executionId: 'execution',
id: 'identifier',
@@ -133,7 +141,10 @@
});
const safeContainers = new Set([
'attempts',
'blockingResults',
'counts',
'dispatch',
'dispatches',
'events',
'items',
'metadata',
@@ -142,6 +153,7 @@
'run',
'runs',
'source',
'signals',
'step',
'steps',
'summary',
@@ -168,9 +180,15 @@
]);
const safeEnumKeys = new Set([
'finishReason',
'assessment',
'cancelReason',
'kind',
'lastResult',
'operation',
'operatorAction',
'outcome',
'runStatus',
'severity',
'stage',
'status',
]);
@@ -178,11 +196,15 @@
'accepted',
'active',
'admission',
'attention_required',
'available',
'blocked',
'cancelled',
'completed',
'completion',
'converging',
'critical',
'clear',
'dispatch',
'dispatching',
'disabled',
@@ -191,12 +213,18 @@
'failed',
'finalization',
'installed',
'inspect',
'invalid',
'identity_mismatch',
'local',
'lost',
'missing',
'model',
'none',
'not_found',
'pending',
'pid_mismatch',
'policy',
'post_model',
'pre_model',
'prompt',
@@ -204,6 +232,8 @@
'queued',
'ready',
'recovery',
'rearm',
'reconcile',
'rejected',
'remote',
'retained',
@@ -217,13 +247,20 @@
'step',
'stop',
'succeeded',
'shutdown',
'system',
'task',
'terminal',
'timed_out',
'timeout',
'tool',
'trigger',
'unknown',
'unsupported',
'user',
'wait',
'warning',
'ok',
'unavailable',
'workflow',
]);
@@ -232,7 +269,7 @@
const freeTextKey =
/text|content|stdout|stderr|command|input|output|environment|reason|error|message|description|name|path|url|uri|host|endpoint/iu;
const numericKey =
/^(?: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;
/^(?:schemaVersion|version|revision|sequence|attempt|priority|limit|offset|size|total|count|exitCode|pending|leased|retryWait|dispatched|blocked|due|expiredLease|identityMismatch|pidMismatch|unsupported|invalid|[A-Za-z0-9_]*(?:AtMs|TimeMs|DurationMs|Bytes|Tokens|Micros|Sequence|Version|Count|Limit|Offset|Size|Total))$/u;
const schemaValue = /^[a-z0-9][a-z0-9./_-]{0,126}@[a-z0-9._-]{1,16}$/u;
class ClusterConsoleEvidenceBundleError extends TypeError {
@@ -24,7 +24,7 @@
<div class="boundary" aria-label="当前权限边界">
<span class="boundary-dot" aria-hidden="true"></span>
<span>本机只读 BFF</span>
<strong>Run · Task · Workflow · Copilot</strong>
<strong>Run · Task · Workflow · Copilot · Optional management</strong>
</div>
</header>
@@ -51,6 +51,7 @@
<nav class="mode-tabs" aria-label="观察面">
<button type="button" class="mode-tab active" data-panel="runtime-panel" aria-pressed="true">运行态</button>
<button type="button" class="mode-tab" data-panel="management-panel" aria-pressed="false">取消可用性</button>
<button type="button" class="mode-tab" data-panel="workflow-panel" aria-pressed="false">工作流</button>
<button type="button" class="mode-tab" data-panel="copilot-panel" aria-pressed="false">Copilot</button>
</nav>
@@ -79,6 +80,25 @@
</div>
</section>
<section id="management-panel" class="mode-panel" hidden>
<div class="control-group">
<div class="control-title"><span>01</span><strong>Project 状态</strong></div>
<button type="button" class="primary" data-read="run_cancellation_status">读取取消可用性</button>
<p class="field-note">只有启动进程显式提供独立 Run management 配置与短期 assertion 时可用;页面不会自动刷新。</p>
</div>
<div class="control-group">
<div class="control-title"><span>02</span><strong>Blocked Runs</strong></div>
<button type="button" data-read="run_cancellation_blocked_list">读取首屏 Blocked Runs</button>
<p class="field-note">固定 16 项快照页。下一页必须在证据条目中再次显式点击。</p>
</div>
<div class="control-group">
<div class="control-title"><span>03</span><strong>单 Run 诊断</strong></div>
<input id="cancellation-run-id" type="text" maxlength="128" autocomplete="off" spellcheck="false" placeholder="run-id" />
<button type="button" data-read="run_cancellation_inspect">读取取消诊断</button>
<p class="field-note">该只读面没有 rearm、stop、retry 或其他 mutation 入口。</p>
</div>
</section>
<section id="workflow-panel" class="mode-panel" hidden>
<label class="field-label" for="package-name">Package</label>
<input id="package-name" type="text" maxlength="63" autocomplete="off" spellcheck="false" placeholder="ops-package" />
@@ -110,7 +130,7 @@
<aside class="trust-note">
<span>Authority boundary</span>
<p>Cluster credential 只由本机进程从私有文件读取。浏览器无法提交任意路径,也没有 start、cancel 或 diagnose 权限入口。脱敏导出只处理本页已读事实,不补读或上传。</p>
<p>Project credential 与可选 Run management authority 只由本机进程从彼此独立的私有文件读取。浏览器无法提交任意路径,也没有 start、stop、retry、rearm 或 diagnose 权限入口。脱敏导出只处理本页已读事实,不补读或上传。</p>
</aside>
</aside>
@@ -141,7 +161,7 @@
<footer>
<span>Loopback only · explicit reads · zero polling</span>
<span>QingLong 3.0 incubation / D-330</span>
<span>QingLong 3.0 incubation / D-369</span>
</footer>
</div>
</body>
@@ -24,25 +24,25 @@ const ASSETS = Object.freeze([
name: 'index.html',
field: 'html',
maximumBytes: 32 * 1024,
digest: '5d452c947a9f1266e4920cf48e7d5116b3f5ef8f9120f681124ed61f0217f5ff',
digest: 'a5a3d46a8493a27b53bd4a253ef38ebaf00d204a1454f4b47a1f1ceff668855f',
}),
Object.freeze({
name: 'app.css',
field: 'css',
maximumBytes: 64 * 1024,
digest: '5cf82b0a88920d106530603a7d407f852312138e5b7af5c422b3bccee785f144',
digest: 'ddfe85971df0b8acfaed8b4bb5f5bcdf679347106294987d928bbb82dc6610ec',
}),
Object.freeze({
name: 'evidence-bundle.js',
field: 'evidenceBundle',
maximumBytes: 32 * 1024,
digest: '6ecb14d2f59d872b889bb42c22bf0c0d2c150c90ea708fb1662d47f17f2e2095',
digest: '739ff786b651de23876fc5f4df5073e211085dfdfa1d2ecb79f53d5c871c6c1d',
}),
Object.freeze({
name: 'app.js',
field: 'javascript',
maximumBytes: 32 * 1024,
digest: 'f109c5b0491ba9a473e3129e35773edf38ac745b403e1f547f8252aa2932cdff',
digest: '7ed994d8f2f5b151a247c5dec1d2841d45d30ff05b14dd1f41c12c5582acf9e6',
}),
] as const);
@@ -1,5 +1,7 @@
#!/usr/bin/env node
import { randomUUID } from 'node:crypto';
import {
executeClusterCopilotCommand,
executeClusterProjectApiRead,
@@ -8,6 +10,20 @@ import {
validateClusterCopilotClientCredentialFile,
} from '../copilot-client/client';
import { readCanonicalFile } from '../management-support/managementClientConfiguration';
import { validateClusterAuthenticatedManagementClientConfiguration } from '../management-support/pluginPackageManagementClient';
import {
createRunCancellationBlockedListCommand,
projectRunCancellationBlockedList,
} from '../run-management/runCancellationBlockedList';
import {
createRunCancellationInspectionCommand,
projectRunCancellationInspection,
} from '../run-management/runCancellationInspection';
import {
createRunCancellationStatusCommand,
projectRunCancellationStatus,
} from '../run-management/runCancellationStatus';
import { executeClusterRunManagementCommand } from '../run-management/runManagementClient';
import { loadClusterCopilotConsoleAssets } from './assets';
import {
CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS,
@@ -25,6 +41,7 @@ const USAGE = [
' ql3-copilot-console --config /absolute/client.json --credential /absolute/credential --session /absolute/session [--port=0..65535]',
' ql3-copilot-console --check --config /absolute/client.json --credential /absolute/credential --session /absolute/session',
' ql3-copilot-console --container-published-loopback --port=1024..65535 --config /absolute/client.json --credential /absolute/credential --session /absolute/session [--check]',
' Optional Run reads: --run-management-config /absolute/run-client.json --run-management-assertion /absolute/assertion.jwt',
'',
'Native mode binds 127.0.0.1. Container mode requires host-loopback port publication.',
'The browser session key remains in a separate owner-private 0600 file.',
@@ -35,12 +52,21 @@ interface ClusterCopilotConsoleCliArguments {
readonly configFile: string;
readonly credentialFile: string;
readonly networkBoundary: 'host-loopback' | 'container-published-loopback';
readonly runManagementAssertionFile?: string;
readonly runManagementConfigFile?: string;
readonly sessionFile: string;
readonly port: number;
}
const SESSION_TOKEN = /^[A-Za-z0-9_-]{43}$/;
const MANAGEMENT_ASSERTION = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
const MAXIMUM_SESSION_BYTES = 128;
const MAXIMUM_MANAGEMENT_ASSERTION_BYTES = 16 * 1024;
const RUN_MANAGEMENT_OPERATIONS = new Set([
'run_cancellation_status',
'run_cancellation_blocked_list',
'run_cancellation_inspect',
]);
function usageFailure(): never {
process.stderr.write(USAGE + '\n');
@@ -77,6 +103,8 @@ export function parseClusterCopilotConsoleCliArguments(
let configFile: string | undefined;
let credentialFile: string | undefined;
let sessionFile: string | undefined;
let runManagementConfigFile: string | undefined;
let runManagementAssertionFile: string | undefined;
let port = 0;
let portSeen = false;
let containerPublishedLoopback = false;
@@ -116,6 +144,28 @@ export function parseClusterCopilotConsoleCliArguments(
index += session.consumed;
continue;
}
const runManagementConfig = argumentValue(
argv,
index,
'--run-management-config',
);
if (runManagementConfig) {
if (runManagementConfigFile !== undefined) return usageFailure();
runManagementConfigFile = runManagementConfig.value;
index += runManagementConfig.consumed;
continue;
}
const runManagementAssertion = argumentValue(
argv,
index,
'--run-management-assertion',
);
if (runManagementAssertion) {
if (runManagementAssertionFile !== undefined) return usageFailure();
runManagementAssertionFile = runManagementAssertion.value;
index += runManagementAssertion.consumed;
continue;
}
const portArgument = argumentValue(argv, index, '--port');
if (portArgument) {
if (portSeen || !/^(?:0|[1-9][0-9]{0,4})$/.test(portArgument.value)) {
@@ -138,6 +188,8 @@ export function parseClusterCopilotConsoleCliArguments(
configFile === undefined ||
credentialFile === undefined ||
sessionFile === undefined ||
(runManagementConfigFile === undefined) !==
(runManagementAssertionFile === undefined) ||
(containerPublishedLoopback && port === 0) ||
(!containerPublishedLoopback && check && port !== 0)
) {
@@ -150,11 +202,128 @@ export function parseClusterCopilotConsoleCliArguments(
networkBoundary: containerPublishedLoopback
? 'container-published-loopback'
: 'host-loopback',
...(runManagementConfigFile !== undefined &&
runManagementAssertionFile !== undefined
? { runManagementConfigFile, runManagementAssertionFile }
: {}),
sessionFile,
port,
});
}
function validateRunManagementAuthority(
parsed: Readonly<ClusterCopilotConsoleCliArguments>,
): boolean {
if (
parsed.runManagementConfigFile === undefined ||
parsed.runManagementAssertionFile === undefined
) {
return false;
}
validateClusterAuthenticatedManagementClientConfiguration(
parsed.runManagementConfigFile,
'run',
);
let bytes: Buffer | undefined;
try {
bytes = readCanonicalFile(
parsed.runManagementAssertionFile,
MAXIMUM_MANAGEMENT_ASSERTION_BYTES,
'private',
);
if (
bytes.some((byte) => byte > 0x7f) ||
!MANAGEMENT_ASSERTION.test(bytes.toString('ascii'))
) {
throw new Error('invalid Run management assertion');
}
return true;
} finally {
bytes?.fill(0);
}
}
function availableOperations(runManagementAuthority: boolean) {
return runManagementAuthority
? CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS
: CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS.filter(
(operation) => !RUN_MANAGEMENT_OPERATIONS.has(operation),
);
}
function commandIdSource(requestId: string): () => string {
let first = true;
return () => {
if (first) {
first = false;
return requestId;
}
return randomUUID();
};
}
async function executeConsoleRead(
request: Readonly<ClusterCopilotConsoleReadRequest>,
parsed: Readonly<ClusterCopilotConsoleCliArguments>,
) {
if (request.operation === 'inspect' || request.operation === 'output') {
return executeClusterCopilotCommand({
configFile: parsed.configFile,
credentialFile: parsed.credentialFile,
command: clusterCopilotConsoleClientCommand(request),
});
}
if (
request.operation === 'run_cancellation_status' ||
request.operation === 'run_cancellation_blocked_list' ||
request.operation === 'run_cancellation_inspect'
) {
if (
parsed.runManagementConfigFile === undefined ||
parsed.runManagementAssertionFile === undefined
) {
throw new Error('Run management authority is disabled');
}
const createUuid = commandIdSource(request.requestId);
const command =
request.operation === 'run_cancellation_status'
? createRunCancellationStatusCommand(request.projectId, createUuid)
: request.operation === 'run_cancellation_blocked_list'
? createRunCancellationBlockedListCommand(
request.projectId,
request.cursor ?? undefined,
createUuid,
)
: createRunCancellationInspectionCommand(
request.projectId,
request.runId,
createUuid,
);
const result = await executeClusterRunManagementCommand({
configFile: parsed.runManagementConfigFile,
assertionFile: parsed.runManagementAssertionFile,
command,
});
const projected =
request.operation === 'run_cancellation_status'
? projectRunCancellationStatus(result)
: request.operation === 'run_cancellation_blocked_list'
? projectRunCancellationBlockedList(result)
: projectRunCancellationInspection(result);
return Object.freeze({
schemaVersion: 1 as const,
requestId: result.requestId,
result: projected as unknown as Readonly<Record<string, unknown>>,
});
}
return executeClusterProjectApiRead({
configFile: parsed.configFile,
credentialFile: parsed.credentialFile,
path: clusterCopilotConsoleProjectReadPath(request),
requestId: request.requestId,
});
}
function readSessionDigest(sessionFile: string): Buffer {
let bytes: Buffer | undefined;
try {
@@ -183,6 +352,8 @@ async function main(): Promise<void> {
const assets = loadClusterCopilotConsoleAssets(__dirname);
validateClusterCopilotClientConfiguration(parsed.configFile);
validateClusterCopilotClientCredentialFile(parsed.credentialFile);
const runManagementAuthority = validateRunManagementAuthority(parsed);
const operations = availableOperations(runManagementAuthority);
const sessionDigest = readSessionDigest(parsed.sessionFile);
if (parsed.check) {
try {
@@ -199,7 +370,10 @@ async function main(): Promise<void> {
publishedHostAddress: '127.0.0.1',
browserCredential: 'forbidden',
clusterCredential: 'server_only',
operations: CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS,
runManagementAuthority: runManagementAuthority
? 'server_only'
: 'disabled',
operations,
mutation: false,
}) + '\n',
);
@@ -214,19 +388,7 @@ async function main(): Promise<void> {
assets,
executor: Object.freeze({
execute(request: Readonly<ClusterCopilotConsoleReadRequest>) {
if (request.operation === 'inspect' || request.operation === 'output') {
return executeClusterCopilotCommand({
configFile: parsed.configFile,
credentialFile: parsed.credentialFile,
command: clusterCopilotConsoleClientCommand(request),
});
}
return executeClusterProjectApiRead({
configFile: parsed.configFile,
credentialFile: parsed.credentialFile,
path: clusterCopilotConsoleProjectReadPath(request),
requestId: request.requestId,
});
return executeConsoleRead(request, parsed);
},
}),
networkBoundary: parsed.networkBoundary,
@@ -244,7 +406,10 @@ async function main(): Promise<void> {
publishedHostAddress: '127.0.0.1',
browserCredential: 'forbidden',
clusterCredential: 'server_only',
operations: CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS,
runManagementAuthority: runManagementAuthority
? 'server_only'
: 'disabled',
operations,
mutation: false,
}) + '\n',
);
@@ -11,6 +11,9 @@ export const CLUSTER_COPILOT_CONSOLE_READ_RESPONSE_SCHEMA =
export const CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS = Object.freeze([
'inspect',
'output',
'run_cancellation_status',
'run_cancellation_blocked_list',
'run_cancellation_inspect',
'run_list',
'run_read',
'run_event_list',
@@ -39,6 +42,10 @@ interface BaseReadRequest<
export type ClusterCopilotConsoleReadRequest =
| (BaseReadRequest<'inspect'> & Readonly<{ sourceRunId: string }>)
| (BaseReadRequest<'output'> & Readonly<{ sourceRunId: string }>)
| BaseReadRequest<'run_cancellation_status'>
| (BaseReadRequest<'run_cancellation_blocked_list'> &
Readonly<{ cursor: string | null }>)
| (BaseReadRequest<'run_cancellation_inspect'> & Readonly<{ runId: string }>)
| (BaseReadRequest<'run_list'> &
Readonly<{
afterCreatedAtMs: number | null;
@@ -98,6 +105,7 @@ export class InvalidClusterCopilotConsoleReadRequestError extends TypeError {
const IDENTITY = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const COPILOT_RUN_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,35}$/;
const RUN_CANCELLATION_CURSOR = /^v1\.[A-Za-z0-9_-]{1,512}$/;
const PACKAGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
const WORKFLOW_ID = /^[a-z][a-z0-9-]{0,62}$/;
const UUID_V4 =
@@ -205,6 +213,36 @@ export function normalizeClusterCopilotConsoleReadRequest(
sourceRunId: record.sourceRunId,
});
}
if (op === 'run_cancellation_status') {
exact(record, op, []);
return Object.freeze({
...common(record),
operation: op,
});
}
if (op === 'run_cancellation_blocked_list') {
exact(record, op, ['cursor']);
if (
record.cursor !== null &&
(typeof record.cursor !== 'string' ||
!RUN_CANCELLATION_CURSOR.test(record.cursor))
)
invalid();
return Object.freeze({
...common(record),
operation: op,
cursor: record.cursor as string | null,
});
}
if (op === 'run_cancellation_inspect') {
exact(record, op, ['runId']);
if (!identifier(record.runId)) invalid();
return Object.freeze({
...common(record),
operation: op,
runId: record.runId,
});
}
if (op === 'run_list') {
exact(record, op, ['afterCreatedAtMs', 'afterRunId', 'limit']);
if (
@@ -433,7 +471,13 @@ export function clusterCopilotConsoleProjectReadPath(
request: Readonly<ClusterCopilotConsoleReadRequest>,
): string {
const normalized = normalizeClusterCopilotConsoleReadRequest(request);
if (normalized.operation === 'inspect' || normalized.operation === 'output')
if (
normalized.operation === 'inspect' ||
normalized.operation === 'output' ||
normalized.operation === 'run_cancellation_status' ||
normalized.operation === 'run_cancellation_blocked_list' ||
normalized.operation === 'run_cancellation_inspect'
)
invalid();
const project = '/api/v3/projects/' + encoded(normalized.projectId);
if (normalized.operation === 'run_list') {
@@ -30,6 +30,9 @@ const LIMITS = Object.freeze({
const OPERATIONS = Object.freeze([
'inspect',
'output',
'run_cancellation_status',
'run_cancellation_blocked_list',
'run_cancellation_inspect',
'run_list',
'run_read',
'run_event_list',
@@ -48,6 +51,17 @@ const REQUEST_FIELDS: Readonly<Record<EvidenceOperation, readonly string[]>> =
Object.freeze({
inspect: Object.freeze(['projectId', 'requestId', 'sourceRunId']),
output: Object.freeze(['projectId', 'requestId', 'sourceRunId']),
run_cancellation_status: Object.freeze(['projectId', 'requestId']),
run_cancellation_blocked_list: Object.freeze([
'cursor',
'projectId',
'requestId',
]),
run_cancellation_inspect: Object.freeze([
'projectId',
'requestId',
'runId',
]),
run_list: Object.freeze([
'afterCreatedAtMs',
'afterRunId',
@@ -121,7 +135,9 @@ const IDENTIFIER_DOMAINS: Readonly<Record<string, string>> = Object.freeze({
afterStepRunId: 'step',
afterTaskId: 'task',
artifactId: 'artifact',
attemptId: 'attempt',
contentDigest: 'digest',
cursor: 'cursor',
diagnosisRunId: 'run',
executionId: 'execution',
id: 'identifier',
@@ -142,7 +158,10 @@ const IDENTIFIER_DOMAINS: Readonly<Record<string, string>> = Object.freeze({
});
const SAFE_CONTAINERS = new Set([
'attempts',
'blockingResults',
'counts',
'dispatch',
'dispatches',
'events',
'items',
'metadata',
@@ -151,6 +170,7 @@ const SAFE_CONTAINERS = new Set([
'run',
'runs',
'source',
'signals',
'step',
'steps',
'summary',
@@ -176,10 +196,16 @@ const SAFE_BOOLEANS = new Set([
'truncated',
]);
const SAFE_ENUM_KEYS = new Set([
'assessment',
'cancelReason',
'finishReason',
'kind',
'lastResult',
'operation',
'operatorAction',
'outcome',
'runStatus',
'severity',
'stage',
'status',
]);
@@ -187,11 +213,15 @@ const SAFE_ENUM_VALUES = new Set([
'accepted',
'active',
'admission',
'attention_required',
'available',
'blocked',
'cancelled',
'completed',
'completion',
'converging',
'critical',
'clear',
'dispatch',
'dispatching',
'disabled',
@@ -200,12 +230,18 @@ const SAFE_ENUM_VALUES = new Set([
'failed',
'finalization',
'installed',
'inspect',
'invalid',
'identity_mismatch',
'local',
'lost',
'missing',
'model',
'none',
'not_found',
'pending',
'pid_mismatch',
'policy',
'post_model',
'pre_model',
'prompt',
@@ -213,6 +249,8 @@ const SAFE_ENUM_VALUES = new Set([
'queued',
'ready',
'recovery',
'rearm',
'reconcile',
'rejected',
'remote',
'retained',
@@ -226,18 +264,25 @@ const SAFE_ENUM_VALUES = new Set([
'step',
'stop',
'succeeded',
'shutdown',
'system',
'task',
'terminal',
'timed_out',
'timeout',
'tool',
'trigger',
'unknown',
'unsupported',
'user',
'wait',
'warning',
'ok',
'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;
/^(?:schemaVersion|version|revision|sequence|attempt|priority|limit|offset|size|total|count|exitCode|pending|leased|retryWait|dispatched|blocked|due|expiredLease|identityMismatch|pidMismatch|unsupported|invalid|[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;
@@ -10,6 +10,11 @@ import {
ClusterCopilotClientRemoteError,
ClusterCopilotClientRequestError,
} from '../copilot-client/client';
import {
ClusterPluginPackageManagementClientConfigurationError,
ClusterPluginPackageManagementClientRemoteError,
ClusterPluginPackageManagementClientRequestError,
} from '../management-support/pluginPackageManagementClient';
import { type ClusterCopilotConsoleAssets } from './assets';
import {
CLUSTER_COPILOT_CONSOLE_READ_RESPONSE_SCHEMA,
@@ -186,6 +191,10 @@ const READ_ROUTES: Readonly<
> = Object.freeze({
'/api/v1/copilot/inspect': 'inspect',
'/api/v1/copilot/output': 'output',
'/api/v1/run-management/cancellation-status': 'run_cancellation_status',
'/api/v1/run-management/blocked-cancellations':
'run_cancellation_blocked_list',
'/api/v1/run-management/cancellation-inspect': 'run_cancellation_inspect',
'/api/v1/observe/run-list': 'run_list',
'/api/v1/observe/run': 'run_read',
'/api/v1/observe/run-events': 'run_event_list',
@@ -294,7 +303,9 @@ async function readJsonBody(request: IncomingMessage): Promise<unknown> {
function remoteFailure(
response: ServerResponse,
error: ClusterCopilotClientRemoteError,
error:
| ClusterCopilotClientRemoteError
| ClusterPluginPackageManagementClientRemoteError,
): void {
const statusCode =
error.statusCode === 404 ? 404 : error.statusCode === 429 ? 429 : 502;
@@ -457,11 +468,17 @@ export async function startClusterCopilotConsoleServer(
code: 'invalid_cluster_copilot_console_read_request',
}),
);
} else if (error instanceof ClusterCopilotClientRemoteError) {
} else if (
error instanceof ClusterCopilotClientRemoteError ||
error instanceof ClusterPluginPackageManagementClientRemoteError
) {
remoteFailure(response, error);
} else if (
error instanceof ClusterCopilotClientConfigurationError ||
error instanceof ClusterCopilotClientRequestError
error instanceof ClusterCopilotClientRequestError ||
error instanceof
ClusterPluginPackageManagementClientConfigurationError ||
error instanceof ClusterPluginPackageManagementClientRequestError
) {
sendJson(
response,
@@ -0,0 +1,73 @@
import { randomUUID } from 'node:crypto';
import type { ClusterRunManagementClientResult } from './runManagementClient';
import {
RUN_CANCELLATION_DISPATCH_INSPECT_REQUEST_SCHEMA,
normalizeClusterRunManagementCommand,
type ClusterRunManagementCancellationInspectCommand,
type ClusterRunManagementCancellationInspectTransportResult,
} from './runManagementTransport';
export const RUN_CANCELLATION_INSPECTION_SCHEMA =
'qinglong/run-cancellation-inspection@v1' as const;
type CancellationDiagnostic =
ClusterRunManagementCancellationInspectTransportResult['diagnostic'];
export type RunCancellationInspectionObservation = Readonly<
{
schemaVersion: 1;
schema: typeof RUN_CANCELLATION_INSPECTION_SCHEMA;
component: 'qinglong3-run-management-client';
event: 'cancellation_inspected';
requestId: string;
} & Omit<CancellationDiagnostic, 'schema'>
>;
export function createRunCancellationInspectionCommand(
projectId: string,
runId: string,
createUuid: () => string = randomUUID,
): Readonly<ClusterRunManagementCancellationInspectCommand> {
const requestId = createUuid();
const auditEventId = createUuid();
let failureAuditEventId = createUuid();
for (
let attempts = 0;
failureAuditEventId === auditEventId && attempts < 3;
attempts += 1
) {
failureAuditEventId = createUuid();
}
return normalizeClusterRunManagementCommand({
schemaVersion: 1,
operation: 'run.cancellation.inspect',
request: {
projectId,
runId,
requestId,
auditEventId,
failureAuditEventId,
body: { schema: RUN_CANCELLATION_DISPATCH_INSPECT_REQUEST_SCHEMA },
},
}) as Readonly<ClusterRunManagementCancellationInspectCommand>;
}
export function projectRunCancellationInspection(
result: Readonly<ClusterRunManagementClientResult>,
): Readonly<RunCancellationInspectionObservation> {
if (result.result.operation !== 'run.cancellation.inspect') {
throw new TypeError(
'Run cancellation inspection requires an inspect result',
);
}
const { schema: _schema, ...diagnostic } = result.result.diagnostic;
return Object.freeze({
schemaVersion: 1,
schema: RUN_CANCELLATION_INSPECTION_SCHEMA,
component: 'qinglong3-run-management-client',
event: 'cancellation_inspected',
requestId: result.requestId,
...diagnostic,
});
}
@@ -224,6 +224,30 @@ test('normalizes Copilot and fixed Project observation operations without arbitr
}),
{ code: 'QL3_CLUSTER_COPILOT_CONSOLE_READ_REQUEST_INVALID' },
);
assert.deepEqual(
normalizeClusterCopilotConsoleReadRequest({
schema: CLUSTER_COPILOT_CONSOLE_READ_REQUEST_SCHEMA,
operation: 'run_cancellation_status',
projectId: 'project-main',
requestId: 'console-read-status',
}),
{
schema: CLUSTER_COPILOT_CONSOLE_READ_REQUEST_SCHEMA,
operation: 'run_cancellation_status',
projectId: 'project-main',
requestId: 'console-read-status',
},
);
assert.throws(
() =>
clusterCopilotConsoleProjectReadPath({
schema: CLUSTER_COPILOT_CONSOLE_READ_REQUEST_SCHEMA,
operation: 'run_cancellation_status',
projectId: 'project-main',
requestId: 'console-read-status',
}),
{ code: 'QL3_CLUSTER_COPILOT_CONSOLE_READ_REQUEST_INVALID' },
);
assert.throws(
() =>
normalizeClusterCopilotConsoleReadRequest({
@@ -527,6 +551,72 @@ test('forwards one exact bounded Run list read and exposes no path field', async
assert.equal(Object.hasOwn(requests[0], 'url'), false);
});
test('routes only the three fixed Run management reads and validates their cursors', async (t) => {
const reads = [];
const { server, headers } = await fixture(async (read) => {
reads.push(read);
return {
schemaVersion: 1,
requestId: read.requestId,
result: { operation: read.operation },
};
});
t.after(() => server.close());
const cases = [
[
'/api/v1/run-management/cancellation-status',
{
schema: CLUSTER_COPILOT_CONSOLE_READ_REQUEST_SCHEMA,
operation: 'run_cancellation_status',
projectId: 'project-main',
requestId: 'console-status-1',
},
],
[
'/api/v1/run-management/blocked-cancellations',
{
schema: CLUSTER_COPILOT_CONSOLE_READ_REQUEST_SCHEMA,
operation: 'run_cancellation_blocked_list',
projectId: 'project-main',
requestId: 'console-blocked-1',
cursor: null,
},
],
[
'/api/v1/run-management/cancellation-inspect',
{
schema: CLUSTER_COPILOT_CONSOLE_READ_REQUEST_SCHEMA,
operation: 'run_cancellation_inspect',
projectId: 'project-main',
requestId: 'console-inspect-1',
runId: 'run-1',
},
],
];
for (const [path, body] of cases) {
const response = await request(server.origin, {
method: 'POST',
path,
headers,
body,
});
assert.equal(response.statusCode, 200);
}
assert.deepEqual(
reads,
cases.map(([, body]) => body),
);
const invalidCursor = await request(server.origin, {
method: 'POST',
path: '/api/v1/run-management/blocked-cancellations',
headers,
body: { ...cases[1][1], cursor: 'opaque-unversioned' },
});
assert.equal(invalidCursor.statusCode, 400);
assert.equal(reads.length, 3);
});
test('returns model text as JSON data only after an explicit output read', async (t) => {
const { server, headers } = await fixture(async (command) => {
assert.equal(command.operation, 'output');
@@ -34,6 +34,11 @@ const consoleOperations = [
'workflow_event_list',
'workflow_step_list',
];
const runManagementOperations = [
'run_cancellation_status',
'run_cancellation_blocked_list',
'run_cancellation_inspect',
];
function privateFile(directory, name, contents) {
const filePath = path.join(directory, name);
@@ -106,6 +111,41 @@ function get(origin) {
});
}
function post(origin, token, path, body) {
const url = new URL(origin);
const bytes = Buffer.from(JSON.stringify(body), 'utf8');
return new Promise((resolve, reject) => {
const request = httpRequest(
{
hostname: '127.0.0.1',
port: Number(url.port),
method: 'POST',
path,
agent: false,
headers: {
authorization: `QL3-Console ${token}`,
origin,
'content-type': 'application/json; charset=utf-8',
'content-length': String(bytes.byteLength),
},
},
(response) => {
const chunks = [];
response.on('data', (chunk) => chunks.push(chunk));
response.on('end', () => {
const text = Buffer.concat(chunks).toString('utf8');
resolve({
statusCode: response.statusCode,
body: JSON.parse(text),
});
});
},
);
request.once('error', reject);
request.end(bytes);
});
}
async function fixture(t) {
const directory = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-copilot-console-cli-')),
@@ -120,18 +160,61 @@ async function fixture(t) {
maxVersion: 'TLSv1.3',
},
(request, response) => {
requests.push({
method: request.method,
path: request.url,
authorization: request.headers.authorization,
tls: request.socket.getProtocol(),
const chunks = [];
request.on('data', (chunk) => chunks.push(chunk));
request.on('end', () => {
const command =
chunks.length === 0
? null
: JSON.parse(Buffer.concat(chunks).toString('utf8'));
requests.push({
method: request.method,
path: request.url,
authorization: request.headers.authorization,
tls: request.socket.getProtocol(),
command,
});
const body =
command?.operation === 'run.cancellation.summary'
? {
schemaVersion: 1,
requestId: command.request.requestId,
result: {
schemaVersion: 1,
operation: 'run.cancellation.summary',
summary: {
schema: 'qinglong/run-cancellation-dispatch-summary@v1',
projectId: command.request.projectId,
observedAtMs: 1_700_000_000_000,
assessment: 'attention_required',
operatorAction: 'inspect',
dispatches: {
total: 1,
pending: 0,
leased: 0,
retryWait: 0,
dispatched: 0,
blocked: 1,
},
signals: { due: 0, expiredLease: 0 },
blockingResults: {
identityMismatch: 1,
pidMismatch: 0,
unsupported: 0,
invalid: 0,
},
oldestBlockedAtMs: 1_699_999_999_000,
},
},
}
: { status: 'ready' };
const bytes = Buffer.from(JSON.stringify(body), 'utf8');
response.writeHead(200, {
'content-type': 'application/json; charset=utf-8',
'content-length': String(bytes.byteLength),
});
response.end(bytes);
});
const bytes = Buffer.from('{"status":"ready"}', 'utf8');
response.writeHead(200, {
'content-type': 'application/json; charset=utf-8',
'content-length': String(bytes.byteLength),
});
response.end(bytes);
},
);
await new Promise((resolve, reject) => {
@@ -160,14 +243,43 @@ async function fixture(t) {
requestTimeoutMs: 2_000,
}),
);
const clientCertificateFile = privateFile(
directory,
'run-client.crt',
fs.readFileSync(path.join(tlsFixture, 'client-cert.pem')),
);
const clientPrivateKeyFile = privateFile(
directory,
'run-client.key',
fs.readFileSync(path.join(tlsFixture, 'client-key.pem')),
);
const runManagementConfigFile = privateFile(
directory,
'run-client.json',
JSON.stringify({
schemaVersion: 1,
endpoint: `https://localhost:${
server.address().port
}/api/v3/runs/management`,
servername: 'localhost',
caFile,
clientCertificateFile,
clientPrivateKeyFile,
requestTimeoutMs: 2_000,
}),
);
const sessionToken = randomBytes(32).toString('base64url');
return {
requests,
configFile,
credentialFile: privateFile(directory, 'credential', credential),
sessionFile: privateFile(
sessionFile: privateFile(directory, 'session', sessionToken),
sessionToken,
runManagementConfigFile,
runManagementAssertionFile: privateFile(
directory,
'session',
randomBytes(32).toString('base64url'),
'run-assertion.jwt',
'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJvcGVyYXRvci0xIn0.c2lnbmF0dXJl',
),
};
}
@@ -178,6 +290,7 @@ test('CLI exposes deterministic help and a low-sensitive failure surface', async
' ql3-copilot-console --config /absolute/client.json --credential /absolute/credential --session /absolute/session [--port=0..65535]',
' ql3-copilot-console --check --config /absolute/client.json --credential /absolute/credential --session /absolute/session',
' ql3-copilot-console --container-published-loopback --port=1024..65535 --config /absolute/client.json --credential /absolute/credential --session /absolute/session [--check]',
' Optional Run reads: --run-management-config /absolute/run-client.json --run-management-assertion /absolute/assertion.jwt',
'',
'Native mode binds 127.0.0.1. Container mode requires host-loopback port publication.',
'The browser session key remains in a separate owner-private 0600 file.',
@@ -231,6 +344,7 @@ test('preflight proves private authority and unauthenticated TLS 1.3 readiness',
publishedHostAddress: '127.0.0.1',
browserCredential: 'forbidden',
clusterCredential: 'server_only',
runManagementAuthority: 'disabled',
operations: consoleOperations,
mutation: false,
});
@@ -240,10 +354,52 @@ test('preflight proves private authority and unauthenticated TLS 1.3 readiness',
path: '/readyz',
authorization: undefined,
tls: 'TLSv1.3',
command: null,
},
]);
});
test('preflight enables exactly three optional Run management reads only when both private files are explicit', async (t) => {
const value = await fixture(t);
const result = await runCli([
'--check',
'--config',
value.configFile,
'--credential',
value.credentialFile,
'--session',
value.sessionFile,
'--run-management-config',
value.runManagementConfigFile,
'--run-management-assertion',
value.runManagementAssertionFile,
]);
assert.equal(result.status, 0, result.stderr);
const fact = JSON.parse(result.stdout);
assert.equal(fact.runManagementAuthority, 'server_only');
assert.deepEqual(fact.operations, [
'inspect',
'output',
...runManagementOperations,
...consoleOperations.slice(2),
]);
assert.equal(fact.mutation, false);
assert.equal(value.requests.length, 1);
const incomplete = await runCli([
'--config',
value.configFile,
'--credential',
value.credentialFile,
'--session',
value.sessionFile,
'--run-management-config',
value.runManagementConfigFile,
]);
assert.equal(incomplete.status, 64);
assert.doesNotMatch(incomplete.stderr, /ql3-copilot-console-cli-/);
});
test('serve mode starts an ephemeral loopback origin and shuts down cleanly', async (t) => {
const value = await fixture(t);
const child = spawn(
@@ -269,6 +425,7 @@ test('serve mode starts an ephemeral loopback origin and shuts down cleanly', as
assert.match(started.origin, /^http:\/\/127\.0\.0\.1:[0-9]+$/);
assert.deepEqual(started.operations, consoleOperations);
assert.equal(started.mutation, false);
assert.equal(started.runManagementAuthority, 'disabled');
assert.equal(started.networkBoundary, 'host-loopback');
assert.equal(started.publishedHostAddress, '127.0.0.1');
const shell = await get(started.origin);
@@ -282,6 +439,66 @@ test('serve mode starts an ephemeral loopback origin and shuts down cleanly', as
assert.deepEqual(result, { status: 0, signal: null });
});
test('serve mode forwards one explicit status click through the optional mTLS Run authority', async (t) => {
const value = await fixture(t);
const child = spawn(
process.execPath,
[
cliPath,
'--config',
value.configFile,
'--credential',
value.credentialFile,
'--session',
value.sessionFile,
'--run-management-config',
value.runManagementConfigFile,
'--run-management-assertion',
value.runManagementAssertionFile,
'--port=0',
],
{ cwd: packageRoot, stdio: ['ignore', 'pipe', 'pipe'] },
);
t.after(() => {
if (child.exitCode === null && child.signalCode === null)
child.kill('SIGKILL');
});
const started = JSON.parse(await firstLine(child.stdout));
assert.equal(started.runManagementAuthority, 'server_only');
const response = await post(
started.origin,
value.sessionToken,
'/api/v1/run-management/cancellation-status',
{
schema: 'qinglong/cluster-copilot-console-read-request@v1',
operation: 'run_cancellation_status',
projectId: 'project-main',
requestId: 'console-status-1',
},
);
assert.equal(response.statusCode, 200);
assert.equal(
response.body.result.result.schema,
'qinglong/run-cancellation-status@v1',
);
assert.equal(response.body.result.result.assessment, 'attention_required');
assert.equal(response.body.result.result.operatorAction, 'inspect');
assert.equal(response.body.result.result.dispatches.blocked, 1);
const management = value.requests.find(
(request) => request.path === '/api/v3/runs/management',
);
assert.equal(management.method, 'POST');
assert.equal(management.command.operation, 'run.cancellation.summary');
assert.equal(management.command.request.requestId, 'console-status-1');
assert.match(management.authorization, /^Bearer [A-Za-z0-9_-]+\./);
child.kill('SIGTERM');
const exit = await new Promise((resolve, reject) => {
child.once('error', reject);
child.once('close', (status, signal) => resolve({ status, signal }));
});
assert.deepEqual(exit, { status: 0, signal: null });
});
test('container mode requires an explicit publish port before any authority read', async () => {
const result = await runCli([
'--container-published-loopback',
@@ -175,6 +175,64 @@ test('resets the undisclosed alias table for every bundle', async () => {
);
});
test('redacts optional Run management observations while preserving fixed availability facts', async () => {
const bundle = await createClusterConsoleEvidenceBundle(
[
{
operation: 'run_cancellation_status',
observedAtMs: 1_700_000_003_000,
request: {
schema: requestSchema,
operation: 'run_cancellation_status',
projectId: 'project-sensitive',
requestId: 'console-request-sensitive',
},
fact: {
schemaVersion: 1,
schema: 'qinglong/run-cancellation-status@v1',
component: 'qinglong3-run-management-client',
event: 'cancellation_status_observed',
requestId: 'console-request-sensitive',
projectId: 'project-sensitive',
observedAtMs: 1_700_000_003_000,
assessment: 'attention_required',
operatorAction: 'inspect',
severity: 'critical',
exitCode: 20,
dispatches: {
total: 1,
pending: 0,
leased: 0,
retryWait: 0,
dispatched: 0,
blocked: 1,
},
signals: { due: 0, expiredLease: 0 },
blockingResults: {
identityMismatch: 1,
pidMismatch: 0,
unsupported: 0,
invalid: 0,
},
oldestBlockedAtMs: 1_700_000_002_000,
},
},
],
1_700_000_004_000,
webcrypto,
);
const entry = bundle.entries[0];
assert.equal(entry.operation, 'run_cancellation_status');
assert.equal(entry.target.projectId, 'project-001');
assert.equal(entry.fact.projectId, 'project-001');
assert.equal(entry.fact.assessment, 'attention_required');
assert.equal(entry.fact.operatorAction, 'inspect');
assert.equal(entry.fact.dispatches.blocked, 1);
assert.equal(entry.fact.blockingResults.identityMismatch, 1);
assert.equal(entry.fact.component, undefined);
assert.doesNotMatch(JSON.stringify(bundle), /project-sensitive/);
});
test('fails closed on widened records, unsafe JSON and every capacity ceiling', async () => {
const error = { code: 'QL3_CLUSTER_CONSOLE_EVIDENCE_BUNDLE_INVALID' };
assert.throws(() => measureClusterConsoleEvidenceRecord(null), error);
@@ -123,6 +123,20 @@ 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_cancellation_status: {
projectId: 'p-management',
requestId: 'q-management-status',
},
run_cancellation_blocked_list: {
cursor: null,
projectId: 'p-management',
requestId: 'q-management-blocked',
},
run_cancellation_inspect: {
projectId: 'p-management',
requestId: 'q-management-inspect',
runId: 'r-management',
},
run_list: {
afterCreatedAtMs: null,
afterRunId: null,
@@ -218,7 +232,7 @@ test('cross-verifies every fixed Console read operation', async (t) => {
);
const result = verifyClusterConsoleEvidenceBundleFile(filePath);
assert.equal(result.status, 'verified');
assert.equal(result.bundle.entryCount, 13);
assert.equal(result.bundle.entryCount, 16);
});
test('CLI is secret-free on success, invalid input and usage errors', async (t) => {
@@ -0,0 +1,93 @@
'use strict';
const assert = require('node:assert/strict');
const test = require('node:test');
const {
createRunCancellationInspectionCommand,
projectRunCancellationInspection,
} = require('../dist/run-management/runCancellationInspection.js');
const uuids = [
'console-request-1',
'019f9400-0000-4000-8000-000000000001',
'019f9400-0000-4000-8000-000000000002',
];
test('creates one read-only cancellation inspection command with caller-bound request identity', () => {
let index = 0;
const command = createRunCancellationInspectionCommand(
'project-1',
'run-1',
() => uuids[index++],
);
assert.deepEqual(command, {
schemaVersion: 1,
operation: 'run.cancellation.inspect',
request: {
projectId: 'project-1',
runId: 'run-1',
requestId: 'console-request-1',
auditEventId: '019f9400-0000-4000-8000-000000000001',
failureAuditEventId: '019f9400-0000-4000-8000-000000000002',
body: {
schema: 'qinglong/run-cancellation-dispatch-inspect@v1',
},
},
});
});
test('projects a validated diagnostic without transport-only nesting', () => {
const observation = projectRunCancellationInspection({
schemaVersion: 1,
requestId: 'console-request-1',
result: {
schemaVersion: 1,
operation: 'run.cancellation.inspect',
diagnostic: {
schema: 'qinglong/run-cancellation-dispatch-diagnostic@v1',
projectId: 'project-1',
runId: 'run-1',
runStatus: 'running',
runVersion: 7,
eventSequence: 9,
cancelRequestedAtMs: 1_700_000_000_000,
cancelReason: 'user',
operatorAction: 'rearm',
dispatch: {
attemptId: 'attempt-1',
status: 'blocked',
version: 3,
dispatchCount: 2,
lastResult: 'identity_mismatch',
createdAtMs: 1_699_999_990_000,
updatedAtMs: 1_700_000_000_000,
},
},
},
});
assert.equal(observation.schema, 'qinglong/run-cancellation-inspection@v1');
assert.equal(observation.requestId, 'console-request-1');
assert.equal(observation.projectId, 'project-1');
assert.equal(observation.runId, 'run-1');
assert.equal(observation.operatorAction, 'rearm');
assert.equal(observation.dispatch.lastResult, 'identity_mismatch');
assert.equal(Object.isFrozen(observation), true);
assert.equal(Object.hasOwn(observation, 'diagnostic'), false);
});
test('rejects a non-inspection result before projection', () => {
assert.throws(
() =>
projectRunCancellationInspection({
schemaVersion: 1,
requestId: 'console-request-1',
result: {
schemaVersion: 1,
operation: 'run.cancellation.summary',
summary: {},
},
}),
/requires an inspect result/,
);
});