feat(ql3): admit copilot diagnosis runs

This commit is contained in:
whyour
2026-08-15 15:07:49 +08:00
parent aae96390cc
commit 0163c94be7
24 changed files with 3078 additions and 22 deletions
+10 -2
View File
@@ -6,11 +6,12 @@
- 目标版本:QingLong 3.x
- 作者:QingLong Maintainers
- 创建日期:2026-07-17
- 最后更新:2026-08-14
- 最后更新:2026-08-15
- 讨论范围:架构与演进路线,不包含最终 UI 视觉方案
最新增量证据(2026-08-14):
最新增量证据(2026-08-15):
- D-315/ADR-0407(已接受):Cluster Copilot 故障诊断不修改或重新打开终态源 Run,而是用源 Run/version、最新已结束 Attempt/status 与日志 Artifact 建立 exact fence,原子创建独立 `copilot_failure_diagnosis` Run、admission event、`ready``qinglong.run.log.excerpt@1.0.0` Tool Step、以其为父节点的 `pending` Model Step 和不可变 receipt。计划只接受 `cluster-control` 的 reviewed snapshot/binding/Policy/subject authority`approval_required`、Tool contract/输入漂移或未显式允许的模型出口均在写库前失败关闭。`ql3_ai` 新增有界 admission ledger 与仅授予 runtime 的 `SECURITY DEFINER` source snapshot,在 SERIALIZABLE 事务内重验 active Project/binding 和源 Run/Attemptresponse-loss replay 对 JSONB 做结构 exact 比较而不依赖无语义的对象键序。Copilot/Plugin 这类 StepRun 聚合从通用 Task orphan recovery 排除,后续由各自状态机恢复,不能伪造顶层 Attempt 或被错误终态化。本阶段只 admission,不执行 Tool/模型、不授予行动权;Tool encrypted completion、Model 解锁和 Copilot encrypted model completion 是下一 Gate。最终 18-package clean build/test 与 backend 1,207 pass/2 条件 skip/0 fail,四项边界审计零 finding且 workspace 仍为 18 package、无单文件/浅平 package14 档 Local Profile artifact 全通过,默认 Edge/Standalone 不引入 Cluster admission。PostgreSQL 18.4 arm64 HA 128/128、timeline `1→2`,报告 SHA-256 为 `a4ed1edec783e3f5b42507c0f8e11b94c59dbe44a57e691017d1445ec9d115e2`,证据审计与 Docker 清理通过。
- D-314/ADR-0406(已接受):Cluster Trusted Tool 的 encrypted completion 不再停留在 storage port。`@qinglong/cluster-control/trusted-tool-result-keyring` 新增只读 projected material authoritycanonical v1 manifest 只含最多 16 个 canonical 32-byte key,不含 generation、active/state/retirementprovider 只有 `resolve(keyId)` 而没有 `active()`;因此 PostgreSQL `trusted-tool-results` catalog 仍是 active/decryptable 状态唯一 authoritycompletion 会以 catalog material proof 再次校验。runtime 每次调用重新执行 direct-root、in-root atomic symlink、single-link、64 KiB、只读/不可执行/other-inaccessible mode、dev/inode/size/mtime 与双 realpath fence,不持有 Kubernetes API、cache、watcher 或 timer。新增能力位于 Cluster Control 既有 `trusted-tool/key-management/`,并把 mounted Secret 与 keyring 的 projected-file/TOCTOU 逻辑收敛到 package-private `security/privateProjectedFile` 真源;公开 mounted Secret 行为不变,不新增 package、依赖、migration、连接、route 或默认 Profile importer。定向共享回归 7/7Cluster Control 完整 234 pass/2 条条件 skip/0 fail;最终 18-package clean build/test 与 backend 1,207 pass/2 条件 skip/0 failpackage/dependency/Edge import/Cluster deployment 四项审计零 finding。workspace 仍为 18 package、无单文件或浅平 packageCluster Control 54 个源码中仅 2 个 binary entry 位于根层,52 个处于嵌套领域目录。14 档 Local Profile artifact 全部通过,默认 Edge/Standalone 保持 2,589,812/2,589,890 bytesAI 保持 3,121,108/3,121,198 bytesMCP 保持 7,315,930/7,316,038 bytes。PostgreSQL 18.4 arm64 HA 125/125、timeline `1→2`,报告 SHA-256 为 `26c817647ed984d8d4627a7cae1c95de06017a5d6d32dd3dfd01414ba029e542`,证据审计与 Docker 容器/网络/卷零残留。下一 Gate 是独立 diagnosis Run 的 Tool/Model Step admission 与 Copilot encrypted model completion,不能借用终态源 Run 或 Plugin Prompt plan。
- D-313/ADR-0405(已接受):新增 `@qinglong/ai/failure-diagnosis-prompt`,把 ADR-0403 的潜在敏感日志投影收敛为固定 system instruction + canonical JSON data envelope;日志只存在于 `log.content` JSON string value,不能通过引号、换行、伪造 role/schema 或 delimiter 拼接出新 message。builder 重新校验完整 trust/redaction/profile byte 契约,拒绝伪造 `safe`、行动权、未知字段与 byte/signal driftenvelope 不带 Run/Attempt、Artifact、path、cursor 或 content digest。部署者必须通过 `qinglong/copilot-model-egress-policy@v1` 显式允许 `potentially_sensitive` 数据进入 `on_device|external` 边界并提供输入/output token 双预算,空 allowlist 与 external 未授权均在 Model Gateway/Provider I/O 前失败关闭。输出只含 content-free egress evidence,并固定要求模型 completion 继承潜在敏感、仅加密持久化、禁止明文审计且无行动权;真正 Cluster Trusted Tool/model completion 仍需后续组合门。能力以 `ql3-ai/src/copilot/failure-diagnosis/` 三个内聚文件和精确 subpath 交付,不新增 package、依赖、迁移、连接或常驻组件。定向 12/12、AI 221 pass/3 条件 skip/0 fail;最终 18-package clean build/test 与 backend 1,207 pass/2 条件 skip/0 fail,四项结构/部署审计零 finding14 档 Local Profile artifact 全部通过。默认 Edge/Standalone 保持 2,589,812/2,589,890 bytes、315 files、56 modulesEdge/Standalone AI 保持 3,121,108/3,121,198 bytes、368 files、61 modulesMCP 保持 7,315,930/7,316,038 bytes、801 files、226 modules,证明未装配 subpath 被完全裁掉。PostgreSQL 18.4 arm64 HA 125/125、timeline `1→2`,报告 SHA-256 为 `2bbc8bdd0d90e6ec9ce82d2afcaec817679dddb82860c5d405a09d5e5458bece`,证据审计与 Docker 零残留。
- D-312/ADR-0404(已接受):`qinglong.run.log.excerpt@1.0.0` 进入显式可选的本机 `ql3-mcp` stdio 产品入口。每次调用固定经过 Owner credential authentication、exact `tool.call:qinglong.run.log.excerpt` + `artifact.read` Policy、durable audit、credential/Pepper fence confirm,再复用同一 SQLite authority 和私有 Artifact reader 完成 ADR-0403 的 Edge 4 KiB/Standalone 8 KiB 双读取安全投影。配置升级为 `qinglong/local-mcp-server@v2` 并要求显式 private `artifactRoot`,旧 v1 不猜测路径而是失败关闭。产物实证否决了 MCP 直接依赖 `local-execution` 的方案:该方案会带入 process/scheduler/croner,达到 7,469,105 bytes/816 files/228 modules;唯一 reader 实现因此归入既有 `local-command-file/artifact-read` 私有文件 authorityExecution 通过兼容 re-export 复用,workspace 仍为 18 package 且没有根层平铺。Local MCP 48/48、Local Execution 41/41、私有文件 3/3、依赖防火墙 54/54;最终 18-package clean build/test 与 backend 1,207 pass/2 条件 skip/0 fail,四项结构/部署审计零 finding14 个 Local Profile artifact 全部通过。默认 Edge/Standalone 保持 2,589,812/2,589,890 bytes、315 files、56 modulesEdge/Standalone MCP 为 7,315,930/7,316,038 bytes、801 files、226 modules、RSS 38,420,480/39,567,360 bytes,闭包不含 `local-execution``local-process``croner`。PostgreSQL 18.4 arm64 HA 125/125、timeline `1→2`,报告 SHA-256 为 `29cd77d80737a3b1ab686c998d05a78c52deffd8add3b31d8035756d5dfcc433`,证据审计与 Docker 零残留。
@@ -6937,6 +6938,13 @@ ADR-0406 已关闭其中的 Cluster result-key material 缺口:只读 projecte
Copilot execution authority 已完成;下一 Gate 必须创建独立 diagnosis Run 的 Tool/Model Step admission
再把 S3 Tool completion、ADR-0405 builder、Model Gateway 与 Copilot 专用 encrypted model completion
串成可恢复闭环。
ADR-0407 已完成该 admission Gate:源失败/超时 Run 和最新已结束 Attempt 只作为不可变 source fence
独立 diagnosis Run 在一个 SERIALIZABLE 事务内同时获得 admission event、ready Tool Step、依赖它的
pending Model Step 与 receiptCluster runtime 的 source snapshot authority 会重验 active Project/binding
和日志 Artifactresponse-loss replay 以结构语义比较 JSONB。该 Run 是 StepRun 编排聚合,不进入通用
Task orphan recovery。当前阶段仍没有执行 Tool 或模型;下一 Gate 必须先形成可信 encrypted Tool
completion,再原子解锁 Model Step,并以 ADR-0405 egress policy 和 Copilot 专用 encrypted model
completion 收敛最终 Run。
## 17. Tool Registry
@@ -0,0 +1,87 @@
# ADR-0407Cluster Copilot Failure Diagnosis Run Admission
- 状态:Accepted
- 日期:2026-08-15
- 关联 RFCQL-RFC-0001 D-315、Phase 2
- 关联 ADRADR-0163、ADR-0226、ADR-0403、ADR-0404、ADR-0405、ADR-0406
## 问题
ADR-0403~0406 已经给出有界日志摘录 Tool、显式模型出口策略和 Cluster result-key material
authority,但仍缺少把一次故障诊断变成 durable execution 的入口。源 Run 已经处于 `failed`
`timed_out` 终态,不能向它追加 Tool/Model Step,也不能复用 Plugin Package Prompt admission
冒充 Copilot 工作流。
若仅在 API 内顺序调用 Tool 和模型,进程崩溃、响应丢失或 PostgreSQL failover 会让系统无法证明
已经接受了什么、下一步应该执行什么;若 admission 直接启动模型,模型又可能在 Tool 结果尚未形成
可信 encrypted completion 时读取未经 fencing 的数据。通用 Task recovery 也不能把这种以 StepRun
编排、没有顶层 RunAttempt 的聚合 Run 当成普通孤儿任务。
## 决策
1. 故障诊断创建独立 `copilot_failure_diagnosis` Run,并以源 Run 作为 `parentRunId`。源 fence 必须精确
固定 Project、Run ID/version、`failed|timed_out` 状态、最新且已结束的 Attempt、兼容的 Attempt
状态和非空日志 Artifact;admission 不修改或重新打开源 Run。
2. 计划只接受 `cluster-control` Profile 中经过 snapshot、Policy、subject 与 binding fencing 的
`qinglong.run.log.excerpt@1.0.0` 内建只读 Tool。`approval_required`、不同 Tool/version、不同
adapter/redaction/audit contract、输入 Artifact 未绑定源 Attempt 或权限漂移全部在数据库写入前
失败关闭。
3. 模型 intent 固定 provider/model、`on_device|external` 边界、响应语言、输出 token 上限和 ADR-0405
egress policy digest。计划与 receipt 均有 domain-separated digest 和字节上限;它们声明最终模型
completion 必须加密、审计不得保存明文且 `actionAuthority=none`,本阶段不授予命令或写 Tool 权限。
4. 一个 SERIALIZABLE PostgreSQL 事务原子创建 diagnosis Run、admission event、`collect-log` Tool
StepRun、`diagnose` Model StepRun 和 admission receipt。Tool Step 初始为 `ready`Model Step 以 Tool
Step 为父节点且初始为 `pending`。本阶段只 admission,不执行 Tool/模型;后续只有可信 Tool
completion 成功后才能解锁 Model Step。
5. `ql3_ai.copilot_failure_diagnosis_admissions` 保存有界 plan/receipt JSONB 及关键列镜像。
`SECURITY DEFINER` source snapshot 函数只授予 `ql3_runtime`,并在同一事务内重新确认 active Project、
最新 active subject binding、源 Run/Attempt 状态与日志 Artifact。request identity 支持 response-loss
replay;同 request 不同事实、任何 durable 镜像漂移或部分证据缺失均冲突或失败关闭。
6. PostgreSQL JSONB 的对象键序不具有语义;replay 采用严格的结构深比较,而不是比较
`JSON.stringify` 文本。数组顺序、值类型、缺失/新增字段仍保持 exact,不能借键序修复放宽证据。
7. `copilot_failure_diagnosis``plugin_package_workflow` 都是 StepRun 编排的聚合 ownership domain
从通用 Task orphan recovery 查询中排除。它们必须由各自 StepRun 状态机恢复,不能伪造顶层
RunAttempt,也不能因不存在 Attempt 而被自动终态化。Cluster activation 在 recovery 未收敛时输出
remaining/failed 计数,便于 failover 诊断。
8. 能力只通过 `@qinglong/ai` 精确 subpath 和既有 Cluster PostgreSQL migration stream 提供;不新增
workspace package、进程、连接池、listener、timer、watcher 或 cache。默认 Edge/Standalone 及其
AI/MCP 制品不导入 PostgreSQL admission,因此低配路由设备没有新增常驻成本。
## 被否决方案
1. **把 Step 追加到源失败 Run**:破坏终态 Run 不可变和 event/version 单调性。
2. **复用 Plugin Package Prompt admission**:两者的 authority、Artifact 密文域、恢复与产品语义不同。
3. **admission 后立即调用 Tool/模型**:无法在 response loss 与主库切换后证明执行边界,也会绕过
Tool completion 对 Model Step 的依赖。
4. **为 diagnosis Run 伪造顶层 Attempt**:把 StepRun 聚合误装成 Task 执行,并让两套恢复状态机争夺
ownership。
5. **用序列化字符串比较 JSONB**PostgreSQL 会重排对象键,合法重放会被错误判为证据损坏。
6. **为 admission 新建 package 或进入 Local Profile**:没有独立进程/制品边界,并向低配设备引入
无用的 Cluster/PostgreSQL 闭包。
## 当前验证
1. admission 定向测试 6/6,覆盖 exact Tool/subject/policy/source fencing、approval 拒绝、模型出口策略、
原子创建、response-loss replay、冲突/损坏证据和 JSONB 键序等价。
2. recovery SQL 4/4、Cluster activation 6/6HA 门额外断言 diagnosis 聚合不会进入通用 recovery
candidate。
3. 18 个 QL3 package 的 clean build/test 全部通过;完整 backend 为 1,207 pass、2 条条件 skip、
0 fail。package boundary、Cluster dependency、Edge import 与 Cluster deployment 四项审计均为
compatible 且零 findingworkspace 仍为 18 个 package`singleSourcePackages=[]`
`shallowSourcePackages=[]`
4. 14 档 Local Profile artifact 全部通过,证明 Cluster-only admission 未进入默认 Edge/Standalone、
AI 或 MCP 的本地部署闭包。
5. PostgreSQL 18.4 arm64 physical HA 为 128/128 Gate、timeline `1→2`;报告 SHA-256 为
`a4ed1edec783e3f5b42507c0f8e11b94c59dbe44a57e691017d1445ec9d115e2`,独立证据审计零 finding
Docker 容器、网络与卷零残留。
## 后续门禁
1. 以 admission 中的 exact plan 启动 Tool Step,并复用 invocation Artifact、S3 reader、catalog 与
ADR-0406 result-key provider 完成加密 Tool completionresponse loss 不得重复执行 adapter。
2. 只有受信 Tool Step 成功后才原子解锁 Model Step;用 ADR-0405 builder 生成 prompt,并由 Model
Gateway 执行 provider credential/配额/价格 fence。
3. 建立 Copilot 专用 encrypted model completion、Run/Step terminalization、取消、deadline 和恢复协议,
证明明文不进入数据库、审计、日志或普通 model completion。
4. 最后开放默认关闭的 Cluster API/CLI/UI/MCP 产品入口,并补多副本并发、真实 S3、外部 Provider
fault injection 与 plaintext negative evidence。
+1
View File
@@ -410,6 +410,7 @@
| [ADR-0404](./ADR-0404-optional-local-mcp-run-log-excerpt-surface.md) | 可选本机 MCP Run 日志摘录产品入口与私有文件边界 | Accepted |
| [ADR-0405](./ADR-0405-bounded-failure-diagnosis-prompt-and-model-egress-policy.md) | 有界故障诊断 Prompt 与显式模型出口策略 | Accepted |
| [ADR-0406](./ADR-0406-cluster-projected-tool-result-key-authority.md) | Cluster Projected Tool Result Key Authority | Accepted |
| [ADR-0407](./ADR-0407-cluster-copilot-failure-diagnosis-run-admission.md) | Cluster Copilot Failure Diagnosis Run Admission | Accepted |
## 规则
+10
View File
@@ -30,6 +30,16 @@
"require": "./dist/copilot/failure-diagnosis/prompt.js",
"default": "./dist/copilot/failure-diagnosis/prompt.js"
},
"./failure-diagnosis-execution-admission": {
"types": "./dist/copilot/failure-diagnosis/executionAdmission.d.ts",
"require": "./dist/copilot/failure-diagnosis/executionAdmission.js",
"default": "./dist/copilot/failure-diagnosis/executionAdmission.js"
},
"./postgres-failure-diagnosis-admission-storage": {
"types": "./dist/copilot/failure-diagnosis/postgresAdmissionRepository.d.ts",
"require": "./dist/copilot/failure-diagnosis/postgresAdmissionRepository.js",
"default": "./dist/copilot/failure-diagnosis/postgresAdmissionRepository.js"
},
"./model-invocation": {
"types": "./dist/model-invocation/modelInvocation.d.ts",
"require": "./dist/model-invocation/modelInvocation.js",
@@ -0,0 +1,191 @@
import type { RunEventRecord, RunRecord } from '@qinglong/runtime-core';
import type {
SecurityPolicyFence,
SecuritySubject,
} from '@qinglong/runtime-core/security';
import type { StepRunMutation } from '@qinglong/runtime-core/step-run';
import type {
ToolInvocationInputArtifactReference,
ToolInvocationPreviewArtifactReference,
} from '@qinglong/runtime-core/tool-invocation-artifact';
import type {
TrustedToolHandlerBindingRegistry,
TrustedToolInvocationPlan,
} from '@qinglong/runtime-core/trusted-tool-invocation';
import type {
FailureDiagnosisModelBoundary,
FailureDiagnosisModelEgressPolicy,
FailureDiagnosisResponseLanguage,
} from '../contracts';
export const COPILOT_FAILURE_DIAGNOSIS_EXECUTION_PLAN_SCHEMA =
'qinglong/copilot-failure-diagnosis-execution-plan@v1' as const;
export const COPILOT_FAILURE_DIAGNOSIS_ADMISSION_RECEIPT_SCHEMA =
'qinglong/copilot-failure-diagnosis-admission-receipt@v1' as const;
export const COPILOT_FAILURE_DIAGNOSIS_SOURCE_RUN_STATUSES = [
'failed',
'timed_out',
] as const;
export const COPILOT_FAILURE_DIAGNOSIS_SOURCE_ATTEMPT_STATUSES = [
'failed',
'timed_out',
'lost',
] as const;
export const MAX_COPILOT_FAILURE_DIAGNOSIS_EXECUTION_PLAN_BYTES = 32 * 1024;
export const MAX_COPILOT_FAILURE_DIAGNOSIS_ADMISSION_RECEIPT_BYTES = 16 * 1024;
export type CopilotFailureDiagnosisSourceRunStatus =
(typeof COPILOT_FAILURE_DIAGNOSIS_SOURCE_RUN_STATUSES)[number];
export type CopilotFailureDiagnosisSourceAttemptStatus =
(typeof COPILOT_FAILURE_DIAGNOSIS_SOURCE_ATTEMPT_STATUSES)[number];
export interface CopilotFailureDiagnosisSourceFence {
readonly runId: string;
readonly runVersion: number;
readonly runStatus: CopilotFailureDiagnosisSourceRunStatus;
readonly attemptId: string;
readonly attemptStatus: CopilotFailureDiagnosisSourceAttemptStatus;
readonly attemptFinishedAtMs: number;
readonly logArtifactId: string;
}
export interface PrepareCopilotFailureDiagnosisModelIntent {
readonly provider: string;
readonly model: string;
readonly modelBoundary: FailureDiagnosisModelBoundary;
readonly responseLanguage: FailureDiagnosisResponseLanguage;
readonly maxOutputTokens: number;
readonly egressPolicy: Readonly<FailureDiagnosisModelEgressPolicy>;
}
export interface CopilotFailureDiagnosisModelIntent
extends PrepareCopilotFailureDiagnosisModelIntent {
readonly egressPolicyDigest: string;
readonly intentDigest: string;
}
export interface CopilotFailureDiagnosisToolIntent {
readonly actionRef: string;
readonly planDigest: string;
readonly actionDigest: string;
readonly invocationActionDigest: string;
readonly snapshotDigest: string;
readonly definitionDigest: string;
readonly bindingDigest: string;
readonly invocationArtifact: Readonly<ToolInvocationInputArtifactReference>;
readonly previewArtifact: Readonly<ToolInvocationPreviewArtifactReference>;
readonly sealedAtMs: number;
}
export interface PrepareCopilotFailureDiagnosisExecutionInput {
readonly requestId: string;
readonly traceId: string;
readonly source: Readonly<CopilotFailureDiagnosisSourceFence>;
readonly toolPlan: Readonly<TrustedToolInvocationPlan>;
readonly bindings: TrustedToolHandlerBindingRegistry;
readonly model: Readonly<PrepareCopilotFailureDiagnosisModelIntent>;
readonly deadlineAtMs: number;
readonly plannedAtMs: number;
}
export interface CopilotFailureDiagnosisExecutionPlan {
readonly schema: typeof COPILOT_FAILURE_DIAGNOSIS_EXECUTION_PLAN_SCHEMA;
readonly requestId: string;
readonly runId: string;
readonly toolStepRunId: string;
readonly modelStepRunId: string;
readonly modelInvocationId: string;
readonly traceId: string;
readonly projectId: string;
readonly requestedBySubject: Readonly<SecuritySubject>;
readonly policyFence: Readonly<SecurityPolicyFence>;
readonly source: Readonly<CopilotFailureDiagnosisSourceFence>;
readonly tool: Readonly<CopilotFailureDiagnosisToolIntent>;
readonly model: Readonly<CopilotFailureDiagnosisModelIntent>;
readonly deadlineAtMs: number;
readonly plannedAtMs: number;
readonly planDigest: string;
}
export interface CopilotFailureDiagnosisAdmissionReceipt {
readonly schema: typeof COPILOT_FAILURE_DIAGNOSIS_ADMISSION_RECEIPT_SCHEMA;
readonly requestId: string;
readonly planDigest: string;
readonly runId: string;
readonly sourceRunId: string;
readonly sourceRunVersion: number;
readonly sourceAttemptId: string;
readonly toolStepRunId: string;
readonly toolStepRunDigest: string;
readonly toolMutationId: string;
readonly toolEventId: string;
readonly modelStepRunId: string;
readonly modelStepRunDigest: string;
readonly modelMutationId: string;
readonly modelEventId: string;
readonly finalRunVersion: 3;
readonly finalRunEventSequence: 3;
readonly admittedAtMs: number;
readonly receiptDigest: string;
}
export interface CopilotFailureDiagnosisAdmissionBundle {
readonly plan: Readonly<CopilotFailureDiagnosisExecutionPlan>;
readonly run: Readonly<RunRecord>;
readonly admissionEvent: Readonly<RunEventRecord>;
readonly toolStepMutation: Readonly<StepRunMutation>;
readonly modelStepMutation: Readonly<StepRunMutation>;
readonly receipt: Readonly<CopilotFailureDiagnosisAdmissionReceipt>;
}
export interface CopilotFailureDiagnosisAdmissionRepository {
findByRequestId(
requestId: string,
): Promise<Readonly<CopilotFailureDiagnosisAdmissionReceipt> | null>;
findPlanByRequestId(
requestId: string,
): Promise<Readonly<CopilotFailureDiagnosisExecutionPlan> | null>;
admit(plan: Readonly<CopilotFailureDiagnosisExecutionPlan>): Promise<
Readonly<{
status: 'created' | 'existing';
receipt: Readonly<CopilotFailureDiagnosisAdmissionReceipt>;
}>
>;
}
export class InvalidCopilotFailureDiagnosisExecutionPlanError extends TypeError {
readonly code = 'COPILOT_FAILURE_DIAGNOSIS_EXECUTION_PLAN_INVALID';
constructor(message: string) {
super(`Copilot failure diagnosis execution plan is invalid: ${message}`);
this.name = 'InvalidCopilotFailureDiagnosisExecutionPlanError';
}
}
export class CopilotFailureDiagnosisAdmissionConflictError extends Error {
readonly code = 'COPILOT_FAILURE_DIAGNOSIS_ADMISSION_CONFLICT';
constructor(message = 'durable diagnosis admission identity changed') {
super(`Copilot failure diagnosis admission conflicts: ${message}`);
this.name = 'CopilotFailureDiagnosisAdmissionConflictError';
}
}
export class CopilotFailureDiagnosisAdmissionNotAllowedError extends Error {
readonly code = 'COPILOT_FAILURE_DIAGNOSIS_ADMISSION_NOT_ALLOWED';
constructor() {
super('The source Run is not eligible for failure diagnosis');
this.name = 'CopilotFailureDiagnosisAdmissionNotAllowedError';
}
}
export class CopilotFailureDiagnosisAdmissionUnavailableError extends Error {
readonly code = 'COPILOT_FAILURE_DIAGNOSIS_ADMISSION_UNAVAILABLE';
constructor(options?: ErrorOptions) {
super('Copilot failure diagnosis admission is unavailable', options);
this.name = 'CopilotFailureDiagnosisAdmissionUnavailableError';
}
}
@@ -0,0 +1,274 @@
import { Buffer } from 'node:buffer';
import type { RunEventRecord, RunRecord } from '@qinglong/runtime-core';
import {
createStepRunMutation,
normalizeStepRunMutation,
} from '@qinglong/runtime-core/step-run';
import {
COPILOT_FAILURE_DIAGNOSIS_ADMISSION_RECEIPT_SCHEMA,
MAX_COPILOT_FAILURE_DIAGNOSIS_ADMISSION_RECEIPT_BYTES,
type CopilotFailureDiagnosisAdmissionBundle,
type CopilotFailureDiagnosisAdmissionReceipt,
type CopilotFailureDiagnosisExecutionPlan,
} from './contracts';
import { normalizeCopilotFailureDiagnosisExecutionPlan } from './plan';
import {
dataRecord,
digest,
exactKeys,
hash,
identity,
integer,
invalid,
runIdentity,
timestamp,
} from './validation';
const EVIDENCE_IDENTITY_DOMAIN =
'qinglong/copilot-failure-diagnosis-admission-evidence-identity@v1\0';
const RECEIPT_DIGEST_DOMAIN =
'qinglong/copilot-failure-diagnosis-admission-receipt-digest@v1\0';
function evidenceIdentity(
prefix: 'cda' | 'cdtm' | 'cdte' | 'cdmm' | 'cdme',
planDigest: string,
): string {
const maximumDigestLength = 35 - prefix.length;
return `${prefix}:${hash(EVIDENCE_IDENTITY_DOMAIN, {
prefix,
planDigest,
}).slice(0, maximumDigestLength)}`;
}
function receiptFields(
value: Omit<CopilotFailureDiagnosisAdmissionReceipt, 'receiptDigest'>,
): object {
return { ...value };
}
export function copilotFailureDiagnosisAdmissionReceiptDigest(
value: Omit<CopilotFailureDiagnosisAdmissionReceipt, 'receiptDigest'>,
): string {
return hash(RECEIPT_DIGEST_DOMAIN, receiptFields(value));
}
export function normalizeCopilotFailureDiagnosisAdmissionReceipt(
value: CopilotFailureDiagnosisAdmissionReceipt,
): Readonly<CopilotFailureDiagnosisAdmissionReceipt> {
const candidate = dataRecord(value, 'admission receipt');
exactKeys(
candidate,
[
'admittedAtMs',
'finalRunEventSequence',
'finalRunVersion',
'modelEventId',
'modelMutationId',
'modelStepRunDigest',
'modelStepRunId',
'planDigest',
'receiptDigest',
'requestId',
'runId',
'schema',
'sourceAttemptId',
'sourceRunId',
'sourceRunVersion',
'toolEventId',
'toolMutationId',
'toolStepRunDigest',
'toolStepRunId',
],
'admission receipt',
);
if (candidate.schema !== COPILOT_FAILURE_DIAGNOSIS_ADMISSION_RECEIPT_SCHEMA) {
return invalid('admission receipt schema is unsupported');
}
const unsigned = Object.freeze({
schema: COPILOT_FAILURE_DIAGNOSIS_ADMISSION_RECEIPT_SCHEMA,
requestId: identity(candidate.requestId, 'request id'),
planDigest: digest(candidate.planDigest, 'plan digest'),
runId: runIdentity(candidate.runId, 'diagnosis Run id'),
sourceRunId: runIdentity(candidate.sourceRunId, 'source Run id'),
sourceRunVersion: integer(
candidate.sourceRunVersion,
1,
2_147_483_647,
'source Run version',
),
sourceAttemptId: runIdentity(
candidate.sourceAttemptId,
'source Attempt id',
),
toolStepRunId: identity(candidate.toolStepRunId, 'Tool StepRun id'),
toolStepRunDigest: digest(
candidate.toolStepRunDigest,
'Tool StepRun digest',
),
toolMutationId: identity(candidate.toolMutationId, 'Tool mutation id'),
toolEventId: identity(candidate.toolEventId, 'Tool event id'),
modelStepRunId: identity(candidate.modelStepRunId, 'model StepRun id'),
modelStepRunDigest: digest(
candidate.modelStepRunDigest,
'model StepRun digest',
),
modelMutationId: identity(candidate.modelMutationId, 'model mutation id'),
modelEventId: identity(candidate.modelEventId, 'model event id'),
finalRunVersion: integer(
candidate.finalRunVersion,
3,
3,
'final Run version',
) as 3,
finalRunEventSequence: integer(
candidate.finalRunEventSequence,
3,
3,
'final Run event sequence',
) as 3,
admittedAtMs: timestamp(candidate.admittedAtMs, 'admitted time'),
} satisfies Omit<CopilotFailureDiagnosisAdmissionReceipt, 'receiptDigest'>);
const receiptDigest = digest(candidate.receiptDigest, 'receipt digest');
if (
copilotFailureDiagnosisAdmissionReceiptDigest(unsigned) !== receiptDigest
) {
return invalid('admission receipt digest does not match');
}
const normalized = Object.freeze({ ...unsigned, receiptDigest });
if (
Buffer.byteLength(JSON.stringify(normalized), 'utf8') >
MAX_COPILOT_FAILURE_DIAGNOSIS_ADMISSION_RECEIPT_BYTES
) {
return invalid('admission receipt exceeds its byte budget');
}
return normalized;
}
export function createCopilotFailureDiagnosisAdmissionBundle(
planValue: CopilotFailureDiagnosisExecutionPlan,
): Readonly<CopilotFailureDiagnosisAdmissionBundle> {
const plan = normalizeCopilotFailureDiagnosisExecutionPlan(planValue);
const admissionEvent = Object.freeze({
id: evidenceIdentity('cda', plan.planDigest),
runId: plan.runId,
sequence: 1,
type: 'copilot.failure_diagnosis.admitted',
dedupeKey: evidenceIdentity('cda', plan.planDigest),
actorType: plan.requestedBySubject.type,
actorId: plan.requestedBySubject.id,
payload: Object.freeze({
planDigest: plan.planDigest,
sourceRunId: plan.source.runId,
sourceRunVersion: plan.source.runVersion,
sourceAttemptId: plan.source.attemptId,
toolPlanDigest: plan.tool.planDigest,
modelIntentDigest: plan.model.intentDigest,
}),
createdAtMs: plan.plannedAtMs,
} satisfies RunEventRecord);
const toolStepMutation = normalizeStepRunMutation(
createStepRunMutation(
{
id: plan.toolStepRunId,
runId: plan.runId,
stepKey: 'collect-log',
kind: 'tool',
definitionRef: `trusted-tool-plan:${plan.tool.planDigest}`,
definitionDigest: plan.tool.definitionDigest,
required: true,
initialStatus: 'ready',
inputRef: `tool-invocation:${plan.tool.invocationArtifact.artifactId}`,
mutationId: evidenceIdentity('cdtm', plan.planDigest),
createdAtMs: plan.plannedAtMs,
},
{
expectedRunVersion: 1,
expectedRunEventSequence: 1,
eventId: evidenceIdentity('cdte', plan.planDigest),
dedupeKey: evidenceIdentity('cdte', plan.planDigest),
actor: plan.requestedBySubject,
},
),
);
const modelStepMutation = normalizeStepRunMutation(
createStepRunMutation(
{
id: plan.modelStepRunId,
runId: plan.runId,
parentStepRunId: plan.toolStepRunId,
stepKey: 'diagnose',
kind: 'model',
definitionRef: `copilot-failure-diagnosis:${plan.model.intentDigest}`,
definitionDigest: plan.model.intentDigest,
required: true,
initialStatus: 'pending',
inputRef: `tool-result-step:${plan.toolStepRunId}`,
mutationId: evidenceIdentity('cdmm', plan.planDigest),
createdAtMs: plan.plannedAtMs,
},
{
expectedRunVersion: 2,
expectedRunEventSequence: 2,
eventId: evidenceIdentity('cdme', plan.planDigest),
dedupeKey: evidenceIdentity('cdme', plan.planDigest),
actor: plan.requestedBySubject,
},
),
);
const run = Object.freeze({
id: plan.runId,
projectId: plan.projectId,
taskId: 'copilot.failure-diagnosis',
taskRevision: 'qinglong/copilot-failure-diagnosis@v1',
taskName: 'Copilot failure diagnosis',
taskSnapshotRef: `copilot-failure-diagnosis:${plan.planDigest}`,
parentRunId: plan.source.runId,
triggerType: 'copilot_failure_diagnosis',
executionOrigin: 'manual' as const,
executionOwner: 'runtime' as const,
triggeredBy: plan.requestedBySubject.id,
requestId: plan.requestId,
status: 'running' as const,
version: 3,
eventSequence: 3,
priority: 0,
idempotencyKey: `copilot-diagnosis:${plan.planDigest.slice(0, 32)}`,
createdAtMs: plan.plannedAtMs,
startedAtMs: plan.plannedAtMs,
} satisfies RunRecord);
const receiptUnsigned = Object.freeze({
schema: COPILOT_FAILURE_DIAGNOSIS_ADMISSION_RECEIPT_SCHEMA,
requestId: plan.requestId,
planDigest: plan.planDigest,
runId: plan.runId,
sourceRunId: plan.source.runId,
sourceRunVersion: plan.source.runVersion,
sourceAttemptId: plan.source.attemptId,
toolStepRunId: plan.toolStepRunId,
toolStepRunDigest: toolStepMutation.stepRun.stepRunDigest,
toolMutationId: toolStepMutation.mutationId,
toolEventId: toolStepMutation.event.id,
modelStepRunId: plan.modelStepRunId,
modelStepRunDigest: modelStepMutation.stepRun.stepRunDigest,
modelMutationId: modelStepMutation.mutationId,
modelEventId: modelStepMutation.event.id,
finalRunVersion: 3 as const,
finalRunEventSequence: 3 as const,
admittedAtMs: plan.plannedAtMs,
});
const receipt = normalizeCopilotFailureDiagnosisAdmissionReceipt({
...receiptUnsigned,
receiptDigest:
copilotFailureDiagnosisAdmissionReceiptDigest(receiptUnsigned),
});
return Object.freeze({
plan,
run,
admissionEvent,
toolStepMutation,
modelStepMutation,
receipt,
});
}
@@ -0,0 +1,315 @@
import {
BUILTIN_RUN_LOG_EXCERPT_ADAPTER,
BUILTIN_RUN_LOG_EXCERPT_AUDIT_CONTRACT,
BUILTIN_RUN_LOG_EXCERPT_REDACTION_CONTRACT,
BUILTIN_RUN_LOG_EXCERPT_TIMEOUT_SECONDS,
BUILTIN_RUN_LOG_EXCERPT_TOOL,
} from '@qinglong/runtime-core/builtin-run-log-excerpt-tool';
import { normalizeTrustedToolInvocationPlan } from '@qinglong/runtime-core/trusted-tool-invocation';
import {
COPILOT_FAILURE_DIAGNOSIS_EXECUTION_PLAN_SCHEMA,
MAX_COPILOT_FAILURE_DIAGNOSIS_EXECUTION_PLAN_BYTES,
type CopilotFailureDiagnosisExecutionPlan,
type CopilotFailureDiagnosisToolIntent,
type PrepareCopilotFailureDiagnosisExecutionInput,
} from './contracts';
import {
assertDeadline,
assertJsonBudget,
dataRecord,
digest,
exactKeys,
failureDiagnosisToolInputDigest,
hash,
identity,
invalid,
normalizeFence,
normalizeModelIntent,
normalizeProjectPolicySubject,
normalizeSourceFence,
normalizeToolIntent,
prepareModelIntent,
runIdentity,
sameFence,
sameSubject,
timestamp,
} from './validation';
const PLAN_DIGEST_DOMAIN =
'qinglong/copilot-failure-diagnosis-execution-plan-digest@v1\0';
const IDENTITY_DOMAIN =
'qinglong/copilot-failure-diagnosis-execution-identity@v1\0';
function executionIdentity(
prefix: 'cdr' | 'cdt' | 'cdm' | 'cdi',
value: Readonly<{
requestId: string;
projectId: string;
sourceRunId: string;
sourceAttemptId: string;
toolPlanDigest: string;
}>,
): string {
return `${prefix}:${hash(IDENTITY_DOMAIN, { prefix, ...value }).slice(
0,
32,
)}`;
}
function sameContract(
left: Readonly<{ id: string; version: string }>,
right: Readonly<{ id: string; version: string }>,
): boolean {
return left.id === right.id && left.version === right.version;
}
function planFields(
value: Omit<CopilotFailureDiagnosisExecutionPlan, 'planDigest'>,
): object {
return {
schema: value.schema,
requestId: value.requestId,
runId: value.runId,
toolStepRunId: value.toolStepRunId,
modelStepRunId: value.modelStepRunId,
modelInvocationId: value.modelInvocationId,
traceId: value.traceId,
projectId: value.projectId,
requestedBySubject: value.requestedBySubject,
policyFence: value.policyFence,
source: value.source,
tool: value.tool,
model: value.model,
deadlineAtMs: value.deadlineAtMs,
plannedAtMs: value.plannedAtMs,
};
}
export function copilotFailureDiagnosisExecutionPlanDigest(
value: Omit<CopilotFailureDiagnosisExecutionPlan, 'planDigest'>,
): string {
return hash(PLAN_DIGEST_DOMAIN, planFields(value));
}
function expectedIdentities(
value: Readonly<{
requestId: string;
projectId: string;
sourceRunId: string;
sourceAttemptId: string;
toolPlanDigest: string;
}>,
): Readonly<{
runId: string;
toolStepRunId: string;
modelStepRunId: string;
modelInvocationId: string;
}> {
return Object.freeze({
runId: executionIdentity('cdr', value),
toolStepRunId: executionIdentity('cdt', value),
modelStepRunId: executionIdentity('cdm', value),
modelInvocationId: executionIdentity('cdi', value),
});
}
export function normalizeCopilotFailureDiagnosisExecutionPlan(
value: CopilotFailureDiagnosisExecutionPlan,
): Readonly<CopilotFailureDiagnosisExecutionPlan> {
const candidate = dataRecord(value, 'execution plan');
exactKeys(
candidate,
[
'deadlineAtMs',
'model',
'modelInvocationId',
'modelStepRunId',
'planDigest',
'plannedAtMs',
'policyFence',
'projectId',
'requestId',
'requestedBySubject',
'runId',
'schema',
'source',
'tool',
'toolStepRunId',
'traceId',
],
'execution plan',
);
if (candidate.schema !== COPILOT_FAILURE_DIAGNOSIS_EXECUTION_PLAN_SCHEMA) {
return invalid('execution plan schema is unsupported');
}
const requestId = identity(candidate.requestId, 'request id');
const projectId = identity(candidate.projectId, 'project id');
const source = normalizeSourceFence(candidate.source);
const tool = normalizeToolIntent(candidate.tool);
const model = normalizeModelIntent(candidate.model);
if (
tool.invocationArtifact.inputDigest !==
failureDiagnosisToolInputDigest(source)
) {
return invalid('Tool input is not bound to the source Run Attempt');
}
const plannedAtMs = timestamp(candidate.plannedAtMs, 'planned time');
const deadlineAtMs = timestamp(candidate.deadlineAtMs, 'deadline');
assertDeadline(plannedAtMs, deadlineAtMs);
if (tool.sealedAtMs > plannedAtMs) {
return invalid('Tool plan was sealed after diagnosis planning');
}
const identityInput = Object.freeze({
requestId,
projectId,
sourceRunId: source.runId,
sourceAttemptId: source.attemptId,
toolPlanDigest: tool.planDigest,
});
const identities = expectedIdentities(identityInput);
const unsigned = Object.freeze({
schema: COPILOT_FAILURE_DIAGNOSIS_EXECUTION_PLAN_SCHEMA,
requestId,
runId: runIdentity(candidate.runId, 'diagnosis Run id'),
toolStepRunId: identity(candidate.toolStepRunId, 'Tool StepRun id'),
modelStepRunId: identity(candidate.modelStepRunId, 'model StepRun id'),
modelInvocationId: identity(
candidate.modelInvocationId,
'model invocation id',
),
traceId: identity(candidate.traceId, 'trace id'),
projectId,
requestedBySubject: normalizeProjectPolicySubject(
candidate.requestedBySubject as never,
),
policyFence: normalizeFence(candidate.policyFence),
source,
tool,
model,
deadlineAtMs,
plannedAtMs,
} satisfies Omit<CopilotFailureDiagnosisExecutionPlan, 'planDigest'>);
if (
unsigned.runId !== identities.runId ||
unsigned.toolStepRunId !== identities.toolStepRunId ||
unsigned.modelStepRunId !== identities.modelStepRunId ||
unsigned.modelInvocationId !== identities.modelInvocationId
) {
return invalid('execution identities are invalid');
}
const planDigest = digest(candidate.planDigest, 'plan digest');
if (copilotFailureDiagnosisExecutionPlanDigest(unsigned) !== planDigest) {
return invalid('plan digest does not match');
}
const normalized = Object.freeze({ ...unsigned, planDigest });
assertJsonBudget(
normalized,
MAX_COPILOT_FAILURE_DIAGNOSIS_EXECUTION_PLAN_BYTES,
'execution plan',
);
return normalized;
}
export function prepareCopilotFailureDiagnosisExecution(
inputValue: PrepareCopilotFailureDiagnosisExecutionInput,
): Readonly<CopilotFailureDiagnosisExecutionPlan> {
const input = dataRecord(inputValue, 'execution input');
exactKeys(
input,
[
'bindings',
'deadlineAtMs',
'model',
'plannedAtMs',
'requestId',
'source',
'toolPlan',
'traceId',
],
'execution input',
);
const source = normalizeSourceFence(input.source);
const toolPlan = normalizeTrustedToolInvocationPlan(
inputValue.toolPlan,
inputValue.bindings,
);
const binding = toolPlan.binding;
if (
toolPlan.status !== 'ready' ||
toolPlan.profile !== 'cluster-control' ||
toolPlan.tool.name !== BUILTIN_RUN_LOG_EXCERPT_TOOL.name ||
toolPlan.tool.version !== BUILTIN_RUN_LOG_EXCERPT_TOOL.version ||
toolPlan.effect !== 'read' ||
toolPlan.risk !== 'medium' ||
toolPlan.permission !== 'tool.call:qinglong.run.log.excerpt' ||
toolPlan.requiredPermissions.length !== 1 ||
toolPlan.requiredPermissions[0] !== 'artifact.read' ||
toolPlan.invocationArtifact.inputDigest !==
failureDiagnosisToolInputDigest(source) ||
binding.executionClass !== 'builtin_in_process' ||
binding.timeoutSeconds !== BUILTIN_RUN_LOG_EXCERPT_TIMEOUT_SECONDS ||
!sameContract(binding.adapter, BUILTIN_RUN_LOG_EXCERPT_ADAPTER) ||
!sameContract(
binding.redactionContract,
BUILTIN_RUN_LOG_EXCERPT_REDACTION_CONTRACT,
) ||
!sameContract(binding.auditContract, BUILTIN_RUN_LOG_EXCERPT_AUDIT_CONTRACT)
) {
return invalid('Tool plan is not the exact Cluster Run log excerpt plan');
}
const requestId = identity(input.requestId, 'request id');
const projectId = identity(toolPlan.projectId, 'project id');
const plannedAtMs = timestamp(input.plannedAtMs, 'planned time');
const deadlineAtMs = timestamp(input.deadlineAtMs, 'deadline');
assertDeadline(plannedAtMs, deadlineAtMs);
if (toolPlan.sealedAtMs > plannedAtMs) {
return invalid('Tool plan was sealed after diagnosis planning');
}
const tool = normalizeToolIntent({
actionRef: toolPlan.actionRef,
planDigest: toolPlan.planDigest,
actionDigest: toolPlan.actionDigest,
invocationActionDigest: toolPlan.invocationActionDigest,
snapshotDigest: toolPlan.snapshotDigest,
definitionDigest: toolPlan.definitionDigest,
bindingDigest: binding.bindingDigest,
invocationArtifact: toolPlan.invocationArtifact,
previewArtifact: toolPlan.previewArtifact,
sealedAtMs: toolPlan.sealedAtMs,
} satisfies CopilotFailureDiagnosisToolIntent);
const model = prepareModelIntent(inputValue.model);
const identityInput = Object.freeze({
requestId,
projectId,
sourceRunId: source.runId,
sourceAttemptId: source.attemptId,
toolPlanDigest: tool.planDigest,
});
const identities = expectedIdentities(identityInput);
const unsigned = Object.freeze({
schema: COPILOT_FAILURE_DIAGNOSIS_EXECUTION_PLAN_SCHEMA,
requestId,
...identities,
traceId: identity(input.traceId, 'trace id'),
projectId,
requestedBySubject: toolPlan.requestedBy,
policyFence: toolPlan.policyFence,
source,
tool,
model,
deadlineAtMs,
plannedAtMs,
} satisfies Omit<CopilotFailureDiagnosisExecutionPlan, 'planDigest'>);
const plan = normalizeCopilotFailureDiagnosisExecutionPlan({
...unsigned,
planDigest: copilotFailureDiagnosisExecutionPlanDigest(unsigned),
});
if (
!sameSubject(plan.requestedBySubject, toolPlan.requestedBy) ||
!sameFence(plan.policyFence, toolPlan.policyFence)
) {
return invalid('Tool authority does not match diagnosis authority');
}
return plan;
}
@@ -0,0 +1,760 @@
import type {
PostgresClient,
PostgresPool,
PostgresQueryable,
} from '@qinglong/runtime-core';
import { isDeepStrictEqual } from 'node:util';
import {
normalizeStepRunRecord,
type StepRunMutation,
type StepRunRecord,
} from '@qinglong/runtime-core/step-run';
import { POSTGRES_MODEL_INVOCATION_SCHEMA } from '../../../migration/modelInvocationMigration';
import {
CopilotFailureDiagnosisAdmissionConflictError,
CopilotFailureDiagnosisAdmissionNotAllowedError,
CopilotFailureDiagnosisAdmissionUnavailableError,
type CopilotFailureDiagnosisAdmissionBundle,
type CopilotFailureDiagnosisAdmissionReceipt,
type CopilotFailureDiagnosisAdmissionRepository,
type CopilotFailureDiagnosisExecutionPlan,
} from './contracts';
import {
createCopilotFailureDiagnosisAdmissionBundle,
normalizeCopilotFailureDiagnosisAdmissionReceipt,
} from './durableEvidence';
import { normalizeCopilotFailureDiagnosisExecutionPlan } from './plan';
import { identity } from './validation';
const ADMISSION_TABLE = 'copilot_failure_diagnosis_admissions';
const SOURCE_SNAPSHOT_FUNCTION =
'copilot_failure_diagnosis_admission_source_snapshot';
const RETRYABLE_SQL_STATES = new Set(['40001', '40P01']);
const MAX_TRANSACTION_ATTEMPTS = 3;
const DIAGNOSIS_RUN_STATUSES = new Set([
'running',
'lost',
'succeeded',
'failed',
'cancelled',
'timed_out',
]);
type Row = Readonly<Record<string, unknown>>;
export interface PostgresCopilotFailureDiagnosisAdmissionMutationGuard {
confirm(
input: Readonly<{
client: PostgresClient;
plan: Readonly<CopilotFailureDiagnosisExecutionPlan>;
replay: boolean;
}>,
): void | Promise<void>;
}
function unavailable(
cause?: unknown,
): CopilotFailureDiagnosisAdmissionUnavailableError {
return new CopilotFailureDiagnosisAdmissionUnavailableError({
cause: cause instanceof Error ? cause : undefined,
});
}
function sqlState(error: unknown): string | undefined {
if (!error || typeof error !== 'object') return undefined;
const value = (error as { code?: unknown }).code;
return typeof value === 'string' ? value : undefined;
}
function mapStorageError(error: unknown): Error {
if (
error instanceof CopilotFailureDiagnosisAdmissionConflictError ||
error instanceof CopilotFailureDiagnosisAdmissionNotAllowedError ||
error instanceof CopilotFailureDiagnosisAdmissionUnavailableError
) {
return error;
}
if (sqlState(error) === '23505') {
return new CopilotFailureDiagnosisAdmissionConflictError(
'a durable diagnosis identity is already bound',
);
}
return unavailable(error);
}
function text(row: Row, key: string): string {
const value = row[key];
if (typeof value !== 'string' || value.length === 0) throw unavailable();
return value;
}
function nullableText(row: Row, key: string): string | null {
const value = row[key];
if (value === null) return null;
return text(row, key);
}
function integer(row: Row, key: string): number {
const value = row[key];
if (typeof value === 'number' && Number.isSafeInteger(value)) return value;
if (typeof value === 'string' && /^(0|[1-9]\d*)$/.test(value)) {
const parsed = Number(value);
if (Number.isSafeInteger(parsed)) return parsed;
}
throw unavailable();
}
function boolean(row: Row, key: string): boolean {
const value = row[key];
if (typeof value !== 'boolean') throw unavailable();
return value;
}
function jsonObject(value: unknown): Record<string, unknown> {
let parsed = value;
if (typeof value === 'string') {
try {
parsed = JSON.parse(value);
} catch (cause) {
throw unavailable(cause);
}
}
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw unavailable();
}
return parsed as Record<string, unknown>;
}
function json(value: unknown): string {
return JSON.stringify(value);
}
export function storedJsonEquals(left: unknown, right: unknown): boolean {
return isDeepStrictEqual(left, right);
}
async function begin(client: PostgresClient): Promise<void> {
await client.query('BEGIN ISOLATION LEVEL SERIALIZABLE');
await client.query(`SELECT set_config('statement_timeout', $1, true)`, [
'5s',
]);
await client.query(`SELECT set_config('lock_timeout', $1, true)`, ['2s']);
await client.query(
`SELECT set_config('idle_in_transaction_session_timeout', $1, true)`,
['5s'],
);
}
async function rollback(client: PostgresClient): Promise<void> {
try {
await client.query('ROLLBACK');
} catch {
// Preserve the original transaction failure.
}
}
async function transaction<T>(
pool: PostgresPool,
work: (client: PostgresClient) => Promise<T>,
): Promise<T> {
for (let attempt = 0; attempt < MAX_TRANSACTION_ATTEMPTS; attempt += 1) {
let client: PostgresClient;
try {
client = await pool.connect();
} catch (cause) {
throw unavailable(cause);
}
let began = false;
try {
await begin(client);
began = true;
const result = await work(client);
await client.query('COMMIT');
began = false;
return result;
} catch (error) {
if (began) await rollback(client);
if (
RETRYABLE_SQL_STATES.has(sqlState(error) ?? '') &&
attempt + 1 < MAX_TRANSACTION_ATTEMPTS
) {
continue;
}
throw mapStorageError(error);
} finally {
client.release();
}
}
throw unavailable();
}
async function assertRunEvidence(
queryable: PostgresQueryable,
bundle: Readonly<CopilotFailureDiagnosisAdmissionBundle>,
): Promise<void> {
const result = await queryable.query<Row>(
`SELECT project_id AS "projectId", task_id AS "taskId",
task_revision AS "taskRevision", task_name AS "taskName",
task_snapshot_ref AS "taskSnapshotRef",
parent_run_id AS "parentRunId", trigger_type AS "triggerType",
execution_origin AS "executionOrigin",
execution_owner AS "executionOwner",
triggered_by AS "triggeredBy", request_id AS "requestId",
status, version, event_sequence AS "eventSequence", priority,
idempotency_key AS "idempotencyKey",
created_at_ms AS "createdAtMs",
started_at_ms AS "startedAtMs"
FROM "ql3"."runs" WHERE id = $1`,
[bundle.run.id],
);
const row = result.rows.length === 1 ? result.rows[0]! : null;
const run = bundle.run;
if (
!row ||
text(row, 'projectId') !== run.projectId ||
text(row, 'taskId') !== run.taskId ||
text(row, 'taskRevision') !== run.taskRevision ||
nullableText(row, 'taskName') !== (run.taskName ?? null) ||
nullableText(row, 'taskSnapshotRef') !== (run.taskSnapshotRef ?? null) ||
nullableText(row, 'parentRunId') !== (run.parentRunId ?? null) ||
text(row, 'triggerType') !== run.triggerType ||
text(row, 'executionOrigin') !== run.executionOrigin ||
text(row, 'executionOwner') !== run.executionOwner ||
nullableText(row, 'triggeredBy') !== (run.triggeredBy ?? null) ||
nullableText(row, 'requestId') !== (run.requestId ?? null) ||
!DIAGNOSIS_RUN_STATUSES.has(text(row, 'status')) ||
integer(row, 'version') < run.version ||
integer(row, 'eventSequence') < run.eventSequence ||
integer(row, 'version') !== integer(row, 'eventSequence') ||
integer(row, 'priority') !== run.priority ||
nullableText(row, 'idempotencyKey') !== (run.idempotencyKey ?? null) ||
integer(row, 'createdAtMs') !== run.createdAtMs ||
integer(row, 'startedAtMs') !== run.startedAtMs
) {
throw unavailable();
}
}
async function assertEventEvidence(
queryable: PostgresQueryable,
bundle: Readonly<CopilotFailureDiagnosisAdmissionBundle>,
): Promise<void> {
const result = await queryable.query<Row>(
`SELECT id, sequence, type, dedupe_key AS "dedupeKey",
actor_type AS "actorType", actor_id AS "actorId",
step_run_id AS "stepRunId", payload,
created_at_ms AS "createdAtMs"
FROM "ql3"."run_events"
WHERE run_id = $1 AND sequence <= 3 ORDER BY sequence`,
[bundle.run.id],
);
const expected = [
bundle.admissionEvent,
bundle.toolStepMutation.event,
bundle.modelStepMutation.event,
];
if (
result.rows.length !== expected.length ||
result.rows.some((row, index) => {
const event = expected[index]!;
return (
text(row, 'id') !== event.id ||
integer(row, 'sequence') !== event.sequence ||
text(row, 'type') !== event.type ||
nullableText(row, 'dedupeKey') !== (event.dedupeKey ?? null) ||
text(row, 'actorType') !== event.actorType ||
nullableText(row, 'actorId') !== (event.actorId ?? null) ||
nullableText(row, 'stepRunId') !== (event.stepRunId ?? null) ||
!storedJsonEquals(row.payload, event.payload) ||
integer(row, 'createdAtMs') !== event.createdAtMs
);
})
) {
throw unavailable();
}
}
function assertStepRow(row: Row, mutation: Readonly<StepRunMutation>): void {
let current: Readonly<StepRunRecord>;
try {
current = normalizeStepRunRecord(
jsonObject(row.stepRunJson) as unknown as StepRunRecord,
);
} catch (cause) {
throw unavailable(cause);
}
const initial = mutation.stepRun;
if (
current.id !== initial.id ||
current.runId !== initial.runId ||
current.parentStepRunId !== initial.parentStepRunId ||
current.stepKey !== initial.stepKey ||
current.kind !== initial.kind ||
current.definitionRef !== initial.definitionRef ||
current.definitionDigest !== initial.definitionDigest ||
current.required !== true ||
current.inputRef !== initial.inputRef ||
current.createdAtMs !== initial.createdAtMs ||
current.version < initial.version ||
text(row, 'stepKey') !== current.stepKey ||
text(row, 'kind') !== current.kind ||
nullableText(row, 'parentStepRunId') !== current.parentStepRunId ||
text(row, 'definitionRef') !== current.definitionRef ||
text(row, 'definitionDigest') !== current.definitionDigest ||
boolean(row, 'required') !== true ||
text(row, 'status') !== current.status ||
integer(row, 'version') !== current.version ||
text(row, 'lastMutationId') !== current.lastMutationId ||
text(row, 'stepRunDigest') !== current.stepRunDigest ||
text(row, 'mutationId') !== mutation.mutationId ||
text(row, 'mutationDigest') !== mutation.mutationDigest ||
text(row, 'eventId') !== mutation.event.id ||
integer(row, 'eventSequence') !== mutation.event.sequence ||
integer(row, 'runVersion') !== mutation.expectedRunVersion + 1 ||
text(row, 'initialStepRunDigest') !== initial.stepRunDigest ||
!storedJsonEquals(row.initialStepRunJson, initial)
) {
throw unavailable();
}
}
async function assertStepEvidence(
queryable: PostgresQueryable,
bundle: Readonly<CopilotFailureDiagnosisAdmissionBundle>,
): Promise<void> {
const tool = bundle.toolStepMutation;
const model = bundle.modelStepMutation;
const result = await queryable.query<Row>(
`SELECT runtime.id, runtime.parent_step_run_id AS "parentStepRunId",
runtime.step_key AS "stepKey", runtime.kind,
runtime.definition_ref AS "definitionRef",
runtime.definition_digest AS "definitionDigest",
runtime.required, runtime.status, runtime.version,
runtime.last_mutation_id AS "lastMutationId",
runtime.step_run_digest AS "stepRunDigest",
runtime.step_run_json AS "stepRunJson",
mutation.mutation_id AS "mutationId",
mutation.mutation_digest AS "mutationDigest",
mutation.event_id AS "eventId",
mutation.event_sequence AS "eventSequence",
mutation.run_version AS "runVersion",
mutation.step_run_digest AS "initialStepRunDigest",
mutation.step_run_json AS "initialStepRunJson"
FROM "ql3"."step_runs" AS runtime
JOIN "ql3"."step_run_mutations" AS mutation
ON mutation.run_id = runtime.run_id
AND mutation.step_run_id = runtime.id
AND mutation.mutation_id = CASE
WHEN runtime.id = $2 THEN $4 ELSE $5 END
WHERE runtime.run_id = $1 AND runtime.id IN ($2, $3)
ORDER BY runtime.id`,
[
bundle.run.id,
tool.stepRun.id,
model.stepRun.id,
tool.mutationId,
model.mutationId,
],
);
if (result.rows.length !== 2) throw unavailable();
const rows = new Map(result.rows.map((row) => [text(row, 'id'), row]));
const toolRow = rows.get(tool.stepRun.id);
const modelRow = rows.get(model.stepRun.id);
if (!toolRow || !modelRow) throw unavailable();
assertStepRow(toolRow, tool);
assertStepRow(modelRow, model);
}
async function assertStoredEvidence(
queryable: PostgresQueryable,
bundle: Readonly<CopilotFailureDiagnosisAdmissionBundle>,
): Promise<void> {
await assertRunEvidence(queryable, bundle);
await assertEventEvidence(queryable, bundle);
await assertStepEvidence(queryable, bundle);
}
type StoredAdmission = Readonly<{
plan: Readonly<CopilotFailureDiagnosisExecutionPlan>;
receipt: Readonly<CopilotFailureDiagnosisAdmissionReceipt>;
}>;
async function findStored(
queryable: PostgresQueryable,
requestId: string,
): Promise<StoredAdmission | null> {
const result = await queryable.query<Row>(
`SELECT request_id AS "requestId", plan_digest AS "planDigest",
run_id AS "runId", project_id AS "projectId",
source_run_id AS "sourceRunId",
source_run_version AS "sourceRunVersion",
source_run_status AS "sourceRunStatus",
source_attempt_id AS "sourceAttemptId",
source_attempt_status AS "sourceAttemptStatus",
source_log_artifact_id AS "sourceLogArtifactId",
tool_plan_digest AS "toolPlanDigest",
tool_action_digest AS "toolActionDigest",
tool_step_run_id AS "toolStepRunId",
model_intent_digest AS "modelIntentDigest",
model_step_run_id AS "modelStepRunId",
admitted_at_ms AS "admittedAtMs",
receipt_digest AS "receiptDigest",
plan_json AS "planJson", receipt_json AS "receiptJson"
FROM "${POSTGRES_MODEL_INVOCATION_SCHEMA}"."${ADMISSION_TABLE}"
WHERE request_id = $1 LIMIT 2`,
[requestId],
);
if (result.rows.length > 1) throw unavailable();
const row = result.rows[0];
if (!row) return null;
try {
const plan = normalizeCopilotFailureDiagnosisExecutionPlan(
jsonObject(
row.planJson,
) as unknown as CopilotFailureDiagnosisExecutionPlan,
);
const receipt = normalizeCopilotFailureDiagnosisAdmissionReceipt(
jsonObject(
row.receiptJson,
) as unknown as CopilotFailureDiagnosisAdmissionReceipt,
);
const bundle = createCopilotFailureDiagnosisAdmissionBundle(plan);
if (
text(row, 'requestId') !== plan.requestId ||
text(row, 'planDigest') !== plan.planDigest ||
text(row, 'runId') !== plan.runId ||
text(row, 'projectId') !== plan.projectId ||
text(row, 'sourceRunId') !== plan.source.runId ||
integer(row, 'sourceRunVersion') !== plan.source.runVersion ||
text(row, 'sourceRunStatus') !== plan.source.runStatus ||
text(row, 'sourceAttemptId') !== plan.source.attemptId ||
text(row, 'sourceAttemptStatus') !== plan.source.attemptStatus ||
text(row, 'sourceLogArtifactId') !== plan.source.logArtifactId ||
text(row, 'toolPlanDigest') !== plan.tool.planDigest ||
text(row, 'toolActionDigest') !== plan.tool.actionDigest ||
text(row, 'toolStepRunId') !== plan.toolStepRunId ||
text(row, 'modelIntentDigest') !== plan.model.intentDigest ||
text(row, 'modelStepRunId') !== plan.modelStepRunId ||
integer(row, 'admittedAtMs') !== receipt.admittedAtMs ||
text(row, 'receiptDigest') !== receipt.receiptDigest ||
!storedJsonEquals(bundle.receipt, receipt)
) {
throw unavailable();
}
await assertStoredEvidence(queryable, bundle);
return Object.freeze({ plan, receipt });
} catch (error) {
if (error instanceof CopilotFailureDiagnosisAdmissionUnavailableError) {
throw error;
}
throw unavailable(error);
}
}
async function assertSourceSnapshot(
client: PostgresClient,
plan: Readonly<CopilotFailureDiagnosisExecutionPlan>,
): Promise<void> {
const result = await client.query<Row>(
`SELECT run_id AS "runId", run_version AS "runVersion",
run_status AS "runStatus", attempt_id AS "attemptId",
attempt_status AS "attemptStatus",
attempt_finished_at_ms AS "attemptFinishedAtMs",
log_artifact_id AS "logArtifactId"
FROM "${POSTGRES_MODEL_INVOCATION_SCHEMA}"."${SOURCE_SNAPSHOT_FUNCTION}"(
$1, $2, $3, $4, $5, $6, $7
)`,
[
plan.projectId,
plan.requestedBySubject.type,
plan.requestedBySubject.id,
plan.policyFence.projectVersion,
plan.policyFence.bindingVersion,
plan.source.runId,
plan.source.attemptId,
],
);
const row = result.rows.length === 1 ? result.rows[0]! : null;
if (!row) throw new CopilotFailureDiagnosisAdmissionNotAllowedError();
const source = plan.source;
if (
text(row, 'runId') !== source.runId ||
integer(row, 'runVersion') !== source.runVersion ||
text(row, 'runStatus') !== source.runStatus ||
text(row, 'attemptId') !== source.attemptId ||
text(row, 'attemptStatus') !== source.attemptStatus ||
integer(row, 'attemptFinishedAtMs') !== source.attemptFinishedAtMs ||
text(row, 'logArtifactId') !== source.logArtifactId
) {
throw new CopilotFailureDiagnosisAdmissionConflictError(
'source Run or final Attempt changed',
);
}
}
async function insertRun(
client: PostgresClient,
bundle: Readonly<CopilotFailureDiagnosisAdmissionBundle>,
): Promise<void> {
const run = bundle.run;
await client.query(
`INSERT INTO "ql3"."runs" (
id, project_id, task_id, task_revision, task_name,
task_snapshot_ref, parent_run_id, trigger_type, execution_origin,
execution_owner, triggered_by, request_id, status, version,
event_sequence, priority, idempotency_key, created_at_ms, started_at_ms
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14,
$15, $16, $17, $18, $19
)`,
[
run.id,
run.projectId,
run.taskId,
run.taskRevision,
run.taskName ?? null,
run.taskSnapshotRef ?? null,
run.parentRunId ?? null,
run.triggerType,
run.executionOrigin,
run.executionOwner,
run.triggeredBy ?? null,
run.requestId ?? null,
run.status,
run.version,
run.eventSequence,
run.priority,
run.idempotencyKey ?? null,
run.createdAtMs,
run.startedAtMs ?? null,
],
);
}
async function insertEvent(
client: PostgresClient,
event: Readonly<CopilotFailureDiagnosisAdmissionBundle['admissionEvent']>,
): Promise<void> {
await client.query(
`INSERT INTO "ql3"."run_events" (
id, run_id, sequence, type, dedupe_key, actor_type, actor_id,
attempt_id, step_run_id, payload, created_at_ms
) VALUES ($1, $2, $3, $4, $5, $6, $7, NULL, $8, $9::jsonb, $10)`,
[
event.id,
event.runId,
event.sequence,
event.type,
event.dedupeKey ?? null,
event.actorType,
event.actorId ?? null,
event.stepRunId ?? null,
json(event.payload),
event.createdAtMs,
],
);
}
async function insertStepEvidence(
client: PostgresClient,
mutation: Readonly<StepRunMutation>,
admittedAtMs: number,
): Promise<void> {
const step = mutation.stepRun;
await client.query(
`INSERT INTO "ql3"."step_runs" (
id, run_id, parent_step_run_id, step_key, kind, definition_ref,
definition_digest, required, status, version, attempt_count,
input_ref, output_ref, approval_request_id, ready_at_ms,
started_at_ms, finished_at_ms, result_code, error_summary,
created_at_ms, updated_at_ms, last_mutation_id, step_run_digest,
step_run_json
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14,
$15, $16, $17, $18, $19, $20, $21, $22, $23, $24::jsonb
)`,
[
step.id,
step.runId,
step.parentStepRunId,
step.stepKey,
step.kind,
step.definitionRef,
step.definitionDigest,
step.required,
step.status,
step.version,
step.attemptCount,
step.inputRef,
step.outputRef,
step.approvalRequestId,
step.readyAtMs,
step.startedAtMs,
step.finishedAtMs,
step.resultCode,
step.errorSummary,
step.createdAtMs,
step.updatedAtMs,
step.lastMutationId,
step.stepRunDigest,
json(step),
],
);
await insertEvent(client, mutation.event);
await client.query(
`INSERT INTO "ql3"."step_run_mutations" (
mutation_id, mutation_digest, run_id, step_run_id,
step_run_digest, event_id, event_sequence, run_version,
step_run_json, committed_at_ms
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10)`,
[
mutation.mutationId,
mutation.mutationDigest,
mutation.runId,
step.id,
step.stepRunDigest,
mutation.event.id,
mutation.event.sequence,
mutation.expectedRunVersion + 1,
json(step),
admittedAtMs,
],
);
}
async function insertAdmission(
client: PostgresClient,
bundle: Readonly<CopilotFailureDiagnosisAdmissionBundle>,
): Promise<void> {
const { plan, receipt } = bundle;
await client.query(
`INSERT INTO "${POSTGRES_MODEL_INVOCATION_SCHEMA}"."${ADMISSION_TABLE}" (
request_id, plan_digest, run_id, project_id,
source_run_id, source_run_version, source_run_status,
source_attempt_id, source_attempt_status, source_log_artifact_id,
tool_plan_digest, tool_action_digest, tool_step_run_id,
model_intent_digest, model_step_run_id, admitted_at_ms,
receipt_digest, plan_json, receipt_json
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14,
$15, $16, $17, $18::jsonb, $19::jsonb
)`,
[
plan.requestId,
plan.planDigest,
plan.runId,
plan.projectId,
plan.source.runId,
plan.source.runVersion,
plan.source.runStatus,
plan.source.attemptId,
plan.source.attemptStatus,
plan.source.logArtifactId,
plan.tool.planDigest,
plan.tool.actionDigest,
plan.toolStepRunId,
plan.model.intentDigest,
plan.modelStepRunId,
receipt.admittedAtMs,
receipt.receiptDigest,
json(plan),
json(receipt),
],
);
}
export class PostgresCopilotFailureDiagnosisAdmissionRepository
implements CopilotFailureDiagnosisAdmissionRepository
{
readonly #pool: PostgresPool;
readonly #mutationGuard:
| PostgresCopilotFailureDiagnosisAdmissionMutationGuard
| undefined;
constructor(
pool: PostgresPool,
mutationGuard?: PostgresCopilotFailureDiagnosisAdmissionMutationGuard,
) {
if (
!pool ||
typeof pool.query !== 'function' ||
typeof pool.connect !== 'function' ||
(mutationGuard !== undefined &&
(!mutationGuard || typeof mutationGuard.confirm !== 'function'))
) {
throw unavailable();
}
this.#pool = pool;
this.#mutationGuard = mutationGuard;
}
async findByRequestId(
requestIdValue: string,
): Promise<Readonly<CopilotFailureDiagnosisAdmissionReceipt> | null> {
const requestId = identity(requestIdValue, 'request id');
try {
return (await findStored(this.#pool, requestId))?.receipt ?? null;
} catch (error) {
throw mapStorageError(error);
}
}
async findPlanByRequestId(
requestIdValue: string,
): Promise<Readonly<CopilotFailureDiagnosisExecutionPlan> | null> {
const requestId = identity(requestIdValue, 'request id');
try {
return (await findStored(this.#pool, requestId))?.plan ?? null;
} catch (error) {
throw mapStorageError(error);
}
}
admit(planValue: Readonly<CopilotFailureDiagnosisExecutionPlan>): Promise<
Readonly<{
status: 'created' | 'existing';
receipt: Readonly<CopilotFailureDiagnosisAdmissionReceipt>;
}>
> {
const plan = normalizeCopilotFailureDiagnosisExecutionPlan(planValue);
return transaction(this.#pool, async (client) => {
const existing = await findStored(client, plan.requestId);
if (existing) {
if (
existing.plan.planDigest !== plan.planDigest ||
!storedJsonEquals(existing.plan, plan)
) {
throw new CopilotFailureDiagnosisAdmissionConflictError(
'requestId is already bound to another diagnosis plan',
);
}
await this.#mutationGuard?.confirm({ client, plan, replay: true });
return Object.freeze({
status: 'existing' as const,
receipt: existing.receipt,
});
}
await this.#mutationGuard?.confirm({ client, plan, replay: false });
await assertSourceSnapshot(client, plan);
const bundle = createCopilotFailureDiagnosisAdmissionBundle(plan);
await insertRun(client, bundle);
await insertEvent(client, bundle.admissionEvent);
await insertStepEvidence(
client,
bundle.toolStepMutation,
bundle.receipt.admittedAtMs,
);
await insertStepEvidence(
client,
bundle.modelStepMutation,
bundle.receipt.admittedAtMs,
);
await insertAdmission(client, bundle);
return Object.freeze({
status: 'created' as const,
receipt: bundle.receipt,
});
});
}
}
@@ -0,0 +1,416 @@
import { Buffer } from 'node:buffer';
import { createHash } from 'node:crypto';
import { normalizeProjectPolicySubject } from '@qinglong/runtime-core/project-policy';
import type {
SecurityPolicyFence,
SecuritySubject,
} from '@qinglong/runtime-core/security';
import {
normalizeToolInvocationInputArtifactReference,
normalizeToolInvocationPreviewArtifactReference,
} from '@qinglong/runtime-core/tool-invocation-artifact';
import { MAX_MODEL_INVOCATION_MS } from '../../../model-gateway/model';
import {
FAILURE_DIAGNOSIS_PROMPT_PROTOCOL,
type FailureDiagnosisModelEgressPolicy,
} from '../contracts';
import {
normalizeFailureDiagnosisModelBoundary,
normalizeFailureDiagnosisModelEgressPolicy,
normalizeFailureDiagnosisResponseLanguage,
} from '../validation';
import {
COPILOT_FAILURE_DIAGNOSIS_SOURCE_ATTEMPT_STATUSES,
COPILOT_FAILURE_DIAGNOSIS_SOURCE_RUN_STATUSES,
InvalidCopilotFailureDiagnosisExecutionPlanError,
type CopilotFailureDiagnosisModelIntent,
type CopilotFailureDiagnosisSourceFence,
type CopilotFailureDiagnosisToolIntent,
type PrepareCopilotFailureDiagnosisModelIntent,
} from './contracts';
const IDENTITY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const RUN_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,35}$/;
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
const MODEL_EGRESS_POLICY_DIGEST_DOMAIN =
'qinglong/copilot-failure-diagnosis-egress-policy-digest@v1\0';
const MODEL_INTENT_DIGEST_DOMAIN =
'qinglong/copilot-failure-diagnosis-model-intent-digest@v1\0';
export function invalid(message: string): never {
throw new InvalidCopilotFailureDiagnosisExecutionPlanError(message);
}
export function dataRecord(
value: unknown,
label: string,
): Record<string, unknown> {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
Object.getPrototypeOf(value) !== Object.prototype
) {
return invalid(`${label} must be a plain object`);
}
return value as Record<string, unknown>;
}
export function exactKeys(
value: object,
expected: readonly string[],
label: string,
): void {
const actual = Reflect.ownKeys(value);
const allowed = new Set(expected);
if (
actual.length !== expected.length ||
actual.some((key) => typeof key !== 'string' || !allowed.has(key)) ||
expected.some((key) => !actual.includes(key))
) {
invalid(`${label} shape is invalid`);
}
}
export function hash(domain: string, value: unknown): string {
return createHash('sha256')
.update(domain, 'utf8')
.update(JSON.stringify(value))
.digest('hex');
}
export function identity(value: unknown, label: string): string {
if (typeof value !== 'string' || !IDENTITY_PATTERN.test(value)) {
return invalid(`${label} is invalid`);
}
return value;
}
export function runIdentity(value: unknown, label: string): string {
if (typeof value !== 'string' || !RUN_ID_PATTERN.test(value)) {
return invalid(`${label} is invalid`);
}
return value;
}
export function digest(value: unknown, label: string): string {
if (typeof value !== 'string' || !DIGEST_PATTERN.test(value)) {
return invalid(`${label} is invalid`);
}
return value;
}
export function integer(
value: unknown,
minimum: number,
maximum: number,
label: string,
): number {
if (
!Number.isSafeInteger(value) ||
(value as number) < minimum ||
(value as number) > maximum
) {
return invalid(`${label} is invalid`);
}
return value as number;
}
export function timestamp(value: unknown, label: string): number {
return integer(value, 0, Number.MAX_SAFE_INTEGER, label);
}
export function normalizeFence(value: unknown): Readonly<SecurityPolicyFence> {
const fence = dataRecord(value, 'policy fence');
exactKeys(fence, ['bindingVersion', 'projectVersion'], 'policy fence');
return Object.freeze({
projectVersion: integer(
fence.projectVersion,
1,
2_147_483_647,
'project version',
),
bindingVersion: integer(
fence.bindingVersion,
1,
2_147_483_647,
'binding version',
),
});
}
export function sameSubject(
left: Readonly<SecuritySubject>,
right: Readonly<SecuritySubject>,
): boolean {
return left.type === right.type && left.id === right.id;
}
export function sameFence(
left: Readonly<SecurityPolicyFence>,
right: Readonly<SecurityPolicyFence>,
): boolean {
return (
left.projectVersion === right.projectVersion &&
left.bindingVersion === right.bindingVersion
);
}
export function normalizeSourceFence(
value: unknown,
): Readonly<CopilotFailureDiagnosisSourceFence> {
const source = dataRecord(value, 'source fence');
exactKeys(
source,
[
'attemptFinishedAtMs',
'attemptId',
'attemptStatus',
'logArtifactId',
'runId',
'runStatus',
'runVersion',
],
'source fence',
);
if (
!COPILOT_FAILURE_DIAGNOSIS_SOURCE_RUN_STATUSES.includes(
source.runStatus as never,
) ||
!COPILOT_FAILURE_DIAGNOSIS_SOURCE_ATTEMPT_STATUSES.includes(
source.attemptStatus as never,
) ||
(source.runStatus === 'failed' &&
source.attemptStatus !== 'failed' &&
source.attemptStatus !== 'lost') ||
(source.runStatus === 'timed_out' && source.attemptStatus !== 'timed_out')
) {
return invalid('source terminal status binding is invalid');
}
return Object.freeze({
runId: runIdentity(source.runId, 'source Run id'),
runVersion: integer(
source.runVersion,
1,
2_147_483_647,
'source Run version',
),
runStatus: source.runStatus,
attemptId: runIdentity(source.attemptId, 'source Attempt id'),
attemptStatus: source.attemptStatus,
attemptFinishedAtMs: timestamp(
source.attemptFinishedAtMs,
'source Attempt finish time',
),
logArtifactId: runIdentity(source.logArtifactId, 'source log Artifact id'),
} as CopilotFailureDiagnosisSourceFence);
}
export function failureDiagnosisToolInputDigest(
source: Readonly<CopilotFailureDiagnosisSourceFence>,
): string {
return createHash('sha256')
.update(
JSON.stringify({ attemptId: source.attemptId, runId: source.runId }),
)
.digest('hex');
}
export function failureDiagnosisEgressPolicyDigest(
value: FailureDiagnosisModelEgressPolicy,
): string {
return hash(
MODEL_EGRESS_POLICY_DIGEST_DOMAIN,
normalizeFailureDiagnosisModelEgressPolicy(value),
);
}
function modelIntentFields(
value: Omit<CopilotFailureDiagnosisModelIntent, 'intentDigest'>,
): object {
return {
promptProtocol: FAILURE_DIAGNOSIS_PROMPT_PROTOCOL,
provider: value.provider,
model: value.model,
modelBoundary: value.modelBoundary,
responseLanguage: value.responseLanguage,
maxOutputTokens: value.maxOutputTokens,
egressPolicy: value.egressPolicy,
egressPolicyDigest: value.egressPolicyDigest,
completion: {
residualSensitivity: 'potentially_sensitive',
persistence: 'encrypted_only',
plaintextAudit: 'forbidden',
actionAuthority: 'none',
},
};
}
export function failureDiagnosisModelIntentDigest(
value: Omit<CopilotFailureDiagnosisModelIntent, 'intentDigest'>,
): string {
return hash(MODEL_INTENT_DIGEST_DOMAIN, modelIntentFields(value));
}
export function prepareModelIntent(
value: PrepareCopilotFailureDiagnosisModelIntent,
): Readonly<CopilotFailureDiagnosisModelIntent> {
const candidate = dataRecord(value, 'model intent');
exactKeys(
candidate,
[
'egressPolicy',
'maxOutputTokens',
'model',
'modelBoundary',
'provider',
'responseLanguage',
],
'model intent',
);
const egressPolicy = normalizeFailureDiagnosisModelEgressPolicy(
candidate.egressPolicy,
);
const modelBoundary = normalizeFailureDiagnosisModelBoundary(
candidate.modelBoundary,
);
const maxOutputTokens = integer(
candidate.maxOutputTokens,
1,
egressPolicy.maxOutputTokens,
'model max output tokens',
);
if (
!egressPolicy.potentiallySensitiveDataBoundaries.includes(modelBoundary)
) {
return invalid('model boundary is not allowed by egress policy');
}
const unsigned = Object.freeze({
provider: identity(candidate.provider, 'model provider'),
model: identity(candidate.model, 'model'),
modelBoundary,
responseLanguage: normalizeFailureDiagnosisResponseLanguage(
candidate.responseLanguage,
),
maxOutputTokens,
egressPolicy,
egressPolicyDigest: failureDiagnosisEgressPolicyDigest(egressPolicy),
});
return Object.freeze({
...unsigned,
intentDigest: failureDiagnosisModelIntentDigest(unsigned),
});
}
export function normalizeModelIntent(
value: unknown,
): Readonly<CopilotFailureDiagnosisModelIntent> {
const candidate = dataRecord(value, 'model intent');
exactKeys(
candidate,
[
'egressPolicy',
'egressPolicyDigest',
'intentDigest',
'maxOutputTokens',
'model',
'modelBoundary',
'provider',
'responseLanguage',
],
'model intent',
);
const normalized = prepareModelIntent({
provider: candidate.provider as never,
model: candidate.model as never,
modelBoundary: candidate.modelBoundary as never,
responseLanguage: candidate.responseLanguage as never,
maxOutputTokens: candidate.maxOutputTokens as never,
egressPolicy: candidate.egressPolicy as never,
});
if (
digest(candidate.egressPolicyDigest, 'egress policy digest') !==
normalized.egressPolicyDigest ||
digest(candidate.intentDigest, 'model intent digest') !==
normalized.intentDigest
) {
return invalid('model intent digest is invalid');
}
return normalized;
}
export function normalizeToolIntent(
value: unknown,
): Readonly<CopilotFailureDiagnosisToolIntent> {
const candidate = dataRecord(value, 'Tool intent');
exactKeys(
candidate,
[
'actionDigest',
'actionRef',
'bindingDigest',
'definitionDigest',
'invocationActionDigest',
'invocationArtifact',
'planDigest',
'previewArtifact',
'sealedAtMs',
'snapshotDigest',
],
'Tool intent',
);
const invocationArtifact = normalizeToolInvocationInputArtifactReference(
candidate.invocationArtifact as never,
);
const previewArtifact = normalizeToolInvocationPreviewArtifactReference(
candidate.previewArtifact as never,
);
const actionDigest = digest(candidate.actionDigest, 'Tool action digest');
if (previewArtifact.actionDigest !== actionDigest) {
return invalid('Tool preview action binding is invalid');
}
return Object.freeze({
actionRef: identity(candidate.actionRef, 'Tool action reference'),
planDigest: digest(candidate.planDigest, 'Tool plan digest'),
actionDigest,
invocationActionDigest: digest(
candidate.invocationActionDigest,
'Tool invocation action digest',
),
snapshotDigest: digest(candidate.snapshotDigest, 'Tool snapshot digest'),
definitionDigest: digest(
candidate.definitionDigest,
'Tool definition digest',
),
bindingDigest: digest(candidate.bindingDigest, 'Tool binding digest'),
invocationArtifact,
previewArtifact,
sealedAtMs: timestamp(candidate.sealedAtMs, 'Tool plan seal time'),
});
}
export function assertDeadline(
plannedAtMs: number,
deadlineAtMs: number,
): void {
if (
deadlineAtMs <= plannedAtMs ||
deadlineAtMs - plannedAtMs > MAX_MODEL_INVOCATION_MS
) {
invalid('diagnosis deadline is invalid');
}
}
export function assertJsonBudget(
value: unknown,
maximumBytes: number,
label: string,
): void {
if (Buffer.byteLength(JSON.stringify(value), 'utf8') > maximumBytes) {
invalid(`${label} exceeds its byte budget`);
}
}
export { normalizeProjectPolicySubject };
@@ -0,0 +1,8 @@
export * from './admission/contracts';
export * from './admission/durableEvidence';
export * from './admission/plan';
export {
failureDiagnosisEgressPolicyDigest,
failureDiagnosisModelIntentDigest,
failureDiagnosisToolInputDigest,
} from './admission/validation';
@@ -0,0 +1,4 @@
export {
PostgresCopilotFailureDiagnosisAdmissionRepository,
type PostgresCopilotFailureDiagnosisAdmissionMutationGuard,
} from './admission/postgresRepository';
@@ -61,6 +61,8 @@ export const POSTGRES_PLUGIN_PACKAGE_PROMPT_OUTPUT_KEY_ROTATION_MIGRATION_ID =
'pg-9016-ai-plugin-package-prompt-output-key-rotation';
export const POSTGRES_PLUGIN_PACKAGE_PROMPT_PRODUCT_AUTHORIZATION_MIGRATION_ID =
'pg-9017-ai-plugin-package-prompt-product-authorization';
export const POSTGRES_COPILOT_FAILURE_DIAGNOSIS_ADMISSION_MIGRATION_ID =
'pg-9018-ai-copilot-failure-diagnosis-admissions';
export const POSTGRES_MODEL_INVOCATION_SCHEMA = 'ql3_ai';
export const LOCAL_MODEL_INVOCATION_MIGRATION_HISTORY_TABLE =
'QingLong3AiSchemaMigrations';
@@ -1,7 +1,4 @@
import type {
PostgresPool,
PostgresQueryable,
} from '@qinglong/runtime-core';
import type { PostgresPool, PostgresQueryable } from '@qinglong/runtime-core';
import {
MigrationStreamHistoryCorruptionError,
runMigrationStream,
@@ -30,11 +27,13 @@ import {
POSTGRES_MODEL_PROVIDER_CREDENTIAL_TEST_CONNECTION_MIGRATION_ID,
POSTGRES_PLUGIN_PACKAGE_PROMPT_OUTPUT_KEY_ROTATION_MIGRATION_ID,
POSTGRES_PLUGIN_PACKAGE_PROMPT_PRODUCT_AUTHORIZATION_MIGRATION_ID,
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_ADMISSION_MIGRATION_ID,
POSTGRES_MODEL_INVOCATION_SCHEMA,
POSTGRES_MODEL_INVOCATION_MIGRATION_HISTORY_TABLE,
} from './identities';
import { postgresCatalogMigrations } from './postgres/catalog';
import { postgresCopilotMigrations } from './postgres/copilot';
import { postgresCoreMigrations } from './postgres/core';
import { postgresCredentialMigrations } from './postgres/credential';
import {
@@ -63,6 +62,7 @@ const POSTGRES_HISTORY_IDENTITY = Object.freeze({
POSTGRES_MODEL_PROVIDER_CREDENTIAL_MANAGEMENT_IDENTITY_MIGRATION_ID,
POSTGRES_MODEL_PROVIDER_CREDENTIAL_TEST_CONNECTION_MIGRATION_ID,
POSTGRES_PLUGIN_PACKAGE_PROMPT_PRODUCT_AUTHORIZATION_MIGRATION_ID,
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_ADMISSION_MIGRATION_ID,
]),
streamId: POSTGRES_MODEL_INVOCATION_MIGRATION_STREAM_ID,
dialect: 'postgresql' as const,
@@ -246,6 +246,7 @@ export const postgresModelInvocationMigrationDefinition: MigrationStreamDefiniti
...postgresPromptBaseMigrations,
...postgresCredentialMigrations,
...postgresPromptExtensionMigrations,
...postgresCopilotMigrations,
]),
});
@@ -0,0 +1,220 @@
import type { PostgresQueryable } from '@qinglong/runtime-core';
import {
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_ADMISSION_MIGRATION_ID,
POSTGRES_MODEL_INVOCATION_SCHEMA,
} from '../identities';
import { defineSqlMigration } from '../shared';
const ADMISSION_TABLE = 'copilot_failure_diagnosis_admissions';
const SOURCE_SNAPSHOT_FUNCTION =
'copilot_failure_diagnosis_admission_source_snapshot';
const POSTGRES_COPILOT_FAILURE_DIAGNOSIS_ADMISSION_TABLE_SQL = `
CREATE TABLE "${POSTGRES_MODEL_INVOCATION_SCHEMA}"."${ADMISSION_TABLE}" (
request_id varchar(128) PRIMARY KEY,
plan_digest char(64) NOT NULL UNIQUE,
run_id varchar(36) NOT NULL UNIQUE,
project_id varchar(128) NOT NULL,
source_run_id varchar(36) NOT NULL,
source_run_version integer NOT NULL,
source_run_status varchar(32) NOT NULL,
source_attempt_id varchar(36) NOT NULL,
source_attempt_status varchar(32) NOT NULL,
source_log_artifact_id varchar(36) NOT NULL,
tool_plan_digest char(64) NOT NULL,
tool_action_digest char(64) NOT NULL,
tool_step_run_id varchar(128) NOT NULL UNIQUE,
model_intent_digest char(64) NOT NULL,
model_step_run_id varchar(128) NOT NULL UNIQUE,
admitted_at_ms bigint NOT NULL,
receipt_digest char(64) NOT NULL UNIQUE,
plan_json jsonb NOT NULL,
receipt_json jsonb NOT NULL,
CONSTRAINT ql3_ai_copilot_diagnosis_admission_run_fk
FOREIGN KEY (run_id) REFERENCES "ql3"."runs" (id) ON DELETE RESTRICT,
CONSTRAINT ql3_ai_copilot_diagnosis_admission_source_run_fk
FOREIGN KEY (source_run_id) REFERENCES "ql3"."runs" (id) ON DELETE RESTRICT,
CONSTRAINT ql3_ai_copilot_diagnosis_admission_source_attempt_fk
FOREIGN KEY (source_attempt_id)
REFERENCES "ql3"."run_attempts" (id) ON DELETE RESTRICT,
CONSTRAINT ql3_ai_copilot_diagnosis_admission_tool_step_fk
FOREIGN KEY (run_id, tool_step_run_id)
REFERENCES "ql3"."step_runs" (run_id, id) ON DELETE RESTRICT,
CONSTRAINT ql3_ai_copilot_diagnosis_admission_model_step_fk
FOREIGN KEY (run_id, model_step_run_id)
REFERENCES "ql3"."step_runs" (run_id, id) ON DELETE RESTRICT,
CONSTRAINT ql3_ai_copilot_diagnosis_admission_identity_check CHECK (
request_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,35}$' AND
project_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
source_run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,35}$' AND
source_attempt_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,35}$' AND
source_log_artifact_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,35}$' AND
tool_step_run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
model_step_run_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' AND
source_run_version BETWEEN 1 AND 2147483647 AND admitted_at_ms >= 0
),
CONSTRAINT ql3_ai_copilot_diagnosis_admission_status_check CHECK (
source_run_status IN ('failed', 'timed_out') AND
source_attempt_status IN ('failed', 'timed_out', 'lost') AND
((source_run_status = 'failed' AND source_attempt_status IN ('failed', 'lost')) OR
(source_run_status = 'timed_out' AND source_attempt_status = 'timed_out'))
),
CONSTRAINT ql3_ai_copilot_diagnosis_admission_digest_check CHECK (
plan_digest ~ '^[0-9a-f]{64}$' AND
tool_plan_digest ~ '^[0-9a-f]{64}$' AND
tool_action_digest ~ '^[0-9a-f]{64}$' AND
model_intent_digest ~ '^[0-9a-f]{64}$' AND
receipt_digest ~ '^[0-9a-f]{64}$'
),
CONSTRAINT ql3_ai_copilot_diagnosis_admission_json_check CHECK (
jsonb_typeof(plan_json) = 'object' AND
octet_length(plan_json::text) BETWEEN 2 AND 32768 AND
plan_json @> jsonb_build_object(
'schema', 'qinglong/copilot-failure-diagnosis-execution-plan@v1',
'requestId', request_id, 'planDigest', plan_digest,
'runId', run_id, 'projectId', project_id,
'toolStepRunId', tool_step_run_id,
'modelStepRunId', model_step_run_id,
'plannedAtMs', admitted_at_ms
) AND
plan_json -> 'source' @> jsonb_build_object(
'runId', source_run_id, 'runVersion', source_run_version,
'runStatus', source_run_status, 'attemptId', source_attempt_id,
'attemptStatus', source_attempt_status,
'logArtifactId', source_log_artifact_id
) AND
plan_json -> 'tool' @> jsonb_build_object(
'planDigest', tool_plan_digest, 'actionDigest', tool_action_digest
) AND
plan_json -> 'model' @> jsonb_build_object(
'intentDigest', model_intent_digest
) AND
jsonb_typeof(receipt_json) = 'object' AND
octet_length(receipt_json::text) BETWEEN 2 AND 16384 AND
receipt_json @> jsonb_build_object(
'schema', 'qinglong/copilot-failure-diagnosis-admission-receipt@v1',
'requestId', request_id, 'planDigest', plan_digest,
'runId', run_id, 'sourceRunId', source_run_id,
'sourceRunVersion', source_run_version,
'sourceAttemptId', source_attempt_id,
'toolStepRunId', tool_step_run_id,
'modelStepRunId', model_step_run_id,
'admittedAtMs', admitted_at_ms, 'receiptDigest', receipt_digest
)
)
)`;
const POSTGRES_COPILOT_FAILURE_DIAGNOSIS_SOURCE_SNAPSHOT_SQL = `
CREATE FUNCTION
"${POSTGRES_MODEL_INVOCATION_SCHEMA}"."${SOURCE_SNAPSHOT_FUNCTION}"(
p_project_id varchar,
p_subject_type varchar,
p_subject_id varchar,
p_project_version integer,
p_binding_version integer,
p_source_run_id varchar,
p_source_attempt_id varchar
)
RETURNS TABLE(
run_id varchar,
run_version integer,
run_status varchar,
attempt_id varchar,
attempt_status varchar,
attempt_finished_at_ms bigint,
log_artifact_id varchar
)
LANGUAGE plpgsql
VOLATILE
SECURITY DEFINER
SET search_path = pg_catalog, ql3, ql3_ai
AS $ql3_ai$
BEGIN
IF NOT pg_has_role(session_user, 'ql3_runtime', 'member') THEN
RAISE EXCEPTION 'Runtime authority is required'
USING ERRCODE = 'insufficient_privilege';
END IF;
PERFORM 1
FROM "ql3"."projects" AS project
JOIN "ql3"."project_role_bindings" AS binding
ON binding.project_id = project.id
AND binding.subject_type = p_subject_type
AND binding.subject_id = p_subject_id
AND binding.version = p_binding_version
WHERE project.id = p_project_id
AND project.status = 'active'
AND project.version = p_project_version
AND binding.state = 'active'
AND binding.version = (
SELECT max(candidate.version)
FROM "ql3"."project_role_bindings" AS candidate
WHERE candidate.project_id = p_project_id
AND candidate.subject_type = p_subject_type
AND candidate.subject_id = p_subject_id
)
FOR SHARE OF project, binding;
IF NOT FOUND THEN
RETURN;
END IF;
RETURN QUERY
SELECT source_run.id, source_run.version, source_run.status,
source_attempt.id, source_attempt.status,
source_attempt.finished_at_ms, source_attempt.log_artifact_id
FROM "ql3"."runs" AS source_run
JOIN "ql3"."run_attempts" AS source_attempt
ON source_attempt.run_id = source_run.id
WHERE source_run.id = p_source_run_id
AND source_run.project_id = p_project_id
AND source_run.status IN ('failed', 'timed_out')
AND source_attempt.id = p_source_attempt_id
AND source_attempt.attempt = (
SELECT max(candidate.attempt)
FROM "ql3"."run_attempts" AS candidate
WHERE candidate.run_id = source_run.id
)
AND source_attempt.status IN ('failed', 'timed_out', 'lost')
AND ((source_run.status = 'failed' AND source_attempt.status IN ('failed', 'lost')) OR
(source_run.status = 'timed_out' AND source_attempt.status = 'timed_out'))
AND source_attempt.finished_at_ms IS NOT NULL
AND source_attempt.log_artifact_id IS NOT NULL
FOR SHARE OF source_run, source_attempt;
END
$ql3_ai$`;
const postgresCopilotFailureDiagnosisAdmissionMigration =
defineSqlMigration<PostgresQueryable>(
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_ADMISSION_MIGRATION_ID,
[
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_ADMISSION_TABLE_SQL,
`CREATE UNIQUE INDEX ql3_ai_copilot_diagnosis_run_steps_uidx
ON "${POSTGRES_MODEL_INVOCATION_SCHEMA}"."${ADMISSION_TABLE}"
(run_id, tool_step_run_id, model_step_run_id)`,
`CREATE INDEX ql3_ai_copilot_diagnosis_source_idx
ON "${POSTGRES_MODEL_INVOCATION_SCHEMA}"."${ADMISSION_TABLE}"
(project_id, source_run_id, admitted_at_ms, request_id)`,
`REVOKE ALL ON TABLE
"${POSTGRES_MODEL_INVOCATION_SCHEMA}"."${ADMISSION_TABLE}"
FROM PUBLIC`,
`GRANT SELECT, INSERT ON TABLE
"${POSTGRES_MODEL_INVOCATION_SCHEMA}"."${ADMISSION_TABLE}"
TO ql3_runtime`,
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_SOURCE_SNAPSHOT_SQL,
`REVOKE ALL ON FUNCTION
"${POSTGRES_MODEL_INVOCATION_SCHEMA}"."${SOURCE_SNAPSHOT_FUNCTION}"(
varchar, varchar, varchar, integer, integer, varchar, varchar
) FROM PUBLIC`,
`GRANT EXECUTE ON FUNCTION
"${POSTGRES_MODEL_INVOCATION_SCHEMA}"."${SOURCE_SNAPSHOT_FUNCTION}"(
varchar, varchar, varchar, integer, integer, varchar, varchar
) TO ql3_runtime`,
],
(context, statement) => context.query(statement).then(() => undefined),
);
export const postgresCopilotMigrations = Object.freeze([
postgresCopilotFailureDiagnosisAdmissionMigration,
]);
@@ -0,0 +1,325 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
BUILTIN_RUN_LOG_EXCERPT_TOOL,
BUILTIN_RUN_LOG_EXCERPT_TOOL_DEFINITION,
createBuiltInRunLogExcerptToolHandlerBinding,
} = require('@qinglong/runtime-core/builtin-run-log-excerpt-tool');
const {
createPluginPackageResourceGenerationFromReferences,
} = require('@qinglong/runtime-core/plugin-package-resource-generation');
const {
createProjectToolDefinitionSnapshot,
projectToolDefinitionRegistry,
} = require('@qinglong/runtime-core/project-tool-definition-snapshot');
const {
prepareToolInvocation,
} = require('@qinglong/runtime-core/tool-registry');
const {
TrustedToolHandlerBindingRegistry,
createTrustedToolInvocationPlan,
} = require('@qinglong/runtime-core/trusted-tool-invocation');
const {
COPILOT_FAILURE_DIAGNOSIS_ADMISSION_RECEIPT_SCHEMA,
COPILOT_FAILURE_DIAGNOSIS_EXECUTION_PLAN_SCHEMA,
InvalidCopilotFailureDiagnosisExecutionPlanError,
createCopilotFailureDiagnosisAdmissionBundle,
failureDiagnosisToolInputDigest,
normalizeCopilotFailureDiagnosisAdmissionReceipt,
normalizeCopilotFailureDiagnosisExecutionPlan,
prepareCopilotFailureDiagnosisExecution,
} = require('../dist/copilot/failure-diagnosis/executionAdmission.js');
const {
storedJsonEquals,
} = require('../dist/copilot/failure-diagnosis/admission/postgresRepository.js');
const DIGEST_A = 'a'.repeat(64);
const DIGEST_B = 'b'.repeat(64);
const DIGEST_C = 'c'.repeat(64);
const SUBJECT = Object.freeze({ type: 'user', id: 'usr-diagnosis-owner' });
const FENCE = Object.freeze({ projectVersion: 3, bindingVersion: 7 });
const SOURCE = Object.freeze({
runId: 'run-source-failed',
runVersion: 8,
runStatus: 'failed',
attemptId: 'attempt-source-final',
attemptStatus: 'failed',
attemptFinishedAtMs: 1_900,
logArtifactId: 'artifact-source-log',
});
test('compares PostgreSQL jsonb evidence independent of object key order', () => {
assert.equal(
storedJsonEquals(
{ outer: { beta: 2, alpha: 1 }, items: [{ right: 2, left: 1 }] },
{ items: [{ left: 1, right: 2 }], outer: { alpha: 1, beta: 2 } },
),
true,
);
assert.equal(
storedJsonEquals({ outer: { alpha: 1 } }, { outer: { alpha: 2 } }),
false,
);
});
function snapshot() {
const generation = createPluginPackageResourceGenerationFromReferences({
installationId: 'install-qinglong-run-log-excerpt',
projectId: 'project-diagnosis',
packageName: 'qinglong',
lockDigest: DIGEST_A,
generation: 1,
previousActiveLockDigest: null,
contentDigest: DIGEST_B,
resources: [],
});
return createProjectToolDefinitionSnapshot({
projectId: 'project-diagnosis',
contributions: [
{
generation,
revisionDigest: DIGEST_C,
definitions: [BUILTIN_RUN_LOG_EXCERPT_TOOL_DEFINITION],
},
],
});
}
function principal() {
return {
subject: SUBJECT,
authenticationId: 'auth-diagnosis-owner',
authenticatedAtMs: 1_000,
expiresAtMs: 10_000,
assurance: 'multi_factor',
};
}
function authorizer(effect = 'allow') {
return {
async authorize() {
return {
effect,
reasons:
effect === 'allow'
? ['role_grant']
: ['agent_action_requires_approval'],
fence: FENCE,
};
},
};
}
async function toolHarness(options = {}) {
const currentSnapshot = snapshot();
const binding = createBuiltInRunLogExcerptToolHandlerBinding(
currentSnapshot,
[options.profile ?? 'cluster-control'],
);
const bindings = new TrustedToolHandlerBindingRegistry(currentSnapshot, [
binding,
]);
const source = options.source ?? SOURCE;
const invocation = await prepareToolInvocation(
projectToolDefinitionRegistry(currentSnapshot),
{
projectId: 'project-diagnosis',
principal: principal(),
nowMs: 1_200,
tool: BUILTIN_RUN_LOG_EXCERPT_TOOL,
input: {
runId: options.inputRunId ?? source.runId,
attemptId: options.inputAttemptId ?? source.attemptId,
},
},
authorizer(options.effect),
);
const bundle = createTrustedToolInvocationPlan(bindings, invocation, {
actionRef: 'diagnosis-log-tool-plan',
inputArtifactId: 'diagnosis-tool-input',
previewArtifactId: 'diagnosis-tool-preview',
artifactKeyId: 'diagnosis-input-key',
artifactKey: Buffer.alloc(32, 0x21),
artifactNonce: Buffer.alloc(12, 0x31),
profile: options.profile ?? 'cluster-control',
preview: {
title: 'Read failed Run log',
summary: 'Reads one bounded redacted log excerpt',
fields: [
{ kind: 'identifier', label: 'Run', value: source.runId },
{ kind: 'identifier', label: 'Attempt', value: source.attemptId },
],
warnings: ['potentially_sensitive_output'],
},
sealedAtMs: 2_000,
});
return { bindings, toolPlan: bundle.plan, source };
}
function model(overrides = {}) {
return {
provider: 'provider-primary',
model: 'model-diagnosis',
modelBoundary: 'external',
responseLanguage: 'zh-CN',
maxOutputTokens: 512,
egressPolicy: {
schema: 'qinglong/copilot-model-egress-policy@v1',
revision: 'policy-diagnosis-v1',
potentiallySensitiveDataBoundaries: ['on_device', 'external'],
maxInputBytes: 64 * 1024,
maxOutputTokens: 1_024,
},
...overrides,
};
}
async function plan(options = {}) {
const harness = await toolHarness(options);
return prepareCopilotFailureDiagnosisExecution({
requestId: 'request-diagnosis-001',
traceId: 'trace-diagnosis-001',
source: harness.source,
toolPlan: harness.toolPlan,
bindings: harness.bindings,
model: model(options.model),
deadlineAtMs: 8_000,
plannedAtMs: 2_100,
});
}
test('creates one independent diagnosis Run with ready Tool and pending Model Steps', async () => {
const current = await plan();
assert.equal(current.schema, COPILOT_FAILURE_DIAGNOSIS_EXECUTION_PLAN_SCHEMA);
assert.equal(current.runId.startsWith('cdr:'), true);
assert.equal(current.toolStepRunId.startsWith('cdt:'), true);
assert.equal(current.modelStepRunId.startsWith('cdm:'), true);
assert.equal(current.modelInvocationId.startsWith('cdi:'), true);
assert.equal(
current.tool.invocationArtifact.inputDigest,
failureDiagnosisToolInputDigest(SOURCE),
);
assert.equal(current.model.egressPolicyDigest.length, 64);
assert.equal(current.model.intentDigest.length, 64);
const bundle = createCopilotFailureDiagnosisAdmissionBundle(current);
assert.equal(bundle.run.parentRunId, SOURCE.runId);
assert.equal(bundle.run.status, 'running');
assert.equal(bundle.run.version, 3);
assert.equal(bundle.run.eventSequence, 3);
assert.equal(bundle.admissionEvent.sequence, 1);
for (const event of [
bundle.admissionEvent,
bundle.toolStepMutation.event,
bundle.modelStepMutation.event,
]) {
assert.equal(Buffer.byteLength(event.id, 'utf8') <= 36, true);
}
assert.deepEqual(
{
kind: bundle.toolStepMutation.stepRun.kind,
status: bundle.toolStepMutation.stepRun.status,
sequence: bundle.toolStepMutation.event.sequence,
},
{ kind: 'tool', status: 'ready', sequence: 2 },
);
assert.deepEqual(
{
kind: bundle.modelStepMutation.stepRun.kind,
status: bundle.modelStepMutation.stepRun.status,
parentStepRunId: bundle.modelStepMutation.stepRun.parentStepRunId,
sequence: bundle.modelStepMutation.event.sequence,
},
{
kind: 'model',
status: 'pending',
parentStepRunId: current.toolStepRunId,
sequence: 3,
},
);
assert.equal(
bundle.receipt.schema,
COPILOT_FAILURE_DIAGNOSIS_ADMISSION_RECEIPT_SCHEMA,
);
assert.equal(bundle.receipt.finalRunVersion, 3);
assert.deepEqual(
normalizeCopilotFailureDiagnosisExecutionPlan(
JSON.parse(JSON.stringify(current)),
),
current,
);
assert.deepEqual(
normalizeCopilotFailureDiagnosisAdmissionReceipt(
JSON.parse(JSON.stringify(bundle.receipt)),
),
bundle.receipt,
);
});
test('rejects a Tool input detached from the source failure fence', async () => {
await assert.rejects(
() => plan({ inputAttemptId: 'attempt-unrelated' }),
InvalidCopilotFailureDiagnosisExecutionPlanError,
);
});
test('rejects approval-pending or non-Cluster Tool authority before Run creation', async () => {
await assert.rejects(
() => plan({ effect: 'require_approval' }),
InvalidCopilotFailureDiagnosisExecutionPlanError,
);
await assert.rejects(
() => plan({ profile: 'edge' }),
InvalidCopilotFailureDiagnosisExecutionPlanError,
);
});
test('rejects inconsistent source terminal evidence and denied model egress', async () => {
await assert.rejects(
() =>
plan({
source: {
...SOURCE,
runStatus: 'timed_out',
attemptStatus: 'failed',
},
}),
InvalidCopilotFailureDiagnosisExecutionPlanError,
);
await assert.rejects(
() =>
plan({
model: {
egressPolicy: {
...model().egressPolicy,
potentiallySensitiveDataBoundaries: ['on_device'],
},
},
}),
InvalidCopilotFailureDiagnosisExecutionPlanError,
);
});
test('fails closed on widened or digest-drifted durable plans', async () => {
const current = await plan();
assert.throws(
() =>
normalizeCopilotFailureDiagnosisExecutionPlan({
...current,
callerAuthority: 'execute',
}),
InvalidCopilotFailureDiagnosisExecutionPlanError,
);
assert.throws(
() =>
normalizeCopilotFailureDiagnosisExecutionPlan({
...current,
source: {
...current.source,
runVersion: current.source.runVersion + 1,
},
}),
InvalidCopilotFailureDiagnosisExecutionPlanError,
);
});
@@ -19,6 +19,7 @@ const {
LOCAL_MODEL_INVOCATION_USAGE_MIGRATION_ID,
LOCAL_MODEL_PRICE_CATALOG_AUTHORIZATION_MIGRATION_ID,
LOCAL_MODEL_PRICE_CATALOG_MIGRATION_ID,
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_ADMISSION_MIGRATION_ID,
POSTGRES_MODEL_INVOCATION_MIGRATION_ID,
POSTGRES_MODEL_INVOCATION_MIGRATION_HISTORY_TABLE,
POSTGRES_MODEL_INVOCATION_MIGRATION_STREAM_ID,
@@ -421,6 +422,10 @@ test('PostgreSQL AI schema is an independent reviewed feature stream', async ()
POSTGRES_PLUGIN_PACKAGE_PROMPT_PRODUCT_AUTHORIZATION_MIGRATION_ID,
'pg-9017-ai-plugin-package-prompt-product-authorization',
);
assert.equal(
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_ADMISSION_MIGRATION_ID,
'pg-9018-ai-copilot-failure-diagnosis-admissions',
);
assert.equal(
POSTGRES_MODEL_INVOCATION_MIGRATION_HISTORY_TABLE,
'ai_schema_migrations',
@@ -499,9 +504,38 @@ test('PostgreSQL AI schema is an independent reviewed feature stream', async ()
/^[0-9a-f]{64}$/,
);
assert.equal(
postgresModelInvocationMigrationDefinition.migrations.length,
17,
postgresModelInvocationMigrationDefinition.migrations[17].checksum,
'cd0837c68ecc6c2bce58d048308d0239397b6347b4483f02372f966c05ae7ad6',
);
assert.equal(
postgresModelInvocationMigrationDefinition.migrations.length,
18,
);
const diagnosisAdmissionStatements = [];
await postgresModelInvocationMigrationDefinition.migrations[17].up({
async query(statement) {
diagnosisAdmissionStatements.push(statement);
return { rows: [] };
},
});
const diagnosisAdmissionSql = diagnosisAdmissionStatements.join('\n');
assert.match(
diagnosisAdmissionSql,
/CREATE TABLE "ql3_ai"\."copilot_failure_diagnosis_admissions"/,
);
assert.match(
diagnosisAdmissionSql,
/CREATE FUNCTION[\s\S]*copilot_failure_diagnosis_admission_source_snapshot/,
);
assert.match(diagnosisAdmissionSql, /SECURITY DEFINER/);
assert.match(diagnosisAdmissionSql, /FOR SHARE OF project, binding/);
assert.match(
diagnosisAdmissionSql,
/FOR SHARE OF source_run, source_attempt/,
);
assert.match(diagnosisAdmissionSql, /TO ql3_runtime/);
assert.doesNotMatch(diagnosisAdmissionSql, /GRANT[^;]*(?:UPDATE|DELETE)/);
const retirementStatements = [];
await postgresModelInvocationMigrationDefinition.migrations[10].up({
@@ -173,7 +173,10 @@ test('readiness binds exact migration history and least-privilege primary author
},
});
assert.equal(report.ready, true);
assert.equal(report.migrationIds.at(-1).startsWith('pg-9017-'), true);
assert.equal(
report.migrationIds.at(-1),
'pg-9018-ai-copilot-failure-diagnosis-admissions',
);
assert.match(
queries[1],
/model_provider_credential_management_identity_keyset_ledger/,
@@ -277,7 +277,7 @@ test('tester readiness freezes migration history and least privilege', async ()
assert.equal(ready.ready, true);
assert.equal(
ready.migrationIds.at(-1),
'pg-9017-ai-plugin-package-prompt-product-authorization',
'pg-9018-ai-copilot-failure-diagnosis-admissions',
);
await assert.rejects(
@@ -34,7 +34,10 @@ WITH observation AS (
FROM "ql3"."runs"
CROSS JOIN observation
WHERE execution_owner = 'runtime'
AND trigger_type <> 'plugin_package_workflow'
AND trigger_type NOT IN (
'plugin_package_workflow',
'copilot_failure_diagnosis'
)
AND (
status = 'created'
OR (
@@ -1,6 +1,8 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const { PostgresClusterControlRecoverySource } = require('../dist/entrypoints/runtime');
const {
PostgresClusterControlRecoverySource,
} = require('../dist/entrypoints/runtime');
function sourceWith(rows, observations = []) {
return new PostgresClusterControlRecoverySource({
@@ -70,7 +72,7 @@ test('reads Run and Attempt recovery candidates through one bounded query', asyn
assert.match(observations[0].text, /execution_owner = 'runtime'/);
assert.match(
observations[0].text,
/trigger_type <> 'plugin_package_workflow'/,
/trigger_type NOT IN \([\s\S]*'plugin_package_workflow',[\s\S]*'copilot_failure_diagnosis'[\s\S]*\)/,
);
assert.match(observations[0].text, /attempt_candidates/);
assert.match(
@@ -81,10 +83,7 @@ test('reads Run and Attempt recovery candidates through one bounded query', asyn
observations[0].text,
/attempt_run\.trigger_type = 'plugin_package_workflow'/,
);
assert.match(
observations[0].text,
/workflow_task\.attempt_id = attempt\.id/,
);
assert.match(observations[0].text, /workflow_task\.attempt_id = attempt\.id/);
assert.match(
observations[0].text,
/lease_expires_at_ms > observation\.observed_at_ms/,
@@ -89,7 +89,10 @@ function assertSafeRecovery(
recovery: ClusterControlStartupRecoverySummary,
): void {
if (!recovery.safe || recovery.remaining !== 0 || recovery.failed !== 0) {
throw new Error('Cluster-control startup recovery did not converge safely');
throw new Error(
'Cluster-control startup recovery did not converge safely ' +
`(remaining=${recovery.remaining}, failed=${recovery.failed})`,
);
}
}
@@ -127,7 +127,7 @@ test('unsafe recovery stops the stack before lifecycles and admission', async ()
},
}),
),
/did not converge safely/,
/did not converge safely \(remaining=1, failed=0\)/,
);
assert.equal(events.includes('start-lifecycles'), false);
assert.equal(events.includes('install-admission'), false);
+393 -1
View File
@@ -21,6 +21,7 @@ const {
createPostgresDatabaseOpener,
PostgresClusterRunCancellationConvergenceRepository,
PostgresClusterRunCancellationRepository,
PostgresClusterControlRecoverySource,
PostgresClusterScheduleRepository,
PostgresRemoteWorkerCompletionRepository,
PostgresRemoteWorkerLeaseControlRepository,
@@ -32,6 +33,7 @@ const {
runPostgresMigrations,
} = require('../packages/ql3-cluster-postgres/dist/migration/migration.js');
const {
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_ADMISSION_MIGRATION_ID,
POSTGRES_MODEL_INVOCATION_MIGRATION_ID,
POSTGRES_MODEL_INVOCATION_PRICING_MIGRATION_ID,
POSTGRES_MODEL_INVOCATION_QUOTA_MIGRATION_ID,
@@ -51,6 +53,31 @@ const {
POSTGRES_PLUGIN_PACKAGE_PROMPT_PRODUCT_AUTHORIZATION_MIGRATION_ID,
migratePostgresModelInvocationFeature,
} = require('../packages/ql3-ai/dist/migration/modelInvocationMigration.js');
const {
prepareCopilotFailureDiagnosisExecution,
} = require('../packages/ql3-ai/dist/copilot/failure-diagnosis/executionAdmission.js');
const {
PostgresCopilotFailureDiagnosisAdmissionRepository,
} = require('../packages/ql3-ai/dist/copilot/failure-diagnosis/postgresAdmissionRepository.js');
const {
BUILTIN_RUN_LOG_EXCERPT_TOOL,
BUILTIN_RUN_LOG_EXCERPT_TOOL_DEFINITION,
createBuiltInRunLogExcerptToolHandlerBinding,
} = require('../packages/ql3-runtime-core/dist/tool-execution/builtin-run-log-excerpt/builtInRunLogExcerptTool.js');
const {
createPluginPackageResourceGenerationFromReferences,
} = require('../packages/ql3-runtime-core/dist/plugin-package/pluginPackageResourceGeneration.js');
const {
createProjectToolDefinitionSnapshot,
projectToolDefinitionRegistry,
} = require('../packages/ql3-runtime-core/dist/tool-execution/tool-registry/projectToolDefinitionSnapshot.js');
const {
prepareToolInvocation,
} = require('../packages/ql3-runtime-core/dist/tool-execution/tool-registry/toolRegistry.js');
const {
TrustedToolHandlerBindingRegistry,
createTrustedToolInvocationPlan,
} = require('../packages/ql3-runtime-core/dist/tool-execution/trustedToolInvocation.js');
const {
MODEL_PROVIDER_CREDENTIAL_TRANSITION_COMMAND_SCHEMA,
ModelProviderCredentialCatalogUnavailableError,
@@ -902,6 +929,8 @@ async function modelInvocationFeatureFacts(pool) {
FROM pg_tables
WHERE schemaname = 'ql3_ai'
AND (
tablename LIKE 'copilot_failure_diagnosis_%'
OR
tablename LIKE 'model_invocation_%'
OR tablename LIKE 'model_price_catalog_%'
OR tablename LIKE 'model_provider_credential_%'
@@ -1424,6 +1453,42 @@ async function modelInvocationFeatureFacts(pool) {
'ql3_ai.plugin_package_prompt_admission_snapshot(varchar,varchar,character,varchar,varchar,integer,integer)',
'EXECUTE'
) AS "promptSnapshotRuntimeOnly",
has_table_privilege(
'ql3_runtime',
'ql3_ai.copilot_failure_diagnosis_admissions', 'SELECT,INSERT'
) AND NOT has_table_privilege(
'ql3_runtime',
'ql3_ai.copilot_failure_diagnosis_admissions', 'UPDATE,DELETE'
) AND NOT EXISTS (
SELECT 1
FROM unnest(ARRAY[
'ql3_admin', 'ql3_package_manager', 'ql3_package_executor',
'ql3_worker_ingress', 'ql3_ai_maintenance',
'ql3_ai_credential_manager', 'ql3_ai_credential_tester'
]::text[]) AS denied(role_name)
WHERE has_table_privilege(
denied.role_name,
'ql3_ai.copilot_failure_diagnosis_admissions',
'SELECT,INSERT,UPDATE,DELETE'
)
) AS "copilotDiagnosisAdmissionRuntimeOnly",
has_function_privilege(
'ql3_runtime',
'ql3_ai.copilot_failure_diagnosis_admission_source_snapshot(varchar,varchar,varchar,integer,integer,varchar,varchar)',
'EXECUTE'
) AND NOT EXISTS (
SELECT 1
FROM unnest(ARRAY[
'ql3_admin', 'ql3_package_manager', 'ql3_package_executor',
'ql3_worker_ingress', 'ql3_ai_maintenance',
'ql3_ai_credential_manager', 'ql3_ai_credential_tester'
]::text[]) AS denied(role_name)
WHERE has_function_privilege(
denied.role_name,
'ql3_ai.copilot_failure_diagnosis_admission_source_snapshot(varchar,varchar,varchar,integer,integer,varchar,varchar)',
'EXECUTE'
)
) AS "copilotDiagnosisSnapshotRuntimeOnly",
has_table_privilege(
'ql3_runtime', 'ql3_ai.ai_schema_migrations', 'SELECT'
) AND NOT has_table_privilege(
@@ -1518,6 +1583,273 @@ async function modelInvocationFeatureFacts(pool) {
};
}
async function copilotFailureDiagnosisAdmissionFacts(pool, requestId) {
const result = await pool.query(
`SELECT admission.request_id AS "requestId",
admission.plan_digest AS "planDigest",
admission.receipt_digest AS "receiptDigest",
admission.source_run_id AS "sourceRunId",
admission.source_attempt_id AS "sourceAttemptId",
admission.tool_step_run_id AS "toolStepRunId",
admission.model_step_run_id AS "modelStepRunId",
run.parent_run_id AS "parentRunId", run.status,
run.version, run.event_sequence AS "eventSequence",
(SELECT count(*)::integer FROM "ql3"."run_events"
WHERE run_id = admission.run_id) AS "eventCount",
(SELECT count(*)::integer FROM "ql3"."step_run_mutations"
WHERE run_id = admission.run_id) AS "mutationCount",
tool.status AS "toolStatus", tool.kind AS "toolKind",
model.status AS "modelStatus", model.kind AS "modelKind",
model.parent_step_run_id AS "modelParentStepRunId",
admission.plan_json::text AS "planJson",
admission.receipt_json::text AS "receiptJson"
FROM "ql3_ai"."copilot_failure_diagnosis_admissions" AS admission
JOIN "ql3"."runs" AS run ON run.id = admission.run_id
JOIN "ql3"."step_runs" AS tool
ON tool.run_id = admission.run_id
AND tool.id = admission.tool_step_run_id
JOIN "ql3"."step_runs" AS model
ON model.run_id = admission.run_id
AND model.id = admission.model_step_run_id
WHERE admission.request_id = $1`,
[requestId],
);
assert.equal(result.rowCount, 1);
const { planJson, receiptJson, ...facts } = result.rows[0];
assert.equal(typeof planJson, 'string');
assert.equal(typeof receiptJson, 'string');
return {
...facts,
planJsonDigest: createHash('sha256').update(planJson).digest('hex'),
receiptJsonDigest: createHash('sha256').update(receiptJson).digest('hex'),
};
}
async function runCopilotFailureDiagnosisAdmissionHaEvidence(options) {
const { primaryPort, migrationPool } = options;
const baseTimeMs = Date.now();
const suffix = `${process.pid}-${randomBytes(3).toString('hex')}`;
const projectId = `diagnosis-${suffix}`;
const subjectId = `diagnosis-owner-${suffix}`;
const sourceRunId = `src-${randomBytes(10).toString('hex')}`;
const sourceAttemptId = `att-${randomBytes(10).toString('hex')}`;
const logArtifactId = `wlog-${createHash('sha256')
.update(suffix)
.digest('hex')
.slice(0, 30)}`;
const requestId = `diagnosis-request-${suffix}`;
const source = Object.freeze({
runId: sourceRunId,
runVersion: 8,
runStatus: 'failed',
attemptId: sourceAttemptId,
attemptStatus: 'failed',
attemptFinishedAtMs: baseTimeMs,
logArtifactId,
});
await migrationPool.query(
`INSERT INTO "ql3"."projects" (
id, name, slug, status, version, created_at_ms, updated_at_ms
) VALUES ($1, $1, $2, 'active', 1, 1, 1)`,
[projectId, projectId],
);
await migrationPool.query(
`INSERT INTO "ql3"."project_role_bindings" (
project_id, subject_type, subject_id, version, state, role,
mutation_id, changed_by_type, changed_by_id, created_at_ms
) VALUES ($1, 'user', $2, 1, 'active', 'owner', $3,
'system', 'ha-contract', 1)`,
[projectId, subjectId, `diagnosis-binding-${suffix}`],
);
await migrationPool.query(
`INSERT INTO "ql3"."runs" (
id, project_id, task_id, task_revision, trigger_type,
execution_origin, execution_owner, status, version, event_sequence,
created_at_ms, started_at_ms, finished_at_ms
) VALUES ($1, $2, 'source-task', 'source-v1', 'manual', 'manual',
'runtime', 'failed', 8, 8, $3, $4, $5)`,
[
sourceRunId,
projectId,
baseTimeMs - 2_000,
baseTimeMs - 1_000,
baseTimeMs,
],
);
await migrationPool.query(
`INSERT INTO "ql3"."run_attempts" (
id, run_id, attempt, status, executor_type, log_artifact_id,
callback_sequence, created_at_ms, started_at_ms, finished_at_ms,
error_code, error_summary
) VALUES ($1, $2, 1, 'failed', 'remote_worker', $3, 0,
$4, $5, $6, 'exit_nonzero', 'bounded failure')`,
[
sourceAttemptId,
sourceRunId,
logArtifactId,
baseTimeMs - 2_000,
baseTimeMs - 1_000,
baseTimeMs,
],
);
const generation = createPluginPackageResourceGenerationFromReferences({
installationId: `diagnosis-tool-${suffix}`,
projectId,
packageName: 'qinglong',
lockDigest: 'a'.repeat(64),
generation: 1,
previousActiveLockDigest: null,
contentDigest: 'b'.repeat(64),
resources: [],
});
const snapshot = createProjectToolDefinitionSnapshot({
projectId,
contributions: [
{
generation,
revisionDigest: 'c'.repeat(64),
definitions: [BUILTIN_RUN_LOG_EXCERPT_TOOL_DEFINITION],
},
],
});
const principal = Object.freeze({
subject: { type: 'user', id: subjectId },
authenticationId: `diagnosis-auth-${suffix}`,
authenticatedAtMs: baseTimeMs - 1_000,
expiresAtMs: baseTimeMs + 60_000,
assurance: 'multi_factor',
});
const invocation = await prepareToolInvocation(
projectToolDefinitionRegistry(snapshot),
{
projectId,
principal,
nowMs: baseTimeMs + 100,
tool: BUILTIN_RUN_LOG_EXCERPT_TOOL,
input: { runId: sourceRunId, attemptId: sourceAttemptId },
},
{
async authorize() {
return {
effect: 'allow',
reasons: ['role_grant'],
fence: { projectVersion: 1, bindingVersion: 1 },
};
},
},
);
const bindings = new TrustedToolHandlerBindingRegistry(snapshot, [
createBuiltInRunLogExcerptToolHandlerBinding(snapshot, ['cluster-control']),
]);
const toolPlan = createTrustedToolInvocationPlan(bindings, invocation, {
actionRef: `diagnosis-log-${suffix}`,
inputArtifactId: `diagnosis-input-${suffix}`,
previewArtifactId: `diagnosis-preview-${suffix}`,
artifactKeyId: `diagnosis-key-${suffix}`,
artifactKey: Buffer.alloc(32, 0x21),
artifactNonce: Buffer.alloc(12, 0x31),
profile: 'cluster-control',
preview: {
title: 'Read failed Run log',
summary: 'Reads one bounded redacted log excerpt',
fields: [
{ kind: 'identifier', label: 'Run', value: sourceRunId },
{ kind: 'identifier', label: 'Attempt', value: sourceAttemptId },
],
warnings: ['potentially_sensitive_output'],
},
sealedAtMs: baseTimeMs + 200,
}).plan;
const plan = prepareCopilotFailureDiagnosisExecution({
requestId,
traceId: `diagnosis-trace-${suffix}`,
source,
toolPlan,
bindings,
model: {
provider: 'provider-primary',
model: 'model-diagnosis',
modelBoundary: 'external',
responseLanguage: 'zh-CN',
maxOutputTokens: 512,
egressPolicy: {
schema: 'qinglong/copilot-model-egress-policy@v1',
revision: 'ha-diagnosis-policy-v1',
potentiallySensitiveDataBoundaries: ['external'],
maxInputBytes: 64 * 1024,
maxOutputTokens: 1_024,
},
},
deadlineAtMs: baseTimeMs + 60_000,
plannedAtMs: baseTimeMs + 300,
});
const runtimeDatabase = await databaseOpener(
'runtime',
databaseUrl(RUNTIME_USER, RUNTIME_PASSWORD, primaryPort),
'ql3-ha-copilot-diagnosis-admission',
)();
try {
const repository = new PostgresCopilotFailureDiagnosisAdmissionRepository(
runtimeDatabase.pool,
);
const first = await repository.admit(plan);
const replay = await repository.admit(plan);
assert.equal(first.status, 'created');
assert.equal(replay.status, 'existing');
assert.deepEqual(replay.receipt, first.receipt);
assert.deepEqual(
await repository.findByRequestId(requestId),
first.receipt,
);
assert.deepEqual(await repository.findPlanByRequestId(requestId), plan);
const facts = await copilotFailureDiagnosisAdmissionFacts(
runtimeDatabase.pool,
requestId,
);
assert.deepEqual(
{
parentRunId: facts.parentRunId,
status: facts.status,
version: facts.version,
eventSequence: facts.eventSequence,
eventCount: facts.eventCount,
mutationCount: facts.mutationCount,
toolStatus: facts.toolStatus,
toolKind: facts.toolKind,
modelStatus: facts.modelStatus,
modelKind: facts.modelKind,
modelParentStepRunId: facts.modelParentStepRunId,
},
{
parentRunId: sourceRunId,
status: 'running',
version: 3,
eventSequence: 3,
eventCount: 3,
mutationCount: 2,
toolStatus: 'ready',
toolKind: 'tool',
modelStatus: 'pending',
modelKind: 'model',
modelParentStepRunId: plan.toolStepRunId,
},
);
return {
requestId,
sourceRunId,
sourceAttemptId,
runId: plan.runId,
toolStepRunId: plan.toolStepRunId,
modelStepRunId: plan.modelStepRunId,
exactReplay: true,
beforePromotion: facts,
};
} finally {
await runtimeDatabase.close();
}
}
async function runModelProviderCredentialCatalogMatrix(options) {
const { primaryPort, migrationPool } = options;
const suffix = `${process.pid}-${randomBytes(3).toString('hex')}`;
@@ -9967,6 +10299,7 @@ async function main(argv = process.argv.slice(2)) {
let networkPartition;
let oldPrimaryRejoin;
let modelInvocationFeaturePromotion;
let copilotFailureDiagnosisAdmission;
let modelProviderCredentialCatalog;
let modelProviderCredentialTestConnection;
let runAttemptLogRetentionEvidence;
@@ -10064,6 +10397,7 @@ async function main(argv = process.argv.slice(2)) {
migrationDatabase.pool,
);
assert.deepEqual(beforePromotion.tables, [
'copilot_failure_diagnosis_admissions',
'model_invocation_completions',
'model_invocation_price_quotes',
'model_invocation_price_settlements',
@@ -10113,6 +10447,7 @@ async function main(argv = process.argv.slice(2)) {
POSTGRES_MODEL_PROVIDER_CREDENTIAL_TEST_CONNECTION_MIGRATION_ID,
POSTGRES_PLUGIN_PACKAGE_PROMPT_OUTPUT_KEY_ROTATION_MIGRATION_ID,
POSTGRES_PLUGIN_PACKAGE_PROMPT_PRODUCT_AUTHORIZATION_MIGRATION_ID,
POSTGRES_COPILOT_FAILURE_DIAGNOSIS_ADMISSION_MIGRATION_ID,
],
);
assert.deepEqual(beforePromotion.privileges, {
@@ -10136,10 +10471,17 @@ async function main(argv = process.argv.slice(2)) {
promptOutputKeyRetirementAuthoritySplit: true,
promptOutputKeyRotationAuthoritySplit: true,
promptSnapshotRuntimeOnly: true,
copilotDiagnosisAdmissionRuntimeOnly: true,
copilotDiagnosisSnapshotRuntimeOnly: true,
migrationHistoryRuntimeReadOnly: true,
modelProviderCredentialManagementAuthoritySplit: true,
});
modelInvocationFeaturePromotion = { beforePromotion };
copilotFailureDiagnosisAdmission =
await runCopilotFailureDiagnosisAdmissionHaEvidence({
primaryPort,
migrationPool: migrationDatabase.pool,
});
} finally {
await migrationDatabase.close();
}
@@ -10420,6 +10762,21 @@ async function main(argv = process.argv.slice(2)) {
state: 'synchronous_remote_apply_ready',
atMs: Number((performance.now() - startedAt).toFixed(3)),
});
await waitFor(async () => {
const replicated = await copilotFailureDiagnosisAdmissionFacts(
standbyDatabase.pool,
copilotFailureDiagnosisAdmission.requestId,
);
return JSON.stringify(replicated) ===
JSON.stringify(copilotFailureDiagnosisAdmission.beforePromotion)
? replicated
: null;
}, 'Copilot failure diagnosis admission WAL replay');
copilotFailureDiagnosisAdmission.replicatedBeforePromotion = true;
timeline.push({
state: 'copilot_failure_diagnosis_admission_replicated',
atMs: Number((performance.now() - startedAt).toFixed(3)),
});
manualRunRetry = await runManualRunRetryHaEvidence({
primaryPort,
primaryDatabase,
@@ -11013,6 +11370,16 @@ async function main(argv = process.argv.slice(2)) {
String(schedulerDueAtMs),
);
const recoveryBeforeControlActivation =
await new PostgresClusterControlRecoverySource(
primaryDatabase.pool,
).listOutstanding(8);
assert.deepEqual(
recoveryBeforeControlActivation.candidates,
[],
'HA fixture must not leave generic Task recovery candidates before control activation',
);
proxy = await startEndpointProxy(primaryPort);
const stableDatabaseUrl = databaseUrl(
RUNTIME_USER,
@@ -11380,6 +11747,18 @@ async function main(argv = process.argv.slice(2)) {
modelInvocationFeaturePromotion.afterPromotion =
promotedModelInvocationFeature;
modelInvocationFeaturePromotion.survivedPromotion = true;
const promotedCopilotFailureDiagnosisAdmission =
await copilotFailureDiagnosisAdmissionFacts(
promotedDatabase.pool,
copilotFailureDiagnosisAdmission.requestId,
);
assert.deepEqual(
promotedCopilotFailureDiagnosisAdmission,
copilotFailureDiagnosisAdmission.beforePromotion,
);
copilotFailureDiagnosisAdmission.afterPromotion =
promotedCopilotFailureDiagnosisAdmission;
copilotFailureDiagnosisAdmission.survivedPromotion = true;
await verifyModelProviderCredentialCatalogAfterPromotion({
promotedPort: standbyPort,
promotedPool: promotedDatabase.pool,
@@ -12134,7 +12513,7 @@ async function main(argv = process.argv.slice(2)) {
FROM "ql3"."worker_credential_deliveries") AS "credentialDeliveries"`,
);
assert.deepEqual(sideEffects.rows, [
{ runs: 76, runEvents: 176, credentialDeliveries: 4 },
{ runs: 78, runEvents: 179, credentialDeliveries: 4 },
]);
timeline.push({
state: 'two_fresh_control_replicas_ready',
@@ -12248,6 +12627,7 @@ async function main(argv = process.argv.slice(2)) {
workerCredentialDeliveryCommitResponseLoss: credentialDelivery.report,
runDomainCommitResponseLoss: domainCommitResponseLoss.report,
modelInvocationFeaturePromotion,
copilotFailureDiagnosisAdmission,
modelProviderCredentialCatalog,
modelProviderCredentialTestConnection,
runAttemptLogRetention,
@@ -12262,6 +12642,18 @@ async function main(argv = process.argv.slice(2)) {
runAttemptLogRetention.survivedPromotion,
packageAuthoritySplitReadinessBeforeAndAfterPromotion: true,
optionalAiFeatureSchemaSurvivesPromotion: true,
copilotFailureDiagnosisAdmissionExactlyReplays:
copilotFailureDiagnosisAdmission.exactReplay &&
copilotFailureDiagnosisAdmission.beforePromotion.eventCount === 3 &&
copilotFailureDiagnosisAdmission.beforePromotion.mutationCount === 2,
copilotFailureDiagnosisAdmissionReplicatesAndSurvivesPromotion:
copilotFailureDiagnosisAdmission.replicatedBeforePromotion &&
copilotFailureDiagnosisAdmission.survivedPromotion,
copilotFailureDiagnosisAdmissionUsesLeastPrivilegeRuntime:
modelInvocationFeaturePromotion.beforePromotion.privileges
.copilotDiagnosisAdmissionRuntimeOnly &&
modelInvocationFeaturePromotion.beforePromotion.privileges
.copilotDiagnosisSnapshotRuntimeOnly,
modelProviderCredentialCatalogSurvivesPromotion:
modelProviderCredentialCatalog.survivedPromotion,
modelProviderCredentialManagementIdentityLedgerSurvivesPromotion:
+2 -2
View File
@@ -268,10 +268,10 @@ test('current QL3 workspace has exactly eighteen reviewed package boundaries', (
{
path: 'packages/ql3-ai',
name: '@qinglong/ai',
sourceFiles: 160,
sourceFiles: 168,
rootSourceFiles: 1,
rootSourceLines: 16,
nestedSourceFiles: 159,
nestedSourceFiles: 167,
rootSourceFileHardCap: 1,
rootSourceLineHardCap: 16,
rootSourceFileRoles: {