feat(ql3): expose run comparison over local mcp

This commit is contained in:
whyour
2026-08-14 16:23:22 +08:00
parent 3b0aa97303
commit a55613afaa
8 changed files with 285 additions and 11 deletions
+8 -3
View File
@@ -11,6 +11,7 @@
最新增量证据(2026-08-13): 最新增量证据(2026-08-13):
- D-309/ADR-0401(已接受):`qinglong.run.compare@1.0.0` 进入可选 `ql3-mcp` stdio 产品入口,复用 Runtime Core 的共享 Definition/projection 和既有 MCP 静态注册循环。每次调用重新执行 Owner credential 认证、`tool.call:qinglong.run.compare` + `run.read` Policy、durable allowed audit、credential fence confirm,再按 baseline→candidate 串行执行两个 Project-scoped SQLite 点查;错误稳定收敛为 `run_compare_unavailable`。该入口是 ADR-0347 的交互式只读 surface,只持久化安全 admission,不冒充 StepRun、encrypted Tool completion 或模型 Trace;内部 Copilot 的受信执行仍必须走完整 completion 链。实现不新增 package、依赖、migration、表、索引、连接、timer、listener、watcher、cache 或网络 endpoint,默认 Edge/Standalone 继续裁掉 MCP package,仅显式 `edge-mcp|standalone-mcp` 承担调用成本。Local MCP 46/46、最终 18-package clean build/test 退出 0、backend 1,206 pass/2 条件 skip/0 failpackage/dependency/Edge/Cluster deployment 审计零 finding;默认 Edge 为 2,589,812 bytes/315 files/56 modules/RSS 11,091,968 bytesEdge-MCP 为 7,219,977 bytes/792 files/217 modules/RSS 38,649,856 bytesStandalone-MCP 为 7,220,085 bytes/792 files/217 modules/RSS 38,043,648 bytes,均在各自门内。
- D-308/ADR-0400(已接受):首个只读 Copilot 的“最近成功/失败运行对比”不再依赖 Prompt 自由拼接两个查询。`qinglong.run.compare@1.0.0` 作为第二个受信内建 Tool,固定 `read/low``run.read``database.read` 和 5 秒 deadline;只有当前 Project Tool snapshot 显式包含 reviewed Definition、产品 composition 显式绑定 adapter 后才可执行。它按 baseline→candidate 串行复用两次有界 Run 点查,absent 与 cross-Project 均为 `found:false`,只输出低敏 Run projection、固定 changed fields 和可证明的 queue/execution/total duration delta;任一时间戳不完整或结束早于开始时不生成对应差值。输出明确标记 `ordered_independent_point_reads`,不冒充数据库事务快照。实现位于既有 `runtime-core/tool-execution/builtin-run-compare/`,只提供两个显式 subpath,不从 package root 导出,不新增 workspace package、依赖、表、migration、连接、timer、listener、watcher、cache 或低配设备常驻开销。最终 18-package clean build/test 退出 0backend 1,208 项为 1,206 pass/2 条件 skip/0 failpackage/dependency/Edge/Cluster deployment 审计零 finding。Edge、Edge AI、Edge MCP 制品为 2,589,812 / 3,121,108 / 7,209,862 bytes,均低于各自上限且未装配 subpath 被发布投影裁掉。 - D-308/ADR-0400(已接受):首个只读 Copilot 的“最近成功/失败运行对比”不再依赖 Prompt 自由拼接两个查询。`qinglong.run.compare@1.0.0` 作为第二个受信内建 Tool,固定 `read/low``run.read``database.read` 和 5 秒 deadline;只有当前 Project Tool snapshot 显式包含 reviewed Definition、产品 composition 显式绑定 adapter 后才可执行。它按 baseline→candidate 串行复用两次有界 Run 点查,absent 与 cross-Project 均为 `found:false`,只输出低敏 Run projection、固定 changed fields 和可证明的 queue/execution/total duration delta;任一时间戳不完整或结束早于开始时不生成对应差值。输出明确标记 `ordered_independent_point_reads`,不冒充数据库事务快照。实现位于既有 `runtime-core/tool-execution/builtin-run-compare/`,只提供两个显式 subpath,不从 package root 导出,不新增 workspace package、依赖、表、migration、连接、timer、listener、watcher、cache 或低配设备常驻开销。最终 18-package clean build/test 退出 0backend 1,208 项为 1,206 pass/2 条件 skip/0 failpackage/dependency/Edge/Cluster deployment 审计零 finding。Edge、Edge AI、Edge MCP 制品为 2,589,812 / 3,121,108 / 7,209,862 bytes,均低于各自上限且未装配 subpath 被发布投影裁掉。
- D-307/ADR-0399(已接受):物理 Edge release archive Gate 使用外部 Ed25519 签发、QingLong verify-only 的两阶段协议。`prepare` exact 重建 owner-private 统一物理报告,要求 direct release service start 已通过,并把 repository、40 位 Git revision、设备/boot、物理报告、release archive、实机 artifact tree/metadata/entrypoint 与 Node digest/version 编入无换行 canonical payload;私钥始终位于 HSM/KMS/离线 operator。`finalize` 以 operator-pinned SPKI 公钥重算 fingerprint,稳定读取并复核所有输入后验证 64-byte detached signature,任一 source/archive/evidence 漂移均失败关闭,输出 `0600` no-replace envelope。通过只把 `release_archive_signature` 替换为 `release_archive_signature_or_attestation``supported:false` 与其余 firmware、整机 flash、migration、断电、固定实机采集和 Cluster 容量 Gate 全部保留。基础 importer 同时把 Edge、SQLite 与 Plugin Package 三个 workload 的 platform/architecture 精确绑定到统一物理观测,release verifier 再要求 observed/Edge/SQLite 为完整 recorder shape 且 Node identity 相同,拒绝跨主机拼接与重算外层摘要后的最小伪造。实现不新增 package、依赖、daemon、listener、timer、watcher 或设备常驻负担;18-package clean build/test 退出 0backend 1,208 项为 1,206 pass/2 条件 skip/0 failpackage/dependency/Edge/service bridge/Cluster deployment 审计零 finding。 - D-307/ADR-0399(已接受):物理 Edge release archive Gate 使用外部 Ed25519 签发、QingLong verify-only 的两阶段协议。`prepare` exact 重建 owner-private 统一物理报告,要求 direct release service start 已通过,并把 repository、40 位 Git revision、设备/boot、物理报告、release archive、实机 artifact tree/metadata/entrypoint 与 Node digest/version 编入无换行 canonical payload;私钥始终位于 HSM/KMS/离线 operator。`finalize` 以 operator-pinned SPKI 公钥重算 fingerprint,稳定读取并复核所有输入后验证 64-byte detached signature,任一 source/archive/evidence 漂移均失败关闭,输出 `0600` no-replace envelope。通过只把 `release_archive_signature` 替换为 `release_archive_signature_or_attestation``supported:false` 与其余 firmware、整机 flash、migration、断电、固定实机采集和 Cluster 容量 Gate 全部保留。基础 importer 同时把 Edge、SQLite 与 Plugin Package 三个 workload 的 platform/architecture 精确绑定到统一物理观测,release verifier 再要求 observed/Edge/SQLite 为完整 recorder shape 且 Node identity 相同,拒绝跨主机拼接与重算外层摘要后的最小伪造。实现不新增 package、依赖、daemon、listener、timer、watcher 或设备常驻负担;18-package clean build/test 退出 0backend 1,208 项为 1,206 pass/2 条件 skip/0 failpackage/dependency/Edge/service bridge/Cluster deployment 审计零 finding。
- D-303/ADR-0391(已接受) - D-303/ADR-0391(已接受)
@@ -6900,9 +6901,13 @@ Copilot 默认不能直接提交修改或运行命令。写操作通过 Tool Reg
`next` 已由 ADR-0162/ADR-0400 提供两个 profile-neutral 受信只读原语: `next` 已由 ADR-0162/ADR-0400 提供两个 profile-neutral 受信只读原语:
`qinglong.run.get@1.0.0``qinglong.run.compare@1.0.0`。后者只接受两个不同 Run ID `qinglong.run.get@1.0.0``qinglong.run.compare@1.0.0`。后者只接受两个不同 Run ID
按固定顺序执行 Project-scoped 点查并返回低敏差异;它明确不声称事务快照,也不读取 按固定顺序执行 Project-scoped 点查并返回低敏差异;它明确不声称事务快照,也不读取
日志、error、command、Artifact 或 Secret。两者目前仍是产品组合显式装配的 Tool, 日志、error、command、Artifact 或 Secret。ADR-0401 已把 `run.compare` 装入可选、受认证的
不是默认向所有 Project、HTTP、MCP 或模型开放的 ambient capability。“最近成功与失败” 本机 `ql3-mcp` stdio surface:只有显式部署 `edge-mcp|standalone-mcp`、配置 Project 与 Owner
的有界选择、日志 range/redaction 和最终 Copilot Prompt 仍需后续产品 Gate。 credential 后才可发现,并且每次调用都经过 Tool Policy、durable Security Audit 和 credential
fence confirm;它没有进入默认 Edge/Standalone application,也未向 HTTP、Cluster 或任意模型
授予 ambient capability。该交互式 MCP 读取不冒充内部 Trusted Tool 的 StepRun/encrypted
completion。“最近成功与失败”的有界选择、日志 range/redaction 和最终 Copilot Prompt 仍需
后续产品 Gate。
## 17. Tool Registry ## 17. Tool Registry
@@ -0,0 +1,88 @@
# ADR-0401:可选本机 MCP Run 对比产品入口
- 状态:Accepted
- 日期:2026-08-14
- 关联 RFCQL-RFC-0001 D-309、Phase 2
- 关联 ADRADR-0347、ADR-0351、ADR-0400
## 问题
ADR-0400 已在 Runtime Core 建立 `qinglong.run.compare@1.0.0` 的稳定 Definition、低敏
projection 和受信 adapter,但尚无产品 composition 使用它。QingLong 3.0 的外部 AI 客户端
因此只能分别调用两个 Run 点查,再自行拼接差异;这无法让服务端统一执行输入约束、Project
屏蔽、差值算法和一致性声明,也没有独立的调用审计理由。
本机已经有由 ADR-0347 建立的可选 `ql3-mcp` stdio 进程。它逐调用执行认证、Tool Policy、
durable Security Audit、credential fence confirmation 和有界 SQLite read,且只在部署者显式
选择 `edge-mcp|standalone-mcp` 制品时加载 MCP SDK。新增对比能力不应再造进程、package、
数据库 authority 或后台服务,也不能把 MCP 依赖带入默认低配 Profile。
## 决策
1. 在既有 `@qinglong/local-mcp-server` 静态只读 Tool 集合中注册
`qinglong.run.compare@1.0.0`。Definition 与执行投影必须直接来自 Runtime Core 的
`/builtin-run-compare-projection` 显式 subpathMCP 不复制 schema、字段裁剪或差值算法。
2. 调用沿用同一固定顺序:每次重新认证 → 授权
`tool.call:qinglong.run.compare` → 授权 `run.read` → durable audit → confirm credential
fence → baseline/candidate 两次有序 Project-scoped point read。允许审计理由固定为
`tool_qinglong_run_compare`repository/投影失败只返回稳定
`run_compare_unavailable`
3. MCP Tool 继续声明 `readOnlyHint=true``destructiveHint=false`
`idempotentHint=true``openWorldHint=false`。Project ID 只来自私有进程配置,客户端不能
选择;找不到与跨 Project 均由共享投影输出 `found:false`
4. 该入口属于 ADR-0347 的轻量交互式只读 surface:它持久记录安全 admission,但不冒充
ADR-0163 的 encrypted Tool execution completion、StepRun 或模型 Trace。Agent/Copilot
内部经 Run/StepRun 执行受信 Tool 时仍必须走完整 start/result completion 链;未来如需让
MCP 调用本身成为 Run,必须独立设计 correlation 与重放语义,不能在本协议中静默增加写入。
5. 不新增 workspace package、依赖、migration、表、索引、连接、timer、listener、watcher、
cache 或网络 endpoint。`LocalSqliteMcpReadDatabase` 现有
`findRunById` authority 已足够,不扩大其读写接口。
## 低配与部署影响
- 默认 Edge/Standalone application 与制品继续不导入 MCP package;未选择 MCP 的路由设备
没有新增安装体积、模块加载或空闲 RSS。
- 选择 MCP 的设备只增加一个静态 descriptor;单次对比最多增加两次串行 Run 点查,不产生
并发数据库连接或后台采样。
- Cluster Control 不通过本 ADR 开放 MCP endpoint。Cluster 的受信 Tool composition、远程
身份、限流和 PostgreSQL completion 仍须独立门禁。
- 实现留在已有 deployable MCP package 内,不为单个 adapter 创建微型 package,也不把
MCP-only glue 移入 Runtime Core。
## 被否决方案
1. **复制 Run compare 到 MCP `tool-projection`**:该语义已有 Runtime Core adapter 和 MCP
两个消费者,复制会产生 Definition 与差值漂移。
2. **让客户端组合两次 `run.get`**:服务端无法冻结读取顺序、一致性声明和审计语义。
3. **把 MCP Server 加入默认 Edge application**:会让不使用 AI 的低配设备承担 MCP SDK
依赖与约 40 MiB 级加载成本。
4. **为 compare 新建 package 或数据库 adapter**:没有独立生命周期、authority 或依赖隔离
收益,现有窄 reader 已满足需求。
5. **把交互式 MCP 读取伪装成完整 Trusted Tool completion**:缺少 StepRun/Artifact/key/
replay correlation 事实,会给恢复和审计造成错误承诺。
## 当前验证
1. Local MCP 46/46Tool discovery、固定 read-only annotations、双 permission、allowed audit
reason、credential confirm、两个有序读取和共享差值投影全部通过。真实 stdio E2E 使用
fresh production migration SQLite、真实 Owner credential 与 Project Policy,比较两个真实
Run 后验证第九条 durable allowed audit。
2. 最终 18-package clean build/test 退出 0backend 1,208 项为 1,206 pass、2 条平台条件
skip、0 fail。package boundary 保持 18 个 package`singleSourcePackages=[]`
`shallowSourcePackages=[]`Cluster dependency、Edge import 与 Cluster deployment 审计
均无 finding。
3. 默认 Edge artifact 为 2,589,812 bytes/315 files/56 loaded modulesimport RSS 增量
11,091,968 bytes,继续完全裁掉 MCP Server 与 SDK。Edge-MCP 为 7,219,977 bytes/
792 files/217 modules、RSS 增量 38,649,856 bytesStandalone-MCP 为 7,220,085 bytes/
792 files/217 modules、RSS 增量 38,043,648 bytes,均低于 16 MiB/1,536 files/48 MiB 门。
4. GitNexus 对 `LOCAL_MCP_READ_TOOLS` 报告 LOW、0 上游;
`createQingLongLocalMcpServer` 为 LOW、1 个直接产品调用方、0 条 execution flow。
dependency firewall 的 `auditSourceImports` 为 LOW、1 个直接测试调用方、0 条 flow;
allowlist 只增加 exact compare projection subpath,宽 authority 负向门仍通过 53/53。
## 后续门禁
1. 增加按 Task 有界选择最近成功/失败 Run 的服务端 Tool,禁止模型执行无界搜索;
2. 日志解释另建 Artifact range、redaction、prompt-injection 和字节预算协议;
3. Copilot 调用走 Run/StepRun 与 encrypted result completion,建立模型/Tool Trace correlation
4. 固定物理 Edge MCP 延迟/RSS 和真实撤权竞态证据。
+1
View File
@@ -404,6 +404,7 @@
| [ADR-0398](./ADR-0398-pre-activation-plugin-package-candidate-qualification.md) | Plugin Package 激活前候选资格校验与自动保留旧版本 | Proposed(v2 私有报告/离线审计已编排、全量/HA 门完成;远端 Kubernetes 成功记录与物理低配门待闭合) | | [ADR-0398](./ADR-0398-pre-activation-plugin-package-candidate-qualification.md) | Plugin Package 激活前候选资格校验与自动保留旧版本 | Proposed(v2 私有报告/离线审计已编排、全量/HA 门完成;远端 Kubernetes 成功记录与物理低配门待闭合) |
| [ADR-0399](./ADR-0399-external-release-attestation-for-physical-edge-evidence.md) | 物理 Edge 证据的外部发布归档证明 | Acceptedexact recorder 重建与离线验签已实现;固定实机 ceremony 待执行) | | [ADR-0399](./ADR-0399-external-release-attestation-for-physical-edge-evidence.md) | 物理 Edge 证据的外部发布归档证明 | Acceptedexact recorder 重建与离线验签已实现;固定实机 ceremony 待执行) |
| [ADR-0400](./ADR-0400-bounded-trusted-run-comparison-tool.md) | 有界、受信的 Run 对比 Tool | Accepted | | [ADR-0400](./ADR-0400-bounded-trusted-run-comparison-tool.md) | 有界、受信的 Run 对比 Tool | Accepted |
| [ADR-0401](./ADR-0401-optional-local-mcp-run-comparison-surface.md) | 可选本机 MCP Run 对比产品入口 | Accepted |
## 规则 ## 规则
@@ -45,6 +45,11 @@ import {
BUILTIN_RUN_STEP_LIST_TOOL_DEFINITION, BUILTIN_RUN_STEP_LIST_TOOL_DEFINITION,
executeBuiltInRunStepListTool, executeBuiltInRunStepListTool,
} from '../tool-projection/runStepList'; } from '../tool-projection/runStepList';
import {
BUILTIN_RUN_COMPARE_TOOL,
BUILTIN_RUN_COMPARE_TOOL_DEFINITION,
executeBuiltInRunCompareTool,
} from '@qinglong/runtime-core/builtin-run-compare-projection';
import { import {
BUILTIN_RUN_READ_TOOL, BUILTIN_RUN_READ_TOOL,
BUILTIN_RUN_READ_TOOL_DEFINITION, BUILTIN_RUN_READ_TOOL_DEFINITION,
@@ -177,6 +182,18 @@ const LOCAL_MCP_READ_TOOLS: readonly LocalMcpReadToolDescriptor[] =
input: ToolJsonValue, input: ToolJsonValue,
) => executeBuiltInRunReadTool(authority.runs, projectId, input), ) => executeBuiltInRunReadTool(authority.runs, projectId, input),
}), }),
Object.freeze({
tool: BUILTIN_RUN_COMPARE_TOOL,
definition: BUILTIN_RUN_COMPARE_TOOL_DEFINITION,
title: 'Compare QingLong Runs',
auditReason: 'tool_qinglong_run_compare',
unavailableCode: 'run_compare_unavailable',
execute: (
authority: LocalMcpReadAuthority,
projectId: string,
input: ToolJsonValue,
) => executeBuiltInRunCompareTool(authority.runs, projectId, input),
}),
Object.freeze({ Object.freeze({
tool: BUILTIN_RUN_EVENT_LIST_TOOL, tool: BUILTIN_RUN_EVENT_LIST_TOOL,
definition: BUILTIN_RUN_EVENT_LIST_TOOL_DEFINITION, definition: BUILTIN_RUN_EVENT_LIST_TOOL_DEFINITION,
@@ -171,6 +171,16 @@ function fixture(options = {}) {
let approvalListReads = 0; let approvalListReads = 0;
let approvalDetailReads = 0; let approvalDetailReads = 0;
let confirmations = 0; let confirmations = 0;
const candidateRun = Object.freeze({
...run(),
id: 'run-2',
taskRevision: 'revision-2',
priority: 1,
createdAtMs: 20,
queuedAtMs: 21,
startedAtMs: 24,
finishedAtMs: 30,
});
const server = createQingLongLocalMcpServer({ const server = createQingLongLocalMcpServer({
projectId: 'default', projectId: 'default',
now: () => NOW, now: () => NOW,
@@ -209,13 +219,14 @@ function fixture(options = {}) {
async listRunsByProject(query) { async listRunsByProject(query) {
events.push('read-list'); events.push('read-list');
listReads += 1; listReads += 1;
const values = [{ ...run(), id: 'run-2', createdAtMs: 20 }, run()]; const values = [candidateRun, run()];
return values.slice(0, query.limit); return values.slice(0, query.limit);
}, },
async findRunById(runId) { async findRunById(runId) {
events.push('read'); events.push('read');
reads += 1; reads += 1;
return runId === 'run-1' ? run(options.runProjectId) : null; if (runId === 'run-1') return run(options.runProjectId);
return runId === 'run-2' ? candidateRun : null;
}, },
async listEvents(runId, query) { async listEvents(runId, query) {
events.push('read-events'); events.push('read-events');
@@ -381,6 +392,7 @@ test('advertises bounded read-only Run Tools and executes auth -> Policy -> Audi
[ [
'qinglong.run.list', 'qinglong.run.list',
'qinglong.run.get', 'qinglong.run.get',
'qinglong.run.compare',
'qinglong.run.events.list', 'qinglong.run.events.list',
'qinglong.run.steps.list', 'qinglong.run.steps.list',
'qinglong.task.get', 'qinglong.task.get',
@@ -448,6 +460,88 @@ test('advertises bounded read-only Run Tools and executes auth -> Policy -> Audi
}); });
}); });
test('compares two 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.compare',
arguments: {
baselineRunId: 'run-1',
candidateRunId: 'run-2',
},
});
assert.equal(response.result.isError, undefined);
assert.deepEqual(response.result.structuredContent, {
baseline: {
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,
},
candidate: {
found: true,
id: 'run-2',
taskId: 'task-1',
taskRevision: 'revision-2',
status: 'succeeded',
version: 3,
eventSequence: 4,
priority: 1,
executionOrigin: 'manual',
executionOwner: 'runtime',
createdAtMs: 20,
queuedAtMs: 21,
startedAtMs: 24,
finishedAtMs: 30,
},
comparable: true,
sameTask: true,
sameTaskRevision: false,
changedFields: ['taskRevision', 'priority'],
queueDelayDeltaMs: 0,
executionDurationDeltaMs: 5,
totalDurationDeltaMs: 7,
consistency: 'ordered_independent_point_reads',
});
assert.deepEqual(value.permissions, [
'tool.call:qinglong.run.compare',
'run.read',
]);
assert.deepEqual(value.events, [
'authenticate',
'policy:tool.call:qinglong.run.compare',
'policy:run.read',
'audit:allowed',
'confirm',
'read',
'read',
]);
assert.deepEqual(value.audits[0].reasons, [
'tool_invocation_allowed',
'tool_qinglong_run_compare',
]);
assert.deepEqual(value.counters(), {
reads: 2,
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) => { test('discovers recent Project Runs through the same fenced admission', async (t) => {
const value = fixture(); const value = fixture();
const connected = await client(value.server, t); const connected = await client(value.server, t);
@@ -461,17 +555,17 @@ test('discovers recent Project Runs through the same fenced admission', async (t
{ {
id: 'run-2', id: 'run-2',
taskId: 'task-1', taskId: 'task-1',
taskRevision: 'revision-1', taskRevision: 'revision-2',
status: 'succeeded', status: 'succeeded',
version: 3, version: 3,
eventSequence: 4, eventSequence: 4,
priority: 0, priority: 1,
executionOrigin: 'manual', executionOrigin: 'manual',
executionOwner: 'runtime', executionOwner: 'runtime',
createdAtMs: 20, createdAtMs: 20,
queuedAtMs: 11, queuedAtMs: 21,
startedAtMs: 12, startedAtMs: 24,
finishedAtMs: 13, finishedAtMs: 30,
}, },
], ],
hasMore: true, hasMore: true,
@@ -74,6 +74,22 @@ async function fixture(t) {
priority: 0, priority: 0,
createdAtMs: NOW - 2_000, createdAtMs: NOW - 2_000,
}); });
await transaction.insertRun({
id: 'run-mcp-e2e-candidate',
projectId: 'default',
taskId: 'task-mcp',
taskRevision: 'revision-2',
taskName: 'MCP test candidate',
triggerType: 'manual',
executionOrigin: 'manual',
executionOwner: 'runtime',
triggeredBy: 'user:mcp-owner',
status: 'created',
version: 0,
eventSequence: 0,
priority: 1,
createdAtMs: NOW - 3_000,
});
await transaction.appendEvent({ await transaction.appendEvent({
id: 'mcp-e2e-event-1', id: 'mcp-e2e-event-1',
runId: 'run-mcp-e2e', runId: 'run-mcp-e2e',
@@ -456,6 +472,7 @@ test('serves the authenticated Run Tool over the real stdio protocol and persist
[ [
'qinglong.run.list', 'qinglong.run.list',
'qinglong.run.get', 'qinglong.run.get',
'qinglong.run.compare',
'qinglong.run.events.list', 'qinglong.run.events.list',
'qinglong.run.steps.list', 'qinglong.run.steps.list',
'qinglong.task.get', 'qinglong.task.get',
@@ -602,7 +619,11 @@ test('serves the authenticated Run Tool over the real stdio protocol and persist
createdAtMs: NOW - 2_000, createdAtMs: NOW - 2_000,
}, },
], ],
hasMore: false, hasMore: true,
next: {
createdAtMs: NOW - 2_000,
runId: 'run-mcp-e2e',
},
}); });
const called = await request('tools/call', { const called = await request('tools/call', {
name: 'qinglong.run.get', name: 'qinglong.run.get',
@@ -622,6 +643,47 @@ test('serves the authenticated Run Tool over the real stdio protocol and persist
executionOwner: 'runtime', executionOwner: 'runtime',
createdAtMs: NOW - 2_000, createdAtMs: NOW - 2_000,
}); });
const compared = await request('tools/call', {
name: 'qinglong.run.compare',
arguments: {
baselineRunId: 'run-mcp-e2e',
candidateRunId: 'run-mcp-e2e-candidate',
},
});
assert.equal(compared.result.isError, undefined, JSON.stringify(compared));
assert.deepEqual(compared.result.structuredContent, {
baseline: {
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,
},
candidate: {
found: true,
id: 'run-mcp-e2e-candidate',
taskId: 'task-mcp',
taskRevision: 'revision-2',
status: 'created',
version: 0,
eventSequence: 0,
priority: 1,
executionOrigin: 'manual',
executionOwner: 'runtime',
createdAtMs: NOW - 3_000,
},
comparable: true,
sameTask: true,
sameTaskRevision: false,
changedFields: ['taskRevision', 'priority'],
consistency: 'ordered_independent_point_reads',
});
const events = await request('tools/call', { const events = await request('tools/call', {
name: 'qinglong.run.events.list', name: 'qinglong.run.events.list',
arguments: { runId: 'run-mcp-e2e', limit: 1 }, arguments: { runId: 'run-mcp-e2e', limit: 1 },
@@ -698,6 +760,11 @@ test('serves the authenticated Run Tool over the real stdio protocol and persist
outcome: 'allowed', outcome: 'allowed',
subjectId: 'mcp-user', subjectId: 'mcp-user',
}, },
{
operationId: 'mcp.tool.call',
outcome: 'allowed',
subjectId: 'mcp-user',
},
], ],
); );
} finally { } finally {
+1
View File
@@ -1013,6 +1013,7 @@ function auditSourceImports(root, packagePath, findings) {
'@qinglong/runtime-core/bounded-run-event-list-projection', '@qinglong/runtime-core/bounded-run-event-list-projection',
'@qinglong/runtime-core/bounded-run-list-projection', '@qinglong/runtime-core/bounded-run-list-projection',
'@qinglong/runtime-core/bounded-run-step-list-projection', '@qinglong/runtime-core/bounded-run-step-list-projection',
'@qinglong/runtime-core/builtin-run-compare-projection',
'@qinglong/runtime-core/builtin-run-read-projection', '@qinglong/runtime-core/builtin-run-read-projection',
'@qinglong/runtime-core/bounded-task-read-projection', '@qinglong/runtime-core/bounded-task-read-projection',
'@qinglong/runtime-core/bounded-task-list-projection', '@qinglong/runtime-core/bounded-task-list-projection',
@@ -192,6 +192,7 @@ test('confines local MCP to its reviewed protocol and read-authority subpaths',
"import { approval } from '@qinglong/runtime-core/approved-action';", "import { approval } from '@qinglong/runtime-core/approved-action';",
"import { runs } from '@qinglong/runtime-core/bounded-run-list-projection';", "import { runs } from '@qinglong/runtime-core/bounded-run-list-projection';",
"import { run } from '@qinglong/runtime-core/run';", "import { run } from '@qinglong/runtime-core/run';",
"import { compare } from '@qinglong/runtime-core/builtin-run-compare-projection';",
"import { tool } from '@qinglong/runtime-core/builtin-run-read-projection';", "import { tool } from '@qinglong/runtime-core/builtin-run-read-projection';",
"import { tasks } from '@qinglong/runtime-core/bounded-task-list-projection';", "import { tasks } from '@qinglong/runtime-core/bounded-task-list-projection';",
"import { task } from '@qinglong/runtime-core/bounded-task-read-projection';", "import { task } from '@qinglong/runtime-core/bounded-task-read-projection';",