feat(ql3): ship runnable local alpha quickstart

This commit is contained in:
whyour
2026-08-28 03:56:54 +08:00
parent 82d74cf56f
commit b7a8104d9f
10 changed files with 540 additions and 15 deletions
+10
View File
@@ -631,6 +631,16 @@ jobs:
--output="${RUNNER_TEMP}/ql3-alpha-local-${{ matrix.image_arch }}"
node scripts/ql3-local-alpha-trial-kit-bundle.cjs \
--mode=audit --bundle="${RUNNER_TEMP}/ql3-alpha-local-${{ matrix.image_arch }}"
QUICKSTART_CONTAINER="ql3-alpha-quickstart-${{ matrix.image_arch }}"
QUICKSTART_ROOT="${RUNNER_TEMP}/ql3-alpha-quickstart-${{ matrix.image_arch }}"
trap 'docker rm --force "${QUICKSTART_CONTAINER}" >/dev/null 2>&1 || true' EXIT
sh "${RUNNER_TEMP}/ql3-alpha-local-${{ matrix.image_arch }}/quickstart.sh" \
edge "${QUICKSTART_ROOT}" "${QUICKSTART_CONTAINER}"
docker stop --time 30 "${QUICKSTART_CONTAINER}"
docker rm "${QUICKSTART_CONTAINER}"
test -s "${QUICKSTART_ROOT}/qinglong3.sqlite"
test -s "${QUICKSTART_ROOT}/owner-delivery/credential-019f8680-143d-4000-8000-000000000021.ready.json"
trap - EXIT
- name: Upload the tested native Local Alpha trial kit
if: github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts && (inputs.alpha_artifact_scope == 'local' || inputs.alpha_artifact_scope == 'all')
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+2
View File
@@ -11,6 +11,8 @@
最新增量证据(2026-08-28):
- D-416/ADR-0511(已实现,首份真实 v3 Trial Kit 待授权):阶段产物的判定从“可下载、可验真”推进到“部署者可直接试运行”。Local Trial Kit schema 升为 `qinglong/alpha-local-trial-kit@v3`,每个 amd64/arm64 bundle 新增由唯一模板生成并绑定 archive、两个 exact image ID、source 和 architecture 的 `quickstart.sh`;离线 auditor 会重新渲染 canonical journey,拒绝即使重算 manifest/checksum 的本地脚本改写。目标路由/NAS 或单节点只需 POSIX shell、`sha256sum` 和 Docker,即可在一个尚不存在的隔离目录完成全包 checksum、镜像 load/identity 复核、128 MiB 短生命周期 operator fresh setup、首 Owner 建立,并按 Edge `128 MiB/64 PID` 或 Standalone `256 MiB/256 PID` 启动 Application、等待 `active` 后输出 logs/stop/remove 指令。显式 artifact job 在 upload 前必须执行将要上传的 exact quickstart、检查 SQLite/Owner delivery 并 graceful stop;普通 push 不生成大 archive。聚焦 materializer/milestone/stage/workflow 回归为 `26/26`,完整 backend 为 `1636 total / 1634 pass / 2 conditional skip / 0 fail`18-package clean build/test 退出 0package boundary 保持 `18 packages / singleSourcePackages=[] / shallowSourcePackages=[]`Cluster dependency、122-module Edge import、镜像/版本与 14 档 Local artifact 均 compatible,基础 Edge/Standalone 仍为 `2,669,390 / 2,669,468 bytes`、325 files、58 modules。历史同源 arm64 镜像对已本地物化为 v3 八文件开发 bundle184,648,192-byte archive 完成 checksum、canonical audit、load 与 exact identityDocker Desktop 在 Owner directory ownership 处按既有 POSIX 不等价失败关闭,未冒充原生 Linux 成功,正式 native exact-script 证据由手动 artifact job 产生。该切片不安装宿主 Node.js,不把 Operator 变成 daemon,不增加镜像 layer、workspace package、端口或稳态 RSS,也不触碰 2.x/migration。当前产物是无外部 listener、AI-excluded 的 headless Alpha runtime,不冒充 2.x Web UI、公开 release、生产 HA 或 LTS;首份真实双架构 v3 下载物仍需维护者授权 `produce_alpha_artifacts=true`
- D-415/ADR-0510(已实现,首份真实 stage index 待授权):二十天开发形成的 Local 与 Cluster 候选现在有统一、面向部署者的最外层阶段交付入口。新增 `qinglong/alpha-stage-index@v1` finalizer/auditor,仅在显式 `produce_alpha_artifacts=true + alpha_artifact_scope=all` 且 Local/Cluster milestone 都成功后运行;它重新离线审计两个索引,要求 version/source/workflow SHA/ref/run/attempt 完全一致,记录两个 milestone manifest digest,并为 amd64/arm64 写出机器可读选择:低配 Edge/Standalone 只下载一个 Local Trial Kit、稳态只运行 Application 且 Operator 短时启动;Cluster 下载 control/admin/worker,按需增加 control-ai。跨索引审计必须同时持有 stage 与两个 milestone,拒绝跨运行混用、内容/选择漂移和额外文件。该索引不重复十个大 archive,不改变 Local-only/Cluster-only 授权路径,也不冒充受保护 tag、GHCR immutable digest、签名、正式 OCI catalog、生产 deployment lock 或 HA;普通 push/PR 不生成实际阶段 artifact。实现不新增 workspace package、runtime dependency、镜像 layer、端口、daemon、timer、连接池或 Edge/Standalone 稳态 RSS。
- D-414/ADR-0509(已实现,首份真实 Cluster milestone artifact 待授权):开发约二十天后,Cluster 阶段产物不再是八个由 workflow 内联 shell 自报 `passed` 的松散 archive。新增统一 `qinglong/alpha-cluster-image@v1` materializer/auditor,固定 control/control-ai/admin/worker 的 repository、OCI title、non-root user 和 amd64/arm64 identity;每个六文件 bundle 含单镜像 Docker archive、精确 SBOM、绑定 source/workflow SHA/ref/run/attempt/role/architecture/image ID 的 verification evidence、README、manifest 与 `SHA256SUMS`,下载后可无 Docker/网络离线复核闭合文件集和内容 digest。新增完整 CI 后置 `cluster-alpha-milestone` finalizer,重新下载八个 artifact,要求同一 version/source/run/attempt 且八个 image ID/archive digest/verification digest 互异,再上传 `qinglong/alpha-cluster-milestone@v1` 三文件索引;没有该索引的部分 archive 只是失败运行中间件。maturity 明确为 `cluster_integration_candidate_not_public_release`,适用于隔离 registry/K3s/Kubernetes 集成,不冒充受保护 tag、GHCR digest、签名、catalog 或生产 HA。实现不新增 workspace package、runtime dependency、镜像 layer、端口、daemon、timer、连接池或 Edge/Standalone 稳态 RSS;首份实际产物仍需维护者明确触发 `produce_alpha_artifacts=true + alpha_artifact_scope=cluster`
@@ -0,0 +1,78 @@
# ADR-0511:可直接试运行的 Local Alpha Quickstart
- 状态:Accepted(首份实际 v3 Trial Kit 待维护者授权)
- 日期:2026-08-28
- 决策:D-416
- 关联:ADR-0193、ADR-0503、ADR-0504、ADR-0506、ADR-0508、ADR-0510
## 背景
开发约二十天后,Local Alpha Trial Kit 已经能够闭合 Application/operator 镜像、SBOM、workflow evidence、manifest 和 checksum,并由双架构 milestone 与跨 Profile stage index 导航。但下载者仍需从长篇运维文档手工拼装 setup、首 Owner ceremony、Application config 和受限 `docker run`。现有 artifact 证明“镜像可验证”,还没有把已在 CI 通过的 fresh 用户旅程变成部署者拿到即可执行的阶段产物。
这对两类部署者都不合理:低配路由/NAS 不应为了试运行先安装 Node.js 24 或理解完整生产 deployment ceremony;普通单节点用户也不应复制多份含 mutation、UID/GID、resource limit 和 authority path 的 JSON。继续增加索引或证据而不关闭最后一公里,会把工程完整性误报为产品可用性。
## 决策
### 1. Local Trial Kit v3 必须携带 canonical quickstart
bundle schema 升为 `qinglong/alpha-local-trial-kit@v3`,闭合目录新增 `quickstart.sh`。materializer 从仓库内唯一模板写入当前 archive 名、Application/operator reference、exact image ID、source revision 和 architecturemanifest 增加 `quickstart` 文件记录,脚本同时进入 `SHA256SUMS`
离线 auditor 不只检查脚本 byte/SHA-256,还根据 manifest 重新渲染 canonical journey 并逐字比较。因此本地重写脚本后即使同时重算 manifest 和 checksum,也不能通过源代码对应版本的 auditor。
### 2. 目标机只依赖 POSIX shell、sha256sum 和 Docker
quickstart 接受 `edge|standalone`、一个尚不存在的 canonical absolute data root,以及可选容器名。它按固定顺序执行:
1. 校验整个 bundle 的 `SHA256SUMS`,加载单一双镜像 archive
2. 重新核对两个镜像的 ID、architecture、numeric user、source revision 和 operator lifecycle/network label
3. 以当前宿主 UID:GID、无网络、只读 rootfs、drop-all capability、no-new-privileges 和 128 MiB operator 上限完成 fresh setup
4. 通过短生命周期 operator 建立首个 Owner,敏感 delivery 只保存在新建 `0700` data root
5. 以 Edge `128 MiB/64 PID` 或 Standalone `256 MiB/256 PID` 上限启动 Application,等待结构化 `active` 事件后才报告成功;
6. 输出 logs、graceful stop、container removal 和保留 data root 的明确命令。
脚本不需要宿主 Node.js、jq、Compose、网络访问或 root。operator 仍然每次只处理一个 command file 后退出,不成为 daemon 或 sidecar。
### 3. quickstart 本身必须经过 exact native artifact 门
显式 `workflow_dispatch + produce_alpha_artifacts=true` 的每个 Local architecture job 在 `create → audit` 后,必须从将要上传的目录执行生成出的 `quickstart.sh`,确认 SQLite、Owner delivery、Application active 与 graceful stop,再允许 upload。原有 Edge/Standalone live journey 继续保留;新增门证明的是“下载目录里的 exact 脚本可以驱动 exact 镜像”,不是另一个源码等价测试。
### 4. 阶段可用边界保持诚实
该 quickstart 只允许 fresh、隔离的新目录,不接受既有目录,不触碰 2.x 数据,不执行 migration/cutover,也不宣称生产升级、HA、公开签名或 LTS。当前 Application 是无外部 listener 的 headless runtime,能验证 3.0 SQLite、Owner authority、调度/插件运行基础和生命周期;它不是 2.x Web UI 的替代品,AI deployment 仍明确 excluded。
Cluster 节点继续使用 Cluster Integration Candidate 和 Kubernetes/CloudNativePG 路径,不能复用 Local quickstart。
## 被拒绝的替代方案
### 只在 README 增加更多手工命令
拒绝。文档无法保证 image identity、资源限制、authority path 和初始化顺序不漂移,也不能由 CI 执行下载者实际拿到的 journey。
### 只提供 Compose 文件
拒绝。Compose 可以描述常驻 Application,但不能安全替代短生命周期 setup 与首 Owner ceremony;把 operator 设为常驻 sidecar会扩大管理 authority。
### 提供 Node.js quickstart
拒绝。它会要求低配目标机额外安装与维护 Node.js,而镜像交付本来只要求容器运行时。
### 自动迁移现有 2.x 目录
拒绝。Alpha quickstart 没有 reconciliation、review、cutover 和 rollback authority,自动接管既有目录会越过已经冻结的迁移边界。
## 影响
- Local Alpha 从“可下载、可验真”前进到“可在 fresh 设备目录一条命令完成初始化并启动”;
- bundle 从七个文件增加为八个小文件,不新增镜像 layer、workspace package、runtime dependency、端口、后台 timer 或稳态 RSS;
- Edge/Standalone 继续共用同一镜像,差异只体现在 quickstart 的 memory/PID limit
- `@v2` 仍是历史 verification-evidence 阶段格式,首份面向部署者的实际下载物必须使用 `@v3`
- 首份真实 amd64/arm64 v3 Trial Kit、Local milestone 与 stage index 仍需维护者显式授权 artifact workflow,普通 push 不生成大归档。
## 验证
- 单元测试覆盖 v3 八文件物化、POSIX shell 语法、exact image reference 注入、脚本/manifest/checksum 篡改,以及重算全部摘要后的非 canonical 脚本拒绝;
- Local milestone 与 Alpha stage index 回归证明 schema 升级没有放宽跨架构、跨 run/attempt 和跨 Profile 闭合;
- Local operator workflow 静态审计要求 `create → audit → quickstart → stop → upload` 顺序;
- 聚焦门为 `26/26`,完整 backend 为 `1636 total / 1634 pass / 2 conditional skip / 0 fail`18-package clean build/test、package/dependency/Edge import、镜像/版本和 14 档 Local artifact audit 全部通过;
- 历史同源 arm64 Application/operator 已物化为 184,648,192-byte v3 开发 bundle并通过 checksum、canonical audit、load 与 exact identityDocker Desktop 在 Owner POSIX directory ownership 处按已知不等价失败关闭,未被记录为 native quickstart 成功;
- 手动 artifact job 在原生 amd64/arm64 runner 上执行将要上传的 exact quickstart;首份实际运行结果待维护者授权触发后记录。
+1
View File
@@ -514,6 +514,7 @@
| [ADR-0508](./ADR-0508-cross-architecture-local-alpha-milestone-closure.md) | 跨架构 Local Alpha 里程碑闭合 | Accepted(首份实际 milestone artifact 待维护者授权) |
| [ADR-0509](./ADR-0509-cluster-alpha-bundle-and-milestone-closure.md) | Cluster Alpha Bundle 与跨架构里程碑闭合 | Accepted(首份实际 Cluster milestone artifact 待维护者授权) |
| [ADR-0510](./ADR-0510-cross-profile-alpha-stage-delivery-index.md) | 跨 Profile Alpha 阶段交付索引 | Accepted(首份实际 stage index 待维护者授权) |
| [ADR-0511](./ADR-0511-runnable-local-alpha-quickstart.md) | 可直接试运行的 Local Alpha Quickstart | Accepted(首份实际 v3 Trial Kit 待维护者授权) |
## 规则
+19 -4
View File
@@ -7,7 +7,7 @@
| 等级 | 面向对象 | 必须通过 | 当前用途 |
| --- | --- | --- | --- |
| Runtime Engineering Candidate | QingLong 开发者、设备兼容测试者 | 单个常驻镜像的 OS 漏洞策略、SBOM/库存、资源门和生命周期 | 验证 runtime 可加载、可启动;缺少管理制品时不能称用户 Alpha |
| Local Alpha Trial Kit | amd64/arm64 路由器、NAS、单机试用者 | 同源 Application + 短生命周期 operator、fresh setup/Owner/active/stop 完整旅程、SBOM/库存与资源门 | 一个去重 Docker archive 完成隔离 fresh 试运行;不承诺生产升级 |
| Local Alpha Trial Kit | amd64/arm64 路由器、NAS、单机试用者 | 同源 Application + 短生命周期 operator、exact quickstart、fresh setup/Owner/active/stop 完整旅程、SBOM/库存与资源门 | POSIX shell 一条命令从去重 Docker archive 完成隔离 fresh 试运行;不承诺生产升级 |
| Cluster Integration Candidate | amd64/arm64 集群测试节点 | OS 漏洞策略、SBOM 与镜像库存复核、non-root identityAdmin 额外通过产品 facade smoke | 导入隔离 registry/测试节点,进行多组件集成;不作为 production HA release |
| Public Release Set | 生产用户 | 受保护 tag、六镜像 multi-arch digestLocal Application/operator + Cluster 四角色)、签名/attestation、私有发布证据、catalog、Local/Cluster 部署与回退闭环 | 尚未实际发布;只能由受保护 release workflow 生成 |
@@ -26,7 +26,7 @@
该本地 archive 不是新的 v2 Local Alpha Trial Kit。它在 ADR-0506 前生成,manifest v1 会无条件写入 `passed`,且 macOS Docker Desktop 因 bind-mount UID 映射无法对 exact 本地 archive 完成 Owner pepper 旅程;原生 CI 证明同源码实现,不自动证明另一个 archive 的 exact image bytes。它因此保留为工程候选,不冒充已获 workflow evidence 的用户 Alpha。
ADR-0506 现要求 `qinglong/alpha-local-trial-kit@v2` 额外包含 `verification-evidence.json`,绑定显式 `workflow_dispatch` 的 source、workflow SHA/ref、run/attempt、架构和两个 image ID。旧 `e3c05862` runtime-only archive、`2620be05` v1 Trial Kit`4239464a` v1 archive 均为历史工程证据,不能通过 v2 auditor。下一项外部里程碑仍是维护者授权 `produce_alpha_artifacts=true`,由同一次原生 milestone job 生成 exact-image evidence 和双架构可下载 archive。
ADR-0506 `qinglong/alpha-local-trial-kit@v2` 首次增加了 source-bound `verification-evidence.json`;ADR-0511 进一步把当前格式升级为 `@v3`,新增不依赖宿主 Node.js 的 canonical `quickstart.sh`,把可验证镜像闭合为可执行的 fresh 用户旅程。旧 `e3c05862` runtime-only archive、`2620be05` v1 Trial Kit`4239464a` v1 archive 与未携带 quickstart 的 v2 均为历史工程证据,不能通过 v3 auditor。下一项外部里程碑仍是维护者授权 `produce_alpha_artifacts=true`,由同一次原生 milestone job 生成并实际执行 exact-image 双架构可下载 archive。
## 生成
@@ -43,7 +43,8 @@ ADR-0506 现要求 `qinglong/alpha-local-trial-kit@v2` 额外包含 `verificatio
Local artifact 含:
- 一个包含 Application 与短生命周期 operator 的 `qinglong3-local-trial-kit-<arch>.docker.tar`;共享 Node 基础层在 archive 中去重;
- schema 为 `qinglong/alpha-local-trial-kit@v2``manifest.json`,通过 `archive/images/sboms/readme/verification` 绑定版本、完整 source commit、架构、两个 image tag/image ID 与文件长度/SHA-256
- schema 为 `qinglong/alpha-local-trial-kit@v3``manifest.json`,通过 `archive/images/sboms/quickstart/readme/verification` 绑定版本、完整 source commit、架构、两个 image tag/image ID 与文件长度/SHA-256
- canonical `quickstart.sh`,在目标 Linux 设备上只依赖 POSIX shell、`sha256sum` 和 Docker,完成 checksum、load、identity、fresh Owner 与 Profile-bound Application active
- `verification-evidence.json` 绑定 `workflow_dispatch` 的 workflow ref/SHA、run ID/attempt、同架构两个 exact image ID 和完整 gate 集;下载者仍须到 GitHub 交叉检查 run,它不替代正式签名;
- 与实际只读镜像 inventory 对账过的 CycloneDX SBOM
- 面向 Local 用户的 README 与覆盖全部内容文件的 `SHA256SUMS`
@@ -54,7 +55,21 @@ Stage index 是 `qinglong/alpha-stage-index@v1` 三文件闭包。它重新审
任何 required job 失败时不上传对应产物。artifact 名和 archive 内的 `ci-*` tag 都表示 commit-bound candidate,不能改名后冒充 `v3.x` release。
## 下载后验证与最小 smoke
## 下载后直接试运行
在同架构原生 Linux Docker 主机上进入解压后的 Local artifact 目录,使用一个尚不存在的
隔离目录:
```sh
sh quickstart.sh edge /opt/qinglong3-alpha-data
```
也可以选择 `standalone` 和自定义容器名。quickstart 会先执行 `SHA256SUMS`,再核对 exact
镜像身份并完成 fresh setup、首 Owner 与 Application active;成功后输出 logs、stop 和
remove 命令。当前是无外部 listener、AI-excluded 的 headless runtime 阶段成果,不是
2.x Web UI 的替代品。
## 手工验证与最小 smoke
在同架构 Linux Docker 主机上进入解压后的 artifact 目录:
+33 -3
View File
@@ -23,7 +23,7 @@ sha256sum --check SHA256SUMS
`manifest.json` 必须满足:
- `schema``qinglong/alpha-local-trial-kit@v2`
- `schema``qinglong/alpha-local-trial-kit@v3`
- `sourceRevision` 是你准备试用的完整 40 位 commit;
- `architecture` 与主机相同;
- `maturity``alpha_candidate_not_public_release`
@@ -40,7 +40,37 @@ node scripts/ql3-local-alpha-trial-kit-bundle.cjs \
任一校验失败都不要加载或运行 archive。
## 加载与最小 smoke
## 一条命令完成 Fresh 试运行
v3 bundle 内的 `quickstart.sh` 不依赖宿主 Node.js、jq 或 Compose,只需要 POSIX
shell、`sha256sum` 和已启动的 Docker。必须选择一个尚不存在、与 2.x/生产数据完全
隔离的绝对路径:
```sh
sh quickstart.sh edge /opt/qinglong3-alpha-data
```
资源较充足的单节点可以把 `edge` 改为 `standalone`。第三个可选参数用于指定容器名:
```sh
sh quickstart.sh standalone /srv/qinglong3-alpha-data ql3-alpha-standalone
```
脚本会自动执行全包 checksum、加载 archive、核对 exact image ID/source/architecture、
以当前 UID:GID 和无网络的短生命周期 operator 完成 fresh setup 与首 Owner 建立,随后按
Profile 资源上限启动 Application。只有容器日志出现结构化 `active` 事件才返回成功。
Owner delivery 保留在新数据目录的 `owner-delivery/`operator command 结果保留在
`results/`;两者都位于 `0700` 私有根内,不会打印 Secret 到终端。
成功输出会给出当前容器的 logs、graceful stop 和 remove 命令。停止/删除容器不会自动
删除数据目录;确认不再需要诊断后由操作者显式删除该 fresh 测试目录。脚本拒绝既有目录,
不能用于升级、迁移或接管 2.x。
当前 Application 是无外部 listener、AI-excluded 的 headless Alpha runtime。该试运行可
验证 3.0 SQLite、Owner authority、调度/插件基础与生命周期,但还不是 2.x Web UI 的
可替代版本。
## 手工加载与最小 smoke
`manifest.json.archive.file` 找到 archive 后加载:
@@ -66,7 +96,7 @@ docker run --rm --read-only --network none --cap-drop ALL \
## Fresh 试运行边界
完整 fresh setup、首 Owner ceremony、Application active、SIGTERM drain、SQLite integrity 和原生 cancellation 必须在 `verification-evidence.json` 指向的同架构 milestone job 中验证。实际部署时仍必须使用独立目录,并让 operator 以最终数据文件 POSIX owner 的 UID/GID 运行;operator 默认无网络且每次只执行一个命令后退出,不应作为 sidecar 或 daemon 常驻。
完整 fresh setup、首 Owner ceremony、Application active、SIGTERM drain、SQLite integrity 和原生 cancellation 必须在 `verification-evidence.json` 指向的同架构 milestone job 中验证。v3 artifact job 还必须从将要上传的目录实际执行 `quickstart.sh` 并完成 graceful stop。实际部署时仍必须使用独立目录,并让 operator 以最终数据文件 POSIX owner 的 UID/GID 运行;operator 默认无网络且每次只执行一个命令后退出,不应作为 sidecar 或 daemon 常驻。
Edge 的验证上限为 Application 128 MiB、0.5 CPU、64 PIDStandalone 为 256 MiB、0.5 CPU、256 PIDoperator 为 128 MiB、0.5 CPU、32 PID。这里的数值是试运行门,不是所有 workload 的容量承诺。
+68 -6
View File
@@ -10,18 +10,24 @@ const { auditClusterImageSbom } = require('./ql3-cluster-image-sbom.cjs');
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
const DEFAULT_ROOT = path.resolve(__dirname, '..');
const SCHEMA = 'qinglong/alpha-local-trial-kit@v2';
const SCHEMA = 'qinglong/alpha-local-trial-kit@v3';
const VERIFICATION_SCHEMA = 'qinglong/alpha-local-trial-kit-verification@v1';
const QUICKSTART_TEMPLATE = path.join(
DEFAULT_ROOT,
'scripts/templates/ql3-local-alpha-quickstart.sh',
);
const ARCHITECTURES = Object.freeze(['amd64', 'arm64']);
const ARCHIVE_MIN_BYTES = 1024;
const MAX_JSON_BYTES = 4 * 1024 * 1024;
const MAX_README_BYTES = 512 * 1024;
const MAX_QUICKSTART_BYTES = 256 * 1024;
const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/u;
const REVISION_PATTERN = /^[0-9a-f]{40}$/u;
const FILES = Object.freeze({
applicationSbom: 'qinglong3-local-application.cdx.json',
operatorSbom: 'qinglong3-local-operator.cdx.json',
verificationEvidence: 'verification-evidence.json',
quickstart: 'quickstart.sh',
readme: 'README.md',
manifest: 'manifest.json',
checksums: 'SHA256SUMS',
@@ -148,9 +154,7 @@ function saveDockerImages(images, archivePath) {
function validateImageReference(value, label) {
if (
typeof value !== 'string' ||
value.length < 3 ||
value.length > 256 ||
/[\s\0]/u.test(value)
!/^[A-Za-z0-9][A-Za-z0-9._:/@-]{2,255}$/u.test(value)
) {
fail(`${label} image reference is invalid`);
}
@@ -383,6 +387,34 @@ function archiveName(architecture) {
return `qinglong3-local-trial-kit-${architecture}.docker.tar`;
}
function renderQuickstart(identity) {
const template = fs.readFileSync(
assertCanonicalFile(
QUICKSTART_TEMPLATE,
MAX_QUICKSTART_BYTES,
'quickstart template',
),
'utf8',
);
const replacements = Object.freeze({
'@@APPLICATION_IMAGE@@': identity.images.application.reference,
'@@APPLICATION_ID@@': identity.images.application.id,
'@@OPERATOR_IMAGE@@': identity.images.operator.reference,
'@@OPERATOR_ID@@': identity.images.operator.id,
'@@ARCHITECTURE@@': identity.architecture,
'@@SOURCE_REVISION@@': identity.sourceRevision,
'@@ARCHIVE@@': identity.archive.file,
});
let rendered = template;
for (const [token, value] of Object.entries(replacements)) {
rendered = rendered.replaceAll(token, value);
}
if (/@@[A-Z_]+@@/u.test(rendered)) {
fail('quickstart template contains an unresolved token');
}
return rendered;
}
function fileRecord(bundleRoot, name) {
const filePath = path.join(bundleRoot, name);
const stat = fs.lstatSync(filePath);
@@ -541,8 +573,19 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
normalized.readme,
path.join(normalized.outputRoot, FILES.readme),
);
const manifestIdentity = {
sourceRevision: normalized.sourceRevision,
architecture: normalized.architecture,
archive: { file: archive },
images: { application, operator },
};
writeExclusive(
path.join(normalized.outputRoot, FILES.quickstart),
renderQuickstart(manifestIdentity),
0o700,
);
const manifest = {
schemaVersion: 3,
schemaVersion: 4,
schema: SCHEMA,
maturity: 'alpha_candidate_not_public_release',
product: 'local',
@@ -555,6 +598,7 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
application: fileRecord(normalized.outputRoot, FILES.applicationSbom),
operator: fileRecord(normalized.outputRoot, FILES.operatorSbom),
},
quickstart: fileRecord(normalized.outputRoot, FILES.quickstart),
readme: fileRecord(normalized.outputRoot, FILES.readme),
verification: fileRecord(
normalized.outputRoot,
@@ -570,6 +614,7 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
FILES.applicationSbom,
FILES.operatorSbom,
FILES.verificationEvidence,
FILES.quickstart,
FILES.readme,
FILES.manifest,
];
@@ -637,10 +682,11 @@ function auditLocalAlphaTrialKit(options) {
'archive',
'images',
'sboms',
'quickstart',
'readme',
'verification',
]) ||
manifest.schemaVersion !== 3 ||
manifest.schemaVersion !== 4 ||
manifest.schema !== SCHEMA ||
manifest.maturity !== 'alpha_candidate_not_public_release' ||
manifest.product !== 'local' ||
@@ -673,6 +719,19 @@ function auditLocalAlphaTrialKit(options) {
FILES.verificationEvidence,
bundleRoot,
);
validateFileRecord(manifest.quickstart, FILES.quickstart, bundleRoot);
const expectedQuickstart = renderQuickstart(manifest);
const actualQuickstart = fs.readFileSync(
assertCanonicalFile(
path.join(bundleRoot, FILES.quickstart),
MAX_QUICKSTART_BYTES,
'quickstart',
),
'utf8',
);
if (actualQuickstart !== expectedQuickstart) {
fail('quickstart differs from the canonical deployment journey');
}
validateFileRecord(manifest.readme, FILES.readme, bundleRoot);
validateOfflineSbom(
readBoundedJson(
@@ -707,6 +766,7 @@ function auditLocalAlphaTrialKit(options) {
FILES.applicationSbom,
FILES.operatorSbom,
FILES.verificationEvidence,
FILES.quickstart,
expectedArchive,
].sort();
const actualFiles = fs
@@ -726,6 +786,7 @@ function auditLocalAlphaTrialKit(options) {
FILES.applicationSbom,
FILES.operatorSbom,
FILES.verificationEvidence,
FILES.quickstart,
FILES.readme,
FILES.manifest,
];
@@ -746,6 +807,7 @@ function auditLocalAlphaTrialKit(options) {
archiveSha256: manifest.archive.sha256,
applicationImageId: manifest.images.application.id,
operatorImageId: manifest.images.operator.id,
quickstartSha256: manifest.quickstart.sha256,
verificationSha256: manifest.verification.sha256,
workflowRunId: verificationEvidence.workflow.runId,
workflowRunAttempt: verificationEvidence.workflow.runAttempt,
@@ -199,6 +199,9 @@ function auditWorkflow(contents, findings) {
'--mode=record-verification',
'--mode=create',
'--mode=audit',
'/quickstart.sh" \\\n edge "${QUICKSTART_ROOT}" "${QUICKSTART_CONTAINER}"',
'docker stop --time 30 "${QUICKSTART_CONTAINER}"',
'test -s "${QUICKSTART_ROOT}/qinglong3.sqlite"',
'--application-sbom="${RUNNER_TEMP}/ql3-local-application.cdx.json"',
'--operator-sbom="${RUNNER_TEMP}/ql3-local-operator.cdx.json"',
'--verification-evidence="${RUNNER_TEMP}/ql3-local-alpha-verification-${{ matrix.image_arch }}.json"',
@@ -222,6 +225,7 @@ function auditWorkflow(contents, findings) {
'--mode=record-verification',
'--mode=create',
'--mode=audit',
'/quickstart.sh"',
'name: Upload the tested native Local Alpha trial kit',
]) {
const index = contents.indexOf(value, cursor + 1);
@@ -0,0 +1,162 @@
#!/bin/sh
set -eu
APPLICATION_IMAGE='@@APPLICATION_IMAGE@@'
APPLICATION_ID='@@APPLICATION_ID@@'
OPERATOR_IMAGE='@@OPERATOR_IMAGE@@'
OPERATOR_ID='@@OPERATOR_ID@@'
ARCHITECTURE='@@ARCHITECTURE@@'
SOURCE_REVISION='@@SOURCE_REVISION@@'
ARCHIVE='@@ARCHIVE@@'
fail() {
printf '%s\n' "QingLong Local Alpha quickstart failed: $*" >&2
exit 1
}
usage() {
printf '%s\n' \
'usage: sh quickstart.sh edge|standalone /absolute/new/data-root [container-name]' >&2
exit 2
}
[ "$#" -ge 2 ] && [ "$#" -le 3 ] || usage
profile=$1
data_root=$2
container_name=${3:-ql3-alpha-local}
case "$profile" in
edge)
memory=128m
pids=64
;;
standalone)
memory=256m
pids=256
;;
*) usage ;;
esac
case "$data_root" in
/|*[!A-Za-z0-9_./-]*|*'/../'*|*'/./'*|*'/..'|*'/.'|*'//'*|*/)
fail 'data root must be a canonical absolute path using A-Z, a-z, 0-9, _, ., / or -'
;;
/*) ;;
*) fail 'data root must be absolute' ;;
esac
case "$container_name" in
''|[_.-]*|*[!A-Za-z0-9_.-]*) fail 'container name is invalid' ;;
esac
command -v docker >/dev/null 2>&1 || fail 'docker is required'
command -v sha256sum >/dev/null 2>&1 || fail 'sha256sum is required'
command -v grep >/dev/null 2>&1 || fail 'grep is required'
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
parent=${data_root%/*}
[ -n "$parent" ] || parent=/
[ -d "$parent" ] || fail 'data root parent does not exist'
parent_real=$(CDPATH= cd -- "$parent" && pwd -P)
[ "$parent_real/${data_root##*/}" = "$data_root" ] || fail 'data root parent is not canonical'
[ ! -e "$data_root" ] || fail 'data root must not already exist'
(CDPATH= cd -- "$script_dir" && sha256sum --check SHA256SUMS)
docker info >/dev/null 2>&1 || fail 'docker daemon is unavailable'
docker load --input "$script_dir/$ARCHIVE" >/dev/null
application_identity=$(docker image inspect --format '{{.Id}}|{{.Architecture}}|{{.Config.User}}|{{index .Config.Labels "org.opencontainers.image.revision"}}' "$APPLICATION_IMAGE")
operator_identity=$(docker image inspect --format '{{.Id}}|{{.Architecture}}|{{.Config.User}}|{{index .Config.Labels "org.opencontainers.image.revision"}}|{{index .Config.Labels "io.qinglong.lifecycle"}}|{{index .Config.Labels "io.qinglong.network"}}' "$OPERATOR_IMAGE")
[ "$application_identity" = "$APPLICATION_ID|$ARCHITECTURE|65532:65532|$SOURCE_REVISION" ] || fail 'application image identity is incompatible'
[ "$operator_identity" = "$OPERATOR_ID|$ARCHITECTURE|65532:65532|$SOURCE_REVISION|short-lived|none-by-default" ] || fail 'operator image identity is incompatible'
old_umask=$(umask)
umask 077
mkdir -m 0700 "$data_root"
for directory in owner-peppers owner-pepper-backup owner-delivery receipts artifacts plugin-staging plugin-activation results; do
mkdir -m 0700 "$data_root/$directory"
done
cat >"$data_root/setup.json" <<EOF
{"schemaVersion":1,"operation":"local.setup.prepare","options":{"deploymentRoot":"/var/lib/qinglong3","databasePath":"/var/lib/qinglong3/qinglong3.sqlite","profile":"$profile","ownerPepperKeyringDirectory":"/var/lib/qinglong3/owner-peppers","ownerPepperBackupDirectory":"/var/lib/qinglong3/owner-pepper-backup","ownerPepperKeyId":"owner-v1","localSecretKeyringPath":"/var/lib/qinglong3/local-secret-keyring.json","busyTimeoutMs":100},"request":{"registerMutationId":"019f8680-143d-4000-8000-000000000011","activateMutationId":"019f8680-143d-4000-8000-000000000012","registeredAtMs":1785254400000,"activatedAtMs":1785254400001}}
EOF
cat >"$data_root/owner-provision.json" <<EOF
{"schemaVersion":1,"operation":"owner.identity.provision","options":{"deploymentRoot":"/var/lib/qinglong3","databasePath":"/var/lib/qinglong3/qinglong3.sqlite","pepperPath":"/var/lib/qinglong3/owner-peppers/b3duZXItdjE.pepper","pepperKeyId":"owner-v1","secretDeliveryDirectory":"/var/lib/qinglong3/owner-delivery","profile":"$profile","busyTimeoutMs":100},"request":{"mutationId":"019f8680-143d-4000-8000-000000000021","requestId":"alpha-trial-owner-provision"}}
EOF
cat >"$data_root/owner-challenge.json" <<EOF
{"schemaVersion":1,"operation":"owner.challenge.issue","options":{"deploymentRoot":"/var/lib/qinglong3","databasePath":"/var/lib/qinglong3/qinglong3.sqlite","pepperPath":"/var/lib/qinglong3/owner-peppers/b3duZXItdjE.pepper","pepperKeyId":"owner-v1","secretDeliveryDirectory":"/var/lib/qinglong3/owner-delivery","profile":"$profile","busyTimeoutMs":100},"request":{"projectId":"default","mutationId":"019f8680-143d-4000-8000-000000000022","requestId":"alpha-trial-owner-challenge"}}
EOF
cat >"$data_root/owner-claim.json" <<EOF
{"schemaVersion":1,"operation":"owner.claim.from-deliveries","options":{"deploymentRoot":"/var/lib/qinglong3","databasePath":"/var/lib/qinglong3/qinglong3.sqlite","pepperPath":"/var/lib/qinglong3/owner-peppers/b3duZXItdjE.pepper","pepperKeyId":"owner-v1","secretDeliveryDirectory":"/var/lib/qinglong3/owner-delivery","profile":"$profile","busyTimeoutMs":100},"request":{"projectId":"default","mutationId":"019f8680-143d-4000-8000-000000000023","requestId":"alpha-trial-owner-claim","credentialMutationId":"019f8680-143d-4000-8000-000000000021","challengeMutationId":"019f8680-143d-4000-8000-000000000022"}}
EOF
cat >"$data_root/local-application.json" <<EOF
{"schema":"qinglong/local-application-process@v2","instanceId":"alpha-trial-local","profile":"$profile","storage":{"mode":"fresh","databasePath":"/var/lib/qinglong3/qinglong3.sqlite","busyTimeoutMs":100},"runtime":{"receiptRoot":"/var/lib/qinglong3/receipts","artifactRoot":"/var/lib/qinglong3/artifacts","secretKeyringPath":"/var/lib/qinglong3/local-secret-keyring.json"},"pluginPackages":{"stagingRoot":"/var/lib/qinglong3/plugin-staging","activationRoot":"/var/lib/qinglong3/plugin-activation","recoverySource":{"mode":"disabled"},"pageSize":4,"maxPages":4,"taskPublicationPageSize":4,"taskPublicationMaxPages":4},"ai":{"deployment":"excluded"}}
EOF
chmod 0600 "$data_root"/*.json
uid=$(id -u)
gid=$(id -g)
run_operator() {
command_name=$1
command_file=$2
docker run --rm --read-only --user "$uid:$gid" --network none \
--cap-drop ALL --security-opt no-new-privileges \
--memory 128m --memory-swap 128m --cpus 0.5 --pids-limit 32 \
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=8m \
--mount "type=bind,src=$data_root,dst=/var/lib/qinglong3" \
"$OPERATOR_IMAGE" "$command_name" run \
--command-file "/var/lib/qinglong3/$command_file" \
>"$data_root/results/$command_file.result.json"
}
run_operator setup setup.json
grep -q '"status":"prepared"' "$data_root/results/setup.json.result.json" || fail 'fresh setup did not report prepared'
run_operator owner owner-provision.json
grep -q '"status":"inserted"' "$data_root/results/owner-provision.json.result.json" || fail 'Owner credential provisioning did not report inserted'
run_operator owner owner-challenge.json
grep -q '"status":"inserted"' "$data_root/results/owner-challenge.json.result.json" || fail 'Owner challenge did not report inserted'
run_operator owner owner-claim.json
grep -q '"status":"inserted"' "$data_root/results/owner-claim.json.result.json" || fail 'Owner claim did not report inserted'
grep -q '"role":"owner"' "$data_root/results/owner-claim.json.result.json" || fail 'Owner claim did not establish the owner role'
ready=0
cleanup() {
if [ "$ready" -ne 1 ]; then
docker rm --force "$container_name" >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT
trap 'exit 130' HUP INT TERM
container_id=$(docker run --detach --name "$container_name" \
--restart unless-stopped --read-only --user "$uid:$gid" --network none \
--cap-drop ALL --security-opt no-new-privileges \
--memory "$memory" --memory-swap "$memory" --cpus 0.5 --pids-limit "$pids" \
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=16m \
--mount "type=bind,src=$data_root,dst=/var/lib/qinglong3" \
"$APPLICATION_IMAGE" --config /var/lib/qinglong3/local-application.json)
printf '%s\n' "$container_id" >"$data_root/container.id"
chmod 0600 "$data_root/container.id"
attempt=0
while [ "$attempt" -lt 45 ]; do
if docker logs "$container_name" 2>&1 | grep -q '"event":"active"'; then
ready=1
break
fi
running=$(docker inspect --format '{{.State.Running}}' "$container_name" 2>/dev/null || true)
[ "$running" = true ] || fail 'application stopped before becoming active'
attempt=$((attempt + 1))
sleep 1
done
[ "$ready" -eq 1 ] || fail 'application did not become active within 45 seconds'
umask "$old_umask"
printf '%s\n' \
"QingLong 3.0 Local Alpha is active ($profile, $ARCHITECTURE)." \
"Data root: $data_root" \
"Owner deliveries: $data_root/owner-delivery" \
"Logs: docker logs $container_name" \
"Stop: docker stop --time 30 $container_name" \
"Remove container: docker rm $container_name" \
'The fresh data root is retained until you remove it explicitly.'
+163 -2
View File
@@ -1,6 +1,7 @@
'use strict';
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
@@ -10,6 +11,7 @@ const {
createLocalAlphaTrialKit,
createLocalAlphaTrialKitVerificationEvidence,
parseArguments,
sha256File,
} = require('../../scripts/ql3-local-alpha-trial-kit-bundle.cjs');
const {
createClusterImageSbom,
@@ -148,13 +150,23 @@ function adapters(overrides = {}) {
test('materializes and offline-audits one closed two-image trial kit', (t) => {
const paths = fixture(t);
const manifest = createLocalAlphaTrialKit(createOptions(paths), adapters());
assert.equal(manifest.schema, 'qinglong/alpha-local-trial-kit@v2');
assert.equal(manifest.schema, 'qinglong/alpha-local-trial-kit@v3');
assert.equal(manifest.sourceRevision, revision);
assert.equal(manifest.architecture, 'arm64');
assert.equal(manifest.images.application.architecture, 'arm64');
assert.equal(manifest.images.operator.architecture, 'arm64');
assert.notEqual(manifest.images.application.id, manifest.images.operator.id);
assert.equal(manifest.verification.file, 'verification-evidence.json');
assert.equal(manifest.quickstart.file, 'quickstart.sh');
const quickstart = path.join(paths.outputRoot, 'quickstart.sh');
const syntax = spawnSync('sh', ['-n', quickstart], { encoding: 'utf8' });
assert.equal(syntax.status, 0, syntax.stderr);
const quickstartContents = fs.readFileSync(quickstart, 'utf8');
assert.match(
quickstartContents,
/QingLong 3\.0 Local Alpha is active \(\$profile, \$ARCHITECTURE\)/,
);
assert.match(quickstartContents, /qinglong3-local-application:test-arm64/);
const report = auditLocalAlphaTrialKit({ bundleRoot: paths.outputRoot });
assert.equal(report.compatible, true);
assert.equal(report.sourceRevision, revision);
@@ -166,6 +178,7 @@ test('materializes and offline-audits one closed two-image trial kit', (t) => {
'qinglong3-local-application.cdx.json',
'qinglong3-local-operator.cdx.json',
'qinglong3-local-trial-kit-arm64.docker.tar',
'quickstart.sh',
'verification-evidence.json',
]);
});
@@ -194,7 +207,13 @@ test('fails closed and removes a partial output on incompatible image identity',
});
test('offline audit rejects archive, file-set, SBOM and verification mutation', (t) => {
for (const mutation of ['archive', 'extra', 'sbom', 'verification']) {
for (const mutation of [
'archive',
'extra',
'quickstart',
'sbom',
'verification',
]) {
const paths = fixture(t);
paths.outputRoot = path.join(paths.fixtureRoot, `bundle-${mutation}`);
createLocalAlphaTrialKit(createOptions(paths), adapters());
@@ -208,6 +227,11 @@ test('offline audit rejects archive, file-set, SBOM and verification mutation',
);
} else if (mutation === 'extra') {
fs.writeFileSync(path.join(paths.outputRoot, 'credential.txt'), 'secret');
} else if (mutation === 'quickstart') {
fs.appendFileSync(
path.join(paths.outputRoot, 'quickstart.sh'),
'# drift\n',
);
} else if (mutation === 'sbom') {
fs.copyFileSync(
path.join(paths.outputRoot, 'qinglong3-local-application.cdx.json'),
@@ -227,6 +251,143 @@ test('offline audit rejects archive, file-set, SBOM and verification mutation',
}
});
test('offline audit rejects a rehashed non-canonical quickstart', (t) => {
const paths = fixture(t);
createLocalAlphaTrialKit(createOptions(paths), adapters());
const quickstart = path.join(paths.outputRoot, 'quickstart.sh');
fs.appendFileSync(quickstart, '# locally rewritten\n');
const manifestPath = path.join(paths.outputRoot, 'manifest.json');
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
manifest.quickstart = {
file: 'quickstart.sh',
sha256: sha256File(quickstart),
bytes: fs.statSync(quickstart).size,
};
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
const checkedFiles = [
'qinglong3-local-trial-kit-arm64.docker.tar',
'qinglong3-local-application.cdx.json',
'qinglong3-local-operator.cdx.json',
'verification-evidence.json',
'quickstart.sh',
'README.md',
'manifest.json',
];
fs.writeFileSync(
path.join(paths.outputRoot, 'SHA256SUMS'),
`${checkedFiles
.map(
(name) =>
`${sha256File(path.join(paths.outputRoot, name)).slice(7)} ${name}`,
)
.join('\n')}\n`,
);
assert.throws(
() => auditLocalAlphaTrialKit({ bundleRoot: paths.outputRoot }),
/quickstart differs from the canonical deployment journey/,
);
});
test('generated quickstart drives the closed fresh Edge journey', (t) => {
const paths = fixture(t);
createLocalAlphaTrialKit(createOptions(paths), adapters());
const fakeBin = path.join(paths.fixtureRoot, 'fake-bin');
fs.mkdirSync(fakeBin);
const dockerLog = path.join(paths.fixtureRoot, 'docker.log');
const fakeDocker = path.join(fakeBin, 'docker');
fs.writeFileSync(
fakeDocker,
`#!/bin/sh
printf '%s\\n' "$*" >>"$FAKE_DOCKER_LOG"
case "$1:$2" in
info:|load:*) exit 0 ;;
image:inspect)
case "$*" in
*local-application*) printf '%s\\n' 'sha256:${'1'.repeat(
64,
)}|arm64|65532:65532|${revision}' ;;
*local-operator*) printf '%s\\n' 'sha256:${'2'.repeat(
64,
)}|arm64|65532:65532|${revision}|short-lived|none-by-default' ;;
*) exit 1 ;;
esac
exit 0
;;
logs:*) printf '%s\\n' '{"event":"active"}'; exit 0 ;;
inspect:*) printf '%s\\n' 'true'; exit 0 ;;
esac
case " $* " in
*' --detach '*) printf '%s\\n' 'fake-container-id'; exit 0 ;;
*'/setup.json'*) printf '%s\\n' '{"status":"prepared"}'; exit 0 ;;
*'/owner-provision.json'*) printf '%s\\n' '{"status":"inserted"}'; exit 0 ;;
*'/owner-challenge.json'*) printf '%s\\n' '{"status":"inserted"}'; exit 0 ;;
*'/owner-claim.json'*) printf '%s\\n' '{"status":"inserted","role":"owner"}'; exit 0 ;;
esac
exit 1
`,
{ mode: 0o755 },
);
const dataRoot = path.join(paths.fixtureRoot, 'quickstart-data');
const run = spawnSync(
'sh',
[
path.join(paths.outputRoot, 'quickstart.sh'),
'edge',
dataRoot,
'ql3-alpha-test',
],
{
encoding: 'utf8',
env: {
...process.env,
FAKE_DOCKER_LOG: dockerLog,
PATH: `${fakeBin}:${process.env.PATH}`,
},
},
);
assert.equal(
run.status,
0,
`${run.stderr}\n${run.stdout}\n${fs.readFileSync(dockerLog, 'utf8')}`,
);
assert.match(
run.stdout,
/QingLong 3\.0 Local Alpha is active \(edge, arm64\)/,
);
assert.equal(
fs.readFileSync(path.join(dataRoot, 'container.id'), 'utf8'),
'fake-container-id\n',
);
assert.equal(
JSON.parse(
fs.readFileSync(
path.join(dataRoot, 'results', 'owner-claim.json.result.json'),
'utf8',
),
).role,
'owner',
);
const calls = fs.readFileSync(dockerLog, 'utf8');
assert.match(
calls,
/load --input .*qinglong3-local-trial-kit-arm64\.docker\.tar/,
);
assert.match(calls, /--memory 128m --memory-swap 128m/);
assert.match(calls, /--pids-limit 64/);
assert.doesNotMatch(calls, /--network (?!none)/);
});
test('create rejects an image reference that could alter the shell journey', (t) => {
const paths = fixture(t);
const options = createOptions(paths);
options.applicationImage = 'qinglong3-local-application:test;unexpected';
assert.throws(
() => createLocalAlphaTrialKit(options, adapters()),
/application image reference is invalid/,
);
assert.equal(fs.existsSync(paths.outputRoot), false);
});
test('create rejects verification detached from the reviewed workflow', (t) => {
const paths = fixture(t);
const evidence = JSON.parse(