mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): add copilot diagnosis output keyring
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
最新增量证据(2026-08-15):
|
最新增量证据(2026-08-15):
|
||||||
|
|
||||||
|
- D-318/ADR-0410(已接受):Cluster Copilot diagnosis Model output 获得独立的只读 projected key authority。canonical `qinglong/copilot-failure-diagnosis-output-projected-keyring@v1` manifest 只允许一个 active key 与最多 16 个 historical 32-byte key;每次 `active()`/`resolve()` 都重新执行 Cluster 私有投影文件的 direct-root、根内 atomic symlink、single-link、mode、dev/inode/size/mtime 与双 realpath fence,不使用 cache、watcher、timer 或 Kubernetes API。该 authority 以结构兼容的本地窄端口位于既有 `cluster-control/copilot/failure-diagnosis/`,只通过 `failure-diagnosis-output-keyring` subpath 发布,避免 Cluster Control 默认源码反向依赖 AI;它不复用 Prompt output、Tool invocation/result 或 Provider credential key domain,也不提前声称 Copilot 产品入口已可达。定向回归 13/13、Cluster Control 239 pass/2 条件 skip、backend 1,207 pass/2 条件 skip、18-package clean build/test、四项架构审计与 14 档 Local artifact 全部通过;workspace 仍无单文件/浅平 package,Cluster Control 58 个源码中 56 个位于嵌套领域目录。默认 Edge/Standalone 保持 2,589,890/2,589,968 bytes,Edge/Standalone AI 保持 3,061,009/3,061,099 bytes,证明 Cluster-only subpath 被裁掉。本 Gate 不改 migration/schema/role/SQL/连接/HA 拓扑,因此数据库基线继续引用 ADR-0409 的 PostgreSQL 18.6 arm64 HA 130/130、timeline `1→2`;下一 Gate 是默认关闭的完整 Cluster Copilot composition,随后补齐 Tool failure、日志 missing/retired、Model admission 前 deadline/cancel 与 outcome-unknown 的 durable terminalization/recovery。
|
||||||
- D-316/ADR-0408(已接受):Cluster Copilot 现在能够从 ADR-0407 的 durable admission 恢复 exact `qinglong.run.log.excerpt@1.0.0` authority,复用通用 Trusted Tool start barrier、加密 success/failure completion、result catalog/rekey 和内建日志 adapter;确定性 start/completion identity 让 response-loss replay 直接打开既有证据,不重复读取日志或执行 adapter。Cluster invocation Artifact 使用独立 projected keyring,提供 active+historical material,但每次读取均重新执行 canonical path/symlink/mode/inode/realpath fence,且不取得 PostgreSQL Tool result generation authority。只有 exact `succeeded` completion 才能通过 `pg-9019` 的 SERIALIZABLE 事务把 Model Step 从 `pending` 原子推进到 `ready`,同时写 RunEvent、StepRunMutation 和 append-only unlock receipt;`failed|timed_out` 不解锁。本 Gate 不执行模型、不终态化 diagnosis Run,下一 Gate 是 ADR-0405 builder + Model Gateway + Copilot encrypted model completion/terminalization。实现仍为 18 个 package,无单文件/浅平 package;AI 175 个源码中 174 个、Cluster Control 56 个中 54 个位于嵌套目录,不新增依赖、进程、连接、timer/watcher/cache 或默认 Edge 成本。18-package clean build/test 全绿,AI 229 pass/3 条件 skip、backend 1,207 pass/2 条件 skip,四项架构审计零 finding;14 档 Local artifact 全通过,默认 Edge/Standalone 为 2,589,890/2,589,968 bytes。PostgreSQL 18.4 arm64 HA 130/130、timeline `1→2`,首次执行只读两次日志、密文不含敏感 fixture,提升后 exact replay 零日志读取;报告 SHA-256 为 `d525a303696e178d777b021b376729bd2c5382fb5eb7bc98466a2b79d3940517`,独立审计与 Docker 清理通过。
|
- D-316/ADR-0408(已接受):Cluster Copilot 现在能够从 ADR-0407 的 durable admission 恢复 exact `qinglong.run.log.excerpt@1.0.0` authority,复用通用 Trusted Tool start barrier、加密 success/failure completion、result catalog/rekey 和内建日志 adapter;确定性 start/completion identity 让 response-loss replay 直接打开既有证据,不重复读取日志或执行 adapter。Cluster invocation Artifact 使用独立 projected keyring,提供 active+historical material,但每次读取均重新执行 canonical path/symlink/mode/inode/realpath fence,且不取得 PostgreSQL Tool result generation authority。只有 exact `succeeded` completion 才能通过 `pg-9019` 的 SERIALIZABLE 事务把 Model Step 从 `pending` 原子推进到 `ready`,同时写 RunEvent、StepRunMutation 和 append-only unlock receipt;`failed|timed_out` 不解锁。本 Gate 不执行模型、不终态化 diagnosis Run,下一 Gate 是 ADR-0405 builder + Model Gateway + Copilot encrypted model completion/terminalization。实现仍为 18 个 package,无单文件/浅平 package;AI 175 个源码中 174 个、Cluster Control 56 个中 54 个位于嵌套目录,不新增依赖、进程、连接、timer/watcher/cache 或默认 Edge 成本。18-package clean build/test 全绿,AI 229 pass/3 条件 skip、backend 1,207 pass/2 条件 skip,四项架构审计零 finding;14 档 Local artifact 全通过,默认 Edge/Standalone 为 2,589,890/2,589,968 bytes。PostgreSQL 18.4 arm64 HA 130/130、timeline `1→2`,首次执行只读两次日志、密文不含敏感 fixture,提升后 exact replay 零日志读取;报告 SHA-256 为 `d525a303696e178d777b021b376729bd2c5382fb5eb7bc98466a2b79d3940517`,独立审计与 Docker 清理通过。
|
||||||
- D-317/ADR-0409(已接受):已解锁的 Cluster failure-diagnosis Model Step 现在从 durable encrypted Tool completion 重开受信日志投影,经 ADR-0405 builder 和既有 Model Gateway 执行;只有显式安装的 Copilot success sink 可以接管成功返回。Copilot `GenerateResult` 使用独立 AES-256-GCM Artifact,绑定 plan、Tool completion、egress evidence 与 Model identity,公开 reference、ModelInvocation completion、RunEvent 和审计均不含明文。通用 `DurableModelInvocationCoordinator` 已改为领域无关的 `ModelInvocationAtomicSuccess<TReference>`,消除对 Plugin Prompt Artifact 的反向依赖;Plugin Prompt 通过 adapter 保持兼容。`pg-9020` 在一个 SERIALIZABLE 事务中原子提交 ciphertext、Model completion、StepRun/Event 与 usage/pricing/quota settlement,再由可重放 finalization 事务把 diagnosis Run 推进为 `succeeded|failed|timed_out`;两事务间崩溃只补 finalization,existing start/completion/finalization replay 均不重复调用 Provider。当前仍不自动终态化 Tool failure、日志 missing/retired、Model admission 前 deadline/cancel,也不把 `outcome_unknown` 冒充失败;Cluster 专用 output projected keyring 与产品 composition 完成前该入口保持不可达。实现仍在既有 18 个 package 的嵌套领域目录内,不新增进程、队列、timer/watcher/cache 或默认 Edge 成本。最终 AI 233 pass/3 条件 skip、18-package clean build/test 与 backend 1,207 pass/2 条件 skip/0 fail,四项架构审计零 finding;workspace 无单文件或浅平 package,AI 183 个源码中 182 个位于嵌套目录。14 档 Local Profile artifact 全部通过,默认 Edge/Standalone 为 2,589,890/2,589,968 bytes,Edge/Standalone AI 为 3,061,009/3,061,099 bytes。PostgreSQL 18.6 arm64 HA 130/130、timeline `1→2`,报告 SHA-256 为 `8401634f30635b45bfb583b02e94ac41f023bf8a0bdbcfd9744ebf459ab0d8f8`,独立证据审计与 Docker 零残留。
|
- D-317/ADR-0409(已接受):已解锁的 Cluster failure-diagnosis Model Step 现在从 durable encrypted Tool completion 重开受信日志投影,经 ADR-0405 builder 和既有 Model Gateway 执行;只有显式安装的 Copilot success sink 可以接管成功返回。Copilot `GenerateResult` 使用独立 AES-256-GCM Artifact,绑定 plan、Tool completion、egress evidence 与 Model identity,公开 reference、ModelInvocation completion、RunEvent 和审计均不含明文。通用 `DurableModelInvocationCoordinator` 已改为领域无关的 `ModelInvocationAtomicSuccess<TReference>`,消除对 Plugin Prompt Artifact 的反向依赖;Plugin Prompt 通过 adapter 保持兼容。`pg-9020` 在一个 SERIALIZABLE 事务中原子提交 ciphertext、Model completion、StepRun/Event 与 usage/pricing/quota settlement,再由可重放 finalization 事务把 diagnosis Run 推进为 `succeeded|failed|timed_out`;两事务间崩溃只补 finalization,existing start/completion/finalization replay 均不重复调用 Provider。当前仍不自动终态化 Tool failure、日志 missing/retired、Model admission 前 deadline/cancel,也不把 `outcome_unknown` 冒充失败;Cluster 专用 output projected keyring 与产品 composition 完成前该入口保持不可达。实现仍在既有 18 个 package 的嵌套领域目录内,不新增进程、队列、timer/watcher/cache 或默认 Edge 成本。最终 AI 233 pass/3 条件 skip、18-package clean build/test 与 backend 1,207 pass/2 条件 skip/0 fail,四项架构审计零 finding;workspace 无单文件或浅平 package,AI 183 个源码中 182 个位于嵌套目录。14 档 Local Profile artifact 全部通过,默认 Edge/Standalone 为 2,589,890/2,589,968 bytes,Edge/Standalone AI 为 3,061,009/3,061,099 bytes。PostgreSQL 18.6 arm64 HA 130/130、timeline `1→2`,报告 SHA-256 为 `8401634f30635b45bfb583b02e94ac41f023bf8a0bdbcfd9744ebf459ab0d8f8`,独立证据审计与 Docker 零残留。
|
||||||
- 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/Attempt;response-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、无单文件/浅平 package;14 档 Local Profile artifact 全通过,默认 Edge/Standalone 不引入 Cluster admission。PostgreSQL 18.4 arm64 HA 128/128、timeline `1→2`,报告 SHA-256 为 `a4ed1edec783e3f5b42507c0f8e11b94c59dbe44a57e691017d1445ec9d115e2`,证据审计与 Docker 清理通过。
|
- 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/Attempt;response-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、无单文件/浅平 package;14 档 Local Profile artifact 全通过,默认 Edge/Standalone 不引入 Cluster admission。PostgreSQL 18.4 arm64 HA 128/128、timeline `1→2`,报告 SHA-256 为 `a4ed1edec783e3f5b42507c0f8e11b94c59dbe44a57e691017d1445ec9d115e2`,证据审计与 Docker 清理通过。
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# ADR-0410:Cluster Copilot Failure Diagnosis Output Key Authority
|
||||||
|
|
||||||
|
- 状态:Accepted
|
||||||
|
- 日期:2026-08-15
|
||||||
|
- 关联 RFC:QL-RFC-0001 D-318、Phase 2
|
||||||
|
- 关联 ADR:ADR-0405、ADR-0406、ADR-0408、ADR-0409
|
||||||
|
|
||||||
|
## 问题
|
||||||
|
|
||||||
|
ADR-0409 已定义 Copilot diagnosis Model output 的独立 AES-256-GCM Artifact 与 key provider 端口,
|
||||||
|
但 Cluster 产品还没有可以注入该端口的部署密钥 authority。若复用 Plugin Prompt output、Tool
|
||||||
|
invocation 或 Tool result keyring,会让一次轮换或退役跨越不同数据域,并使最小权限部署无法证明
|
||||||
|
哪个进程能够加解密诊断结果。
|
||||||
|
|
||||||
|
同时,路由设备不应因为 Cluster Copilot 能力增加任何 keyring、watcher 或常驻内存;Cluster 节点
|
||||||
|
也不能把 Kubernetes Secret/CSI projection 的瞬时路径、可写文件或跨目录 symlink 当成可信密钥。
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
1. 新增独立 schema `qinglong/copilot-failure-diagnosis-output-projected-keyring@v1`。manifest 只包含
|
||||||
|
一个 `activeKeyId` 和最多 16 个 canonical 32-byte key;key ID 有界,JSON 必须 exact 且 canonical。
|
||||||
|
2. `ClusterCopilotFailureDiagnosisOutputProjectedKeyring` 实现 ADR-0409 的 `active()` 与
|
||||||
|
`resolve(keyId)` 端口。每次操作重新打开 projection,不持有 cache、watcher、timer、Kubernetes
|
||||||
|
API client 或轮换权限;返回的 key material 由调用者使用后清零。
|
||||||
|
3. 文件读取复用 Cluster Control package-private `PrivateProjectedFileReader`:配置根必须是 direct
|
||||||
|
absolute directory;允许 Kubernetes atomic-writer 的根内 symlink,但最终 regular file 必须单链接、
|
||||||
|
只读、不可执行、other-inaccessible,并在读取前后复验 dev/inode/size/mtime 与双 realpath fence。
|
||||||
|
4. Copilot output key ID/material 不得与 Plugin Prompt output、Tool invocation、Tool result 或 Provider
|
||||||
|
credential 互换。不同 schema 即使 key bytes 相同也拒绝;部署文档必须要求独立 Secret 和 mount root。
|
||||||
|
5. 能力内聚在现有 `@qinglong/cluster-control` 的 `copilot/failure-diagnosis/` 目录,只通过显式
|
||||||
|
`failure-diagnosis-output-keyring` subpath 发布;不新增 workspace package、依赖、进程、Pod、连接、
|
||||||
|
route 或默认 Profile import。
|
||||||
|
6. 本 Gate 只建立 runtime key custody,不提前伪造产品可用性。完整 Copilot composition 仍需同时装配
|
||||||
|
admission、Tool invocation/result keyring、Artifact/log reader、Model Gateway、PostgreSQL repository
|
||||||
|
与恢复状态机;该入口在后续 Gate 完成前保持不可达。
|
||||||
|
|
||||||
|
## 被否决方案
|
||||||
|
|
||||||
|
1. **复用 Plugin Prompt output keyring**:两个产品域的 retention、授权和泄漏半径不同。
|
||||||
|
2. **复用 Tool invocation/result keyring**:混淆请求、Tool 结果与模型结论的加密用途。
|
||||||
|
3. **从环境变量读取 base64 key**:密钥进入进程环境与诊断面,且无法安全保留历史 key。
|
||||||
|
4. **启动时读取一次并永久缓存**:轮换无法生效,退役和文件漂移也无法失败关闭。
|
||||||
|
5. **为 keyring 新建 package**:没有独立部署进程或依赖边界,只会增加薄包。
|
||||||
|
|
||||||
|
## 验证标准
|
||||||
|
|
||||||
|
1. 覆盖 canonical manifest、active rotation、historical resolve、missing key、wrong schema、缺失 active、
|
||||||
|
非 32-byte material、可写 projection 与根外 symlink。
|
||||||
|
2. Cluster Control 完整构建/测试、package/dependency/Edge import/Cluster deployment 审计全部通过。
|
||||||
|
3. workspace 保持 18 个 package、无单文件或浅平 package;14 档 Local Profile artifact 证明默认
|
||||||
|
Edge/Standalone 与本地 AI/MCP 不携带该 Cluster-only subpath。
|
||||||
|
|
||||||
|
## 后续门禁
|
||||||
|
|
||||||
|
1. 在独立默认关闭的 Cluster Copilot composition 中同时装配 admission、Tool、Model 与本 keyring,
|
||||||
|
不能把 Plugin Prompt gateway 的 completion sink 当作 Copilot sink。
|
||||||
|
2. 建立 Tool failure、日志 missing/retired、Model admission 前 deadline/cancel 与 outcome-unknown
|
||||||
|
resolution 的 durable terminalization/recovery。
|
||||||
|
3. 再开放经 authentication、Policy、audit 和 exact source fence 的 Cluster API/CLI/UI/MCP 入口。
|
||||||
|
|
||||||
|
## 当前验证
|
||||||
|
|
||||||
|
1. 定向 keyring 与 package-boundary 回归 13/13;Cluster Control 完整测试 241 项中 239 pass、2 条
|
||||||
|
环境条件 skip、0 fail。
|
||||||
|
2. 18 个 QL3 package 从清理 `dist` 开始顺序构建并执行全部 package tests,退出码为 0;backend
|
||||||
|
全量 1,209 项中 1,207 pass、2 条环境条件 skip、0 fail。
|
||||||
|
3. package boundary、dependency firewall、Edge import 与 Cluster deployment 四项架构审计均为
|
||||||
|
`compatible: true`、零 finding。workspace 仍为 18 个 package,`singleSourcePackages` 与
|
||||||
|
`shallowSourcePackages` 均为空;Cluster Control 的 58 个源码中,56 个位于嵌套领域目录,根层仅
|
||||||
|
保留 2 个 binary entry。
|
||||||
|
4. 14 档 Local Profile artifact 全部通过。默认 Edge/Standalone 仍为
|
||||||
|
2,589,890/2,589,968 bytes,Edge/Standalone AI 仍为 3,061,009/3,061,099 bytes;新增
|
||||||
|
Cluster-only subpath 未进入默认、本地 AI 或 MCP 产品闭包。
|
||||||
|
5. 本 Gate 不修改 migration、schema、PostgreSQL role、SQL、连接或 HA 拓扑,因此不重复制造一份与
|
||||||
|
变更无关的数据库实证;ADR-0409 的 PostgreSQL 18.6 arm64 HA 130/130、timeline `1→2` 仍是当前
|
||||||
|
数据库基线。
|
||||||
@@ -413,6 +413,7 @@
|
|||||||
| [ADR-0407](./ADR-0407-cluster-copilot-failure-diagnosis-run-admission.md) | Cluster Copilot Failure Diagnosis Run Admission | Accepted |
|
| [ADR-0407](./ADR-0407-cluster-copilot-failure-diagnosis-run-admission.md) | Cluster Copilot Failure Diagnosis Run Admission | Accepted |
|
||||||
| [ADR-0408](./ADR-0408-cluster-copilot-failure-diagnosis-tool-execution.md) | Cluster Copilot Failure Diagnosis Tool Execution 与原子 Model 解锁 | Accepted |
|
| [ADR-0408](./ADR-0408-cluster-copilot-failure-diagnosis-tool-execution.md) | Cluster Copilot Failure Diagnosis Tool Execution 与原子 Model 解锁 | Accepted |
|
||||||
| [ADR-0409](./ADR-0409-cluster-copilot-failure-diagnosis-model-execution.md) | Cluster Copilot Failure Diagnosis Model Execution、密文输出与 Run 终态化 | Accepted |
|
| [ADR-0409](./ADR-0409-cluster-copilot-failure-diagnosis-model-execution.md) | Cluster Copilot Failure Diagnosis Model Execution、密文输出与 Run 终态化 | Accepted |
|
||||||
|
| [ADR-0410](./ADR-0410-cluster-copilot-failure-diagnosis-output-key-authority.md) | Cluster Copilot Failure Diagnosis Output Projected Key Authority | Accepted |
|
||||||
|
|
||||||
## 规则
|
## 规则
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,11 @@
|
|||||||
"require": "./dist/application-runtime/aiProductionApplication.js",
|
"require": "./dist/application-runtime/aiProductionApplication.js",
|
||||||
"default": "./dist/application-runtime/aiProductionApplication.js"
|
"default": "./dist/application-runtime/aiProductionApplication.js"
|
||||||
},
|
},
|
||||||
|
"./failure-diagnosis-output-keyring": {
|
||||||
|
"types": "./dist/copilot/failure-diagnosis/outputProjectedKeyring.d.ts",
|
||||||
|
"require": "./dist/copilot/failure-diagnosis/outputProjectedKeyring.js",
|
||||||
|
"default": "./dist/copilot/failure-diagnosis/outputProjectedKeyring.js"
|
||||||
|
},
|
||||||
"./http": {
|
"./http": {
|
||||||
"types": "./dist/transport/httpSurface.d.ts",
|
"types": "./dist/transport/httpSurface.d.ts",
|
||||||
"require": "./dist/transport/httpSurface.js",
|
"require": "./dist/transport/httpSurface.js",
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
import { Buffer } from 'node:buffer';
|
||||||
|
import { createHash } from 'node:crypto';
|
||||||
|
|
||||||
|
export const CLUSTER_COPILOT_FAILURE_DIAGNOSIS_OUTPUT_KEYRING_MANIFEST_SCHEMA =
|
||||||
|
'qinglong/copilot-failure-diagnosis-output-projected-keyring@v1' as const;
|
||||||
|
export const MAX_CLUSTER_COPILOT_FAILURE_DIAGNOSIS_OUTPUT_KEYRING_BYTES =
|
||||||
|
64 * 1024;
|
||||||
|
export const MAX_CLUSTER_COPILOT_FAILURE_DIAGNOSIS_OUTPUT_PROJECTED_KEYS = 16;
|
||||||
|
|
||||||
|
const KEY_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
||||||
|
const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;
|
||||||
|
const PROJECTION_DIGEST_DOMAIN = Buffer.from(
|
||||||
|
'qinglong/copilot-failure-diagnosis-output-projected-keyring-digest@v1\0',
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface ClusterCopilotFailureDiagnosisOutputKeyringManifest {
|
||||||
|
readonly schema: typeof CLUSTER_COPILOT_FAILURE_DIAGNOSIS_OUTPUT_KEYRING_MANIFEST_SCHEMA;
|
||||||
|
readonly activeKeyId: string;
|
||||||
|
readonly keys: Readonly<Record<string, string>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClusterCopilotFailureDiagnosisOutputKeyringSummary {
|
||||||
|
readonly schemaVersion: 1;
|
||||||
|
readonly activeKeyId: string;
|
||||||
|
readonly keyIds: readonly string[];
|
||||||
|
readonly projectionDigest: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClusterCopilotFailureDiagnosisOutputKeyMaterial {
|
||||||
|
readonly keyId: string;
|
||||||
|
readonly key: Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InvalidClusterCopilotFailureDiagnosisOutputKeyringManifestError extends TypeError {
|
||||||
|
readonly code =
|
||||||
|
'QL3_CLUSTER_COPILOT_FAILURE_DIAGNOSIS_OUTPUT_KEYRING_MANIFEST_INVALID';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super('Cluster Copilot failure diagnosis output keyring manifest is invalid');
|
||||||
|
this.name =
|
||||||
|
'InvalidClusterCopilotFailureDiagnosisOutputKeyringManifestError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function invalid(): never {
|
||||||
|
throw new InvalidClusterCopilotFailureDiagnosisOutputKeyringManifestError();
|
||||||
|
}
|
||||||
|
|
||||||
|
function dataRecord(value: unknown): Record<string, unknown> {
|
||||||
|
if (
|
||||||
|
!value ||
|
||||||
|
typeof value !== 'object' ||
|
||||||
|
Array.isArray(value) ||
|
||||||
|
Object.getPrototypeOf(value) !== Object.prototype
|
||||||
|
) {
|
||||||
|
return invalid();
|
||||||
|
}
|
||||||
|
return value as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function exactKeys(value: object, expected: readonly string[]): boolean {
|
||||||
|
const actual = Object.keys(value).sort();
|
||||||
|
const canonical = [...expected].sort();
|
||||||
|
return (
|
||||||
|
actual.length === canonical.length &&
|
||||||
|
actual.every((key, index) => key === canonical[index])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeClusterCopilotFailureDiagnosisOutputKeyringManifest(
|
||||||
|
value: unknown,
|
||||||
|
): Readonly<ClusterCopilotFailureDiagnosisOutputKeyringManifest> {
|
||||||
|
const manifest = dataRecord(value);
|
||||||
|
if (
|
||||||
|
!exactKeys(manifest, ['activeKeyId', 'keys', 'schema']) ||
|
||||||
|
manifest.schema !==
|
||||||
|
CLUSTER_COPILOT_FAILURE_DIAGNOSIS_OUTPUT_KEYRING_MANIFEST_SCHEMA ||
|
||||||
|
typeof manifest.activeKeyId !== 'string' ||
|
||||||
|
!KEY_ID_PATTERN.test(manifest.activeKeyId)
|
||||||
|
) {
|
||||||
|
return invalid();
|
||||||
|
}
|
||||||
|
const keys = dataRecord(manifest.keys);
|
||||||
|
const entries = Object.entries(keys).sort(([left], [right]) =>
|
||||||
|
left < right ? -1 : left > right ? 1 : 0,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
entries.length < 1 ||
|
||||||
|
entries.length >
|
||||||
|
MAX_CLUSTER_COPILOT_FAILURE_DIAGNOSIS_OUTPUT_PROJECTED_KEYS
|
||||||
|
) {
|
||||||
|
return invalid();
|
||||||
|
}
|
||||||
|
const normalized: (readonly [string, string])[] = [];
|
||||||
|
for (const [keyId, encoded] of entries) {
|
||||||
|
let material: Buffer | undefined;
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
!KEY_ID_PATTERN.test(keyId) ||
|
||||||
|
typeof encoded !== 'string' ||
|
||||||
|
!BASE64URL_PATTERN.test(encoded)
|
||||||
|
) {
|
||||||
|
return invalid();
|
||||||
|
}
|
||||||
|
material = Buffer.from(encoded, 'base64url');
|
||||||
|
if (
|
||||||
|
material.byteLength !== 32 ||
|
||||||
|
material.toString('base64url') !== encoded
|
||||||
|
) {
|
||||||
|
return invalid();
|
||||||
|
}
|
||||||
|
normalized.push(Object.freeze([keyId, encoded] as const));
|
||||||
|
} finally {
|
||||||
|
material?.fill(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const normalizedKeys = Object.freeze(Object.fromEntries(normalized));
|
||||||
|
if (normalizedKeys[manifest.activeKeyId] === undefined) return invalid();
|
||||||
|
return Object.freeze({
|
||||||
|
schema:
|
||||||
|
CLUSTER_COPILOT_FAILURE_DIAGNOSIS_OUTPUT_KEYRING_MANIFEST_SCHEMA,
|
||||||
|
activeKeyId: manifest.activeKeyId,
|
||||||
|
keys: normalizedKeys,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseClusterCopilotFailureDiagnosisOutputKeyringManifest(
|
||||||
|
bytes: Buffer,
|
||||||
|
): Readonly<ClusterCopilotFailureDiagnosisOutputKeyringManifest> {
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
!Buffer.isBuffer(bytes) ||
|
||||||
|
bytes.byteLength < 1 ||
|
||||||
|
bytes.byteLength >
|
||||||
|
MAX_CLUSTER_COPILOT_FAILURE_DIAGNOSIS_OUTPUT_KEYRING_BYTES
|
||||||
|
) {
|
||||||
|
return invalid();
|
||||||
|
}
|
||||||
|
return normalizeClusterCopilotFailureDiagnosisOutputKeyringManifest(
|
||||||
|
JSON.parse(bytes.toString('utf8')),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
error instanceof
|
||||||
|
InvalidClusterCopilotFailureDiagnosisOutputKeyringManifestError
|
||||||
|
) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return invalid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canonicalClusterCopilotFailureDiagnosisOutputKeyringManifest(
|
||||||
|
value: ClusterCopilotFailureDiagnosisOutputKeyringManifest,
|
||||||
|
): Buffer {
|
||||||
|
const manifest =
|
||||||
|
normalizeClusterCopilotFailureDiagnosisOutputKeyringManifest(value);
|
||||||
|
return Buffer.from(`${JSON.stringify(manifest)}\n`, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveClusterCopilotFailureDiagnosisOutputKeyringMaterial(
|
||||||
|
value: ClusterCopilotFailureDiagnosisOutputKeyringManifest,
|
||||||
|
keyId: string,
|
||||||
|
): Readonly<ClusterCopilotFailureDiagnosisOutputKeyMaterial> | null {
|
||||||
|
if (typeof keyId !== 'string' || !KEY_ID_PATTERN.test(keyId)) {
|
||||||
|
return invalid();
|
||||||
|
}
|
||||||
|
const manifest =
|
||||||
|
normalizeClusterCopilotFailureDiagnosisOutputKeyringManifest(value);
|
||||||
|
const encoded = manifest.keys[keyId];
|
||||||
|
if (encoded === undefined) return null;
|
||||||
|
const key = Buffer.from(encoded, 'base64url');
|
||||||
|
if (key.byteLength !== 32 || key.toString('base64url') !== encoded) {
|
||||||
|
key.fill(0);
|
||||||
|
return invalid();
|
||||||
|
}
|
||||||
|
return Object.freeze({ keyId, key });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function summarizeClusterCopilotFailureDiagnosisOutputKeyringManifest(
|
||||||
|
value: ClusterCopilotFailureDiagnosisOutputKeyringManifest,
|
||||||
|
): Readonly<ClusterCopilotFailureDiagnosisOutputKeyringSummary> {
|
||||||
|
const manifest =
|
||||||
|
normalizeClusterCopilotFailureDiagnosisOutputKeyringManifest(value);
|
||||||
|
const keyIds = Object.freeze(Object.keys(manifest.keys).sort());
|
||||||
|
return Object.freeze({
|
||||||
|
schemaVersion: 1 as const,
|
||||||
|
activeKeyId: manifest.activeKeyId,
|
||||||
|
keyIds,
|
||||||
|
projectionDigest: createHash('sha256')
|
||||||
|
.update(PROJECTION_DIGEST_DOMAIN)
|
||||||
|
.update(JSON.stringify(manifest))
|
||||||
|
.digest('hex'),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
import { Buffer } from 'node:buffer';
|
||||||
|
|
||||||
|
import { PrivateProjectedFileReader } from '../../security/privateProjectedFile';
|
||||||
|
import {
|
||||||
|
MAX_CLUSTER_COPILOT_FAILURE_DIAGNOSIS_OUTPUT_KEYRING_BYTES,
|
||||||
|
canonicalClusterCopilotFailureDiagnosisOutputKeyringManifest,
|
||||||
|
parseClusterCopilotFailureDiagnosisOutputKeyringManifest,
|
||||||
|
resolveClusterCopilotFailureDiagnosisOutputKeyringMaterial,
|
||||||
|
summarizeClusterCopilotFailureDiagnosisOutputKeyringManifest,
|
||||||
|
type ClusterCopilotFailureDiagnosisOutputKeyMaterial,
|
||||||
|
type ClusterCopilotFailureDiagnosisOutputKeyringManifest,
|
||||||
|
type ClusterCopilotFailureDiagnosisOutputKeyringSummary,
|
||||||
|
} from './outputKeyringManifest';
|
||||||
|
|
||||||
|
const DATA_FILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,252}$/;
|
||||||
|
|
||||||
|
export {
|
||||||
|
CLUSTER_COPILOT_FAILURE_DIAGNOSIS_OUTPUT_KEYRING_MANIFEST_SCHEMA,
|
||||||
|
MAX_CLUSTER_COPILOT_FAILURE_DIAGNOSIS_OUTPUT_KEYRING_BYTES,
|
||||||
|
MAX_CLUSTER_COPILOT_FAILURE_DIAGNOSIS_OUTPUT_PROJECTED_KEYS,
|
||||||
|
InvalidClusterCopilotFailureDiagnosisOutputKeyringManifestError,
|
||||||
|
canonicalClusterCopilotFailureDiagnosisOutputKeyringManifest,
|
||||||
|
normalizeClusterCopilotFailureDiagnosisOutputKeyringManifest,
|
||||||
|
parseClusterCopilotFailureDiagnosisOutputKeyringManifest,
|
||||||
|
resolveClusterCopilotFailureDiagnosisOutputKeyringMaterial,
|
||||||
|
summarizeClusterCopilotFailureDiagnosisOutputKeyringManifest,
|
||||||
|
type ClusterCopilotFailureDiagnosisOutputKeyringManifest,
|
||||||
|
type ClusterCopilotFailureDiagnosisOutputKeyringSummary,
|
||||||
|
} from './outputKeyringManifest';
|
||||||
|
|
||||||
|
export interface ClusterCopilotFailureDiagnosisOutputProjectedKeyringOptions {
|
||||||
|
readonly rootDirectory: string;
|
||||||
|
readonly dataFileName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClusterCopilotFailureDiagnosisOutputKeyProvider {
|
||||||
|
active(): Promise<ClusterCopilotFailureDiagnosisOutputKeyMaterial>;
|
||||||
|
resolve(
|
||||||
|
keyId: string,
|
||||||
|
): Promise<ClusterCopilotFailureDiagnosisOutputKeyMaterial | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ClusterCopilotFailureDiagnosisOutputProjectedKeyringUnavailableError extends Error {
|
||||||
|
readonly code =
|
||||||
|
'QL3_CLUSTER_COPILOT_FAILURE_DIAGNOSIS_OUTPUT_PROJECTED_KEYRING_UNAVAILABLE';
|
||||||
|
|
||||||
|
constructor(options?: ErrorOptions) {
|
||||||
|
super(
|
||||||
|
'Projected Cluster Copilot failure diagnosis output keyring is unavailable',
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
this.name =
|
||||||
|
'ClusterCopilotFailureDiagnosisOutputProjectedKeyringUnavailableError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unavailable(
|
||||||
|
cause?: unknown,
|
||||||
|
): ClusterCopilotFailureDiagnosisOutputProjectedKeyringUnavailableError {
|
||||||
|
return new ClusterCopilotFailureDiagnosisOutputProjectedKeyringUnavailableError(
|
||||||
|
{ cause: cause instanceof Error ? cause : undefined },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dataFileName(value: unknown): string {
|
||||||
|
if (typeof value !== 'string' || !DATA_FILE_NAME.test(value)) {
|
||||||
|
throw unavailable();
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readManifest(
|
||||||
|
reader: PrivateProjectedFileReader,
|
||||||
|
fileName: string,
|
||||||
|
): Promise<Readonly<ClusterCopilotFailureDiagnosisOutputKeyringManifest>> {
|
||||||
|
let bytes: Buffer | undefined;
|
||||||
|
let canonical: Buffer | undefined;
|
||||||
|
try {
|
||||||
|
bytes = await reader.read(fileName);
|
||||||
|
const manifest =
|
||||||
|
parseClusterCopilotFailureDiagnosisOutputKeyringManifest(bytes);
|
||||||
|
canonical =
|
||||||
|
canonicalClusterCopilotFailureDiagnosisOutputKeyringManifest(manifest);
|
||||||
|
if (!canonical.equals(bytes)) throw unavailable();
|
||||||
|
return manifest;
|
||||||
|
} catch (cause) {
|
||||||
|
throw cause instanceof
|
||||||
|
ClusterCopilotFailureDiagnosisOutputProjectedKeyringUnavailableError
|
||||||
|
? cause
|
||||||
|
: unavailable(cause);
|
||||||
|
} finally {
|
||||||
|
bytes?.fill(0);
|
||||||
|
canonical?.fill(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read-only, no-cache Copilot diagnosis output key authority. */
|
||||||
|
export class ClusterCopilotFailureDiagnosisOutputProjectedKeyring
|
||||||
|
implements ClusterCopilotFailureDiagnosisOutputKeyProvider
|
||||||
|
{
|
||||||
|
readonly #reader: PrivateProjectedFileReader;
|
||||||
|
readonly #dataFileName: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
options: ClusterCopilotFailureDiagnosisOutputProjectedKeyringOptions,
|
||||||
|
) {
|
||||||
|
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
||||||
|
throw unavailable();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.#reader = new PrivateProjectedFileReader({
|
||||||
|
rootDirectory: options.rootDirectory,
|
||||||
|
minimumBytes: 1,
|
||||||
|
maximumBytes:
|
||||||
|
MAX_CLUSTER_COPILOT_FAILURE_DIAGNOSIS_OUTPUT_KEYRING_BYTES,
|
||||||
|
access: 'read_only_keyring',
|
||||||
|
});
|
||||||
|
this.#dataFileName = dataFileName(options.dataFileName ?? 'keyring.json');
|
||||||
|
} catch (cause) {
|
||||||
|
throw unavailable(cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async verify(): Promise<
|
||||||
|
Readonly<ClusterCopilotFailureDiagnosisOutputKeyringSummary>
|
||||||
|
> {
|
||||||
|
return summarizeClusterCopilotFailureDiagnosisOutputKeyringManifest(
|
||||||
|
await readManifest(this.#reader, this.#dataFileName),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async active(): Promise<ClusterCopilotFailureDiagnosisOutputKeyMaterial> {
|
||||||
|
try {
|
||||||
|
const manifest = await readManifest(this.#reader, this.#dataFileName);
|
||||||
|
const material =
|
||||||
|
resolveClusterCopilotFailureDiagnosisOutputKeyringMaterial(
|
||||||
|
manifest,
|
||||||
|
manifest.activeKeyId,
|
||||||
|
);
|
||||||
|
if (!material) throw unavailable();
|
||||||
|
return material;
|
||||||
|
} catch (cause) {
|
||||||
|
throw cause instanceof
|
||||||
|
ClusterCopilotFailureDiagnosisOutputProjectedKeyringUnavailableError
|
||||||
|
? cause
|
||||||
|
: unavailable(cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async resolve(
|
||||||
|
keyId: string,
|
||||||
|
): Promise<ClusterCopilotFailureDiagnosisOutputKeyMaterial | null> {
|
||||||
|
try {
|
||||||
|
return resolveClusterCopilotFailureDiagnosisOutputKeyringMaterial(
|
||||||
|
await readManifest(this.#reader, this.#dataFileName),
|
||||||
|
keyId,
|
||||||
|
);
|
||||||
|
} catch (cause) {
|
||||||
|
throw cause instanceof
|
||||||
|
ClusterCopilotFailureDiagnosisOutputProjectedKeyringUnavailableError
|
||||||
|
? cause
|
||||||
|
: unavailable(cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createClusterCopilotFailureDiagnosisOutputProjectedKeyring(
|
||||||
|
options: ClusterCopilotFailureDiagnosisOutputProjectedKeyringOptions,
|
||||||
|
): Promise<
|
||||||
|
Readonly<ClusterCopilotFailureDiagnosisOutputProjectedKeyring>
|
||||||
|
> {
|
||||||
|
const provider =
|
||||||
|
new ClusterCopilotFailureDiagnosisOutputProjectedKeyring(options);
|
||||||
|
await provider.verify();
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
+178
@@ -0,0 +1,178 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs/promises');
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { afterEach, test } = require('node:test');
|
||||||
|
|
||||||
|
const rootExport = require('@qinglong/cluster-control');
|
||||||
|
const {
|
||||||
|
CLUSTER_COPILOT_FAILURE_DIAGNOSIS_OUTPUT_KEYRING_MANIFEST_SCHEMA,
|
||||||
|
ClusterCopilotFailureDiagnosisOutputProjectedKeyring,
|
||||||
|
ClusterCopilotFailureDiagnosisOutputProjectedKeyringUnavailableError,
|
||||||
|
InvalidClusterCopilotFailureDiagnosisOutputKeyringManifestError,
|
||||||
|
canonicalClusterCopilotFailureDiagnosisOutputKeyringManifest,
|
||||||
|
createClusterCopilotFailureDiagnosisOutputProjectedKeyring,
|
||||||
|
normalizeClusterCopilotFailureDiagnosisOutputKeyringManifest,
|
||||||
|
} = require('@qinglong/cluster-control/failure-diagnosis-output-keyring');
|
||||||
|
|
||||||
|
const roots = [];
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await Promise.all(
|
||||||
|
roots
|
||||||
|
.splice(0)
|
||||||
|
.map((root) => fs.rm(root, { recursive: true, force: true })),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function tempRoot(label = 'ql3-copilot-output-keyring-') {
|
||||||
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), label));
|
||||||
|
roots.push(root);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
function manifest(activeKeyId, keys) {
|
||||||
|
return Object.freeze({
|
||||||
|
schema:
|
||||||
|
CLUSTER_COPILOT_FAILURE_DIAGNOSIS_OUTPUT_KEYRING_MANIFEST_SCHEMA,
|
||||||
|
activeKeyId,
|
||||||
|
keys: Object.freeze(keys),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function publish(root, generationName, value, mode = 0o440) {
|
||||||
|
const generation = path.join(root, generationName);
|
||||||
|
await fs.mkdir(generation, { mode: 0o750 });
|
||||||
|
const target = path.join(generation, 'keyring.json');
|
||||||
|
await fs.writeFile(
|
||||||
|
target,
|
||||||
|
canonicalClusterCopilotFailureDiagnosisOutputKeyringManifest(value),
|
||||||
|
{ mode },
|
||||||
|
);
|
||||||
|
await fs.chmod(target, mode);
|
||||||
|
const next = path.join(root, '..data-next');
|
||||||
|
await fs.symlink(generationName, next);
|
||||||
|
await fs.rename(next, path.join(root, '..data'));
|
||||||
|
try {
|
||||||
|
await fs.symlink('..data/keyring.json', path.join(root, 'keyring.json'));
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== 'EEXIST') throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('projected Copilot output keyring rotates active material without caching', async () => {
|
||||||
|
const root = await tempRoot();
|
||||||
|
const keyOne = Buffer.alloc(32, 0x31);
|
||||||
|
const keyTwo = Buffer.alloc(32, 0x32);
|
||||||
|
await publish(
|
||||||
|
root,
|
||||||
|
'..2026_08_15_01',
|
||||||
|
manifest('copilot-output-one', {
|
||||||
|
'copilot-output-one': keyOne.toString('base64url'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const provider =
|
||||||
|
await createClusterCopilotFailureDiagnosisOutputProjectedKeyring({
|
||||||
|
rootDirectory: root,
|
||||||
|
});
|
||||||
|
const first = await provider.active();
|
||||||
|
assert.equal(first.keyId, 'copilot-output-one');
|
||||||
|
assert.deepEqual(Buffer.from(first.key), keyOne);
|
||||||
|
first.key.fill(0);
|
||||||
|
|
||||||
|
await publish(
|
||||||
|
root,
|
||||||
|
'..2026_08_15_02',
|
||||||
|
manifest('copilot-output-two', {
|
||||||
|
'copilot-output-one': keyOne.toString('base64url'),
|
||||||
|
'copilot-output-two': keyTwo.toString('base64url'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const second = await provider.active();
|
||||||
|
assert.equal(second.keyId, 'copilot-output-two');
|
||||||
|
assert.deepEqual(Buffer.from(second.key), keyTwo);
|
||||||
|
second.key.fill(0);
|
||||||
|
const historical = await provider.resolve('copilot-output-one');
|
||||||
|
assert.ok(historical);
|
||||||
|
assert.deepEqual(Buffer.from(historical.key), keyOne);
|
||||||
|
historical.key.fill(0);
|
||||||
|
assert.equal(await provider.resolve('missing-key'), null);
|
||||||
|
|
||||||
|
const summary = await provider.verify();
|
||||||
|
assert.deepEqual(summary.keyIds, [
|
||||||
|
'copilot-output-one',
|
||||||
|
'copilot-output-two',
|
||||||
|
]);
|
||||||
|
assert.equal(summary.activeKeyId, 'copilot-output-two');
|
||||||
|
assert.match(summary.projectionDigest, /^[0-9a-f]{64}$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('manifest rejects missing active, wrong domains and non-canonical material', () => {
|
||||||
|
const key = Buffer.alloc(32, 0x41).toString('base64url');
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
normalizeClusterCopilotFailureDiagnosisOutputKeyringManifest({
|
||||||
|
...manifest('missing-key', { 'copilot-output-one': key }),
|
||||||
|
}),
|
||||||
|
InvalidClusterCopilotFailureDiagnosisOutputKeyringManifestError,
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
normalizeClusterCopilotFailureDiagnosisOutputKeyringManifest({
|
||||||
|
...manifest('copilot-output-one', { 'copilot-output-one': key }),
|
||||||
|
schema: 'qinglong/cluster-tool-invocation-projected-keyring@v1',
|
||||||
|
}),
|
||||||
|
InvalidClusterCopilotFailureDiagnosisOutputKeyringManifestError,
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
normalizeClusterCopilotFailureDiagnosisOutputKeyringManifest(
|
||||||
|
manifest('copilot-output-one', {
|
||||||
|
'copilot-output-one': Buffer.alloc(31, 0x41).toString('base64url'),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
InvalidClusterCopilotFailureDiagnosisOutputKeyringManifestError,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
rootExport.ClusterCopilotFailureDiagnosisOutputProjectedKeyring,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('projected Copilot output keyring rejects writable and escaping files', async () => {
|
||||||
|
const writableRoot = await tempRoot();
|
||||||
|
const key = Buffer.alloc(32, 0x51).toString('base64url');
|
||||||
|
await publish(
|
||||||
|
writableRoot,
|
||||||
|
'..2026_08_15_01',
|
||||||
|
manifest('copilot-output-one', { 'copilot-output-one': key }),
|
||||||
|
0o640,
|
||||||
|
);
|
||||||
|
await assert.rejects(
|
||||||
|
() =>
|
||||||
|
new ClusterCopilotFailureDiagnosisOutputProjectedKeyring({
|
||||||
|
rootDirectory: writableRoot,
|
||||||
|
}).verify(),
|
||||||
|
ClusterCopilotFailureDiagnosisOutputProjectedKeyringUnavailableError,
|
||||||
|
);
|
||||||
|
|
||||||
|
const escapeRoot = await tempRoot();
|
||||||
|
const outsideRoot = await tempRoot('ql3-copilot-output-outside-');
|
||||||
|
const outsideFile = path.join(outsideRoot, 'keyring.json');
|
||||||
|
await fs.writeFile(
|
||||||
|
outsideFile,
|
||||||
|
canonicalClusterCopilotFailureDiagnosisOutputKeyringManifest(
|
||||||
|
manifest('copilot-output-one', { 'copilot-output-one': key }),
|
||||||
|
),
|
||||||
|
{ mode: 0o440 },
|
||||||
|
);
|
||||||
|
await fs.chmod(outsideFile, 0o440);
|
||||||
|
await fs.symlink(outsideFile, path.join(escapeRoot, 'keyring.json'));
|
||||||
|
await assert.rejects(
|
||||||
|
() =>
|
||||||
|
new ClusterCopilotFailureDiagnosisOutputProjectedKeyring({
|
||||||
|
rootDirectory: escapeRoot,
|
||||||
|
}).verify(),
|
||||||
|
ClusterCopilotFailureDiagnosisOutputProjectedKeyringUnavailableError,
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -385,10 +385,10 @@ test('current QL3 workspace has exactly eighteen reviewed package boundaries', (
|
|||||||
rootSourceFileRoles: clusterControl.rootSourceFileRoles,
|
rootSourceFileRoles: clusterControl.rootSourceFileRoles,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sourceFiles: 56,
|
sourceFiles: 58,
|
||||||
rootSourceFiles: 2,
|
rootSourceFiles: 2,
|
||||||
rootSourceLines: 195,
|
rootSourceLines: 195,
|
||||||
nestedSourceFiles: 54,
|
nestedSourceFiles: 56,
|
||||||
rootSourceFileRoles: {
|
rootSourceFileRoles: {
|
||||||
'aiCli.ts': 'binary_entry',
|
'aiCli.ts': 'binary_entry',
|
||||||
'cli.ts': 'binary_entry',
|
'cli.ts': 'binary_entry',
|
||||||
|
|||||||
Reference in New Issue
Block a user