feat(ql3): gate console capacity evidence

This commit is contained in:
whyour
2026-08-20 10:04:21 +08:00
parent 0a5f1448f1
commit 3979707f6d
7 changed files with 2289 additions and 1 deletions
+94 -1
View File
@@ -542,7 +542,7 @@ jobs:
- name: Verify native runner architecture - name: Verify native runner architecture
run: node -e "if (process.arch !== '${{ matrix.node_arch }}') throw new Error('unexpected architecture ' + process.arch)" run: node -e "if (process.arch !== '${{ matrix.node_arch }}') throw new Error('unexpected architecture ' + process.arch)"
- name: Test exact SBOM and release contract failures - name: Test exact SBOM and release contract failures
run: node --test test/back/ql3ClusterImageSbom.test.cjs test/back/ql3ClusterImageReleaseAudit.test.cjs run: node --test test/back/ql3ClusterImageSbom.test.cjs test/back/ql3ClusterImageReleaseAudit.test.cjs test/back/ql3ClusterCopilotConsoleCapacityEvidence.test.cjs
- name: Audit deployment and image release contracts - name: Audit deployment and image release contracts
run: | run: |
pnpm audit:cluster-deployment:ql3 pnpm audit:cluster-deployment:ql3
@@ -594,6 +594,40 @@ jobs:
IMAGE: qinglong3-cluster-admin:ci-${{ matrix.image_arch }} IMAGE: qinglong3-cluster-admin:ci-${{ matrix.image_arch }}
QL3_CLUSTER_ADMIN_PRODUCT_LIVE: '1' QL3_CLUSTER_ADMIN_PRODUCT_LIVE: '1'
run: node scripts/ql3-cluster-admin-product-live-contract.cjs --image="${IMAGE}" run: node scripts/ql3-cluster-admin-product-live-contract.cjs --image="${IMAGE}"
- name: Capture the fixed Cluster Copilot Console capacity envelope
if: matrix.image == 'admin'
timeout-minutes: 10
env:
IMAGE: qinglong3-cluster-admin:ci-${{ matrix.image_arch }}
QL3_CLUSTER_COPILOT_CONSOLE_CAPACITY_LIVE: '1'
SOURCE_REPOSITORY: ${{ github.repository }}
SOURCE_REVISION: ${{ github.sha }}
SOURCE_WORKFLOW: ${{ github.workflow }}
SOURCE_RUN_ID: ${{ github.run_id }}
SOURCE_RUN_ATTEMPT: ${{ github.run_attempt }}
run: |
mkdir -p "${RUNNER_TEMP}/ql3-cluster-console-capacity"
node scripts/ql3-cluster-copilot-console-capacity-evidence.cjs \
--mode=capture \
--repository="${SOURCE_REPOSITORY}" \
--revision="${SOURCE_REVISION}" \
--workflow="${SOURCE_WORKFLOW}" \
--run-id="${SOURCE_RUN_ID}" \
--run-attempt="${SOURCE_RUN_ATTEMPT}" \
--architecture="${{ matrix.node_arch }}" \
--image="${IMAGE}" \
--output="${RUNNER_TEMP}/ql3-cluster-console-capacity/${{ matrix.node_arch }}.json"
- name: Upload native Cluster Copilot Console capacity evidence
if: matrix.image == 'admin'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ql3-cluster-console-capacity-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.node_arch }}
path: ${{ runner.temp }}/ql3-cluster-console-capacity/${{ matrix.node_arch }}.json
if-no-files-found: error
retention-days: 14
compression-level: 0
overwrite: false
include-hidden-files: false
- name: Generate the reviewed application SBOM - name: Generate the reviewed application SBOM
run: >- run: >-
node scripts/ql3-cluster-image-sbom.cjs node scripts/ql3-cluster-image-sbom.cjs
@@ -614,6 +648,65 @@ jobs:
--image=${{ matrix.image }} --image=${{ matrix.image }}
--inventory-root=/opt/qinglong/node_modules --inventory-root=/opt/qinglong/node_modules
cluster-console-capacity-release-evidence:
name: Cross-architecture Cluster Copilot Console capacity evidence
needs: cluster-image
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '24.18.0'
- name: Download native x64 Console capacity evidence
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ql3-cluster-console-capacity-${{ github.run_id }}-${{ github.run_attempt }}-x64
path: ${{ runner.temp }}/ql3-cluster-console-capacity/x64
- name: Download native arm64 Console capacity evidence
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ql3-cluster-console-capacity-${{ github.run_id }}-${{ github.run_attempt }}-arm64
path: ${{ runner.temp }}/ql3-cluster-console-capacity/arm64
- name: Merge and audit the source-bound Console capacity evidence
env:
SOURCE_REPOSITORY: ${{ github.repository }}
SOURCE_REVISION: ${{ github.sha }}
SOURCE_WORKFLOW: ${{ github.workflow }}
SOURCE_RUN_ID: ${{ github.run_id }}
SOURCE_RUN_ATTEMPT: ${{ github.run_attempt }}
run: |
node scripts/ql3-cluster-copilot-console-capacity-evidence.cjs \
--mode=merge \
--repository="${SOURCE_REPOSITORY}" \
--revision="${SOURCE_REVISION}" \
--workflow="${SOURCE_WORKFLOW}" \
--run-id="${SOURCE_RUN_ID}" \
--run-attempt="${SOURCE_RUN_ATTEMPT}" \
--x64="${RUNNER_TEMP}/ql3-cluster-console-capacity/x64/x64.json" \
--arm64="${RUNNER_TEMP}/ql3-cluster-console-capacity/arm64/arm64.json" \
--output="${RUNNER_TEMP}/ql3-cluster-console-capacity/cross-architecture.json"
node scripts/ql3-cluster-copilot-console-capacity-evidence.cjs \
--mode=audit \
--repository="${SOURCE_REPOSITORY}" \
--revision="${SOURCE_REVISION}" \
--workflow="${SOURCE_WORKFLOW}" \
--run-id="${SOURCE_RUN_ID}" \
--run-attempt="${SOURCE_RUN_ATTEMPT}" \
--report="${RUNNER_TEMP}/ql3-cluster-console-capacity/cross-architecture.json"
- name: Upload cross-architecture Console capacity evidence
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ql3-cluster-console-capacity-release-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/ql3-cluster-console-capacity/cross-architecture.json
if-no-files-found: error
retention-days: 14
compression-level: 0
overwrite: false
include-hidden-files: false
image-oci: image-oci:
name: ${{ matrix.image }} multi-architecture OCI evidence name: ${{ matrix.image }} multi-architecture OCI evidence
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
+17
View File
@@ -11,6 +11,23 @@
最新增量证据(2026-08-20): 最新增量证据(2026-08-20):
- D-370/ADR-0463(实现与本地协议门已接受;首份原生 CI 双架构报告待实际 workflow 产生):为显式按需的 Cluster Admin
Copilot Console 增加固定 Linux x64/arm64 容量与 assertion 生命周期发布证据。两个原生 Admin image matrix job 分别在
`192 MiB / swap 0 / 0.25 CPU / 32 PIDs`、只读 root、非 root、cap-drop ALL、no-new-privileges、默认 seccomp、8 MiB tmpfs 与
loopback-only publication 下执行四次用户驱动读取,并从 Console 自身 cgroup v2 采集 memory max/peak/events、swap、CPU 与 PID;要求至少
32 MiB headroom 且 `max/oom/oom_kill/oom_group_kill` 不增加。隔离 synthetic TLS 1.3/mTLS management verifier 验证
`initial accepted → atomic rotated accepted → expired rejected → rotated recovered`,Console 全程不重启、不轮询、不重试、不缓存、不 mutation,过期
assertion 只投影为 502/`assertion_expired` 低敏事实。每架构报告绑定 repository/revision/workflow/run ID/attempt 和独立 image ID,使用 nofollow
bounded input、`wx/0600` output 与 domain-separated canonical SHA-256;独立只读 job 以 commit-pinned artifact actions 精确下载、合并并离线重审。
实现保持在单一 CI 脚本和既有 image release audit 中,没有新增 package、dependency、binary、服务、端口、数据库对象或部署 workload,避免制造单文件微包;
Edge/Standalone closure 不含 Console。D-370 聚焦证据与发布审计为 `108/108`,连同 image SBOM 的原生 job 聚焦集合为 `120/120`18-package
clean build/逐包测试退出 0,完整 backend 工作区门为 `1,503 total / 1,501 pass / 2 conditional skip / 0 fail`(含一条不会提交的既有用户测试;D-370
提交范围为 `1,502 total / 1,500 pass / 2 skip`),六项架构审计和 14 档 Local artifact
audit 全部 compatible。基础 Edge/Standalone 仍为 `2,589,998 / 2,590,076` bytesApplication+AI 为 `4,493,151 / 4,493,283` bytesMCP 为
`7,315,930 / 7,316,038` bytes。本机只能验证协议、Docker inspect 契约和 workflow 装配,尚无本提交的原生
GitHub x64/arm64 memory peak、artifact digest 或 run URL,因此不能把实现状态写成最终现场证据。192 MiB 只回答 workstation Console 的有界空载读取,
不是物理路由器最低配置,也不是 Cluster 节点吞吐、故障恢复或容量规划。
- D-369/ADR-0462(已接受):在既有 operator-workstation Copilot Console 内完成显式可选、默认关闭的 Run management 只读纵切,新增固定 - D-369/ADR-0462(已接受):在既有 operator-workstation Copilot Console 内完成显式可选、默认关闭的 Run management 只读纵切,新增固定
`run_cancellation_status``run_cancellation_blocked_list``run_cancellation_inspect` 三种 browser/BFF operation。只有同时提供独立 `run_cancellation_status``run_cancellation_blocked_list``run_cancellation_inspect` 三种 browser/BFF operation。只有同时提供独立
`--run-management-config` 与 owner-private `--run-management-assertion` 才启用,Project API credential、浏览器 session 与 Run 专用 TLS 1.3/mTLS/OIDC `--run-management-config` 与 owner-private `--run-management-assertion` 才启用,Project API credential、浏览器 session 与 Run 专用 TLS 1.3/mTLS/OIDC
@@ -0,0 +1,107 @@
# ADR-0463:原生双架构 Console 容量与 Assertion 生命周期证据
- 状态:Accepted(证据协议、原生采集与 CI 聚合门已实现;首份 GitHub Actions 双架构报告待实际运行产生)
- 日期:2026-08-20
- 关联 RFCQL-RFC-0001 D-370、PR-5、PR-7
- 关联 ADRADR-0088、ADR-0281、ADR-0322、ADR-0329、ADR-0462
- AmendsADR-0462 中尚未取得的 compact Console Linux x64/arm64 实测边界
## 上下文
D-369 已把 Run cancellation status、blocked page 与 inspect 作为显式可选、用户驱动的只读纵切接入
Cluster Copilot Console,并继续声明 compact workstation 容器预算为 `192 MiB / 0.25 CPU / 32 PIDs`
静态 limit、macOS Docker Desktop 或单架构开发机都不能证明生产镜像在原生 Linux x64 与 arm64 上确实满足该
envelope,也不能证明 Console 在不重启时会为每次点击重新读取轮换后的短期 assertion,并低敏拒绝过期凭据。
部署用户跨度很大:Edge/Standalone 可能运行在小型路由设备,而 Cluster operator workstation 与集群节点有
完全不同的资源和可用性要求。因此本门只能回答“按需启动的 Cluster Admin Console 是否在固定 CI 容器预算内
完成四次有界管理读取”,不能把结果外推为物理 Edge 最低配置、Cluster 节点吞吐或多节点容量规划。
## 决策
1. 新增单一 CI/审核脚本 `scripts/ql3-cluster-copilot-console-capacity-evidence.cjs`,提供 `capture``merge`
`audit` 三种模式。它不新增 workspace package、生产 dependency、binary、服务、数据库对象或部署 workload;
证据职责不足以成立独立 package,避免再次制造只有一个文件的微包。
2. `capture` 只允许 Node `v24.18.0` 的原生 Linux `x64``arm64`,并要求显式
`QL3_CLUSTER_COPILOT_CONSOLE_CAPACITY_LIVE=1`。镜像必须是当前 matrix 刚构建的独立 Admin image
architecture、content ID、size 和 `10001:10001` runtime user 均从 Docker inspect 取得。
3. Console 使用精确 compact envelope`192 MiB` memory、memory-swap 与 memory 相同从而使 cgroup v2
`memory.swap.max=0``0.25 CPU``32 PIDs`、只读 root、cap-drop ALL、no-new-privileges、默认 seccomp、
`8 MiB` noexec/nosuid/nodev tmpfs、非 root user 和仅 `127.0.0.1` 发布端口。authority volume 只读挂载,
management fixture 只存在于隔离 Docker network 且不发布宿主端口。
4. 在四次请求前后从 Console 自身 cgroup v2 读取 `memory.max``memory.peak``memory.swap.max`
`memory.events``cpu.max``pids.max/current`,并从 `/proc/self/status` 读取 NoNewPrivs/seccomp。peak 必须至少
保留 `32 MiB` headroom`max/oom/oom_kill/oom_group_kill` 不能增加。container start identity 前后相同,
因而不能通过重启掩盖累积峰值或 assertion reload 缺陷。
5. synthetic management verifier 只接受 TLS 1.3 和受信 client certificate,固定 service SAN 与
`/api/v3/runs/management`,且只记录 label、TLS、method、path、operation、mutation 等低敏事实。它不是外部
IdP 或生产 authorization attestation。
6. 用户驱动序列固定为 `initial_accepted → rotated_accepted → expired_rejected → rotated_recovered`。Assertion
通过同一只读 volume 内的 `write wx → chown → atomic rename` 原位轮换;Console 每次 POST 重新读取文件,
不能重启、轮询、重试、缓存或触发 mutation。过期 assertion 必须由上游 401 被 BFF 投影为 502 和
`assertion_expired`,响应与日志不能泄露 assertion、authority path 或 service identity。
7. 每个原生 matrix job 生成 exact-shape、source-bound 架构报告,绑定 repository、40 位 revision、workflow、
run ID 与 run attempt。输入为有大小上限、nofollow 的普通 JSON;输出只以 `wx/0600` 新建。内容使用固定
domain separator 的 canonical SHA-256,未知字段、非有限数字、过深或过多节点均失败关闭。
8. Artifact 名精确包含 run ID、attempt 与 architectureupload/download action 固定到完整 commit,禁止
overwrite。独立只读 job 必须等待完整 `cluster-image` matrix,分别下载 x64/arm64,不使用 pattern 或
merge-multiplemerge 重验同源与不同 image ID,随后在同一 job 离线 audit 合并报告。
9. 合并报告显式保留四条 limitation192 MiB workstation envelope 不是 Cluster 吞吐/容量规划;native CI
不是物理 Edge 最低配置、断电、闪存、热环境或 soak 证明;synthetic mTLS verifier 不是外部 IdP 证明;
workflow source binding 不是密码学硬件 attestation。
## 被拒绝的替代方案
### 在 macOS arm64 上用 QEMU 生成双架构结果
拒绝。标签与模拟执行不能代替两个原生 Linux runner;本地只能验证协议、负向门禁和工作流装配。
### 把容量采集拆成多个 workspace package
拒绝。capture、merge、audit 只服务一个 CI evidence contract,没有独立生产依赖、版本、入口或运行时生命周期。
拆分会扩大 package 数和维护面,并重现 `packages/*/src` 只有一个平铺文件的问题。
### 把 192 MiB 写成路由设备最低配置
拒绝。Console 是显式按需的 Cluster operator workstation 工具,Edge/Standalone artifact closure 不包含它;
固定路由器仍需明确硬件、内核、文件系统、断电、闪存、热环境和长时间 soak 证据。
### 把空载 Console 峰值写成 Cluster 容量
拒绝。该门只有四次串行读,没有副本、数据库延迟、并发 operator、Worker、queue、failover 或恢复负载。
Cluster 容量规划必须由独立多节点负载与故障恢复门完成。
### 为 assertion 轮换加入 watcher、polling 或重启
拒绝。既有 server 的每次点击读取已经提供最小、可审计的 reload 语义;watcher/timer 会增加 idle 成本,重启会
破坏 session 并掩盖实际 reload 行为。
## 验证与当前证据状态
- D-370 证据协议与 CI 发布审计聚焦门为 `108/108`;原生 `cluster-image` 聚焦集合连同 SBOM 为 `120/120`。覆盖双架构正向 merge/audit、headroom、swap、OOM、PID、
schema widening、assertion sequence/TLS/mTLS/mutation、digest 篡改、跨 run、重复 image、symlink、overwrite、
live opt-in,以及 capture、matrix dependency、offline audit 的负向 workflow drift。
- 本机 Docker inspect 探针确认 SecurityOpt 为 `no-new-privileges`tmpfs 保留精确 `size=8m` 表示;探针容器已
删除。该探针只用于修正静态 inspect 契约,不冒充 native Linux x64/arm64 容量证据。
- CI 脚本与 workflow 已实现,但当前提交尚未在 GitHub Actions 产生同一 run 的两个原生 architecture artifact。
因此本 ADR 接受的是实现和门禁设计,不能宣称已经取得最终 memory peak、image ID、artifact digest 或 run URL。
- 该变化不改 package/dependency tree、Edge/Standalone runtime closure、PostgreSQL schema/role/Pool 或 HA 拓扑,
不重新占有数据库 HA 证据。
- 18-package clean build 与逐包测试在允许 loopback listener 的宿主环境退出 0Worker Runtime 独立复核为
`133/133`。当前完整 backend 工作区门为 `1,503 total / 1,501 pass / 2 conditional skip / 0 fail`;其中包含一项
与本切片无关、保持未跟踪且不会提交的用户测试,因此 D-370 提交范围对应 `1,502 total / 1,500 pass / 2 skip`。package boundary、dependency、Edge import、Cluster deployment、
Console 与 Console distribution 六项审计均 compatible、零 findingworkspace 仍为 18 packages、无 single/shallow
packageCluster Admin 为 `125 source / 124 nested`
- 14 档 Local artifact audit 全部 compatible;基础 Edge/Standalone 精确为 `2,589,998 / 2,590,076` bytes
Application+AI 为 `4,493,151 / 4,493,283` bytesMCP 为 `7,315,930 / 7,316,038` bytes,证明 CI-only evidence
没有进入低配部署闭包。
## 后续边界
- 推送后记录首个成功 workflow run、x64/arm64 bundle digest、cross-architecture release digest、各架构 image ID/
size/memory peak/headroom 与 artifact retention;若任何架构超过 envelope,必须修实现或调整经过 RFC 评审的
budget,不能放宽 validator 伪造成功。
- 物理 Edge 支持下限继续由 ADR-0088 类型的固定设备矩阵证明;Cluster 节点容量继续由多节点数据库、Worker、
queue、failover 与恢复负载证明。
- 后续若 Console 引入新的 read operation 或生产 dependency,必须重新采集双架构报告并证明仍无 mutation、
polling、authority leak 和 Edge closure 变化。
File diff suppressed because it is too large Load Diff
+144
View File
@@ -51,6 +51,8 @@ function auditClusterImageCiWorkflow(
const workflow = yaml.load(source); const workflow = yaml.load(source);
const clusterImageJob = workflow?.jobs?.['cluster-image']; const clusterImageJob = workflow?.jobs?.['cluster-image'];
const localImageJob = workflow?.jobs?.['local-image']; const localImageJob = workflow?.jobs?.['local-image'];
const consoleCapacityJob =
workflow?.jobs?.['cluster-console-capacity-release-evidence'];
const expectedNativeMatrix = [ const expectedNativeMatrix = [
{ {
runner: 'ubuntu-24.04', runner: 'ubuntu-24.04',
@@ -282,6 +284,11 @@ function auditClusterImageCiWorkflow(
/pnpm audit:image-release:ql3/, /pnpm audit:image-release:ql3/,
'image CI must audit the shared release workflow contract', 'image CI must audit the shared release workflow contract',
); );
requirePattern(
source,
/test\/back\/ql3ClusterCopilotConsoleCapacityEvidence\.test\.cjs/,
'native image CI must run the Console capacity evidence protocol tests',
);
requirePattern( requirePattern(
source, source,
/pnpm audit:deployment-lock-surfaces:ql3/, /pnpm audit:deployment-lock-surfaces:ql3/,
@@ -341,6 +348,135 @@ function auditClusterImageCiWorkflow(
/name: Run the bounded Cluster Admin product facade\s+if: matrix\.image == 'admin'\s+env:\s+IMAGE: qinglong3-cluster-admin:ci-\$\{\{ matrix\.image_arch \}\}\s+QL3_CLUSTER_ADMIN_PRODUCT_LIVE: '1'\s+run: node scripts\/ql3-cluster-admin-product-live-contract\.cjs --image="\$\{IMAGE\}"/, /name: Run the bounded Cluster Admin product facade\s+if: matrix\.image == 'admin'\s+env:\s+IMAGE: qinglong3-cluster-admin:ci-\$\{\{ matrix\.image_arch \}\}\s+QL3_CLUSTER_ADMIN_PRODUCT_LIVE: '1'\s+run: node scripts\/ql3-cluster-admin-product-live-contract\.cjs --image="\$\{IMAGE\}"/,
'native admin image CI must run the bounded product facade contract', 'native admin image CI must run the bounded product facade contract',
); );
const capacityCapture = clusterImageJob?.steps?.find(
({ name }) =>
name === 'Capture the fixed Cluster Copilot Console capacity envelope',
);
const nativeCapacityUpload = clusterImageJob?.steps?.find(
({ name }) =>
name === 'Upload native Cluster Copilot Console capacity evidence',
);
const expectedSourceEnvironment = {
SOURCE_REPOSITORY: '${{ github.repository }}',
SOURCE_REVISION: '${{ github.sha }}',
SOURCE_WORKFLOW: '${{ github.workflow }}',
SOURCE_RUN_ID: '${{ github.run_id }}',
SOURCE_RUN_ATTEMPT: '${{ github.run_attempt }}',
};
if (
capacityCapture?.if !== "matrix.image == 'admin'" ||
capacityCapture?.['timeout-minutes'] !== 10 ||
JSON.stringify(capacityCapture?.env) !==
JSON.stringify({
IMAGE: 'qinglong3-cluster-admin:ci-${{ matrix.image_arch }}',
QL3_CLUSTER_COPILOT_CONSOLE_CAPACITY_LIVE: '1',
...expectedSourceEnvironment,
}) ||
typeof capacityCapture?.run !== 'string' ||
!capacityCapture.run.includes(
'node scripts/ql3-cluster-copilot-console-capacity-evidence.cjs',
) ||
!capacityCapture.run.includes('--mode=capture') ||
!capacityCapture.run.includes('--architecture="${{ matrix.node_arch }}"') ||
!capacityCapture.run.includes('--image="${IMAGE}"') ||
!capacityCapture.run.includes(
'--output="${RUNNER_TEMP}/ql3-cluster-console-capacity/${{ matrix.node_arch }}.json"',
) ||
nativeCapacityUpload?.if !== "matrix.image == 'admin'" ||
nativeCapacityUpload?.uses !==
'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' ||
JSON.stringify(nativeCapacityUpload?.with) !==
JSON.stringify({
name: 'ql3-cluster-console-capacity-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.node_arch }}',
path: '${{ runner.temp }}/ql3-cluster-console-capacity/${{ matrix.node_arch }}.json',
'if-no-files-found': 'error',
'retention-days': 14,
'compression-level': 0,
overwrite: false,
'include-hidden-files': false,
})
) {
throw new Error(
'native admin image CI must capture and retain the exact source-bound Console capacity envelope',
);
}
const capacitySteps = consoleCapacityJob?.steps;
const x64Download = capacitySteps?.find(
({ name }) => name === 'Download native x64 Console capacity evidence',
);
const arm64Download = capacitySteps?.find(
({ name }) => name === 'Download native arm64 Console capacity evidence',
);
const capacityMerge = capacitySteps?.find(
({ name }) =>
name === 'Merge and audit the source-bound Console capacity evidence',
);
const capacityUpload = capacitySteps?.find(
({ name }) =>
name === 'Upload cross-architecture Console capacity evidence',
);
const downloadAction =
'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c';
if (
consoleCapacityJob?.needs !== 'cluster-image' ||
consoleCapacityJob?.['runs-on'] !== 'ubuntu-24.04' ||
consoleCapacityJob?.['timeout-minutes'] !== 5 ||
JSON.stringify(consoleCapacityJob?.permissions) !==
JSON.stringify({ contents: 'read' }) ||
!Array.isArray(capacitySteps) ||
capacitySteps.length !== 6 ||
capacitySteps[0]?.uses !== 'actions/checkout@v6' ||
capacitySteps[1]?.uses !== 'actions/setup-node@v6' ||
JSON.stringify(capacitySteps[1]?.with) !==
JSON.stringify({ 'node-version': '24.18.0' }) ||
x64Download?.uses !== downloadAction ||
JSON.stringify(x64Download?.with) !==
JSON.stringify({
name: 'ql3-cluster-console-capacity-${{ github.run_id }}-${{ github.run_attempt }}-x64',
path: '${{ runner.temp }}/ql3-cluster-console-capacity/x64',
}) ||
arm64Download?.uses !== downloadAction ||
JSON.stringify(arm64Download?.with) !==
JSON.stringify({
name: 'ql3-cluster-console-capacity-${{ github.run_id }}-${{ github.run_attempt }}-arm64',
path: '${{ runner.temp }}/ql3-cluster-console-capacity/arm64',
}) ||
JSON.stringify(capacityMerge?.env) !==
JSON.stringify(expectedSourceEnvironment) ||
typeof capacityMerge?.run !== 'string' ||
(
capacityMerge.run.match(
/node scripts\/ql3-cluster-copilot-console-capacity-evidence\.cjs/g,
) || []
).length !== 2 ||
!capacityMerge.run.includes('--mode=merge') ||
!capacityMerge.run.includes('--mode=audit') ||
!capacityMerge.run.includes(
'--x64="${RUNNER_TEMP}/ql3-cluster-console-capacity/x64/x64.json"',
) ||
!capacityMerge.run.includes(
'--arm64="${RUNNER_TEMP}/ql3-cluster-console-capacity/arm64/arm64.json"',
) ||
!capacityMerge.run.includes(
'--report="${RUNNER_TEMP}/ql3-cluster-console-capacity/cross-architecture.json"',
) ||
capacityUpload?.uses !==
'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' ||
JSON.stringify(capacityUpload?.with) !==
JSON.stringify({
name: 'ql3-cluster-console-capacity-release-${{ github.run_id }}-${{ github.run_attempt }}',
path: '${{ runner.temp }}/ql3-cluster-console-capacity/cross-architecture.json',
'if-no-files-found': 'error',
'retention-days': 14,
'compression-level': 0,
overwrite: false,
'include-hidden-files': false,
})
) {
throw new Error(
'Console capacity release evidence must merge and audit exact native x64 and arm64 reports with read-only authority',
);
}
requirePattern( requirePattern(
adminProductLiveContract, adminProductLiveContract,
/runOperatorContextContract\(image\);[\s\S]*operatorContext: true,[\s\S]*contextPreflight: true,[\s\S]*contextReadiness: true/, /runOperatorContextContract\(image\);[\s\S]*operatorContext: true,[\s\S]*contextPreflight: true,[\s\S]*contextReadiness: true/,
@@ -390,6 +526,14 @@ function auditClusterImageCiWorkflow(
clusterAdminOperatorContext: true, clusterAdminOperatorContext: true,
clusterAdminContextPreflight: true, clusterAdminContextPreflight: true,
clusterAdminContextReadiness: true, clusterAdminContextReadiness: true,
clusterCopilotConsoleCapacityEvidence: {
nativeArchitectures: ['x64', 'arm64'],
memoryLimitMiB: 192,
minimumHeadroomMiB: 32,
assertionRotation: true,
assertionExpiryRejected: true,
sourceBound: true,
},
releaseVersionAudit: true, releaseVersionAudit: true,
deploymentLockMaterialization: true, deploymentLockMaterialization: true,
ociAttestations: true, ociAttestations: true,
@@ -0,0 +1,402 @@
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const { test } = require('node:test');
const {
ASSERTION_SEQUENCE,
LIMITATIONS,
MEMORY_MAX_BYTES,
MINIMUM_MEMORY_HEADROOM_BYTES,
createArchitectureEvidence,
evidenceDigest,
mergeCrossArchitectureEvidence,
normalizeSource,
readJsonFile,
validateArchitectureEvidence,
validateObservation,
validateReleaseEvidence,
} = require('../../scripts/ql3-cluster-copilot-console-capacity-evidence.cjs');
const root = path.resolve(__dirname, '../..');
const scriptPath = path.join(
root,
'scripts/ql3-cluster-copilot-console-capacity-evidence.cjs',
);
const scriptSource = fs.readFileSync(scriptPath, 'utf8');
function fixtureSource(overrides = {}) {
return {
repository: 'whyour/qinglong',
revision: 'a'.repeat(40),
workflow: 'QingLong 3.0 CI',
runId: '123456',
runAttempt: 1,
...overrides,
};
}
function memoryEvents(overrides = {}) {
return {
low: 0,
high: 0,
max: 0,
oom: 0,
oomKill: 0,
oomGroupKill: 0,
...overrides,
};
}
function fixtureObservation(architecture, overrides = {}) {
const peak = 96 * 1024 * 1024;
return {
schemaVersion: 1,
observedAtMs: 1_700_000_000_000,
platform: 'linux',
architecture,
image: {
architecture: architecture === 'x64' ? 'amd64' : 'arm64',
id: `sha256:${architecture === 'x64' ? '1' : '2'}`.padEnd(
71,
architecture === 'x64' ? '1' : '2',
),
bytes: architecture === 'x64' ? 120_000_000 : 119_000_000,
user: '10001:10001',
},
runtime: { node: 'v24.18.0', uid: 10001, gid: 10001 },
envelope: {
memoryMaxBytes: MEMORY_MAX_BYTES,
memoryPeakBytes: peak,
memoryHeadroomBytes: MEMORY_MAX_BYTES - peak,
swapMaxBytes: 0,
cpuQuotaMicros: 25_000,
cpuPeriodMicros: 100_000,
pidsMax: 32,
pidsCurrent: 5,
noNewPrivileges: 1,
seccompMode: 2,
readOnlyRoot: true,
tmpfsBytes: 8 * 1024 * 1024,
publishedHostAddress: '127.0.0.1',
capabilityDrop: 'ALL',
memoryEventsBefore: memoryEvents(),
memoryEventsAfter: memoryEvents(),
},
assertionLifecycle: {
requestCount: 4,
sequence: [...ASSERTION_SEQUENCE],
tlsVersion: 'TLSv1.3',
mutualTls: true,
consoleRestarted: false,
mutation: false,
operation: 'run.cancellation.summary',
expiredConsoleStatus: 502,
expiredCode: 'assertion_expired',
},
...overrides,
};
}
function fixtureArchitecture(architecture, source = fixtureSource()) {
return createArchitectureEvidence({
source,
architecture,
observation: fixtureObservation(architecture),
});
}
function sourceArguments(source = fixtureSource()) {
return [
`--repository=${source.repository}`,
`--revision=${source.revision}`,
`--workflow=${source.workflow}`,
`--run-id=${source.runId}`,
`--run-attempt=${source.runAttempt}`,
];
}
function writeJson(filePath, value) {
fs.writeFileSync(filePath, `${JSON.stringify(value)}\n`, 'utf8');
}
function runCli(arguments_, env = {}) {
return spawnSync(process.execPath, [scriptPath, ...arguments_], {
encoding: 'utf8',
env: { ...process.env, ...env },
});
}
test('creates exact native reports and a source-bound cross-architecture release report', () => {
const source = fixtureSource();
const x64 = fixtureArchitecture('x64', source);
const arm64 = fixtureArchitecture('arm64', source);
const release = mergeCrossArchitectureEvidence({ source, x64, arm64 });
assert.equal(
x64.fixture,
'qinglong/cluster-console-capacity-architecture-evidence@v1',
);
assert.equal(
release.fixture,
'qinglong/cluster-console-capacity-cross-architecture-evidence@v1',
);
assert.deepEqual(
release.architectures.map(({ architecture }) => architecture),
['x64', 'arm64'],
);
assert.equal(release.gates.releaseEvidenceComplete, true);
assert.equal(release.gates.passed, true);
assert.deepEqual(release.assertionLifecycle.sequence, ASSERTION_SEQUENCE);
assert.deepEqual(release.limitations, LIMITATIONS);
assert.equal(release.releaseDigest.length, 64);
assert.notEqual(x64.bundleDigest, arm64.bundleDigest);
assert.equal(validateReleaseEvidence(release, source), release);
});
test('rejects memory pressure, OOM, swap, PID and widened envelope observations', () => {
const architecture = 'x64';
const base = fixtureObservation(architecture);
const cases = [
[
{
envelope: {
...base.envelope,
memoryPeakBytes: MEMORY_MAX_BYTES - MINIMUM_MEMORY_HEADROOM_BYTES + 1,
memoryHeadroomBytes: MINIMUM_MEMORY_HEADROOM_BYTES - 1,
},
},
/resource envelope drifted/,
],
[
{
envelope: {
...base.envelope,
memoryEventsAfter: memoryEvents({ oomKill: 1 }),
},
},
/memory event oomKill changed/,
],
[
{ envelope: { ...base.envelope, swapMaxBytes: 1024 } },
/resource envelope drifted/,
],
[
{ envelope: { ...base.envelope, pidsCurrent: 33 } },
/resource envelope drifted/,
],
[
{ envelope: { ...base.envelope, unexpected: true } },
/envelope fields are invalid/,
],
];
for (const [override, expected] of cases) {
assert.throws(
() =>
validateObservation(
fixtureObservation(architecture, override),
architecture,
),
expected,
);
}
});
test('rejects assertion lifecycle, native identity and mutation drift', () => {
const base = fixtureObservation('arm64');
for (const assertionLifecycle of [
{ ...base.assertionLifecycle, requestCount: 3 },
{
...base.assertionLifecycle,
sequence: ['initial_accepted', 'expired_rejected'],
},
{ ...base.assertionLifecycle, tlsVersion: 'TLSv1.2' },
{ ...base.assertionLifecycle, mutualTls: false },
{ ...base.assertionLifecycle, consoleRestarted: true },
{ ...base.assertionLifecycle, mutation: true },
{ ...base.assertionLifecycle, operation: 'run.cancellation.rearm' },
{ ...base.assertionLifecycle, expiredCode: 'assertion_invalid' },
]) {
assert.throws(
() =>
validateObservation(
fixtureObservation('arm64', { assertionLifecycle }),
'arm64',
),
/assertion lifecycle drifted/,
);
}
assert.throws(
() => validateObservation(fixtureObservation('arm64'), 'x64'),
/native identity is invalid/,
);
});
test('rejects tampering, cross-run mixing and duplicate image identity', () => {
const source = fixtureSource();
const x64 = fixtureArchitecture('x64', source);
const arm64 = fixtureArchitecture('arm64', source);
assert.throws(
() =>
validateArchitectureEvidence(
{ ...x64, bundleDigest: '0'.repeat(64) },
source,
'x64',
),
/digest or gates drifted/,
);
assert.throws(
() =>
mergeCrossArchitectureEvidence({
source,
x64,
arm64: fixtureArchitecture(
'arm64',
fixtureSource({ revision: 'b'.repeat(40) }),
),
}),
/belongs to another source/,
);
const duplicateObservation = fixtureObservation('arm64');
duplicateObservation.image.id = x64.observation.image.id;
const duplicate = createArchitectureEvidence({
source,
architecture: 'arm64',
observation: duplicateObservation,
});
assert.throws(
() => mergeCrossArchitectureEvidence({ source, x64, arm64: duplicate }),
/independently measured images/,
);
});
test('rejects coercible source fields and oversized canonical evidence', () => {
for (const source of [
fixtureSource({ repository: 123 }),
fixtureSource({ revision: 123 }),
fixtureSource({ runId: 123456 }),
]) {
assert.throws(
() => normalizeSource(source),
/source (repository|revision|runId)/,
);
}
assert.throws(
() => evidenceDigest(Array.from({ length: 100_000 }, () => null)),
/node budget exceeded/,
);
});
test('CLI merges, audits and refuses overwrite, symlink and source drift', (t) => {
const temporaryDirectory = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-console-capacity-evidence-')),
);
t.after(() =>
fs.rmSync(temporaryDirectory, { recursive: true, force: true }),
);
const source = fixtureSource();
const x64Path = path.join(temporaryDirectory, 'x64.json');
const arm64Path = path.join(temporaryDirectory, 'arm64.json');
const outputPath = path.join(temporaryDirectory, 'cross.json');
writeJson(x64Path, fixtureArchitecture('x64', source));
writeJson(arm64Path, fixtureArchitecture('arm64', source));
const merged = runCli([
'--mode=merge',
...sourceArguments(source),
`--x64=${x64Path}`,
`--arm64=${arm64Path}`,
`--output=${outputPath}`,
]);
assert.equal(merged.status, 0, merged.stderr);
assert.equal(fs.statSync(outputPath).mode & 0o777, 0o600);
const report = readJsonFile(outputPath, 'release evidence');
validateReleaseEvidence(report, source);
const audit = runCli([
'--mode=audit',
...sourceArguments(source),
`--report=${outputPath}`,
]);
assert.equal(audit.status, 0, audit.stderr);
assert.equal(JSON.parse(audit.stdout).passed, true);
const overwrite = runCli([
'--mode=merge',
...sourceArguments(source),
`--x64=${x64Path}`,
`--arm64=${arm64Path}`,
`--output=${outputPath}`,
]);
assert.notEqual(overwrite.status, 0);
assert.match(overwrite.stderr, /output must be a new private file/);
const drift = runCli([
'--mode=audit',
...sourceArguments(fixtureSource({ revision: 'b'.repeat(40) })),
`--report=${outputPath}`,
]);
assert.notEqual(drift.status, 0);
assert.match(drift.stderr, /belongs to another source/);
const linkPath = path.join(temporaryDirectory, 'report-link.json');
fs.symlinkSync(outputPath, linkPath);
const symlink = runCli([
'--mode=audit',
...sourceArguments(source),
`--report=${linkPath}`,
]);
assert.notEqual(symlink.status, 0);
assert.match(symlink.stderr, /readable non-symlink file/);
});
test('capture fails closed before Docker without explicit live opt-in', () => {
const result = runCli([
'--mode=capture',
...sourceArguments(),
`--architecture=${process.arch === 'arm64' ? 'arm64' : 'x64'}`,
'--image=qinglong3-cluster-admin:ci-test',
'--output=/tmp/ql3-console-capacity-should-not-exist.json',
]);
assert.notEqual(result.status, 0);
assert.match(
result.stderr,
/QL3_CLUSTER_COPILOT_CONSOLE_CAPACITY_LIVE=1 is required/,
);
});
test('live source freezes native cgroup v2, isolation and assertion rotation mechanics', () => {
for (const contract of [
"process.platform !== 'linux' || process.arch !== architecture",
'process.version !== NODE_VERSION',
"'--memory',\n '192m'",
"'--memory-swap',\n '192m'",
"'--cpus',\n '0.25'",
"'--pids-limit',\n String(PIDS_MAX)",
"'--read-only'",
"'--cap-drop',\n 'ALL'",
"'--security-opt',\n 'no-new-privileges'",
'`127.0.0.1:${port}:${port}/tcp`',
"memoryPeakBytes: integer('memory.peak')",
'memoryEvents: { low: events.low',
"swapMaxBytes: integer('memory.swap.max')",
'requestCert: true',
'rejectUnauthorized: true',
"minVersion: 'TLSv1.3'",
"fs.renameSync(next, '/authority/assertion.jwt')",
"operation: 'run.cancellation.summary'",
'consoleRestarted: false',
'mutation: false',
'created.network = true',
"cleanupDocker(['network', 'rm', network])",
]) {
assert.ok(scriptSource.includes(contract), `missing ${contract}`);
}
assert.doesNotMatch(scriptSource, /run\.cancellation\.(?:rearm|stop|retry)/);
assert.doesNotMatch(scriptSource, /--privileged|--network[= ]host/);
});
@@ -30,6 +30,14 @@ test('accepts the reviewed native CI and digest release contracts', () => {
clusterAdminOperatorContext: true, clusterAdminOperatorContext: true,
clusterAdminContextPreflight: true, clusterAdminContextPreflight: true,
clusterAdminContextReadiness: true, clusterAdminContextReadiness: true,
clusterCopilotConsoleCapacityEvidence: {
nativeArchitectures: ['x64', 'arm64'],
memoryLimitMiB: 192,
minimumHeadroomMiB: 32,
assertionRotation: true,
assertionExpiryRejected: true,
sourceBound: true,
},
releaseVersionAudit: true, releaseVersionAudit: true,
deploymentLockMaterialization: true, deploymentLockMaterialization: true,
ociAttestations: true, ociAttestations: true,
@@ -315,6 +323,50 @@ test('rejects removal of the native Cluster Admin product facade gate', () => {
); );
}); });
test('rejects a Console capacity capture without the exact native live opt-in', () => {
const mutated = ciSource.replace(
"QL3_CLUSTER_COPILOT_CONSOLE_CAPACITY_LIVE: '1'",
"QL3_CLUSTER_COPILOT_CONSOLE_CAPACITY_LIVE: '0'",
);
assert.throws(
() => auditClusterImageCiWorkflow(mutated),
/exact source-bound Console capacity envelope/,
);
});
test('rejects removal of the Console capacity evidence protocol tests', () => {
const mutated = ciSource.replace(
'test/back/ql3ClusterCopilotConsoleCapacityEvidence.test.cjs',
'test/back/ql3ClusterCopilotConsoleCapacityEvidence.removed.cjs',
);
assert.throws(
() => auditClusterImageCiWorkflow(mutated),
/Console capacity evidence protocol tests/,
);
});
test('rejects Console capacity evidence that is not gated by the native image matrix', () => {
const mutated = ciSource.replace(
'cluster-console-capacity-release-evidence:\n name: Cross-architecture Cluster Copilot Console capacity evidence\n needs: cluster-image',
'cluster-console-capacity-release-evidence:\n name: Cross-architecture Cluster Copilot Console capacity evidence\n needs: image-oci',
);
assert.throws(
() => auditClusterImageCiWorkflow(mutated),
/exact native x64 and arm64 reports/,
);
});
test('rejects removal of the offline Console capacity release audit', () => {
const mutated = ciSource.replace(
' --mode=audit \\\n',
' --mode=merge \\\n',
);
assert.throws(
() => auditClusterImageCiWorkflow(mutated),
/exact native x64 and arm64 reports/,
);
});
test('rejects a Cluster Admin live gate that omits operator context injection', () => { test('rejects a Cluster Admin live gate that omits operator context injection', () => {
const contract = fs.readFileSync( const contract = fs.readFileSync(
path.join(root, 'scripts/ql3-cluster-admin-product-live-contract.cjs'), path.join(root, 'scripts/ql3-cluster-admin-product-live-contract.cjs'),