feat(ql3): establish 3.0 incubation baseline

This commit is contained in:
whyour
2026-08-12 00:25:26 +08:00
parent 4bf92dcfeb
commit c699c32461
2817 changed files with 779642 additions and 653 deletions
@@ -0,0 +1,186 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
createApprovalRequest,
} = require('@qinglong/runtime-core/approved-action');
const {
createToolInvocationPreviewArtifact,
} = require('@qinglong/runtime-core/tool-invocation-artifact');
const {
BUILTIN_APPROVAL_GET_TOOL,
BUILTIN_APPROVAL_GET_TOOL_DEFINITION,
BuiltInApprovalGetToolUnavailableError,
InvalidBuiltInApprovalGetToolError,
executeBuiltInApprovalGetTool,
} = require('../dist/tool-projection/approvalGet.js');
function detail() {
const previewArtifact = createToolInvocationPreviewArtifact({
artifactId: 'preview-1',
projectId: 'default',
actionRef: 'tool:approval-1',
actionDigest: 'a'.repeat(64),
redactionContractDigest: 'c'.repeat(64),
sealedAtMs: 1_000,
preview: {
title: 'Run task',
summary: 'Runs the selected task once.',
fields: [
{ kind: 'identifier', label: 'Task', value: 'task-1' },
{ kind: 'redacted', label: 'Token', value: null },
],
warnings: ['external_effect'],
},
});
const request = createApprovalRequest({
id: 'approval-1',
projectId: 'default',
action: {
permission: 'run.start',
actionType: 'tool.invoke',
actionRef: previewArtifact.actionRef,
actionDigest: previewArtifact.actionDigest,
previewDigest: previewArtifact.previewDigest,
},
risk: 'medium',
decisionMode: 'human_confirmation',
requestedBy: { type: 'agent', id: 'private-agent' },
requestedAtMs: 1_000,
expiresAtMs: 61_000,
requestFence: { projectVersion: 1, bindingVersion: 2 },
});
return Object.freeze({ request, preview: previewArtifact.preview });
}
test('defines an exact dual-authorized read-only Approval detail Tool', () => {
assert.deepEqual(BUILTIN_APPROVAL_GET_TOOL, {
name: 'qinglong.approval.get',
version: '1.0.0',
});
assert.equal(BUILTIN_APPROVAL_GET_TOOL_DEFINITION.effect, 'read');
assert.equal(BUILTIN_APPROVAL_GET_TOOL_DEFINITION.risk, 'low');
assert.deepEqual(BUILTIN_APPROVAL_GET_TOOL_DEFINITION.requiredPermissions, [
'approval.read',
'artifact.read',
]);
});
test('projects only Approval metadata and the redacted preview document', async () => {
let captured;
const output = await executeBuiltInApprovalGetTool(
{
async getApprovalRequestDetail(query) {
captured = query;
return detail();
},
},
'default',
{ requestId: 'approval-1' },
);
assert.deepEqual(captured, { projectId: 'default', requestId: 'approval-1' });
assert.deepEqual(output, {
found: true,
approval: {
requestId: 'approval-1',
version: 1,
state: 'pending',
risk: 'medium',
decisionMode: 'human_confirmation',
permission: 'run.start',
actionType: 'tool.invoke',
requestedByType: 'agent',
requestedAtMs: 1_000,
expiresAtMs: 61_000,
previewAvailable: true,
preview: {
title: 'Run task',
summary: 'Runs the selected task once.',
fields: [
{ kind: 'identifier', label: 'Task', value: 'task-1' },
{ kind: 'redacted', label: 'Token' },
],
warnings: ['external_effect'],
},
},
});
const serialized = JSON.stringify(output);
for (const hidden of [
'private-agent',
'actionRef',
'actionDigest',
'previewDigest',
'artifactDigest',
'redactionContractDigest',
'requestFence',
]) {
assert.equal(serialized.includes(hidden), false);
}
});
test('masks an absent request and reports an unavailable preview explicitly', async () => {
const missing = await executeBuiltInApprovalGetTool(
{ async getApprovalRequestDetail() { return null; } },
'default',
{ requestId: 'missing' },
);
assert.deepEqual(missing, { found: false });
const withoutPreview = detail();
const output = await executeBuiltInApprovalGetTool(
{
async getApprovalRequestDetail() {
return { request: withoutPreview.request, preview: null };
},
},
'default',
{ requestId: 'approval-1' },
);
assert.equal(output.approval.previewAvailable, false);
assert.equal(Object.hasOwn(output.approval, 'preview'), false);
});
test('rejects widened input before reading and fails closed on binding drift', async () => {
let reads = 0;
const source = {
async getApprovalRequestDetail() {
reads += 1;
return null;
},
};
for (const input of [null, {}, { requestId: '' }, { requestId: 'a', extra: true }]) {
await assert.rejects(
executeBuiltInApprovalGetTool(source, 'default', input),
InvalidBuiltInApprovalGetToolError,
);
}
assert.equal(reads, 0);
const value = detail();
await assert.rejects(
executeBuiltInApprovalGetTool(
{ async getApprovalRequestDetail() { return value; } },
'other',
{ requestId: 'approval-1' },
),
BuiltInApprovalGetToolUnavailableError,
);
await assert.rejects(
executeBuiltInApprovalGetTool(
{ async getApprovalRequestDetail() { throw new Error('private'); } },
'default',
{ requestId: 'approval-1' },
),
BuiltInApprovalGetToolUnavailableError,
);
await assert.rejects(
executeBuiltInApprovalGetTool(
{
async getApprovalRequestDetail() {
return { ...value, extra: true };
},
},
'default',
{ requestId: 'approval-1' },
),
BuiltInApprovalGetToolUnavailableError,
);
});
@@ -0,0 +1,193 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
createApprovalRequest,
} = require('@qinglong/runtime-core/approved-action');
const {
BUILTIN_APPROVAL_LIST_DEFAULT_LIMIT,
BUILTIN_APPROVAL_LIST_MAX_LIMIT,
BUILTIN_APPROVAL_LIST_TOOL,
BUILTIN_APPROVAL_LIST_TOOL_DEFINITION,
BuiltInApprovalListToolUnavailableError,
InvalidBuiltInApprovalListToolError,
executeBuiltInApprovalListTool,
} = require('../dist/tool-projection/approvalList.js');
function approval(id, requestedAtMs, overrides = {}) {
return createApprovalRequest({
id,
projectId: 'default',
action: {
permission: 'run.start',
actionType: 'tool.invoke',
actionRef: `private:${id}`,
actionDigest: 'a'.repeat(64),
previewDigest: 'b'.repeat(64),
},
risk: 'medium',
decisionMode: 'human_confirmation',
requestedBy: { type: 'agent', id: 'private-agent' },
requestedAtMs,
expiresAtMs: requestedAtMs + 60_000,
requestFence: { projectVersion: 1, bindingVersion: 2 },
...overrides,
});
}
test('defines one bounded low-risk approval.read Tool', () => {
assert.deepEqual(BUILTIN_APPROVAL_LIST_TOOL, {
name: 'qinglong.approval.list',
version: '1.0.0',
});
assert.equal(BUILTIN_APPROVAL_LIST_TOOL_DEFINITION.effect, 'read');
assert.equal(BUILTIN_APPROVAL_LIST_TOOL_DEFINITION.risk, 'low');
assert.deepEqual(BUILTIN_APPROVAL_LIST_TOOL_DEFINITION.requiredPermissions, [
'approval.read',
]);
assert.equal(BUILTIN_APPROVAL_LIST_DEFAULT_LIMIT, 32);
assert.equal(BUILTIN_APPROVAL_LIST_MAX_LIMIT, 64);
});
test('projects bounded Approval state without authority or sensitive evidence', async () => {
let captured;
const output = await executeBuiltInApprovalListTool(
{
async listApprovalRequests(query) {
captured = query;
return Object.freeze({
requests: Object.freeze([approval('approval-2', 2_000)]),
truncated: true,
next: Object.freeze({ updatedAtMs: 2_000, requestId: 'approval-2' }),
});
},
},
'default',
{ after: { updatedAtMs: 3_000, requestId: 'approval-3' }, limit: 1 },
);
assert.deepEqual(captured, {
projectId: 'default',
limit: 1,
after: { updatedAtMs: 3_000, requestId: 'approval-3' },
});
assert.deepEqual(output, {
approvals: [
{
requestId: 'approval-2',
version: 1,
state: 'pending',
risk: 'medium',
decisionMode: 'human_confirmation',
permission: 'run.start',
actionType: 'tool.invoke',
requestedByType: 'agent',
requestedAtMs: 2_000,
expiresAtMs: 62_000,
updatedAtMs: 2_000,
},
],
hasMore: true,
next: { updatedAtMs: 2_000, requestId: 'approval-2' },
});
const serialized = JSON.stringify(output);
for (const hidden of [
'private',
'actionRef',
'actionDigest',
'previewDigest',
'requestFence',
'projectId',
]) {
assert.equal(serialized.includes(hidden), false);
}
});
test('defaults to 32 and returns no cursor for a complete page', async () => {
let captured;
const output = await executeBuiltInApprovalListTool(
{
async listApprovalRequests(query) {
captured = query;
return { requests: [], truncated: false };
},
},
'default',
{},
);
assert.deepEqual(captured, { projectId: 'default', limit: 32 });
assert.deepEqual(output, { approvals: [], hasMore: false });
});
test('rejects invalid input before reading', async () => {
let reads = 0;
const source = {
async listApprovalRequests() {
reads += 1;
return { requests: [], truncated: false };
},
};
for (const input of [
null,
{ limit: 65 },
{ after: { updatedAtMs: -1, requestId: 'approval-1' } },
{ after: { updatedAtMs: 1, requestId: '' } },
{ after: { updatedAtMs: 1, requestId: 'approval-1', extra: true } },
{ unexpected: true },
]) {
await assert.rejects(
executeBuiltInApprovalListTool(source, 'default', input),
InvalidBuiltInApprovalListToolError,
);
}
assert.equal(reads, 0);
});
test('fails closed on cross-Project, unordered, oversized or inconsistent pages', async () => {
for (const { page, input = {} } of [
{
page: {
requests: [approval('approval-2', 2_000, { projectId: 'other' })],
truncated: false,
},
},
{
page: {
requests: [approval('approval-1', 1_000), approval('approval-2', 2_000)],
truncated: false,
},
},
{
page: {
requests: [approval('approval-2', 2_000), approval('approval-1', 1_000)],
truncated: false,
},
input: { limit: 1 },
},
{
page: { requests: [approval('approval-1', 1_000)], truncated: true },
},
{
page: {
requests: [approval('approval-1', 1_000)],
truncated: true,
next: { updatedAtMs: 1_000, requestId: 'approval-other' },
},
},
{
page: { requests: [{ projectId: 'default' }], truncated: false },
},
]) {
await assert.rejects(
executeBuiltInApprovalListTool(
{
async listApprovalRequests() {
return page;
},
},
'default',
input,
),
BuiltInApprovalListToolUnavailableError,
);
}
});
@@ -0,0 +1,64 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
LOCAL_MCP_SERVER_CONFIG_SCHEMA,
normalizeLocalMcpServerConfig,
readLocalMcpServerConfig,
} = require('@qinglong/local-mcp-server/config');
function candidate(root) {
return {
schema: LOCAL_MCP_SERVER_CONFIG_SCHEMA,
profile: 'edge',
projectId: 'default',
deploymentRoot: root,
databasePath: path.join(root, 'data', 'qinglong3.sqlite'),
ownerPepperKeyringDirectory: path.join(root, 'owner-peppers'),
credentialFilePath: path.join(root, 'operator', 'credential.json'),
busyTimeoutMs: 500,
};
}
test('accepts an exact private MCP config with deployment-root descendants', (t) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-mcp-config-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
fs.chmodSync(root, 0o700);
const filePath = path.join(root, 'mcp.json');
fs.writeFileSync(filePath, `${JSON.stringify(candidate(root))}\n`, {
mode: 0o600,
});
assert.deepEqual(readLocalMcpServerConfig(filePath), candidate(root));
});
test('rejects public config files, extra keys and authority paths outside deployment root', () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-mcp-config-bad-'));
try {
fs.chmodSync(root, 0o700);
const filePath = path.join(root, 'mcp.json');
fs.writeFileSync(filePath, `${JSON.stringify(candidate(root))}\n`, {
mode: 0o644,
});
assert.throws(() => readLocalMcpServerConfig(filePath), {
code: 'LOCAL_MCP_SERVER_CONFIG_INVALID',
});
assert.throws(
() => normalizeLocalMcpServerConfig({ ...candidate(root), extra: true }),
{ code: 'LOCAL_MCP_SERVER_CONFIG_INVALID' },
);
assert.throws(
() =>
normalizeLocalMcpServerConfig({
...candidate(root),
credentialFilePath: path.join(os.tmpdir(), 'credential.json'),
}),
{ code: 'LOCAL_MCP_SERVER_CONFIG_INVALID' },
);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
@@ -0,0 +1,872 @@
const assert = require('node:assert/strict');
const { randomUUID } = require('node:crypto');
const { test } = require('node:test');
const { InMemoryTransport } = require('@modelcontextprotocol/server');
const { createQingLongLocalMcpServer } = require('@qinglong/local-mcp-server');
const {
createApprovalRequest,
} = require('@qinglong/runtime-core/approved-action');
const {
createToolInvocationPreviewArtifact,
} = require('@qinglong/runtime-core/tool-invocation-artifact');
const {
createTaskDefinitionRecord,
} = require('@qinglong/runtime-core/task-definition');
const NOW = 50_000;
const PRINCIPAL = Object.freeze({
subject: Object.freeze({ type: 'user', id: 'mcp-user' }),
authenticationId: 'mcp-test:principal',
authenticatedAtMs: NOW - 1,
expiresAtMs: NOW + 60_000,
assurance: 'local_console',
});
function run(projectId = 'default') {
return Object.freeze({
id: 'run-1',
projectId,
taskId: 'task-1',
taskRevision: 'revision-1',
status: 'succeeded',
version: 3,
eventSequence: 4,
priority: 0,
executionOrigin: 'manual',
executionOwner: 'runtime',
createdAtMs: 10,
queuedAtMs: 11,
startedAtMs: 12,
finishedAtMs: 13,
});
}
function runEvent(sequence) {
return Object.freeze({
id: `event-${sequence}`,
runId: 'run-1',
sequence,
type: `run.event.${sequence}`,
actorType: 'system',
actorId: 'private-actor',
attemptId: 'private-attempt',
payload: Object.freeze({ secret: 'must-not-leak' }),
createdAtMs: 100 + sequence,
});
}
function task(taskId = 'task-1', projectId = 'default') {
return createTaskDefinitionRecord(
{
projectId,
taskId,
expectedRevision: 1,
mutationId: '123e4567-e89b-42d3-a456-426614174302',
name: 'Example Task',
description: 'private description',
kind: 'script',
spec: {
schema: 'qinglong/script@v1',
config: { command: 'private command' },
},
labels: { private: 'label' },
enabled: true,
occurredAtMs: 20,
},
10,
);
}
function trigger(triggerId = 'trigger-1') {
return Object.freeze({
projectId: 'default',
triggerId,
revision: 2,
mutationId: 'private-mutation',
taskId: 'task-1',
taskRevision: 2,
taskContentDigest: 'private-task-digest',
spec: Object.freeze({
schema: 'qinglong/cron@v1',
config: Object.freeze({
expression: 'private cron expression',
timezone: 'private timezone',
}),
}),
enabled: true,
contentDigest: 'private-trigger-digest',
createdAtMs: 10,
updatedAtMs: 30,
});
}
function approval(id = 'approval-1', requestedAtMs = 40) {
return createApprovalRequest({
id,
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,
expiresAtMs: requestedAtMs + 60_000,
requestFence: { projectVersion: 2, bindingVersion: 3 },
});
}
function approvalWithPreview() {
const previewArtifact = createToolInvocationPreviewArtifact({
artifactId: 'preview-approval-detail',
projectId: 'default',
actionRef: 'private-action-ref',
actionDigest: 'a'.repeat(64),
redactionContractDigest: 'c'.repeat(64),
sealedAtMs: 40,
preview: {
title: 'Start one run',
summary: 'Starts the selected task once.',
fields: [
{ kind: 'identifier', label: 'Task', value: 'task-1' },
{ kind: 'redacted', label: 'Secret', value: null },
],
warnings: ['external_effect'],
},
});
const request = createApprovalRequest({
id: 'approval-detail',
projectId: 'default',
action: {
permission: 'run.start',
actionType: 'tool.invoke',
actionRef: previewArtifact.actionRef,
actionDigest: previewArtifact.actionDigest,
previewDigest: previewArtifact.previewDigest,
},
risk: 'medium',
decisionMode: 'human_confirmation',
requestedBy: { type: 'agent', id: 'private-agent' },
requestedAtMs: 40,
expiresAtMs: 60_040,
requestFence: { projectVersion: 2, bindingVersion: 3 },
});
return Object.freeze({ request, preview: previewArtifact.preview });
}
function fixture(options = {}) {
const events = [];
const permissions = [];
const audits = [];
let reads = 0;
let listReads = 0;
let eventReads = 0;
let taskListReads = 0;
let triggerListReads = 0;
let approvalListReads = 0;
let approvalDetailReads = 0;
let confirmations = 0;
const server = createQingLongLocalMcpServer({
projectId: 'default',
now: () => NOW,
randomUuid: randomUUID,
authenticate: async () => {
events.push('authenticate');
if (options.authentication === 'rejected') return null;
if (options.authentication === 'unavailable') throw new Error('hidden');
return Object.freeze({
principal: PRINCIPAL,
async confirm() {
events.push('confirm');
confirmations += 1;
},
});
},
policy: {
async authorize(_principal, _projectId, permission) {
events.push(`policy:${permission}`);
permissions.push(permission);
return Object.freeze({
effect: options.policy ?? 'allow',
reasons: Object.freeze(['test_policy']),
fence: Object.freeze({ projectVersion: 2, bindingVersion: 3 }),
});
},
},
audit: {
async record(record) {
events.push(`audit:${record.outcome}`);
if (options.auditUnavailable) throw new Error('hidden');
audits.push(record);
},
},
runs: {
async listRunsByProject(query) {
events.push('read-list');
listReads += 1;
const values = [{ ...run(), id: 'run-2', createdAtMs: 20 }, run()];
return values.slice(0, query.limit);
},
async findRunById(runId) {
events.push('read');
reads += 1;
return runId === 'run-1' ? run(options.runProjectId) : null;
},
async listEvents(runId, query) {
events.push('read-events');
eventReads += 1;
if (runId !== 'run-1') return [];
const after = query?.afterSequence ?? 0;
const limit = query?.limit ?? 100;
return [runEvent(1), runEvent(2), runEvent(3)]
.filter(({ sequence }) => sequence > after)
.slice(0, limit);
},
},
stepRuns: {
async listByRun() {
return Object.freeze({
stepRuns: Object.freeze([]),
truncated: false,
});
},
},
taskDefinitions: {
async findCurrentTaskDefinition(projectId, taskId) {
events.push('read-task');
taskListReads += 1;
return taskId === 'task-1'
? task(taskId, options.taskProjectId ?? projectId)
: null;
},
async listTaskDefinitions(query) {
events.push('read-tasks');
taskListReads += 1;
const definitions = [task('task-1'), task('task-2')].slice(
0,
query.limit,
);
return Object.freeze({
definitions: Object.freeze(definitions),
truncated: query.limit < 2,
...(query.limit < 2
? { next: Object.freeze({ taskId: definitions.at(-1).taskId }) }
: {}),
});
},
},
triggers: {
async listTriggers(query) {
events.push('read-triggers');
triggerListReads += 1;
const triggers = [trigger('trigger-1'), trigger('trigger-2')].slice(
0,
query.limit,
);
return Object.freeze({
triggers: Object.freeze(triggers),
truncated: query.limit < 2,
...(query.limit < 2
? {
next: Object.freeze({
triggerId: triggers.at(-1).triggerId,
}),
}
: {}),
});
},
},
approvals: {
async listApprovalRequests(query) {
events.push('read-approvals');
approvalListReads += 1;
const requests = [
approval('approval-2', 40),
approval('approval-1', 30),
].slice(0, query.limit);
return Object.freeze({
requests: Object.freeze(requests),
truncated: query.limit < 2,
...(query.limit < 2
? {
next: Object.freeze({
updatedAtMs: requests.at(-1).requestedAtMs,
requestId: requests.at(-1).id,
}),
}
: {}),
});
},
async getApprovalRequestDetail(query) {
events.push('read-approval-detail');
approvalDetailReads += 1;
return options.approvalDetail?.(query) ?? null;
},
},
});
return {
server,
events,
permissions,
audits,
counters: () => ({
reads,
listReads,
eventReads,
taskListReads,
triggerListReads,
approvalListReads,
approvalDetailReads,
confirmations,
}),
};
}
async function client(server, t) {
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
const pending = new Map();
clientTransport.onmessage = (message) => {
const waiter = pending.get(message.id);
if (waiter) {
pending.delete(message.id);
waiter(message);
}
};
await server.connect(serverTransport);
await clientTransport.start();
t.after(async () => {
await clientTransport.close();
await server.close();
});
let nextId = 1;
const request = (method, params = undefined) => {
const id = nextId++;
return new Promise((resolve, reject) => {
pending.set(id, resolve);
clientTransport
.send({
jsonrpc: '2.0',
id,
method,
...(params === undefined ? {} : { params }),
})
.catch(reject);
});
};
const initialized = await request('initialize', {
protocolVersion: '2025-11-25',
capabilities: {},
clientInfo: { name: 'ql3-test', version: '1.0.0' },
});
assert.equal(initialized.result.protocolVersion, '2025-11-25');
await clientTransport.send({
jsonrpc: '2.0',
method: 'notifications/initialized',
});
return { request };
}
test('advertises bounded read-only Run Tools and executes auth -> Policy -> Audit -> confirm -> read', async (t) => {
const value = fixture();
const connected = await client(value.server, t);
const listed = await connected.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',
],
);
for (const tool of listed.result.tools) {
assert.deepEqual(tool.annotations, {
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
readOnlyHint: true,
});
}
const response = await connected.request('tools/call', {
name: 'qinglong.run.get',
arguments: { runId: 'run-1' },
});
assert.equal(response.result.isError, undefined);
assert.deepEqual(response.result.structuredContent, {
found: true,
id: 'run-1',
taskId: 'task-1',
taskRevision: 'revision-1',
status: 'succeeded',
version: 3,
eventSequence: 4,
priority: 0,
executionOrigin: 'manual',
executionOwner: 'runtime',
createdAtMs: 10,
queuedAtMs: 11,
startedAtMs: 12,
finishedAtMs: 13,
});
assert.deepEqual(value.permissions, [
'tool.call:qinglong.run.get',
'run.read',
]);
assert.deepEqual(value.events, [
'authenticate',
'policy:tool.call:qinglong.run.get',
'policy:run.read',
'audit:allowed',
'confirm',
'read',
]);
assert.deepEqual(value.audits[0].reasons, [
'tool_invocation_allowed',
'tool_qinglong_run_get',
]);
assert.deepEqual(value.counters(), {
reads: 1,
listReads: 0,
eventReads: 0,
taskListReads: 0,
triggerListReads: 0,
approvalListReads: 0,
approvalDetailReads: 0,
confirmations: 1,
});
});
test('discovers recent Project Runs through the same fenced admission', async (t) => {
const value = fixture();
const connected = await client(value.server, t);
const response = await connected.request('tools/call', {
name: 'qinglong.run.list',
arguments: { limit: 1 },
});
assert.equal(response.result.isError, undefined);
assert.deepEqual(response.result.structuredContent, {
runs: [
{
id: 'run-2',
taskId: 'task-1',
taskRevision: 'revision-1',
status: 'succeeded',
version: 3,
eventSequence: 4,
priority: 0,
executionOrigin: 'manual',
executionOwner: 'runtime',
createdAtMs: 20,
queuedAtMs: 11,
startedAtMs: 12,
finishedAtMs: 13,
},
],
hasMore: true,
next: { createdAtMs: 20, runId: 'run-2' },
});
assert.deepEqual(value.permissions, [
'tool.call:qinglong.run.list',
'run.read',
]);
assert.deepEqual(value.events, [
'authenticate',
'policy:tool.call:qinglong.run.list',
'policy:run.read',
'audit:allowed',
'confirm',
'read-list',
]);
assert.deepEqual(value.audits[0].reasons, [
'tool_invocation_allowed',
'tool_qinglong_run_list',
]);
assert.deepEqual(value.counters(), {
reads: 0,
listReads: 1,
eventReads: 0,
taskListReads: 0,
triggerListReads: 0,
approvalListReads: 0,
approvalDetailReads: 0,
confirmations: 1,
});
});
test('lists a payload-free Run event page through the same fenced admission', async (t) => {
const value = fixture();
const connected = await client(value.server, t);
const response = await connected.request('tools/call', {
name: 'qinglong.run.events.list',
arguments: { runId: 'run-1', afterSequence: 1, limit: 1 },
});
assert.equal(response.result.isError, undefined);
assert.deepEqual(response.result.structuredContent, {
found: true,
events: [
{
sequence: 2,
type: 'run.event.2',
actorType: 'system',
createdAtMs: 102,
},
],
hasMore: true,
nextAfterSequence: 2,
});
assert.equal(JSON.stringify(response).includes('must-not-leak'), false);
assert.equal(JSON.stringify(response).includes('private-'), false);
assert.deepEqual(value.permissions, [
'tool.call:qinglong.run.events.list',
'run.read',
]);
assert.deepEqual(value.events, [
'authenticate',
'policy:tool.call:qinglong.run.events.list',
'policy:run.read',
'audit:allowed',
'confirm',
'read',
'read-events',
]);
assert.deepEqual(value.audits[0].reasons, [
'tool_invocation_allowed',
'tool_qinglong_run_events_list',
]);
assert.deepEqual(value.counters(), {
reads: 1,
listReads: 0,
eventReads: 1,
taskListReads: 0,
triggerListReads: 0,
approvalListReads: 0,
approvalDetailReads: 0,
confirmations: 1,
});
});
test('discovers low-sensitive Tasks through task.read admission', async (t) => {
const value = fixture();
const connected = await client(value.server, t);
const response = await connected.request('tools/call', {
name: 'qinglong.task.list',
arguments: { limit: 1 },
});
assert.equal(response.result.isError, undefined);
assert.deepEqual(response.result.structuredContent, {
tasks: [
{
taskId: 'task-1',
revision: 2,
name: 'Example Task',
kind: 'script',
specSchema: 'qinglong/script@v1',
enabled: true,
updatedAtMs: 20,
},
],
hasMore: true,
next: { taskId: 'task-1' },
});
assert.equal(JSON.stringify(response).includes('private'), false);
assert.deepEqual(value.permissions, [
'tool.call:qinglong.task.list',
'task.read',
]);
assert.deepEqual(value.events, [
'authenticate',
'policy:tool.call:qinglong.task.list',
'policy:task.read',
'audit:allowed',
'confirm',
'read-tasks',
]);
assert.deepEqual(value.audits[0].reasons, [
'tool_invocation_allowed',
'tool_qinglong_task_list',
]);
assert.deepEqual(value.counters(), {
reads: 0,
listReads: 0,
eventReads: 0,
taskListReads: 1,
triggerListReads: 0,
approvalListReads: 0,
approvalDetailReads: 0,
confirmations: 1,
});
});
test('reads one current Task fence through task.read admission', async (t) => {
const value = fixture();
const connected = await client(value.server, t);
const response = await connected.request('tools/call', {
name: 'qinglong.task.get',
arguments: { taskId: 'task-1' },
});
assert.equal(response.result.isError, undefined);
assert.deepEqual(response.result.structuredContent, {
found: true,
taskId: 'task-1',
revision: 2,
name: 'Example Task',
kind: 'script',
specSchema: 'qinglong/script@v1',
enabled: true,
contentDigest: task().contentDigest,
createdAtMs: 10,
updatedAtMs: 20,
});
assert.equal(JSON.stringify(response).includes('private'), false);
assert.deepEqual(value.permissions, [
'tool.call:qinglong.task.get',
'task.read',
]);
assert.deepEqual(value.events, [
'authenticate',
'policy:tool.call:qinglong.task.get',
'policy:task.read',
'audit:allowed',
'confirm',
'read-task',
]);
assert.deepEqual(value.audits[0].reasons, [
'tool_invocation_allowed',
'tool_qinglong_task_get',
]);
assert.deepEqual(value.counters(), {
reads: 0,
listReads: 0,
eventReads: 0,
taskListReads: 1,
triggerListReads: 0,
approvalListReads: 0,
approvalDetailReads: 0,
confirmations: 1,
});
const maskedValue = fixture({ taskProjectId: 'other' });
const maskedClient = await client(maskedValue.server, t);
const masked = await maskedClient.request('tools/call', {
name: 'qinglong.task.get',
arguments: { taskId: 'task-1' },
});
assert.deepEqual(masked.result.structuredContent, { found: false });
});
test('discovers low-sensitive Triggers through trigger.read admission', async (t) => {
const value = fixture();
const connected = await client(value.server, t);
const response = await connected.request('tools/call', {
name: 'qinglong.trigger.list',
arguments: { limit: 1 },
});
assert.equal(response.result.isError, undefined);
assert.deepEqual(response.result.structuredContent, {
triggers: [
{
triggerId: 'trigger-1',
revision: 2,
taskId: 'task-1',
taskRevision: 2,
specSchema: 'qinglong/cron@v1',
enabled: true,
updatedAtMs: 30,
},
],
hasMore: true,
next: { triggerId: 'trigger-1' },
});
assert.equal(JSON.stringify(response).includes('private'), false);
assert.deepEqual(value.permissions, [
'tool.call:qinglong.trigger.list',
'trigger.read',
]);
assert.deepEqual(value.events, [
'authenticate',
'policy:tool.call:qinglong.trigger.list',
'policy:trigger.read',
'audit:allowed',
'confirm',
'read-triggers',
]);
assert.deepEqual(value.audits[0].reasons, [
'tool_invocation_allowed',
'tool_qinglong_trigger_list',
]);
assert.deepEqual(value.counters(), {
reads: 0,
listReads: 0,
eventReads: 0,
taskListReads: 0,
triggerListReads: 1,
approvalListReads: 0,
approvalDetailReads: 0,
confirmations: 1,
});
});
test('discovers low-sensitive Approvals through approval.read admission', async (t) => {
const value = fixture();
const connected = await client(value.server, t);
const response = await connected.request('tools/call', {
name: 'qinglong.approval.list',
arguments: { limit: 1 },
});
assert.equal(response.result.isError, undefined);
assert.deepEqual(response.result.structuredContent, {
approvals: [
{
requestId: 'approval-2',
version: 1,
state: 'pending',
risk: 'medium',
decisionMode: 'human_confirmation',
permission: 'run.start',
actionType: 'tool.invoke',
requestedByType: 'agent',
requestedAtMs: 40,
expiresAtMs: 60_040,
updatedAtMs: 40,
},
],
hasMore: true,
next: { updatedAtMs: 40, requestId: 'approval-2' },
});
assert.equal(JSON.stringify(response).includes('private'), false);
assert.deepEqual(value.permissions, [
'tool.call:qinglong.approval.list',
'approval.read',
]);
assert.deepEqual(value.events, [
'authenticate',
'policy:tool.call:qinglong.approval.list',
'policy:approval.read',
'audit:allowed',
'confirm',
'read-approvals',
]);
assert.deepEqual(value.audits[0].reasons, [
'tool_invocation_allowed',
'tool_qinglong_approval_list',
]);
assert.deepEqual(value.counters(), {
reads: 0,
listReads: 0,
eventReads: 0,
taskListReads: 0,
triggerListReads: 0,
approvalListReads: 1,
approvalDetailReads: 0,
confirmations: 1,
});
});
test('reads one redacted Approval preview through approval.read and artifact.read', async (t) => {
const detail = approvalWithPreview();
const value = fixture({ approvalDetail: () => detail });
const connected = await client(value.server, t);
const response = await connected.request('tools/call', {
name: 'qinglong.approval.get',
arguments: { requestId: 'approval-detail' },
});
assert.equal(response.result.isError, undefined);
assert.deepEqual(response.result.structuredContent, {
found: true,
approval: {
requestId: 'approval-detail',
version: 1,
state: 'pending',
risk: 'medium',
decisionMode: 'human_confirmation',
permission: 'run.start',
actionType: 'tool.invoke',
requestedByType: 'agent',
requestedAtMs: 40,
expiresAtMs: 60_040,
previewAvailable: true,
preview: {
title: 'Start one run',
summary: 'Starts the selected task once.',
fields: [
{ kind: 'identifier', label: 'Task', value: 'task-1' },
{ kind: 'redacted', label: 'Secret' },
],
warnings: ['external_effect'],
},
},
});
const serialized = JSON.stringify(response);
for (const hidden of [
'private-agent',
'private-action-ref',
'actionDigest',
'previewDigest',
'artifactId',
'redactionContractDigest',
]) {
assert.equal(serialized.includes(hidden), false);
}
assert.deepEqual(value.permissions, [
'tool.call:qinglong.approval.get',
'approval.read',
'artifact.read',
]);
assert.deepEqual(value.events, [
'authenticate',
'policy:tool.call:qinglong.approval.get',
'policy:approval.read',
'policy:artifact.read',
'audit:allowed',
'confirm',
'read-approval-detail',
]);
assert.deepEqual(value.counters(), {
reads: 0,
listReads: 0,
eventReads: 0,
taskListReads: 0,
triggerListReads: 0,
approvalListReads: 0,
approvalDetailReads: 1,
confirmations: 1,
});
});
test('masks cross-Project Runs and fails closed before reads on auth, Policy or Audit denial', async (t) => {
const crossProject = fixture({ runProjectId: 'other' });
const crossClient = await client(crossProject.server, t);
const cross = await crossClient.request('tools/call', {
name: 'qinglong.run.get',
arguments: { runId: 'run-1' },
});
assert.deepEqual(cross.result.structuredContent, { found: false });
for (const options of [
{ authentication: 'rejected' },
{ authentication: 'unavailable' },
{ policy: 'deny' },
{ auditUnavailable: true },
]) {
const denied = fixture(options);
const deniedClient = await client(denied.server, t);
const result = await deniedClient.request('tools/call', {
name: 'qinglong.run.get',
arguments: { runId: 'run-1' },
});
assert.equal(result.result.isError, true);
assert.equal(denied.counters().reads, 0);
}
});
@@ -0,0 +1,114 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
LOCAL_MCP_SERVER_CONFIG_SCHEMA,
} = require('@qinglong/local-mcp-server/config');
const {
openProductionLocalMcpServer,
} = require('@qinglong/local-mcp-server/process');
test('opens one bounded database authority and reuses production authentication per server call', async () => {
const config = Object.freeze({
schema: LOCAL_MCP_SERVER_CONFIG_SCHEMA,
profile: 'edge',
projectId: 'default',
deploymentRoot: '/srv/qinglong',
databasePath: '/srv/qinglong/data/qinglong3.sqlite',
ownerPepperKeyringDirectory: '/srv/qinglong/owner-peppers',
credentialFilePath: '/srv/qinglong/operator/credential.json',
busyTimeoutMs: 250,
});
const calls = [];
let closes = 0;
const database = {
projectPolicy: {
async resolve() {
return null;
},
},
securityAudit: { async record() {} },
runs: {
async listRunsByProject() {
return [];
},
async findRunById() {
return null;
},
async listEvents() {
return [];
},
},
stepRuns: {
async listByRun() {
return { stepRuns: [], truncated: false };
},
},
taskDefinitions: {
async findCurrentTaskDefinition() {
return null;
},
async listTaskDefinitions() {
return { definitions: [], truncated: false };
},
},
triggers: {
async listTriggers() {
return { triggers: [], truncated: false };
},
},
approvals: {
async listApprovalRequests() {
return { requests: [], truncated: false };
},
async getApprovalRequestDetail() {
return null;
},
},
apiCredentials: {
async resolve() {
return null;
},
},
ownerPepper: {
async resolveKey() {
return null;
},
},
async close() {
closes += 1;
},
};
const active = await openProductionLocalMcpServer(
{ configFilePath: '/srv/qinglong/mcp.json' },
{
readConfig(filePath) {
calls.push(['config', filePath]);
return config;
},
async openDatabase(options) {
calls.push(['database', options]);
return database;
},
async authenticate(_database, options) {
calls.push(['authenticate', options]);
return null;
},
},
);
assert.equal(active.createServer().constructor.name, 'McpServer');
assert.deepEqual(calls, [
['config', '/srv/qinglong/mcp.json'],
[
'database',
{
databasePath: '/srv/qinglong/data/qinglong3.sqlite',
profile: 'edge',
busyTimeoutMs: 250,
},
],
]);
await active.close();
await active.close();
assert.equal(closes, 1);
});
@@ -0,0 +1,171 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
BUILTIN_RUN_EVENT_LIST_DEFAULT_LIMIT,
BUILTIN_RUN_EVENT_LIST_MAX_LIMIT,
BUILTIN_RUN_EVENT_LIST_TOOL,
BUILTIN_RUN_EVENT_LIST_TOOL_DEFINITION,
BuiltInRunEventListToolUnavailableError,
InvalidBuiltInRunEventListToolError,
executeBuiltInRunEventListTool,
} = require('../dist/tool-projection/runEventList.js');
function run(projectId = 'default') {
return Object.freeze({ id: 'run-1', projectId });
}
function event(sequence, overrides = {}) {
return Object.freeze({
id: `event-${sequence}`,
runId: 'run-1',
sequence,
type: `run.event.${sequence}`,
dedupeKey: `private-dedupe-${sequence}`,
actorType: 'system',
actorId: 'private-actor',
attemptId: 'private-attempt',
stepRunId: 'private-step',
payload: Object.freeze({ secret: 'must-not-leak' }),
createdAtMs: 1_000 + sequence,
...overrides,
});
}
test('defines one bounded low-risk Run event list Tool', () => {
assert.deepEqual(BUILTIN_RUN_EVENT_LIST_TOOL, {
name: 'qinglong.run.events.list',
version: '1.0.0',
});
assert.equal(BUILTIN_RUN_EVENT_LIST_TOOL_DEFINITION.effect, 'read');
assert.equal(BUILTIN_RUN_EVENT_LIST_TOOL_DEFINITION.risk, 'low');
assert.deepEqual(
BUILTIN_RUN_EVENT_LIST_TOOL_DEFINITION.requiredPermissions,
['run.read'],
);
assert.equal(BUILTIN_RUN_EVENT_LIST_DEFAULT_LIMIT, 32);
assert.equal(BUILTIN_RUN_EVENT_LIST_MAX_LIMIT, 64);
});
test('returns an ordered payload-free page and a stable cursor', async () => {
const calls = [];
const result = await executeBuiltInRunEventListTool(
{
async findRunById(runId) {
calls.push(['run', runId]);
return run();
},
async listEvents(runId, options) {
calls.push(['events', runId, options]);
return [event(3), event(4), event(5)];
},
},
'default',
{ runId: 'run-1', afterSequence: 2, limit: 2 },
);
assert.deepEqual(result, {
found: true,
events: [
{
sequence: 3,
type: 'run.event.3',
actorType: 'system',
createdAtMs: 1_003,
},
{
sequence: 4,
type: 'run.event.4',
actorType: 'system',
createdAtMs: 1_004,
},
],
hasMore: true,
nextAfterSequence: 4,
});
assert.deepEqual(calls, [
['run', 'run-1'],
['events', 'run-1', { afterSequence: 2, limit: 3 }],
]);
assert.equal(JSON.stringify(result).includes('must-not-leak'), false);
assert.equal(JSON.stringify(result).includes('private-'), false);
});
test('masks absent and cross-Project Runs without reading events', async () => {
for (const value of [null, run('other')]) {
let eventReads = 0;
const result = await executeBuiltInRunEventListTool(
{
async findRunById() {
return value;
},
async listEvents() {
eventReads += 1;
return [];
},
},
'default',
{ runId: 'run-1', afterSequence: 7 },
);
assert.deepEqual(result, {
found: false,
events: [],
hasMore: false,
nextAfterSequence: 7,
});
assert.equal(eventReads, 0);
}
});
test('rejects malformed input and fails closed on storage or event corruption', async () => {
const runs = {
async findRunById() {
return run();
},
async listEvents() {
return [];
},
};
for (const input of [
null,
{},
{ runId: '' },
{ runId: 'run-1', limit: 65 },
{ runId: 'run-1', afterSequence: -1 },
{ runId: 'run-1', unexpected: true },
]) {
await assert.rejects(
executeBuiltInRunEventListTool(runs, 'default', input),
InvalidBuiltInRunEventListToolError,
);
}
await assert.rejects(
executeBuiltInRunEventListTool(
{
async findRunById() {
throw new Error('hidden');
},
async listEvents() {
return [];
},
},
'default',
{ runId: 'run-1' },
),
BuiltInRunEventListToolUnavailableError,
);
await assert.rejects(
executeBuiltInRunEventListTool(
{
async findRunById() {
return run();
},
async listEvents() {
return [event(2), event(1)];
},
},
'default',
{ runId: 'run-1' },
),
BuiltInRunEventListToolUnavailableError,
);
});
@@ -0,0 +1,154 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
BUILTIN_RUN_LIST_DEFAULT_LIMIT,
BUILTIN_RUN_LIST_MAX_LIMIT,
BUILTIN_RUN_LIST_TOOL,
BUILTIN_RUN_LIST_TOOL_DEFINITION,
BuiltInRunListToolUnavailableError,
InvalidBuiltInRunListToolError,
executeBuiltInRunListTool,
} = require('../dist/tool-projection/runList.js');
function run(id, createdAtMs, overrides = {}) {
return Object.freeze({
id,
projectId: 'default',
taskId: `task-${id}`,
taskRevision: 'revision-1',
taskName: 'private-name',
triggerType: 'manual',
executionOrigin: 'manual',
executionOwner: 'runtime',
triggeredBy: 'private-actor',
requestId: 'private-request',
status: 'succeeded',
version: 2,
eventSequence: 3,
priority: 0,
inputRef: 'private-input',
outputRef: 'private-output',
createdAtMs,
finishedAtMs: createdAtMs + 1,
...overrides,
});
}
test('defines one bounded low-risk Project Run discovery Tool', () => {
assert.deepEqual(BUILTIN_RUN_LIST_TOOL, {
name: 'qinglong.run.list',
version: '1.0.0',
});
assert.equal(BUILTIN_RUN_LIST_TOOL_DEFINITION.effect, 'read');
assert.equal(BUILTIN_RUN_LIST_TOOL_DEFINITION.risk, 'low');
assert.deepEqual(BUILTIN_RUN_LIST_TOOL_DEFINITION.requiredPermissions, [
'run.read',
]);
assert.equal(BUILTIN_RUN_LIST_DEFAULT_LIMIT, 32);
assert.equal(BUILTIN_RUN_LIST_MAX_LIMIT, 64);
});
test('returns a descending low-sensitive page and stable continuation', async () => {
const calls = [];
const result = await executeBuiltInRunListTool(
{
async listRunsByProject(query) {
calls.push(query);
return [run('run-c', 30), run('run-b', 20), run('run-a', 10)];
},
},
'default',
{ limit: 2 },
);
assert.deepEqual(calls, [{ projectId: 'default', limit: 3 }]);
assert.deepEqual(result, {
runs: [
{
id: 'run-c',
taskId: 'task-run-c',
taskRevision: 'revision-1',
status: 'succeeded',
version: 2,
eventSequence: 3,
priority: 0,
executionOrigin: 'manual',
executionOwner: 'runtime',
createdAtMs: 30,
finishedAtMs: 31,
},
{
id: 'run-b',
taskId: 'task-run-b',
taskRevision: 'revision-1',
status: 'succeeded',
version: 2,
eventSequence: 3,
priority: 0,
executionOrigin: 'manual',
executionOwner: 'runtime',
createdAtMs: 20,
finishedAtMs: 21,
},
],
hasMore: true,
next: { createdAtMs: 20, runId: 'run-b' },
});
assert.equal(JSON.stringify(result).includes('private-'), false);
});
test('passes an exact cursor and fails closed on malformed or corrupt pages', async () => {
const seen = [];
const reader = {
async listRunsByProject(query) {
seen.push(query);
return [];
},
};
assert.deepEqual(
await executeBuiltInRunListTool(reader, 'default', {
after: { createdAtMs: 20, runId: 'run-b' },
}),
{ runs: [], hasMore: false },
);
assert.deepEqual(seen, [
{
projectId: 'default',
limit: BUILTIN_RUN_LIST_DEFAULT_LIMIT + 1,
after: { createdAtMs: 20, runId: 'run-b' },
},
]);
for (const input of [
null,
{ limit: 65 },
{ unexpected: true },
{ after: null },
{ after: { createdAtMs: -1, runId: 'run-b' } },
{ after: { createdAtMs: 20, runId: 'run-b', extra: true } },
]) {
await assert.rejects(
executeBuiltInRunListTool(reader, 'default', input),
InvalidBuiltInRunListToolError,
);
}
for (const rows of [
[run('run-a', 10, { projectId: 'other' })],
[run('run-a', 10), run('run-b', 20)],
Array.from({ length: 34 }, (_, index) => run(`run-${index}`, 100 - index)),
]) {
await assert.rejects(
executeBuiltInRunListTool(
{
async listRunsByProject() {
return rows;
},
},
'default',
{},
),
BuiltInRunListToolUnavailableError,
);
}
});
@@ -0,0 +1,166 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
BUILTIN_RUN_STEP_LIST_DEFAULT_LIMIT,
BUILTIN_RUN_STEP_LIST_MAX_LIMIT,
BUILTIN_RUN_STEP_LIST_TOOL,
BUILTIN_RUN_STEP_LIST_TOOL_DEFINITION,
BuiltInRunStepListToolUnavailableError,
InvalidBuiltInRunStepListToolError,
executeBuiltInRunStepListTool,
} = require('../dist/tool-projection/runStepList.js');
const {
createStepRunRecord,
} = require('../../ql3-runtime-core/dist/run/stepRun.js');
function run(projectId = 'default') {
return { id: 'run-1', projectId };
}
function step(id, stepKey) {
return createStepRunRecord({
id,
runId: 'run-1',
parentStepRunId: 'step-parent',
stepKey,
kind: 'tool',
definitionRef: 'tool:private.internal@1.0.0',
definitionDigest: 'a'.repeat(64),
required: true,
initialStatus: 'ready',
inputRef: 'artifact:private-input',
mutationId: `create-${id}`,
createdAtMs: 1_000,
});
}
test('defines one bounded low-risk Run Step list Tool', () => {
assert.deepEqual(BUILTIN_RUN_STEP_LIST_TOOL, {
name: 'qinglong.run.steps.list',
version: '1.0.0',
});
assert.equal(BUILTIN_RUN_STEP_LIST_TOOL_DEFINITION.effect, 'read');
assert.equal(BUILTIN_RUN_STEP_LIST_TOOL_DEFINITION.risk, 'low');
assert.deepEqual(BUILTIN_RUN_STEP_LIST_TOOL_DEFINITION.requiredPermissions, [
'run.read',
]);
assert.equal(BUILTIN_RUN_STEP_LIST_DEFAULT_LIMIT, 32);
assert.equal(BUILTIN_RUN_STEP_LIST_MAX_LIMIT, 64);
});
test('returns a low-sensitive page and omits nullable fields', async () => {
const calls = [];
const first = step('step-1', 'build');
const second = step('step-2', 'deploy');
const result = await executeBuiltInRunStepListTool(
{
async findRunById() {
return run();
},
},
{
async listByRun(query) {
calls.push(query);
return {
stepRuns: [first, second],
truncated: true,
next: { stepKey: second.stepKey, id: second.id },
};
},
},
'default',
{
runId: 'run-1',
afterStepKey: 'admit',
afterStepRunId: 'step-0',
limit: 2,
},
);
assert.deepEqual(calls, [
{
runId: 'run-1',
limit: 2,
after: { stepKey: 'admit', id: 'step-0' },
},
]);
assert.equal(result.found, true);
assert.equal(result.steps.length, 2);
assert.equal(result.steps[0].parentStepRunId, 'step-parent');
assert.equal(Object.hasOwn(result.steps[0], 'startedAtMs'), false);
assert.deepEqual(result.next, {
stepKey: 'deploy',
stepRunId: 'step-2',
});
const serialized = JSON.stringify(result);
assert.equal(serialized.includes('private.internal'), false);
assert.equal(serialized.includes('private-input'), false);
});
test('masks Project mismatch and rejects malformed or corrupt input', async () => {
let reads = 0;
assert.deepEqual(
await executeBuiltInRunStepListTool(
{
async findRunById() {
return run('other');
},
},
{
async listByRun() {
reads += 1;
return { stepRuns: [], truncated: false };
},
},
'default',
{ runId: 'run-1' },
),
{ found: false, steps: [], hasMore: false },
);
assert.equal(reads, 0);
for (const input of [
{},
{ runId: 'run-1', afterStepKey: 'build' },
{ runId: 'run-1', afterStepRunId: 'step-1' },
{ runId: 'run-1', limit: 65 },
{ runId: 'run-1', unexpected: true },
]) {
await assert.rejects(
executeBuiltInRunStepListTool(
{
async findRunById() {
return run();
},
},
{
async listByRun() {
return { stepRuns: [], truncated: false };
},
},
'default',
input,
),
InvalidBuiltInRunStepListToolError,
);
}
await assert.rejects(
executeBuiltInRunStepListTool(
{
async findRunById() {
return run();
},
},
{
async listByRun() {
return {
stepRuns: [step('step-2', 'deploy'), step('step-1', 'build')],
truncated: false,
};
},
},
'default',
{ runId: 'run-1' },
),
BuiltInRunStepListToolUnavailableError,
);
});
@@ -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();
}
});
@@ -0,0 +1,109 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
createTaskDefinitionRecord,
} = require('@qinglong/runtime-core/task-definition');
const {
BUILTIN_TASK_GET_TOOL_DEFINITION,
BuiltInTaskGetToolUnavailableError,
InvalidBuiltInTaskGetToolError,
executeBuiltInTaskGetTool,
} = require('../dist/tool-projection/taskGet.js');
function task(overrides = {}) {
return createTaskDefinitionRecord(
{
projectId: 'default',
taskId: 'task-1',
expectedRevision: 1,
mutationId: '123e4567-e89b-42d3-a456-426614174301',
name: 'Example Task',
description: 'private description',
kind: 'script',
spec: {
schema: 'qinglong/script@v1',
config: { command: 'private command' },
},
labels: { private: 'label' },
enabled: true,
occurredAtMs: 20,
...overrides,
},
10,
);
}
test('defines an exact low-risk task.read Tool', () => {
assert.equal(BUILTIN_TASK_GET_TOOL_DEFINITION.name, 'qinglong.task.get');
assert.equal(BUILTIN_TASK_GET_TOOL_DEFINITION.version, '1.0.0');
assert.equal(BUILTIN_TASK_GET_TOOL_DEFINITION.effect, 'read');
assert.equal(BUILTIN_TASK_GET_TOOL_DEFINITION.risk, 'low');
assert.deepEqual(BUILTIN_TASK_GET_TOOL_DEFINITION.requiredPermissions, [
'task.read',
]);
});
test('reads one current Task and omits private definition fields', async () => {
const definition = task({ enabled: false });
let captured;
const output = await executeBuiltInTaskGetTool(
{
async findCurrentTaskDefinition(projectId, taskId) {
captured = [projectId, taskId];
return definition;
},
},
'default',
{ taskId: 'task-1' },
);
assert.deepEqual(captured, ['default', 'task-1']);
assert.deepEqual(output, {
found: true,
taskId: 'task-1',
revision: 2,
name: 'Example Task',
kind: 'script',
specSchema: 'qinglong/script@v1',
enabled: false,
contentDigest: definition.contentDigest,
createdAtMs: 10,
updatedAtMs: 20,
});
const serialized = JSON.stringify(output);
for (const hidden of [
'private description',
'private command',
'private',
'mutationId',
]) {
assert.equal(serialized.includes(hidden), false);
}
});
test('masks absence and maps invalid or unavailable reads', async () => {
assert.deepEqual(
await executeBuiltInTaskGetTool(
{ async findCurrentTaskDefinition() { return null; } },
'default',
{ taskId: 'task-absent' },
),
{ found: false },
);
await assert.rejects(
executeBuiltInTaskGetTool(
{ async findCurrentTaskDefinition() { return null; } },
'default',
{ taskId: '', extra: true },
),
InvalidBuiltInTaskGetToolError,
);
await assert.rejects(
executeBuiltInTaskGetTool(
{ async findCurrentTaskDefinition() { throw new Error('offline'); } },
'default',
{ taskId: 'task-1' },
),
BuiltInTaskGetToolUnavailableError,
);
});
@@ -0,0 +1,162 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
BUILTIN_TASK_LIST_TOOL_DEFINITION,
BuiltInTaskListToolUnavailableError,
InvalidBuiltInTaskListToolError,
executeBuiltInTaskListTool,
} = require('../dist/tool-projection/taskList.js');
function definition(taskId, overrides = {}) {
return Object.freeze({
projectId: 'default',
taskId,
revision: 2,
mutationId: 'private-mutation',
name: `Task ${taskId}`,
description: 'private description',
kind: 'script',
spec: Object.freeze({
schema: 'qinglong/script@v1',
config: Object.freeze({ command: 'private command' }),
}),
labels: Object.freeze({ private: 'label' }),
enabled: true,
contentDigest: 'private-digest',
createdAtMs: 10,
updatedAtMs: 20,
...overrides,
});
}
test('defines an exact low-risk task.read Tool', () => {
assert.equal(BUILTIN_TASK_LIST_TOOL_DEFINITION.name, 'qinglong.task.list');
assert.equal(BUILTIN_TASK_LIST_TOOL_DEFINITION.version, '1.0.0');
assert.equal(BUILTIN_TASK_LIST_TOOL_DEFINITION.effect, 'read');
assert.equal(BUILTIN_TASK_LIST_TOOL_DEFINITION.risk, 'low');
assert.deepEqual(BUILTIN_TASK_LIST_TOOL_DEFINITION.requiredPermissions, [
'task.read',
]);
});
test('projects bounded current Tasks without private definition fields', async () => {
let captured;
const output = await executeBuiltInTaskListTool(
{
async listTaskDefinitions(query) {
captured = query;
return Object.freeze({
definitions: Object.freeze([definition('task-b')]),
truncated: true,
next: Object.freeze({ taskId: 'task-b' }),
});
},
},
'default',
{ after: { taskId: 'task-a' }, limit: 1 },
);
assert.deepEqual(captured, {
projectId: 'default',
limit: 1,
after: { taskId: 'task-a' },
});
assert.deepEqual(output, {
tasks: [
{
taskId: 'task-b',
revision: 2,
name: 'Task task-b',
kind: 'script',
specSchema: 'qinglong/script@v1',
enabled: true,
updatedAtMs: 20,
},
],
hasMore: true,
next: { taskId: 'task-b' },
});
const serialized = JSON.stringify(output);
for (const hidden of [
'private-mutation',
'private description',
'private command',
'private-digest',
'label',
]) {
assert.equal(serialized.includes(hidden), false);
}
});
test('defaults to 32 and returns no cursor for a complete page', async () => {
let captured;
const output = await executeBuiltInTaskListTool(
{
async listTaskDefinitions(query) {
captured = query;
return { definitions: [], truncated: false };
},
},
'default',
{},
);
assert.deepEqual(captured, { projectId: 'default', limit: 32 });
assert.deepEqual(output, { tasks: [], hasMore: false });
});
test('rejects invalid input before reading', async () => {
let reads = 0;
const source = {
async listTaskDefinitions() {
reads += 1;
return { definitions: [], truncated: false };
},
};
await assert.rejects(
executeBuiltInTaskListTool(source, 'default', { limit: 65 }),
InvalidBuiltInTaskListToolError,
);
await assert.rejects(
executeBuiltInTaskListTool(source, 'default', { after: { taskId: '' } }),
InvalidBuiltInTaskListToolError,
);
assert.equal(reads, 0);
});
test('fails closed on cross-Project, unordered, oversized or inconsistent pages', async () => {
for (const page of [
{
definitions: [definition('task-a', { projectId: 'other' })],
truncated: false,
},
{
definitions: [definition('task-b'), definition('task-a')],
truncated: false,
},
{
definitions: [definition('task-a'), definition('task-b')],
truncated: false,
},
{ definitions: [definition('task-a')], truncated: true },
{
definitions: [definition('task-a')],
truncated: true,
next: { taskId: 'other' },
},
]) {
await assert.rejects(
executeBuiltInTaskListTool(
{
async listTaskDefinitions() {
return page;
},
},
'default',
page.definitions.length === 2 && page.definitions[0].taskId === 'task-a'
? { limit: 1 }
: {},
),
BuiltInTaskListToolUnavailableError,
);
}
});
@@ -0,0 +1,194 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
BUILTIN_TRIGGER_LIST_DEFAULT_LIMIT,
BUILTIN_TRIGGER_LIST_MAX_LIMIT,
BUILTIN_TRIGGER_LIST_TOOL,
BUILTIN_TRIGGER_LIST_TOOL_DEFINITION,
BuiltInTriggerListToolUnavailableError,
InvalidBuiltInTriggerListToolError,
executeBuiltInTriggerListTool,
} = require('../dist/tool-projection/triggerList.js');
function trigger(triggerId, overrides = {}) {
return Object.freeze({
projectId: 'default',
triggerId,
revision: 2,
mutationId: 'private-mutation',
taskId: 'task-1',
taskRevision: 3,
taskContentDigest: 'private-task-digest',
spec: Object.freeze({
schema: 'qinglong/cron@v1',
config: Object.freeze({
expression: 'private cron expression',
timezone: 'private timezone',
misfire: 'private misfire policy',
}),
}),
enabled: true,
contentDigest: 'private-trigger-digest',
createdAtMs: 10,
updatedAtMs: 20,
...overrides,
});
}
test('defines one bounded low-risk trigger.read Tool', () => {
assert.deepEqual(BUILTIN_TRIGGER_LIST_TOOL, {
name: 'qinglong.trigger.list',
version: '1.0.0',
});
assert.equal(BUILTIN_TRIGGER_LIST_TOOL_DEFINITION.effect, 'read');
assert.equal(BUILTIN_TRIGGER_LIST_TOOL_DEFINITION.risk, 'low');
assert.deepEqual(BUILTIN_TRIGGER_LIST_TOOL_DEFINITION.requiredPermissions, [
'trigger.read',
]);
assert.equal(BUILTIN_TRIGGER_LIST_DEFAULT_LIMIT, 32);
assert.equal(BUILTIN_TRIGGER_LIST_MAX_LIMIT, 64);
});
test('projects bounded current Triggers without schedule configuration', async () => {
let captured;
const output = await executeBuiltInTriggerListTool(
{
async listTriggers(query) {
captured = query;
return Object.freeze({
triggers: Object.freeze([trigger('trigger-b')]),
truncated: true,
next: Object.freeze({ triggerId: 'trigger-b' }),
});
},
},
'default',
{ after: { triggerId: 'trigger-a' }, limit: 1 },
);
assert.deepEqual(captured, {
projectId: 'default',
limit: 1,
after: { triggerId: 'trigger-a' },
});
assert.deepEqual(output, {
triggers: [
{
triggerId: 'trigger-b',
revision: 2,
taskId: 'task-1',
taskRevision: 3,
specSchema: 'qinglong/cron@v1',
enabled: true,
updatedAtMs: 20,
},
],
hasMore: true,
next: { triggerId: 'trigger-b' },
});
const serialized = JSON.stringify(output);
for (const hidden of [
'private',
'expression',
'timezone',
'misfire',
'contentDigest',
'projectId',
]) {
assert.equal(serialized.includes(hidden), false);
}
});
test('defaults to 32 and returns no cursor for a complete page', async () => {
let captured;
const output = await executeBuiltInTriggerListTool(
{
async listTriggers(query) {
captured = query;
return { triggers: [], truncated: false };
},
},
'default',
{},
);
assert.deepEqual(captured, { projectId: 'default', limit: 32 });
assert.deepEqual(output, { triggers: [], hasMore: false });
});
test('rejects invalid input before reading', async () => {
let reads = 0;
const source = {
async listTriggers() {
reads += 1;
return { triggers: [], truncated: false };
},
};
for (const input of [
null,
{ limit: 65 },
{ after: { triggerId: '' } },
{ after: { triggerId: 'trigger-a', extra: true } },
{ unexpected: true },
]) {
await assert.rejects(
executeBuiltInTriggerListTool(source, 'default', input),
InvalidBuiltInTriggerListToolError,
);
}
assert.equal(reads, 0);
});
test('fails closed on cross-Project, unordered, oversized or inconsistent pages', async () => {
for (const { page, input = {} } of [
{
page: {
triggers: [trigger('trigger-a', { projectId: 'other' })],
truncated: false,
},
},
{
page: {
triggers: [trigger('trigger-b'), trigger('trigger-a')],
truncated: false,
},
},
{
page: {
triggers: [trigger('trigger-a'), trigger('trigger-b')],
truncated: false,
},
input: { limit: 1 },
},
{
page: { triggers: [trigger('trigger-a')], truncated: true },
},
{
page: {
triggers: [trigger('trigger-a')],
truncated: true,
next: { triggerId: 'trigger-b' },
},
},
{
page: {
triggers: [
trigger('trigger-a', { spec: { schema: 'invalid', config: {} } }),
],
truncated: false,
},
},
]) {
await assert.rejects(
executeBuiltInTriggerListTool(
{
async listTriggers() {
return page;
},
},
'default',
input,
),
BuiltInTriggerListToolUnavailableError,
);
}
});