mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 10:32:40 +08:00
feat(ql3): expose copilot diagnosis read model
This commit is contained in:
@@ -29,7 +29,8 @@ const {
|
||||
function enabledEnvironment(overrides = {}) {
|
||||
return {
|
||||
QL3_CLUSTER_AI_ENABLED: 'true',
|
||||
QL3_CLUSTER_AI_PROVIDER_AUTHORITY_FILE: '/var/run/qinglong/ai/providers.json',
|
||||
QL3_CLUSTER_AI_PROVIDER_AUTHORITY_FILE:
|
||||
'/var/run/qinglong/ai/providers.json',
|
||||
QL3_CLUSTER_AI_SECRET_ROOT: '/var/run/qinglong/ai/provider-secrets',
|
||||
...overrides,
|
||||
};
|
||||
@@ -120,26 +121,30 @@ async function projectedFile(root, name, bytes) {
|
||||
test('Copilot composition is explicit, shares the Prompt gateway and injects one route capability', async () => {
|
||||
const secretRoot = await mkdtemp(join(tmpdir(), 'ql3-cluster-ai-secret-'));
|
||||
const configRoot = await mkdtemp(join(tmpdir(), 'ql3-copilot-config-'));
|
||||
const invocationRoot = await mkdtemp(join(tmpdir(), 'ql3-copilot-invocation-'));
|
||||
const invocationRoot = await mkdtemp(
|
||||
join(tmpdir(), 'ql3-copilot-invocation-'),
|
||||
);
|
||||
const resultRoot = await mkdtemp(join(tmpdir(), 'ql3-copilot-result-'));
|
||||
const outputRoot = await mkdtemp(join(tmpdir(), 'ql3-copilot-output-'));
|
||||
const key = Buffer.alloc(32, 0x55).toString('base64url');
|
||||
const config = Buffer.from(`${JSON.stringify({
|
||||
schema: 'qinglong/cluster-copilot-failure-diagnosis-config@v1',
|
||||
provider: 'provider-primary',
|
||||
model: 'model-diagnosis',
|
||||
modelBoundary: 'external',
|
||||
responseLanguage: 'zh-CN',
|
||||
maxOutputTokens: 512,
|
||||
executionTimeoutMs: 60_000,
|
||||
egressPolicy: {
|
||||
schema: 'qinglong/copilot-model-egress-policy@v1',
|
||||
revision: 'cluster-copilot-v1',
|
||||
potentiallySensitiveDataBoundaries: ['external'],
|
||||
maxInputBytes: 64 * 1024,
|
||||
maxOutputTokens: 1024,
|
||||
},
|
||||
})}\n`);
|
||||
const config = Buffer.from(
|
||||
`${JSON.stringify({
|
||||
schema: 'qinglong/cluster-copilot-failure-diagnosis-config@v1',
|
||||
provider: 'provider-primary',
|
||||
model: 'model-diagnosis',
|
||||
modelBoundary: 'external',
|
||||
responseLanguage: 'zh-CN',
|
||||
maxOutputTokens: 512,
|
||||
executionTimeoutMs: 60_000,
|
||||
egressPolicy: {
|
||||
schema: 'qinglong/copilot-model-egress-policy@v1',
|
||||
revision: 'cluster-copilot-v1',
|
||||
potentiallySensitiveDataBoundaries: ['external'],
|
||||
maxInputBytes: 64 * 1024,
|
||||
maxOutputTokens: 1024,
|
||||
},
|
||||
})}\n`,
|
||||
);
|
||||
const invocation = canonicalClusterToolInvocationKeyringManifest({
|
||||
schema: CLUSTER_TOOL_INVOCATION_KEYRING_MANIFEST_SCHEMA,
|
||||
activeKeyId: 'invocation-key-1',
|
||||
@@ -163,8 +168,10 @@ test('Copilot composition is explicit, shares the Prompt gateway and injects one
|
||||
const fakePool = { query() {}, connect() {} };
|
||||
const artifactStore = { put() {}, inspect() {}, readLogRange() {} };
|
||||
const copilot = Object.freeze({ execute() {} });
|
||||
const copilotRead = Object.freeze({ inspect() {}, readOutput() {} });
|
||||
let registeredSink;
|
||||
let created;
|
||||
let createdRead;
|
||||
let controlOptions;
|
||||
try {
|
||||
await Promise.all([
|
||||
@@ -205,22 +212,41 @@ test('Copilot composition is explicit, shares the Prompt gateway and injects one
|
||||
async recordWithAtomicSuccess() {},
|
||||
});
|
||||
return {
|
||||
status: 'active', profile: 'cluster', readiness: {}, capability: gateway,
|
||||
prompts: {}, promptCatalog: {}, promptExecutions: {},
|
||||
promptExecutionInspections: {}, async stop() { return 'stopped'; },
|
||||
status: 'active',
|
||||
profile: 'cluster',
|
||||
readiness: {},
|
||||
capability: gateway,
|
||||
prompts: {},
|
||||
promptCatalog: {},
|
||||
promptExecutions: {},
|
||||
promptExecutionInspections: {},
|
||||
async stop() {
|
||||
return 'stopped';
|
||||
},
|
||||
};
|
||||
},
|
||||
async createCopilot(options) {
|
||||
created = options;
|
||||
return copilot;
|
||||
},
|
||||
createCopilotRead(options) {
|
||||
createdRead = options;
|
||||
return copilotRead;
|
||||
},
|
||||
async startControl(options) {
|
||||
controlOptions = options;
|
||||
return {
|
||||
status: 'active', address: { host: '127.0.0.1', port: 5800 },
|
||||
evidence: {}, recovery: { safe: true, remaining: 0, failed: 0 },
|
||||
unavailable: new Promise(() => {}), availabilityStatus() { return 'ready'; },
|
||||
async stop() { return 'stopped'; },
|
||||
status: 'active',
|
||||
address: { host: '127.0.0.1', port: 5800 },
|
||||
evidence: {},
|
||||
recovery: { safe: true, remaining: 0, failed: 0 },
|
||||
unavailable: new Promise(() => {}),
|
||||
availabilityStatus() {
|
||||
return 'ready';
|
||||
},
|
||||
async stop() {
|
||||
return 'stopped';
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -229,10 +255,19 @@ test('Copilot composition is explicit, shares the Prompt gateway and injects one
|
||||
assert.equal(created.gateway, gateway);
|
||||
assert.equal(created.successfulCompletion, registeredSink);
|
||||
assert.equal(created.artifactStore, artifactStore);
|
||||
assert.equal(createdRead.pool, fakePool);
|
||||
assert.equal(typeof createdRead.prepared.outputKeys.resolve, 'function');
|
||||
assert.equal(controlOptions.copilotFailureDiagnosis.capability, copilot);
|
||||
assert.equal(
|
||||
controlOptions.copilotFailureDiagnosis.readCapability,
|
||||
copilotRead,
|
||||
);
|
||||
assert.equal(await application.stop(), 'stopped');
|
||||
} finally {
|
||||
config.fill(0); invocation.fill(0); result.fill(0); output.fill(0);
|
||||
config.fill(0);
|
||||
invocation.fill(0);
|
||||
result.fill(0);
|
||||
output.fill(0);
|
||||
await Promise.all([
|
||||
rm(secretRoot, { recursive: true, force: true }),
|
||||
rm(configRoot, { recursive: true, force: true }),
|
||||
@@ -362,7 +397,9 @@ test('output-enabled AI composition wires exact and request-keyed protected read
|
||||
promptExecutionInspections: { inspectAuthorized() {} },
|
||||
promptOutputs,
|
||||
promptExecutionOutputs,
|
||||
async stop() { return 'stopped'; },
|
||||
async stop() {
|
||||
return 'stopped';
|
||||
},
|
||||
};
|
||||
},
|
||||
async startControl(options) {
|
||||
@@ -373,8 +410,12 @@ test('output-enabled AI composition wires exact and request-keyed protected read
|
||||
evidence: {},
|
||||
recovery: { safe: true, remaining: 0, failed: 0 },
|
||||
unavailable: new Promise(() => {}),
|
||||
availabilityStatus() { return 'ready'; },
|
||||
async stop() { return 'stopped'; },
|
||||
availabilityStatus() {
|
||||
return 'ready';
|
||||
},
|
||||
async stop() {
|
||||
return 'stopped';
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
CLUSTER_COPILOT_FAILURE_DIAGNOSIS_INSPECTION_RESPONSE_SCHEMA,
|
||||
CLUSTER_COPILOT_FAILURE_DIAGNOSIS_OUTPUT_READ_RESPONSE_SCHEMA,
|
||||
CLUSTER_CONTROL_COPILOT_FAILURE_DIAGNOSIS_INSPECTION_ROUTE,
|
||||
CLUSTER_CONTROL_COPILOT_FAILURE_DIAGNOSIS_OUTPUT_READ_ROUTE,
|
||||
createClusterControlCopilotFailureDiagnosisInspectionRoute,
|
||||
createClusterControlCopilotFailureDiagnosisOutputReadRoute,
|
||||
} = require('@qinglong/cluster-control/copilot-read-routes');
|
||||
|
||||
function authorized(path, body = null) {
|
||||
return {
|
||||
request: {
|
||||
requestId: 'transport-request-1',
|
||||
method: 'GET',
|
||||
path,
|
||||
query: {},
|
||||
headers: {},
|
||||
signal: new AbortController().signal,
|
||||
body,
|
||||
},
|
||||
principal: {
|
||||
subject: { type: 'api_app', id: 'app-1' },
|
||||
authenticationId: 'credential-1',
|
||||
authenticatedAtMs: 1,
|
||||
expiresAtMs: 10_000,
|
||||
assurance: 'service',
|
||||
},
|
||||
operationId: 'copilot.failure_diagnosis.read',
|
||||
permission: 'run.read',
|
||||
projectId: 'project-1',
|
||||
policyFence: { projectVersion: 3, bindingVersion: 7 },
|
||||
};
|
||||
}
|
||||
|
||||
const parameters = {
|
||||
projectId: 'project-1',
|
||||
runId: 'source-run-1',
|
||||
requestId: 'diagnosis-request-1',
|
||||
};
|
||||
|
||||
function running(overrides = {}) {
|
||||
return {
|
||||
schema: 'qinglong/copilot-failure-diagnosis-inspection-result@v1',
|
||||
status: 'running',
|
||||
projectId: 'project-1',
|
||||
sourceRunId: 'source-run-1',
|
||||
requestId: 'diagnosis-request-1',
|
||||
diagnosisRunId: 'diagnosis-run-1',
|
||||
outcome: null,
|
||||
stage: null,
|
||||
reason: null,
|
||||
outputAvailable: false,
|
||||
admittedAtMs: 100,
|
||||
finalizedAtMs: null,
|
||||
usage: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('defines separate run.read inspection and artifact.read output routes', () => {
|
||||
assert.deepEqual(CLUSTER_CONTROL_COPILOT_FAILURE_DIAGNOSIS_INSPECTION_ROUTE, {
|
||||
method: 'GET',
|
||||
path: '/api/v3/projects/{projectId}/runs/{runId}/copilot/failure-diagnoses/{requestId}',
|
||||
operationId: 'copilot.failure_diagnosis.read',
|
||||
permission: 'run.read',
|
||||
projectParameter: 'projectId',
|
||||
});
|
||||
assert.deepEqual(
|
||||
CLUSTER_CONTROL_COPILOT_FAILURE_DIAGNOSIS_OUTPUT_READ_ROUTE,
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/api/v3/projects/{projectId}/runs/{runId}/copilot/failure-diagnoses/{requestId}/output',
|
||||
operationId: 'copilot.failure_diagnosis.output.read',
|
||||
permission: 'artifact.read',
|
||||
projectParameter: 'projectId',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('projects a request-keyed running inspection and passes only trusted target facts', async () => {
|
||||
let command;
|
||||
const route = createClusterControlCopilotFailureDiagnosisInspectionRoute({
|
||||
async inspect(value) {
|
||||
command = value;
|
||||
return running();
|
||||
},
|
||||
});
|
||||
const request = authorized(
|
||||
'/api/v3/projects/project-1/runs/source-run-1/copilot/failure-diagnoses/diagnosis-request-1',
|
||||
);
|
||||
const result = await route.handle(request, parameters);
|
||||
assert.deepEqual(command, {
|
||||
principal: request.principal,
|
||||
projectId: 'project-1',
|
||||
sourceRunId: 'source-run-1',
|
||||
requestId: 'diagnosis-request-1',
|
||||
});
|
||||
assert.deepEqual(result, {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
schema: CLUSTER_COPILOT_FAILURE_DIAGNOSIS_INSPECTION_RESPONSE_SCHEMA,
|
||||
status: 'running',
|
||||
projectId: 'project-1',
|
||||
sourceRunId: 'source-run-1',
|
||||
requestId: 'diagnosis-request-1',
|
||||
diagnosisRunId: 'diagnosis-run-1',
|
||||
outcome: null,
|
||||
stage: null,
|
||||
reason: null,
|
||||
outputAvailable: false,
|
||||
admittedAtMs: 100,
|
||||
finalizedAtMs: null,
|
||||
usage: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('projects terminal cancellation and settled Model usage without private fields', async () => {
|
||||
for (const [value, expected] of [
|
||||
[
|
||||
running({
|
||||
status: 'terminal',
|
||||
outcome: 'cancelled',
|
||||
stage: 'cancellation',
|
||||
reason: 'cancellation_requested',
|
||||
finalizedAtMs: 200,
|
||||
usage: {
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
totalTokens: 0,
|
||||
currency: 'USD',
|
||||
costMicros: 0,
|
||||
},
|
||||
}),
|
||||
'cancellation',
|
||||
],
|
||||
[
|
||||
running({
|
||||
status: 'terminal',
|
||||
outcome: 'succeeded',
|
||||
stage: 'model',
|
||||
reason: null,
|
||||
outputAvailable: true,
|
||||
finalizedAtMs: 200,
|
||||
usage: {
|
||||
inputTokens: 11,
|
||||
outputTokens: 7,
|
||||
totalTokens: 18,
|
||||
currency: 'USD',
|
||||
costMicros: 29,
|
||||
},
|
||||
}),
|
||||
'model',
|
||||
],
|
||||
]) {
|
||||
const route = createClusterControlCopilotFailureDiagnosisInspectionRoute({
|
||||
async inspect() {
|
||||
return value;
|
||||
},
|
||||
});
|
||||
const result = await route.handle(authorized('/read'), parameters);
|
||||
assert.equal(result.statusCode, 200);
|
||||
assert.equal(result.body.stage, expected);
|
||||
assert.equal(JSON.stringify(result).includes('provider'), false);
|
||||
assert.equal(JSON.stringify(result).includes('modelId'), false);
|
||||
}
|
||||
});
|
||||
|
||||
test('masks absent reads and fails closed on invalid input or widened results', async () => {
|
||||
let calls = 0;
|
||||
const route = createClusterControlCopilotFailureDiagnosisInspectionRoute({
|
||||
async inspect() {
|
||||
calls += 1;
|
||||
return {
|
||||
schema: 'qinglong/copilot-failure-diagnosis-inspection-result@v1',
|
||||
status: 'not_found',
|
||||
projectId: 'project-1',
|
||||
sourceRunId: 'source-run-1',
|
||||
requestId: 'diagnosis-request-1',
|
||||
};
|
||||
},
|
||||
});
|
||||
assert.equal(
|
||||
(await route.handle(authorized('/read'), parameters)).statusCode,
|
||||
404,
|
||||
);
|
||||
assert.equal(
|
||||
(await route.handle(authorized('/read', {}), parameters)).statusCode,
|
||||
400,
|
||||
);
|
||||
assert.equal(
|
||||
(
|
||||
await route.handle(authorized('/read'), {
|
||||
...parameters,
|
||||
requestId: '../private',
|
||||
})
|
||||
).statusCode,
|
||||
400,
|
||||
);
|
||||
assert.equal(calls, 1);
|
||||
|
||||
const widened = createClusterControlCopilotFailureDiagnosisInspectionRoute({
|
||||
async inspect() {
|
||||
return running({ privateModel: 'must not cross' });
|
||||
},
|
||||
});
|
||||
assert.equal(
|
||||
(await widened.handle(authorized('/read'), parameters)).statusCode,
|
||||
503,
|
||||
);
|
||||
});
|
||||
|
||||
test('returns only decrypted diagnosis content and low-sensitive Artifact metadata', async () => {
|
||||
let command;
|
||||
const route = createClusterControlCopilotFailureDiagnosisOutputReadRoute({
|
||||
async readOutput(value) {
|
||||
command = value;
|
||||
return {
|
||||
schema: 'qinglong/copilot-failure-diagnosis-output-read-result@v1',
|
||||
status: 'available',
|
||||
projectId: value.projectId,
|
||||
sourceRunId: value.sourceRunId,
|
||||
requestId: value.requestId,
|
||||
diagnosisRunId: 'diagnosis-run-1',
|
||||
reference: {
|
||||
artifactId: 'cdo:artifact-1',
|
||||
artifactDigest: 'a'.repeat(64),
|
||||
contentDigest: 'b'.repeat(64),
|
||||
outputBytes: Buffer.byteLength('diagnosis'),
|
||||
sealedAtMs: 200,
|
||||
},
|
||||
result: {
|
||||
text: 'diagnosis',
|
||||
finishReason: 'stop',
|
||||
usage: { inputTokens: 3, outputTokens: 2, totalTokens: 5 },
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
const request = authorized('/output');
|
||||
request.operationId = 'copilot.failure_diagnosis.output.read';
|
||||
request.permission = 'artifact.read';
|
||||
const result = await route.handle(request, parameters);
|
||||
assert.equal(result.statusCode, 200);
|
||||
assert.equal(
|
||||
result.body.schema,
|
||||
CLUSTER_COPILOT_FAILURE_DIAGNOSIS_OUTPUT_READ_RESPONSE_SCHEMA,
|
||||
);
|
||||
assert.equal(result.body.result.text, 'diagnosis');
|
||||
assert.equal('provider' in result.body.result, false);
|
||||
assert.equal('model' in result.body.result, false);
|
||||
assert.equal(command.principal, request.principal);
|
||||
});
|
||||
|
||||
test('masks absent output and maps dependency/cipher failures to one 503 code', async () => {
|
||||
const absent = createClusterControlCopilotFailureDiagnosisOutputReadRoute({
|
||||
async readOutput(value) {
|
||||
return {
|
||||
schema: 'qinglong/copilot-failure-diagnosis-output-read-result@v1',
|
||||
status: 'not_found',
|
||||
projectId: value.projectId,
|
||||
sourceRunId: value.sourceRunId,
|
||||
requestId: value.requestId,
|
||||
};
|
||||
},
|
||||
});
|
||||
assert.equal(
|
||||
(await absent.handle(authorized('/output'), parameters)).statusCode,
|
||||
404,
|
||||
);
|
||||
|
||||
const unavailable =
|
||||
createClusterControlCopilotFailureDiagnosisOutputReadRoute({
|
||||
async readOutput() {
|
||||
throw new Error('private key failure');
|
||||
},
|
||||
});
|
||||
assert.deepEqual(
|
||||
await unavailable.handle(authorized('/output'), parameters),
|
||||
{
|
||||
statusCode: 503,
|
||||
body: { code: 'copilot_failure_diagnosis_output_read_unavailable' },
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -730,6 +730,8 @@ test('optionally exposes Prompt execution behind shared admission and policy', a
|
||||
'prompt.execution.output.read',
|
||||
'prompt.output.read',
|
||||
'copilot.failure_diagnosis.execute',
|
||||
'copilot.failure_diagnosis.read',
|
||||
'copilot.failure_diagnosis.output.read',
|
||||
]);
|
||||
const response = await invoke(
|
||||
stack,
|
||||
@@ -783,9 +785,30 @@ test('optionally exposes Copilot diagnosis behind shared authentication, Policy
|
||||
terminalizationRequired: false,
|
||||
};
|
||||
},
|
||||
async inspect(value) {
|
||||
return {
|
||||
schema: 'qinglong/copilot-failure-diagnosis-inspection-result@v1',
|
||||
status: 'not_found',
|
||||
projectId: value.projectId,
|
||||
sourceRunId: value.sourceRunId,
|
||||
requestId: value.requestId,
|
||||
};
|
||||
},
|
||||
async readOutput(value) {
|
||||
return {
|
||||
schema: 'qinglong/copilot-failure-diagnosis-output-read-result@v1',
|
||||
status: 'not_found',
|
||||
projectId: value.projectId,
|
||||
sourceRunId: value.sourceRunId,
|
||||
requestId: value.requestId,
|
||||
};
|
||||
},
|
||||
};
|
||||
const stack = createProductionClusterControlApplicationStack(input, {
|
||||
copilotFailureDiagnosis: { capability },
|
||||
copilotFailureDiagnosis: {
|
||||
capability,
|
||||
readCapability: capability,
|
||||
},
|
||||
});
|
||||
const result = await invoke(
|
||||
stack,
|
||||
@@ -811,6 +834,29 @@ test('optionally exposes Copilot diagnosis behind shared authentication, Policy
|
||||
'audit:copilot.failure_diagnosis.execute:allowed',
|
||||
'diagnose:run-1',
|
||||
]);
|
||||
|
||||
const inspection = await invoke(
|
||||
stack,
|
||||
metadata(
|
||||
'/api/v3/projects/project-1/runs/run-1/copilot/failure-diagnoses/diagnosis-request-1',
|
||||
),
|
||||
);
|
||||
const output = await invoke(
|
||||
stack,
|
||||
metadata(
|
||||
'/api/v3/projects/project-1/runs/run-1/copilot/failure-diagnoses/diagnosis-request-1/output',
|
||||
),
|
||||
);
|
||||
assert.equal(inspection.statusCode, 404);
|
||||
assert.equal(output.statusCode, 404);
|
||||
assert.equal(
|
||||
events.includes('audit:copilot.failure_diagnosis.read:allowed'),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
events.includes('audit:copilot.failure_diagnosis.output.read:allowed'),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('keeps the Copilot route absent by default and never invokes it after Policy denial', async () => {
|
||||
@@ -830,6 +876,15 @@ test('keeps the Copilot route absent by default and never invokes it after Polic
|
||||
defaultStack.admission.prepare(request),
|
||||
(error) => error?.statusCode === 404 && error?.code === 'route_not_found',
|
||||
);
|
||||
for (const path of [
|
||||
'/api/v3/projects/project-1/runs/run-1/copilot/failure-diagnoses/diagnosis-request-1',
|
||||
'/api/v3/projects/project-1/runs/run-1/copilot/failure-diagnoses/diagnosis-request-1/output',
|
||||
]) {
|
||||
await assert.rejects(
|
||||
defaultStack.admission.prepare(metadata(path)),
|
||||
(error) => error?.statusCode === 404 && error?.code === 'route_not_found',
|
||||
);
|
||||
}
|
||||
|
||||
let calls = 0;
|
||||
const deniedFixture = fixture({
|
||||
|
||||
Reference in New Issue
Block a user