mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 19:29:13 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,706 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { spawn } = require('node:child_process');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
provisionLocalOwnerPepperKey,
|
||||
} = require('@qinglong/local-owner-console/pepper-custody');
|
||||
const { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
|
||||
const {
|
||||
openLocalSqliteRuntimeDatabase,
|
||||
} = require('@qinglong/local-sqlite/runtime');
|
||||
const {
|
||||
apiCredentialSecretDigest,
|
||||
formatApiCredentialToken,
|
||||
} = require('@qinglong/runtime-core/api-credential-token');
|
||||
const {
|
||||
approvalRequestDigest,
|
||||
createApprovalRequest,
|
||||
} = require('@qinglong/runtime-core/approved-action');
|
||||
const {
|
||||
createTaskDefinitionRecord,
|
||||
} = require('@qinglong/runtime-core/task-definition');
|
||||
const { createTriggerRecord } = require('@qinglong/runtime-core/trigger');
|
||||
|
||||
const NOW = Date.now();
|
||||
const PEPPER_KEY_ID = 'mcp-owner-v1';
|
||||
const CREDENTIAL_ID = 'mcp-owner';
|
||||
const PEPPER_BYTES = Buffer.alloc(32, 31);
|
||||
const PEPPER = PEPPER_BYTES.toString('base64url');
|
||||
const SECRET = Buffer.alloc(32, 32).toString('base64url');
|
||||
|
||||
function privateDirectory(parent, name) {
|
||||
const value = path.join(parent, name);
|
||||
fs.mkdirSync(value, { mode: 0o700 });
|
||||
return value;
|
||||
}
|
||||
|
||||
async function fixture(t) {
|
||||
const deploymentRoot = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'ql3-mcp-stdio-'),
|
||||
);
|
||||
fs.chmodSync(deploymentRoot, 0o700);
|
||||
t.after(() => fs.rmSync(deploymentRoot, { recursive: true, force: true }));
|
||||
const dataDirectory = privateDirectory(deploymentRoot, 'data');
|
||||
const operatorDirectory = privateDirectory(deploymentRoot, 'operator');
|
||||
const ownerPepperKeyringDirectory = privateDirectory(
|
||||
deploymentRoot,
|
||||
'owner-peppers',
|
||||
);
|
||||
const databasePath = path.join(dataDirectory, 'qinglong3.sqlite');
|
||||
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
|
||||
const runtime = await openLocalSqliteRuntimeDatabase({
|
||||
databasePath,
|
||||
profile: 'edge',
|
||||
});
|
||||
await runtime.runRepository.transaction(async (transaction) => {
|
||||
await transaction.insertRun({
|
||||
id: 'run-mcp-e2e',
|
||||
projectId: 'default',
|
||||
taskId: 'task-mcp',
|
||||
taskRevision: 'revision-1',
|
||||
taskName: 'MCP test',
|
||||
triggerType: 'manual',
|
||||
executionOrigin: 'manual',
|
||||
executionOwner: 'runtime',
|
||||
triggeredBy: 'user:mcp-owner',
|
||||
status: 'created',
|
||||
version: 0,
|
||||
eventSequence: 2,
|
||||
priority: 0,
|
||||
createdAtMs: NOW - 2_000,
|
||||
});
|
||||
await transaction.appendEvent({
|
||||
id: 'mcp-e2e-event-1',
|
||||
runId: 'run-mcp-e2e',
|
||||
sequence: 1,
|
||||
type: 'run.created',
|
||||
actorType: 'user',
|
||||
actorId: 'mcp-user',
|
||||
payload: Object.freeze({ secret: 'must-not-leak' }),
|
||||
createdAtMs: NOW - 1_999,
|
||||
});
|
||||
await transaction.appendEvent({
|
||||
id: 'mcp-e2e-event-2',
|
||||
runId: 'run-mcp-e2e',
|
||||
sequence: 2,
|
||||
type: 'run.queued',
|
||||
actorType: 'system',
|
||||
payload: Object.freeze({ private: 'must-not-leak' }),
|
||||
createdAtMs: NOW - 1_998,
|
||||
});
|
||||
});
|
||||
await runtime.close();
|
||||
|
||||
const pepperSummary = provisionLocalOwnerPepperKey({
|
||||
keyringDirectory: ownerPepperKeyringDirectory,
|
||||
pepperKeyId: PEPPER_KEY_ID,
|
||||
randomBytes: () => Buffer.from(PEPPER_BYTES),
|
||||
});
|
||||
const database = new DatabaseSync(databasePath);
|
||||
let taskContentDigest;
|
||||
try {
|
||||
database.exec('BEGIN IMMEDIATE');
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3LocalOwnerPepperKeys" (
|
||||
"pepper_key_id", "material_digest", "backup_digest", "state",
|
||||
"version", "register_mutation_id", "activate_mutation_id",
|
||||
"registered_at_ms", "activated_at_ms"
|
||||
) VALUES (?, ?, ?, 'active', 2, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
PEPPER_KEY_ID,
|
||||
pepperSummary.digest,
|
||||
'b'.repeat(64),
|
||||
'92000000-0000-4000-8000-000000000001',
|
||||
'92000000-0000-4000-8000-000000000002',
|
||||
NOW - 1_900,
|
||||
NOW - 1_800,
|
||||
);
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3LocalOwnerPepperActivations" (
|
||||
"generation", "mutation_id", "expected_generation",
|
||||
"previous_pepper_key_id", "active_pepper_key_id",
|
||||
"material_digest", "backup_digest", "activated_at_ms"
|
||||
) VALUES (1, ?, 0, NULL, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
'92000000-0000-4000-8000-000000000002',
|
||||
PEPPER_KEY_ID,
|
||||
pepperSummary.digest,
|
||||
'b'.repeat(64),
|
||||
NOW - 1_800,
|
||||
);
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3IdentitySubjects" (
|
||||
"subject_type", "subject_id", "status", "version",
|
||||
"created_at_ms", "updated_at_ms"
|
||||
) VALUES ('user', 'mcp-user', 'active', 1, ?, ?)`,
|
||||
)
|
||||
.run(NOW - 1_700, NOW - 1_700);
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3ApiCredentials" (
|
||||
"credential_id", "version", "state", "subject_type",
|
||||
"subject_id", "secret_digest", "created_at_ms",
|
||||
"not_before_at_ms", "expires_at_ms"
|
||||
) VALUES (?, 1, 'active', 'user', 'mcp-user', ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
CREDENTIAL_ID,
|
||||
apiCredentialSecretDigest(PEPPER, CREDENTIAL_ID, SECRET),
|
||||
NOW - 1_600,
|
||||
NOW - 1_600,
|
||||
NOW + 600_000,
|
||||
);
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3ApiCredentialPepperBindings" (
|
||||
"credential_id", "credential_version", "pepper_key_id"
|
||||
) VALUES (?, 1, ?)`,
|
||||
)
|
||||
.run(CREDENTIAL_ID, PEPPER_KEY_ID);
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3ProjectRoleBindings" (
|
||||
"project_id", "subject_type", "subject_id", "version", "state",
|
||||
"role", "mutation_id", "changed_by_type", "changed_by_id",
|
||||
"created_at_ms"
|
||||
) VALUES (
|
||||
'default', 'user', 'mcp-user', 1, 'active', 'owner',
|
||||
'mcp-owner-binding', 'user', 'mcp-user', ?
|
||||
)`,
|
||||
)
|
||||
.run(NOW - 1_500);
|
||||
const taskDefinition = createTaskDefinitionRecord(
|
||||
{
|
||||
projectId: 'default',
|
||||
taskId: 'task-mcp',
|
||||
expectedRevision: null,
|
||||
mutationId: '92000000-0000-4000-8000-000000000003',
|
||||
name: 'MCP Task',
|
||||
kind: 'script',
|
||||
spec: {
|
||||
schema: 'qinglong/script@v1',
|
||||
config: { command: 'private command' },
|
||||
},
|
||||
labels: { private: 'label' },
|
||||
enabled: true,
|
||||
occurredAtMs: NOW - 1_400,
|
||||
},
|
||||
NOW - 1_400,
|
||||
);
|
||||
taskContentDigest = taskDefinition.contentDigest;
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3TaskDefinitions" (
|
||||
"project_id", "task_id", "current_revision",
|
||||
"created_at_ms", "updated_at_ms"
|
||||
) VALUES (?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
taskDefinition.projectId,
|
||||
taskDefinition.taskId,
|
||||
taskDefinition.revision,
|
||||
taskDefinition.createdAtMs,
|
||||
taskDefinition.updatedAtMs,
|
||||
);
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3TaskDefinitionRevisions" (
|
||||
"project_id", "task_id", "revision", "mutation_id",
|
||||
"name", "description", "kind", "spec_json", "labels_json",
|
||||
"enabled", "content_digest", "created_at_ms"
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
taskDefinition.projectId,
|
||||
taskDefinition.taskId,
|
||||
taskDefinition.revision,
|
||||
taskDefinition.mutationId,
|
||||
taskDefinition.name,
|
||||
null,
|
||||
taskDefinition.kind,
|
||||
JSON.stringify(taskDefinition.spec),
|
||||
JSON.stringify(taskDefinition.labels),
|
||||
1,
|
||||
taskDefinition.contentDigest,
|
||||
taskDefinition.updatedAtMs,
|
||||
);
|
||||
const trigger = createTriggerRecord(
|
||||
{
|
||||
projectId: 'default',
|
||||
triggerId: 'trigger-mcp',
|
||||
expectedRevision: null,
|
||||
mutationId: '92000000-0000-4000-8000-000000000004',
|
||||
taskId: taskDefinition.taskId,
|
||||
taskRevision: taskDefinition.revision,
|
||||
taskContentDigest: taskDefinition.contentDigest,
|
||||
spec: {
|
||||
schema: 'qinglong/cron@v1',
|
||||
config: {
|
||||
expression: '*/5 * * * *',
|
||||
timezone: 'Etc/UTC',
|
||||
misfirePolicy: 'skip',
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
occurredAtMs: NOW - 1_300,
|
||||
},
|
||||
NOW - 1_300,
|
||||
);
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3Triggers" (
|
||||
"project_id", "trigger_id", "task_id", "current_revision",
|
||||
"created_at_ms", "updated_at_ms"
|
||||
) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
trigger.projectId,
|
||||
trigger.triggerId,
|
||||
trigger.taskId,
|
||||
trigger.revision,
|
||||
trigger.createdAtMs,
|
||||
trigger.updatedAtMs,
|
||||
);
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3TriggerRevisions" (
|
||||
"project_id", "trigger_id", "revision", "mutation_id",
|
||||
"task_id", "task_revision", "task_content_digest",
|
||||
"spec_json", "enabled", "content_digest", "created_at_ms"
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
trigger.projectId,
|
||||
trigger.triggerId,
|
||||
trigger.revision,
|
||||
trigger.mutationId,
|
||||
trigger.taskId,
|
||||
trigger.taskRevision,
|
||||
trigger.taskContentDigest,
|
||||
JSON.stringify(trigger.spec),
|
||||
1,
|
||||
trigger.contentDigest,
|
||||
trigger.updatedAtMs,
|
||||
);
|
||||
const approval = createApprovalRequest({
|
||||
id: 'approval-mcp',
|
||||
projectId: 'default',
|
||||
action: {
|
||||
permission: 'run.start',
|
||||
actionType: 'tool.invoke',
|
||||
actionRef: 'private-action-ref',
|
||||
actionDigest: 'a'.repeat(64),
|
||||
previewDigest: 'b'.repeat(64),
|
||||
},
|
||||
risk: 'medium',
|
||||
decisionMode: 'human_confirmation',
|
||||
requestedBy: { type: 'agent', id: 'private-agent' },
|
||||
requestedAtMs: NOW - 1_200,
|
||||
expiresAtMs: NOW + 60_000,
|
||||
requestFence: { projectVersion: 1, bindingVersion: 1 },
|
||||
});
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3ApprovalRequests" (
|
||||
"request_id", "project_id", "version", "state", "action_type",
|
||||
"action_ref", "action_digest", "preview_digest",
|
||||
"requested_by_type", "requested_by_id", "decision_id",
|
||||
"consumption_id", "dispatch_id", "expires_at_ms", "request_json",
|
||||
"request_digest", "updated_at_ms"
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
approval.id,
|
||||
approval.projectId,
|
||||
approval.version,
|
||||
approval.state,
|
||||
approval.action.actionType,
|
||||
approval.action.actionRef,
|
||||
approval.action.actionDigest,
|
||||
approval.action.previewDigest,
|
||||
approval.requestedBy.type,
|
||||
approval.requestedBy.id,
|
||||
approval.decisionId,
|
||||
approval.consumptionId,
|
||||
approval.dispatchId,
|
||||
approval.expiresAtMs,
|
||||
JSON.stringify(approval),
|
||||
approvalRequestDigest(approval),
|
||||
approval.requestedAtMs,
|
||||
);
|
||||
database.exec('COMMIT');
|
||||
} catch (error) {
|
||||
if (database.isTransaction) database.exec('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
fs.chmodSync(databasePath, 0o600);
|
||||
|
||||
const credentialFilePath = path.join(operatorDirectory, 'credential.json');
|
||||
fs.writeFileSync(
|
||||
credentialFilePath,
|
||||
`${JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
kind: 'qinglong3-local-identity-credential-presentation',
|
||||
token: formatApiCredentialToken(CREDENTIAL_ID, SECRET),
|
||||
})}\n`,
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
const configFilePath = path.join(deploymentRoot, 'mcp.json');
|
||||
fs.writeFileSync(
|
||||
configFilePath,
|
||||
`${JSON.stringify({
|
||||
schema: 'qinglong/local-mcp-server@v1',
|
||||
profile: 'edge',
|
||||
projectId: 'default',
|
||||
deploymentRoot,
|
||||
databasePath,
|
||||
ownerPepperKeyringDirectory,
|
||||
credentialFilePath,
|
||||
busyTimeoutMs: 500,
|
||||
})}\n`,
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
return {
|
||||
configFilePath,
|
||||
databasePath,
|
||||
taskContentDigest,
|
||||
};
|
||||
}
|
||||
|
||||
test('serves the authenticated Run Tool over the real stdio protocol and persists allowed audit', async (t) => {
|
||||
const value = await fixture(t);
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
[
|
||||
path.resolve(__dirname, '../dist/cli.js'),
|
||||
'--config',
|
||||
value.configFilePath,
|
||||
],
|
||||
{ stdio: ['pipe', 'pipe', 'pipe'] },
|
||||
);
|
||||
t.after(() => {
|
||||
if (child.exitCode === null) child.kill('SIGKILL');
|
||||
});
|
||||
let stderr = '';
|
||||
child.stderr.setEncoding('utf8');
|
||||
child.stderr.on('data', (chunk) => {
|
||||
stderr += chunk;
|
||||
});
|
||||
child.stdout.setEncoding('utf8');
|
||||
let buffered = '';
|
||||
const pending = new Map();
|
||||
child.stdout.on('data', (chunk) => {
|
||||
buffered += chunk;
|
||||
for (;;) {
|
||||
const newline = buffered.indexOf('\n');
|
||||
if (newline < 0) break;
|
||||
const line = buffered.slice(0, newline);
|
||||
buffered = buffered.slice(newline + 1);
|
||||
if (!line) continue;
|
||||
const message = JSON.parse(line);
|
||||
const resolve = pending.get(message.id);
|
||||
if (resolve) {
|
||||
pending.delete(message.id);
|
||||
resolve(message);
|
||||
}
|
||||
}
|
||||
});
|
||||
let id = 0;
|
||||
const request = (method, params) => {
|
||||
id += 1;
|
||||
child.stdin.write(
|
||||
`${JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
method,
|
||||
...(params === undefined ? {} : { params }),
|
||||
})}\n`,
|
||||
);
|
||||
return new Promise((resolve, reject) => {
|
||||
pending.set(id, resolve);
|
||||
const timer = setTimeout(
|
||||
() => reject(new Error(`timeout: ${method}`)),
|
||||
5_000,
|
||||
);
|
||||
timer.unref();
|
||||
});
|
||||
};
|
||||
|
||||
const initialized = await request('initialize', {
|
||||
protocolVersion: '2025-11-25',
|
||||
capabilities: {},
|
||||
clientInfo: { name: 'ql3-e2e', version: '1.0.0' },
|
||||
});
|
||||
assert.equal(initialized.result.protocolVersion, '2025-11-25');
|
||||
child.stdin.write(
|
||||
`${JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
method: 'notifications/initialized',
|
||||
})}\n`,
|
||||
);
|
||||
const listed = await request('tools/list', {});
|
||||
assert.deepEqual(
|
||||
listed.result.tools.map((tool) => tool.name),
|
||||
[
|
||||
'qinglong.run.list',
|
||||
'qinglong.run.get',
|
||||
'qinglong.run.events.list',
|
||||
'qinglong.run.steps.list',
|
||||
'qinglong.task.get',
|
||||
'qinglong.task.list',
|
||||
'qinglong.trigger.list',
|
||||
'qinglong.approval.list',
|
||||
'qinglong.approval.get',
|
||||
],
|
||||
);
|
||||
const tasks = await request('tools/call', {
|
||||
name: 'qinglong.task.list',
|
||||
arguments: { limit: 1 },
|
||||
});
|
||||
assert.equal(tasks.result.isError, undefined, JSON.stringify(tasks));
|
||||
assert.deepEqual(tasks.result.structuredContent, {
|
||||
tasks: [
|
||||
{
|
||||
taskId: 'task-mcp',
|
||||
revision: 1,
|
||||
name: 'MCP Task',
|
||||
kind: 'script',
|
||||
specSchema: 'qinglong/script@v1',
|
||||
enabled: true,
|
||||
updatedAtMs: NOW - 1_400,
|
||||
},
|
||||
],
|
||||
hasMore: false,
|
||||
});
|
||||
assert.equal(JSON.stringify(tasks).includes('private'), false);
|
||||
const currentTask = await request('tools/call', {
|
||||
name: 'qinglong.task.get',
|
||||
arguments: { taskId: 'task-mcp' },
|
||||
});
|
||||
assert.equal(
|
||||
currentTask.result.isError,
|
||||
undefined,
|
||||
JSON.stringify(currentTask),
|
||||
);
|
||||
assert.deepEqual(currentTask.result.structuredContent, {
|
||||
found: true,
|
||||
taskId: 'task-mcp',
|
||||
revision: 1,
|
||||
name: 'MCP Task',
|
||||
kind: 'script',
|
||||
specSchema: 'qinglong/script@v1',
|
||||
enabled: true,
|
||||
contentDigest: value.taskContentDigest,
|
||||
createdAtMs: NOW - 1_400,
|
||||
updatedAtMs: NOW - 1_400,
|
||||
});
|
||||
assert.equal(JSON.stringify(currentTask).includes('private'), false);
|
||||
const triggers = await request('tools/call', {
|
||||
name: 'qinglong.trigger.list',
|
||||
arguments: { limit: 1 },
|
||||
});
|
||||
assert.equal(triggers.result.isError, undefined, JSON.stringify(triggers));
|
||||
assert.deepEqual(triggers.result.structuredContent, {
|
||||
triggers: [
|
||||
{
|
||||
triggerId: 'trigger-mcp',
|
||||
revision: 1,
|
||||
taskId: 'task-mcp',
|
||||
taskRevision: 1,
|
||||
specSchema: 'qinglong/cron@v1',
|
||||
enabled: true,
|
||||
updatedAtMs: NOW - 1_300,
|
||||
},
|
||||
],
|
||||
hasMore: false,
|
||||
});
|
||||
assert.equal(JSON.stringify(triggers).includes('*/5'), false);
|
||||
assert.equal(JSON.stringify(triggers).includes('Etc/UTC'), false);
|
||||
const approvals = await request('tools/call', {
|
||||
name: 'qinglong.approval.list',
|
||||
arguments: { limit: 1 },
|
||||
});
|
||||
assert.equal(approvals.result.isError, undefined, JSON.stringify(approvals));
|
||||
assert.deepEqual(approvals.result.structuredContent, {
|
||||
approvals: [
|
||||
{
|
||||
requestId: 'approval-mcp',
|
||||
version: 1,
|
||||
state: 'pending',
|
||||
risk: 'medium',
|
||||
decisionMode: 'human_confirmation',
|
||||
permission: 'run.start',
|
||||
actionType: 'tool.invoke',
|
||||
requestedByType: 'agent',
|
||||
requestedAtMs: NOW - 1_200,
|
||||
expiresAtMs: NOW + 60_000,
|
||||
updatedAtMs: NOW - 1_200,
|
||||
},
|
||||
],
|
||||
hasMore: false,
|
||||
});
|
||||
assert.equal(JSON.stringify(approvals).includes('private'), false);
|
||||
const approvalDetail = await request('tools/call', {
|
||||
name: 'qinglong.approval.get',
|
||||
arguments: { requestId: 'approval-mcp' },
|
||||
});
|
||||
assert.equal(
|
||||
approvalDetail.result.isError,
|
||||
undefined,
|
||||
JSON.stringify(approvalDetail),
|
||||
);
|
||||
assert.deepEqual(approvalDetail.result.structuredContent, {
|
||||
found: true,
|
||||
approval: {
|
||||
requestId: 'approval-mcp',
|
||||
version: 1,
|
||||
state: 'pending',
|
||||
risk: 'medium',
|
||||
decisionMode: 'human_confirmation',
|
||||
permission: 'run.start',
|
||||
actionType: 'tool.invoke',
|
||||
requestedByType: 'agent',
|
||||
requestedAtMs: NOW - 1_200,
|
||||
expiresAtMs: NOW + 60_000,
|
||||
previewAvailable: false,
|
||||
},
|
||||
});
|
||||
assert.equal(JSON.stringify(approvalDetail).includes('private'), false);
|
||||
const discovered = await request('tools/call', {
|
||||
name: 'qinglong.run.list',
|
||||
arguments: { limit: 1 },
|
||||
});
|
||||
assert.equal(
|
||||
discovered.result.isError,
|
||||
undefined,
|
||||
JSON.stringify(discovered),
|
||||
);
|
||||
assert.deepEqual(discovered.result.structuredContent, {
|
||||
runs: [
|
||||
{
|
||||
id: 'run-mcp-e2e',
|
||||
taskId: 'task-mcp',
|
||||
taskRevision: 'revision-1',
|
||||
status: 'created',
|
||||
version: 0,
|
||||
eventSequence: 2,
|
||||
priority: 0,
|
||||
executionOrigin: 'manual',
|
||||
executionOwner: 'runtime',
|
||||
createdAtMs: NOW - 2_000,
|
||||
},
|
||||
],
|
||||
hasMore: false,
|
||||
});
|
||||
const called = await request('tools/call', {
|
||||
name: 'qinglong.run.get',
|
||||
arguments: { runId: 'run-mcp-e2e' },
|
||||
});
|
||||
assert.equal(called.result.isError, undefined, JSON.stringify(called));
|
||||
assert.deepEqual(called.result.structuredContent, {
|
||||
found: true,
|
||||
id: 'run-mcp-e2e',
|
||||
taskId: 'task-mcp',
|
||||
taskRevision: 'revision-1',
|
||||
status: 'created',
|
||||
version: 0,
|
||||
eventSequence: 2,
|
||||
priority: 0,
|
||||
executionOrigin: 'manual',
|
||||
executionOwner: 'runtime',
|
||||
createdAtMs: NOW - 2_000,
|
||||
});
|
||||
const events = await request('tools/call', {
|
||||
name: 'qinglong.run.events.list',
|
||||
arguments: { runId: 'run-mcp-e2e', limit: 1 },
|
||||
});
|
||||
assert.equal(events.result.isError, undefined, JSON.stringify(events));
|
||||
assert.deepEqual(events.result.structuredContent, {
|
||||
found: true,
|
||||
events: [
|
||||
{
|
||||
sequence: 1,
|
||||
type: 'run.created',
|
||||
actorType: 'user',
|
||||
createdAtMs: NOW - 1_999,
|
||||
},
|
||||
],
|
||||
hasMore: true,
|
||||
nextAfterSequence: 1,
|
||||
});
|
||||
assert.equal(JSON.stringify(events).includes('must-not-leak'), false);
|
||||
child.stdin.end();
|
||||
const exitCode = await new Promise((resolve) => child.once('exit', resolve));
|
||||
assert.equal(exitCode, 0, stderr);
|
||||
assert.equal(stderr, '');
|
||||
|
||||
const database = new DatabaseSync(value.databasePath, { readOnly: true });
|
||||
try {
|
||||
const audit = database
|
||||
.prepare(
|
||||
`SELECT operation_id AS "operationId", outcome, subject_id AS "subjectId"
|
||||
FROM "QingLong3SecurityAuditEvents"
|
||||
WHERE operation_id = 'mcp.tool.call'`,
|
||||
)
|
||||
.all();
|
||||
assert.deepEqual(
|
||||
audit.map((row) => ({ ...row })),
|
||||
[
|
||||
{
|
||||
operationId: 'mcp.tool.call',
|
||||
outcome: 'allowed',
|
||||
subjectId: 'mcp-user',
|
||||
},
|
||||
{
|
||||
operationId: 'mcp.tool.call',
|
||||
outcome: 'allowed',
|
||||
subjectId: 'mcp-user',
|
||||
},
|
||||
{
|
||||
operationId: 'mcp.tool.call',
|
||||
outcome: 'allowed',
|
||||
subjectId: 'mcp-user',
|
||||
},
|
||||
{
|
||||
operationId: 'mcp.tool.call',
|
||||
outcome: 'allowed',
|
||||
subjectId: 'mcp-user',
|
||||
},
|
||||
{
|
||||
operationId: 'mcp.tool.call',
|
||||
outcome: 'allowed',
|
||||
subjectId: 'mcp-user',
|
||||
},
|
||||
{
|
||||
operationId: 'mcp.tool.call',
|
||||
outcome: 'allowed',
|
||||
subjectId: 'mcp-user',
|
||||
},
|
||||
{
|
||||
operationId: 'mcp.tool.call',
|
||||
outcome: 'allowed',
|
||||
subjectId: 'mcp-user',
|
||||
},
|
||||
{
|
||||
operationId: 'mcp.tool.call',
|
||||
outcome: 'allowed',
|
||||
subjectId: 'mcp-user',
|
||||
},
|
||||
],
|
||||
);
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user