feat(ql3): add unified cluster operator cli

This commit is contained in:
whyour
2026-08-12 21:42:41 +08:00
parent bd0621c9f2
commit 2934123155
18 changed files with 1010 additions and 5 deletions
+6
View File
@@ -548,6 +548,12 @@ jobs:
echo "unexpected image contract: ${actual}" >&2
exit 1
fi
- name: Run the bounded Cluster Admin product facade
if: matrix.image == 'admin'
env:
IMAGE: qinglong3-cluster-admin:ci-${{ matrix.image_arch }}
QL3_CLUSTER_ADMIN_PRODUCT_LIVE: '1'
run: node scripts/ql3-cluster-admin-product-live-contract.cjs --image="${IMAGE}"
- name: Generate the reviewed application SBOM
run: >-
node scripts/ql3-cluster-image-sbom.cjs
@@ -93,4 +93,4 @@ COPY --from=assembled --chown=10001:10001 /opt/qinglong ./
USER 10001:10001
ENTRYPOINT ["node", "/opt/qinglong/node_modules/@qinglong/cluster-admin/dist/plugin-package/recovery/pluginPackageRecoveryCli.js"]
ENTRYPOINT ["node", "/opt/qinglong/node_modules/@qinglong/cluster-admin/dist/product-cli/cli.js"]
+10
View File
@@ -39,6 +39,16 @@ roots resolve to 84 external packages and it adds only `runtime-core`,
database role therefore exist only in an ephemeral Job image, never in
`cluster-control`.
Its default entrypoint is the bounded `ql3-cluster-admin` product facade. The
facade exposes only seven operator-facing remote clients: `package`,
`package-kubernetes`, `worker-credential`, `approval`, `run`, `automation` and
`model-credential`. It delegates with the current Node executable, an exact
same-image target and `shell=false`; arguments remain opaque. Server
`*-manage` processes, migration, recovery, executors, Prompt output key custody
and garbage collection remain separate explicit binaries and Kubernetes
operations. Existing Jobs continue to name their exact binary and do not
inherit facade authority.
The runtime image installs only the 43 external packages reachable from the
five exact production roots in its production-only lock plus `runtime-core`,
`cluster-postgres` and `cluster-control`: 46 runtime components in total. The
+14
View File
@@ -11,6 +11,20 @@
最新增量证据(2026-08-12):
- D-300/ADR-0388(已接受)
Cluster operator 已获得统一 `ql3-cluster-admin <command>` 产品入口,能力内聚在既有 `@qinglong/cluster-admin/product-cli`,不新增
package 或依赖。catalog 只允许 Package(直连/Kubernetes tunnel)、Worker Credential、Approval、Run、Automation、Model
Credential 七个现有强认证远程 client;服务端 `*-manage`、migration、recovery、executor、AI migration 与 Prompt output key
custody/GC 继续使用隔离 binary 和显式 Kubernetes operation。dispatcher 使用冻结同制品 target、当前 Node、`shell=false` 与 opaque
argv,并复验 canonical non-symlink target/package manifest,未知或损坏安装只输出低敏错误。Admin image 默认入口改为 facade,但现有
Job/Deployment 的 24 个 Admin image 引用均显式覆盖 commandauthority 与生命周期不变;该规则已进入部署审计与突变测试。Local/Edge、
Cluster Control 和 Worker 制品不加载此能力。facade 6/6、deployment 49/49、CI/release contract 48/48;真实 arm64 Admin image 为
330,444,072 bytes,在 `10001:10001`、read-only root、network none、capabilities none、0.25 CPU、128 MiB/32 PIDs 下 7/7 委派通过。
Cluster Admin 完整 package 为 291 pass/2 条件 skip18-package clean build/test 退出 0backend 为 1,184 pass/2 skip。workspace
保持 18 packageCluster Admin 96 source 中 95 nested/1 root,无 single-source/shallow package,五项边界审计零 finding。14 个 Local
Profile artifact 全部 compatible,最小 Edge 2,467,343 bytes/295 files/53 modules,最大 Standalone MCP 7,168,978 bytes/778 files/
213 modules,证明路由设备制品不受影响。PostgreSQL 18.4 arm64 HA 123 项 gate 全绿、timeline `1→2`,证据 SHA-256 为
`f5df5998d505b6d5af552e627bd3f83983bc347d08d94da5a2d73fde46eab0a4`,离线审计及 Docker 资源清理均通过。完整回归见 ADR-0388。
- D-299/ADR-0387(已接受)
Local `run.cancel` 已从“HTTP durable intent 与 `/proc` stop 分别验证”推进为同一次真实 Linux 组合门。门先用既有离线 pack/prune
生成可选 `edge/standalone-application-api` 最终制品,再在锁定 Node 24 arm64 Linux 容器内以 non-root、read-only root、network none、
@@ -0,0 +1,45 @@
# ADR-0388:有界统一 Cluster Operator 产品 CLI
- 状态:Accepted
- 日期:2026-08-12
- 关联 RFCQL-RFC-0001 D-300
- 前置决策:ADR-0127、ADR-0249、ADR-0259、ADR-0264、ADR-0271、ADR-0356、ADR-0365、ADR-0383
## 上下文
Local 部署者已有 `ql3 <command>` 可发现入口,Cluster operator 却仍需记忆二十多个 `ql3-*-client|manage|execute` binary。直接把所有 Cluster Admin 命令挂到一个 facade 会模糊远程客户端、常驻管理服务、数据库 migration、recovery、executor 与 key-custody 的 authority;新建 CLI package 又会恢复只有少量转发表文件的微包。
Cluster Admin 已是独立、短生命周期、非路由设备的发布镜像和 package。统一 operator UX 应留在该边界内,并且只能聚合调用方已经持有认证材料的远程客户端,不得借命令名称把服务端或数据库写 authority 交给普通工作站。
## 决策
1. 在既有 `@qinglong/cluster-admin``product-cli/` 领域目录增加 `ql3-cluster-admin` binary,不新增 workspace package、第三方依赖、数据库对象、角色、连接、listener、timer、watcher、cache 或 sidecar。
2. facade 只暴露七个固定远程客户端:`package``package-kubernetes``worker-credential``approval``run``automation``model-credential`。每项映射到同一安装中已经发布的一个 `*-client` binary。
3. `*-manage` 服务进程、migration、Plugin recovery、Worker/Package executor、provider test executor、AI migration、Prompt output rotation/retirement/recovery/GC 不进入 catalog;既有 binary 均保留,Kubernetes operation 继续显式指定精确入口。
4. dispatcher 只使用冻结 catalog、当前 `process.execPath` 和参数数组,固定 `shell=false`,不查询 `PATH`。目标必须是当前 package canonical `dist/` 内的 non-symlink regular filepackage manifest 必须是 canonical non-symlink、64 KiB 内且 name/version 合法。子命令参数作为 opaque argv 原样转发。
5. facade 只转发 `SIGINT|SIGTERM|SIGHUP`,保持 child exit code 或 `128+signal`;未知命令与安装损坏只返回不含用户输入、路径或认证材料的低敏 JSON。
6. Cluster Admin image 默认 ENTRYPOINT 改为 facade,使无参数运行提供可发现帮助。所有生产 Job/Deployment 已显式覆盖 command,因此不改变其 authority 或生命周期。CI 在 amd64/arm64 Admin matrix 中以 non-root、read-only root、network none、capabilities none、128 MiB/32 PIDs 运行真实镜像门。
7. Local/Edge Profile、Local image、Cluster Control image 与 Worker image均不安装或加载该 facade。package boundary 仍为 18Cluster Admin 根仍只有既有 migration binary;两个新增 source 全部位于 `product-cli/`
## 不采用方案
- **把所有 Cluster binary 暴露为子命令**:可发现性不能消除服务、执行和密钥 authority 隔离。
- **删除原 binary**:会破坏 Kubernetes manifest、自动化脚本和已有运维协议。
- **通过 shell、PATH 或用户路径查找命令**:允许环境劫持、通配符和参数重解释。
- **复用 Local `ql3` package**:会让 Edge/Standalone 安装 Kubernetes/PostgreSQL 管理依赖,并混合两种部署模型。
- **新建 Cluster CLI package**:只有同镜像静态路由职责,不形成新的部署、authority 或重依赖边界。
## 验收门
- catalog、help/version、opaque argv、unknown/path traversal、symlink target/manifest、真实 7-client help delegation、退出码与有界信号转发;
- package manifest 和 Admin Dockerfile 的正反向部署审计,CI/release audit 必须要求 native image live gate
- Cluster Admin 完整 package、backend、18-package clean build/test、package/dependency/deployment/image release boundary 全绿;
- 真实 arm64 Admin image 在受限 envelope 下通过,并记录 image bytesPostgreSQL HA 不回归;
- Local 14 Profile artifact 与默认 Local image 不因 Cluster facade 变化。
## 当前证据
- facade 6/6、Cluster deployment 49/49、CI/release contract 48/48;部署审计复核全部 24 个 Admin image 引用都显式覆盖 `command`,并以突变测试锁定。真实 arm64 Admin image 为 330,444,072 bytes`10001:10001`、只读根、无网络、无 capabilities、0.25 CPU、128 MiB/32 PIDs 下 7/7 客户端 help 委派成功。
- Cluster Admin 完整 package 为 293 tests、291 pass、2 条件 skip、0 fail18-package clean build/test 退出 0backend 为 1,186 tests、1,184 pass、2 skip、0 fail。workspace 保持 18 packageCluster Admin 为 96 source,其中 95 nested、1 个既有 root binary entry`singleSourcePackages=[]``shallowSourcePackages=[]`package/dependency/deployment/image release/Local image audit 零 finding。
- 14 个 Local Profile artifact 全部 compatible,最小 Edge 为 2,467,343 bytes/295 files/53 modules、最终运行 RSS 11,091,968 bytes;最大 Standalone MCP 为 7,168,978 bytes/778 files/213 modules、RSS 38,420,480 bytes,证明 Cluster facade 不进入路由设备与本机 Profile。
- PostgreSQL 18.4 arm64 physical HA 123 项 gate 全绿、timeline `1→2`,报告 SHA-256 `f5df5998d505b6d5af552e627bd3f83983bc347d08d94da5a2d73fde46eab0a4`;离线证据审计零 finding,测试容器、network 与 volume 零残留。
+1
View File
@@ -391,6 +391,7 @@
| [ADR-0385](./ADR-0385-strong-local-run-stop-product-entry.md) | 强认证的 Local Run Stop 产品入口与原子审计 | Accepted |
| [ADR-0386](./ADR-0386-cluster-run-management-kubernetes-live-evidence.md) | Cluster Run Management Kubernetes 多节点实证 | Accepted |
| [ADR-0387](./ADR-0387-local-api-cancellation-linux-live-evidence.md) | Local API Cancellation Linux 组合实证门 | Accepted |
| [ADR-0388](./ADR-0388-bounded-unified-cluster-product-cli.md) | 有界统一 Cluster Operator 产品 CLI | Accepted |
## 规则
+1
View File
@@ -77,6 +77,7 @@
"test:vault-transit-custody-live:ql3": "pnpm --filter @qinglong/ai build && pnpm --filter @qinglong/cluster-admin build && node scripts/ql3-vault-transit-prompt-output-custody-live-contract.cjs",
"test:postgres-backup-prompt-output-recovery-live:ql3": "pnpm --filter @qinglong/ai build && pnpm --filter @qinglong/cluster-admin build && node scripts/ql3-postgres-prompt-output-recovery-live-contract.cjs",
"audit:cluster-deployment:ql3": "node scripts/ql3-cluster-deployment-audit.cjs",
"test:cluster-admin-product-live:ql3": "node scripts/ql3-cluster-admin-product-live-contract.cjs",
"audit:provider-credential-management-deployment:ql3": "node scripts/ql3-provider-credential-management-deployment-audit.cjs",
"audit:provider-credential-test-deployment:ql3": "node scripts/ql3-provider-credential-test-deployment-audit.cjs",
"audit:worker-deployment:ql3": "node scripts/ql3-worker-deployment-audit.cjs",
+1
View File
@@ -342,6 +342,7 @@
"test:automation-integration": "node ../../scripts/ql3-build-package-closure.cjs && node --test test/automationManagementPostgres.integration.test.cjs"
},
"bin": {
"ql3-cluster-admin": "dist/product-cli/cli.js",
"ql3-plugin-package-recover": "dist/plugin-package/recovery/pluginPackageRecoveryCli.js",
"ql3-plugin-package-manage": "dist/plugin-package/management/pluginPackageManagementCli.js",
"ql3-plugin-package-client": "dist/plugin-package/management/pluginPackageManagementClientCli.js",
@@ -0,0 +1,142 @@
#!/usr/bin/env node
import { spawn, type ChildProcess } from 'node:child_process';
import { constants } from 'node:os';
import { resolveQingLong3ClusterProductCommand } from './productCommand';
const FORWARDED_SIGNALS = Object.freeze([
'SIGINT',
'SIGTERM',
'SIGHUP',
] as const);
export interface QingLong3ClusterProductSignalChild {
readonly exitCode: number | null;
readonly signalCode: NodeJS.Signals | null;
kill(signal: NodeJS.Signals): boolean;
}
export interface QingLong3ClusterProductSignalHost {
on(signal: NodeJS.Signals, handler: () => void): unknown;
off(signal: NodeJS.Signals, handler: () => void): unknown;
}
export function clusterProductSignalExitCode(
signal: NodeJS.Signals | null,
): number {
if (signal === null) return 1;
const number = constants.signals[signal];
return typeof number === 'number' ? 128 + number : 1;
}
function lowSensitivityFailure(
code: string,
message: string,
): Readonly<Record<string, string | number>> {
return Object.freeze({
schemaVersion: 1,
component: 'qinglong3-cluster-product-cli',
code,
message,
});
}
export function forwardClusterProductSignals(
child: QingLong3ClusterProductSignalChild,
signalHost: QingLong3ClusterProductSignalHost = process,
): () => void {
const handlers = FORWARDED_SIGNALS.map((signal) => {
const handler = (): void => {
if (child.exitCode === null && child.signalCode === null) {
child.kill(signal);
}
};
signalHost.on(signal, handler);
return Object.freeze({ signal, handler });
});
return () => {
for (const { signal, handler } of handlers) {
signalHost.off(signal, handler);
}
};
}
function invoke(targetFilePath: string, argv: readonly string[]): void {
let child: ChildProcess;
try {
child = spawn(process.execPath, [targetFilePath, ...argv], {
stdio: 'inherit',
env: process.env,
shell: false,
windowsHide: true,
});
} catch {
process.stderr.write(
`${JSON.stringify(
lowSensitivityFailure(
'QL3_CLUSTER_PRODUCT_COMMAND_START_FAILED',
'QingLong 3.0 Cluster product command could not start',
),
)}\n`,
);
process.exitCode = 1;
return;
}
const removeSignalHandlers = forwardClusterProductSignals(child);
let settled = false;
const settle = (exitCode: number): void => {
if (settled) return;
settled = true;
removeSignalHandlers();
process.exitCode = exitCode;
};
child.once('error', () => {
process.stderr.write(
`${JSON.stringify(
lowSensitivityFailure(
'QL3_CLUSTER_PRODUCT_COMMAND_START_FAILED',
'QingLong 3.0 Cluster product command could not start',
),
)}\n`,
);
settle(1);
});
child.once('close', (code, signal) => {
settle(code ?? clusterProductSignalExitCode(signal));
});
}
function main(argv: readonly string[]): void {
try {
const resolution = resolveQingLong3ClusterProductCommand(argv, __dirname);
if (resolution.kind === 'help' || resolution.kind === 'version') {
process.stdout.write(`${resolution.output}\n`);
return;
}
if (resolution.kind === 'invalid') {
process.stderr.write(
`${JSON.stringify(
lowSensitivityFailure(resolution.code, resolution.message),
)}\n`,
);
process.exitCode = 64;
return;
}
invoke(resolution.targetFilePath, resolution.argv);
} catch {
process.stderr.write(
`${JSON.stringify(
lowSensitivityFailure(
'QL3_CLUSTER_PRODUCT_CLI_INSTALLATION_INVALID',
'QingLong 3.0 Cluster product command installation is invalid',
),
)}\n`,
);
process.exitCode = 1;
}
}
if (require.main === module) {
main(process.argv.slice(2));
}
@@ -0,0 +1,218 @@
import { lstatSync, readFileSync, realpathSync } from 'node:fs';
import { isAbsolute, relative, resolve, sep } from 'node:path';
export interface QingLong3ClusterProductCommandDefinition {
readonly name: string;
readonly binary: string;
readonly target: string;
readonly description: string;
}
export type QingLong3ClusterProductCommandResolution =
| Readonly<{ kind: 'help'; output: string }>
| Readonly<{ kind: 'version'; output: string }>
| Readonly<{
kind: 'invoke';
command: QingLong3ClusterProductCommandDefinition;
targetFilePath: string;
argv: readonly string[];
}>
| Readonly<{
kind: 'invalid';
code: 'QL3_CLUSTER_PRODUCT_CLI_USAGE_INVALID';
message: string;
}>;
const PACKAGE_NAME = '@qinglong/cluster-admin';
const MAXIMUM_PACKAGE_MANIFEST_BYTES = 64 * 1024;
const SEMVER_PATTERN =
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
export const QINGLONG3_CLUSTER_PRODUCT_COMMANDS: readonly QingLong3ClusterProductCommandDefinition[] =
Object.freeze([
Object.freeze({
name: 'package',
binary: 'ql3-plugin-package-client',
target: 'plugin-package/management/pluginPackageManagementClientCli.js',
description: 'manage Plugin Packages through the authenticated API',
}),
Object.freeze({
name: 'package-kubernetes',
binary: 'ql3-plugin-package-client-kubernetes',
target:
'plugin-package/management/pluginPackageManagementKubernetesClientCli.js',
description: 'manage Plugin Packages through a bounded Kubernetes tunnel',
}),
Object.freeze({
name: 'worker-credential',
binary: 'ql3-worker-credential-client',
target: 'worker-credential/workerCredentialManagementClientCli.js',
description: 'manage Worker credentials through the authenticated API',
}),
Object.freeze({
name: 'approval',
binary: 'ql3-approval-client',
target: 'approval-management/approvalManagementClientCli.js',
description: 'inspect and decide human approvals',
}),
Object.freeze({
name: 'run',
binary: 'ql3-run-client',
target: 'run-management/runManagementClientCli.js',
description: 'retry or stop Runs under strong authentication',
}),
Object.freeze({
name: 'automation',
binary: 'ql3-automation-client',
target: 'automation-management/automationManagementClientCli.js',
description: 'manage Task and Trigger definitions',
}),
Object.freeze({
name: 'model-credential',
binary: 'ql3-provider-credential-client',
target:
'model-provider-credential/modelProviderCredentialManagementClientCli.js',
description: 'manage model provider credentials',
}),
]);
function installationPaths(moduleDirectory: string): Readonly<{
distRoot: string;
packageRoot: string;
packageManifestPath: string;
}> {
const distRoot = resolve(moduleDirectory, '..');
const packageRoot = resolve(distRoot, '..');
return Object.freeze({
distRoot,
packageRoot,
packageManifestPath: resolve(packageRoot, 'package.json'),
});
}
function isInside(parent: string, candidate: string): boolean {
const pathFromParent = relative(parent, candidate);
return (
pathFromParent !== '' &&
pathFromParent !== '..' &&
!pathFromParent.startsWith(`..${sep}`) &&
!isAbsolute(pathFromParent)
);
}
function resolveInstalledTarget(
distRoot: string,
definition: QingLong3ClusterProductCommandDefinition,
): string {
const lexicalTarget = resolve(distRoot, definition.target);
if (!isInside(distRoot, lexicalTarget)) {
throw new Error('Cluster product command target escapes package dist root');
}
const targetStatus = lstatSync(lexicalTarget, { throwIfNoEntry: false });
if (
targetStatus === undefined ||
!targetStatus.isFile() ||
targetStatus.isSymbolicLink()
) {
throw new Error('Cluster product command target is unavailable');
}
const canonicalDistRoot = realpathSync(distRoot);
const canonicalTarget = realpathSync(lexicalTarget);
if (!isInside(canonicalDistRoot, canonicalTarget)) {
throw new Error(
'Cluster product command target escapes canonical package root',
);
}
return lexicalTarget;
}
export function loadQingLong3ClusterProductVersion(
moduleDirectory: string,
): string {
const { packageRoot, packageManifestPath } =
installationPaths(moduleDirectory);
const status = lstatSync(packageManifestPath, { throwIfNoEntry: false });
if (
status === undefined ||
!status.isFile() ||
status.isSymbolicLink() ||
status.size <= 0 ||
status.size > MAXIMUM_PACKAGE_MANIFEST_BYTES ||
realpathSync(packageManifestPath) !==
resolve(realpathSync(packageRoot), 'package.json')
) {
throw new Error('Cluster product package manifest is unavailable');
}
const manifest = JSON.parse(readFileSync(packageManifestPath, 'utf8')) as {
readonly name?: unknown;
readonly version?: unknown;
};
if (
manifest.name !== PACKAGE_NAME ||
typeof manifest.version !== 'string' ||
!SEMVER_PATTERN.test(manifest.version)
) {
throw new Error('Cluster product package identity is invalid');
}
return manifest.version;
}
export function qingLong3ClusterProductHelp(): string {
const longestName = Math.max(
...QINGLONG3_CLUSTER_PRODUCT_COMMANDS.map(({ name }) => name.length),
);
const commands = QINGLONG3_CLUSTER_PRODUCT_COMMANDS.map(
({ name, description }) => ` ${name.padEnd(longestName)} ${description}`,
).join('\n');
return [
'Usage: ql3-cluster-admin <command> [arguments]',
'',
'Remote client commands:',
commands,
'',
'Use `ql3-cluster-admin <command> --help` for command-specific usage.',
'Server, migration, recovery, executor and key-custody authorities remain isolated.',
].join('\n');
}
export function resolveQingLong3ClusterProductCommand(
argv: readonly string[],
moduleDirectory: string,
): QingLong3ClusterProductCommandResolution {
if (
argv.length === 0 ||
(argv.length === 1 &&
(argv[0] === '--help' || argv[0] === '-h' || argv[0] === 'help'))
) {
return Object.freeze({
kind: 'help',
output: qingLong3ClusterProductHelp(),
});
}
if (
argv.length === 1 &&
(argv[0] === '--version' || argv[0] === '-V' || argv[0] === 'version')
) {
return Object.freeze({
kind: 'version',
output: loadQingLong3ClusterProductVersion(moduleDirectory),
});
}
const command = QINGLONG3_CLUSTER_PRODUCT_COMMANDS.find(
(candidate) => candidate.name === argv[0],
);
if (command === undefined) {
return Object.freeze({
kind: 'invalid',
code: 'QL3_CLUSTER_PRODUCT_CLI_USAGE_INVALID',
message: 'unknown QingLong 3.0 Cluster product command',
});
}
const { distRoot } = installationPaths(moduleDirectory);
return Object.freeze({
kind: 'invoke',
command,
targetFilePath: resolveInstalledTarget(distRoot, command),
argv: Object.freeze(argv.slice(1)),
});
}
@@ -0,0 +1,219 @@
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const { EventEmitter } = require('node:events');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const packageRoot = path.resolve(__dirname, '..');
const moduleDirectory = path.join(packageRoot, 'dist', 'product-cli');
const cliPath = path.join(moduleDirectory, 'cli.js');
const manifest = JSON.parse(
fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'),
);
const {
QINGLONG3_CLUSTER_PRODUCT_COMMANDS,
loadQingLong3ClusterProductVersion,
qingLong3ClusterProductHelp,
resolveQingLong3ClusterProductCommand,
} = require('../dist/product-cli/productCommand.js');
const {
clusterProductSignalExitCode,
forwardClusterProductSignals,
} = require('../dist/product-cli/cli.js');
function runCli(args) {
return spawnSync(process.execPath, [cliPath, ...args], {
cwd: packageRoot,
encoding: 'utf8',
});
}
test('catalog exposes only reviewed remote clients from the same package', () => {
assert.equal(manifest.bin['ql3-cluster-admin'], 'dist/product-cli/cli.js');
assert.equal(QINGLONG3_CLUSTER_PRODUCT_COMMANDS.length, 7);
assert.equal(
new Set(QINGLONG3_CLUSTER_PRODUCT_COMMANDS.map(({ name }) => name)).size,
QINGLONG3_CLUSTER_PRODUCT_COMMANDS.length,
);
assert.equal(
new Set(QINGLONG3_CLUSTER_PRODUCT_COMMANDS.map(({ binary }) => binary))
.size,
QINGLONG3_CLUSTER_PRODUCT_COMMANDS.length,
);
for (const command of QINGLONG3_CLUSTER_PRODUCT_COMMANDS) {
assert.equal(manifest.bin[command.binary], `dist/${command.target}`);
assert.equal(
fs.lstatSync(path.join(packageRoot, 'dist', command.target)).isFile(),
true,
);
assert.equal(command.binary.includes('-client'), true);
}
for (const forbidden of [
'ql3-cluster-migrate',
'ql3-plugin-package-recover',
'ql3-plugin-package-manage',
'ql3-plugin-package-execute',
'ql3-worker-credential-manage',
'ql3-worker-credential-execute',
'ql3-prompt-output-key-rotate',
'ql3-prompt-output-gc',
]) {
assert.equal(
QINGLONG3_CLUSTER_PRODUCT_COMMANDS.some(
({ binary }) => binary === forbidden,
),
false,
);
}
});
test('help and version are bounded installation-derived product facts', () => {
const help = qingLong3ClusterProductHelp();
assert.match(help, /^Usage: ql3-cluster-admin <command> \[arguments\]/);
assert.match(help, /\n run\s+retry or stop Runs/);
assert.match(help, /Server, migration, recovery, executor and key-custody/);
assert.equal(help.includes('plugin-package-manage'), false);
assert.equal(
loadQingLong3ClusterProductVersion(moduleDirectory),
manifest.version,
);
assert.deepEqual(resolveQingLong3ClusterProductCommand([], moduleDirectory), {
kind: 'help',
output: help,
});
assert.deepEqual(
resolveQingLong3ClusterProductCommand(['--version'], moduleDirectory),
{ kind: 'version', output: manifest.version },
);
});
test('resolves only static remote-client targets and preserves opaque arguments', () => {
const argv = [
'run',
'--config=/private/client config.json',
'--literal=$() && *',
];
const result = resolveQingLong3ClusterProductCommand(argv, moduleDirectory);
assert.equal(result.kind, 'invoke');
assert.equal(result.command.binary, 'ql3-run-client');
assert.equal(
result.targetFilePath,
path.join(
packageRoot,
'dist',
'run-management',
'runManagementClientCli.js',
),
);
assert.deepEqual(result.argv, argv.slice(1));
assert.equal(Object.isFrozen(result.argv), true);
assert.equal(Object.isFrozen(result), true);
for (const candidate of [
'../../tmp/owned',
'/absolute/command',
'run/../../owned',
'run-manage',
'migrate',
'execute',
]) {
const rejected = resolveQingLong3ClusterProductCommand(
[candidate, '--help'],
moduleDirectory,
);
assert.equal(rejected.kind, 'invalid');
assert.equal(rejected.code, 'QL3_CLUSTER_PRODUCT_CLI_USAGE_INVALID');
}
});
test('rejects symlink targets and package manifests', (t) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-cluster-product-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const fakePackageRoot = path.join(root, 'package');
const fakeModuleDirectory = path.join(fakePackageRoot, 'dist', 'product-cli');
const targetDirectory = path.join(fakePackageRoot, 'dist', 'run-management');
fs.mkdirSync(fakeModuleDirectory, { recursive: true });
fs.mkdirSync(targetDirectory, { recursive: true });
const external = path.join(root, 'external.js');
fs.writeFileSync(external, 'process.exit(0);\n');
fs.symlinkSync(
external,
path.join(targetDirectory, 'runManagementClientCli.js'),
);
assert.throws(
() =>
resolveQingLong3ClusterProductCommand(
['run', '--help'],
fakeModuleDirectory,
),
/unavailable/,
);
fs.writeFileSync(
path.join(fakePackageRoot, 'package.real.json'),
JSON.stringify({ name: '@qinglong/cluster-admin', version: '3.0.0' }),
);
fs.symlinkSync(
path.join(fakePackageRoot, 'package.real.json'),
path.join(fakePackageRoot, 'package.json'),
);
assert.throws(
() => loadQingLong3ClusterProductVersion(fakeModuleDirectory),
/unavailable/,
);
});
test('binary exposes help/version and delegates without a shell', () => {
const help = runCli(['--help']);
assert.equal(help.status, 0);
assert.match(help.stdout, /^Usage: ql3-cluster-admin <command>/);
assert.equal(help.stderr, '');
const version = runCli(['--version']);
assert.equal(version.status, 0);
assert.equal(version.stdout.trim(), manifest.version);
assert.equal(version.stderr, '');
const delegatedHelp = runCli(['run', '--help']);
assert.equal(delegatedHelp.status, 0);
assert.match(delegatedHelp.stdout, /^Usage: ql3-run-client /);
assert.equal(delegatedHelp.stderr, '');
const rejected = runCli(['../../tmp/not-a-command']);
assert.equal(rejected.status, 64);
assert.equal(rejected.stdout, '');
const failure = JSON.parse(rejected.stderr);
assert.equal(failure.code, 'QL3_CLUSTER_PRODUCT_CLI_USAGE_INVALID');
assert.equal(JSON.stringify(failure).includes('/tmp'), false);
});
test('forwards only bounded signals and removes handlers', () => {
const signalHost = new EventEmitter();
const received = [];
const child = {
exitCode: null,
signalCode: null,
kill(signal) {
received.push(signal);
return true;
},
};
const remove = forwardClusterProductSignals(child, signalHost);
signalHost.emit('SIGINT');
signalHost.emit('SIGTERM');
signalHost.emit('SIGHUP');
assert.deepEqual(received, ['SIGINT', 'SIGTERM', 'SIGHUP']);
assert.equal(clusterProductSignalExitCode('SIGINT'), 130);
assert.equal(clusterProductSignalExitCode('SIGTERM'), 143);
remove();
signalHost.emit('SIGTERM');
assert.deepEqual(received, ['SIGINT', 'SIGTERM', 'SIGHUP']);
child.exitCode = 0;
const removeTerminal = forwardClusterProductSignals(child, signalHost);
signalHost.emit('SIGTERM');
removeTerminal();
assert.deepEqual(received, ['SIGINT', 'SIGTERM', 'SIGHUP']);
});
@@ -0,0 +1,151 @@
'use strict';
const { execFileSync } = require('node:child_process');
const IMAGE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/:@-]{0,255}$/u;
const ENTRYPOINT = [
'node',
'/opt/qinglong/node_modules/@qinglong/cluster-admin/dist/product-cli/cli.js',
];
const COMMANDS = Object.freeze([
Object.freeze({
name: 'package',
usage: 'Usage: ql3-plugin-package-client ',
}),
Object.freeze({
name: 'package-kubernetes',
usage: 'Usage: ql3-plugin-package-client-kubernetes ',
}),
Object.freeze({
name: 'worker-credential',
usage: 'Usage: ql3-worker-credential-client ',
}),
Object.freeze({ name: 'approval', usage: 'Usage: ql3-approval-client ' }),
Object.freeze({ name: 'run', usage: 'Usage: ql3-run-client ' }),
Object.freeze({ name: 'automation', usage: 'Usage: ql3-automation-client ' }),
Object.freeze({
name: 'model-credential',
usage: 'Usage: ql3-provider-credential-client ',
}),
]);
function fail(message) {
throw new Error(`ql3 Cluster Admin product live contract failed: ${message}`);
}
function parseArguments(argv) {
if (argv.length !== 1) fail('exactly one --image argument is required');
const match = /^--image=(.+)$/u.exec(argv[0]);
if (!match || !IMAGE_PATTERN.test(match[1]))
fail('image argument is invalid');
return match[1];
}
function docker(args, options = {}) {
return execFileSync('docker', args, {
encoding: 'utf8',
maxBuffer: 1024 * 1024,
stdio: ['ignore', 'pipe', 'pipe'],
...options,
});
}
function runImage(image, args) {
return docker([
'run',
'--rm',
'--read-only',
'--network',
'none',
'--cap-drop',
'ALL',
'--security-opt',
'no-new-privileges',
'--user',
'10001:10001',
'--pids-limit',
'32',
'--memory',
'128m',
'--cpus',
'0.25',
'--tmpfs',
'/tmp:rw,noexec,nosuid,nodev,size=8m,mode=700',
image,
...args,
]);
}
function main() {
if (process.env.QL3_CLUSTER_ADMIN_PRODUCT_LIVE !== '1') {
fail('QL3_CLUSTER_ADMIN_PRODUCT_LIVE=1 is required');
}
const image = parseArguments(process.argv.slice(2));
const inspected = JSON.parse(docker(['image', 'inspect', image]));
if (!Array.isArray(inspected) || inspected.length !== 1) {
fail('image inspection shape is invalid');
}
const fact = inspected[0];
if (
fact?.Os !== 'linux' ||
(fact?.Architecture !== 'amd64' && fact?.Architecture !== 'arm64') ||
fact?.Config?.User !== '10001:10001' ||
JSON.stringify(fact?.Config?.Entrypoint) !== JSON.stringify(ENTRYPOINT) ||
!Number.isSafeInteger(fact?.Size) ||
fact.Size <= 0
) {
fail('image platform, identity, entrypoint or size contract drifted');
}
const help = runImage(image, ['--help']);
if (
!help.startsWith('Usage: ql3-cluster-admin <command> [arguments]\n') ||
!help.includes(
'Server, migration, recovery, executor and key-custody authorities remain isolated.',
)
) {
fail('product help contract drifted');
}
for (const { name, usage } of COMMANDS) {
const output = runImage(image, [name, '--help']);
if (!output.startsWith(usage)) {
fail(`${name} delegation contract drifted`);
}
}
const version = runImage(image, ['--version']).trim();
if (version !== '3.0.0-alpha.0') fail('product version contract drifted');
process.stdout.write(
`${JSON.stringify({
schemaVersion: 1,
image,
architecture: fact.Architecture,
user: fact.Config.User,
imageBytes: fact.Size,
commandCount: COMMANDS.length,
isolation: Object.freeze({
readOnlyRoot: true,
network: 'none',
capabilities: 'none',
noNewPrivileges: true,
pids: 32,
memoryBytes: 128 * 1024 * 1024,
cpus: 0.25,
}),
compatible: true,
})}\n`,
);
}
if (require.main === module) {
try {
main();
} catch (error) {
process.stderr.write(
`${error instanceof Error ? error.message : 'unknown failure'}\n`,
);
process.exitCode = 1;
}
}
module.exports = { parseArguments };
+72 -2
View File
@@ -68,6 +68,68 @@ function yamlDocuments(readFile, filePath) {
return parsed;
}
function kubernetesYamlFiles(directory) {
return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const filePath = path.join(directory, entry.name);
if (entry.isDirectory()) return kubernetesYamlFiles(filePath);
return entry.isFile() && /\.ya?ml$/u.test(entry.name) ? [filePath] : [];
});
}
function podSpecFor(document) {
if (document?.kind === 'CronJob') {
return document.spec?.jobTemplate?.spec?.template?.spec;
}
if (
['DaemonSet', 'Deployment', 'Job', 'StatefulSet'].includes(document?.kind)
) {
return document.spec?.template?.spec;
}
return undefined;
}
function assertClusterAdminImageCommands(readFile, root, findings) {
const kubernetesRoot = path.join(root, 'deploy/kubernetes/ql3-cluster');
let references = 0;
for (const filePath of kubernetesYamlFiles(kubernetesRoot)) {
for (const document of yamlDocuments(readFile, filePath)) {
const podSpec = podSpecFor(document);
for (const section of ['initContainers', 'containers']) {
for (const container of podSpec?.[section] ?? []) {
if (container?.image !== 'qinglong3-cluster-admin:3.0.0-alpha.0') {
continue;
}
references += 1;
if (
!Array.isArray(container.command) ||
container.command.length === 0
) {
findings.push(
finding(
'QL3_CLUSTER_ADMIN_IMAGE_COMMAND_IMPLICIT',
`${path.relative(root, filePath)} ${document.kind}/${
document.metadata?.name ?? 'unnamed'
} ${section}/${
container.name ?? 'unnamed'
} must explicitly override the Cluster Admin image command`,
),
);
}
}
}
}
}
if (references === 0) {
findings.push(
finding(
'QL3_CLUSTER_ADMIN_IMAGE_REFERENCE_MISSING',
'The Kubernetes deployment must contain reviewed Cluster Admin image references',
),
);
}
return references;
}
function namedResource(resources, kind, name) {
return resources.find(
(resource) => resource?.kind === kind && resource?.metadata?.name === name,
@@ -294,6 +356,7 @@ function assertExactExternalClosure(readFile, root, findings) {
path.join(root, 'packages/ql3-cluster-admin/package.json'),
);
if (
adminManifest.bin?.['ql3-cluster-admin'] !== 'dist/product-cli/cli.js' ||
adminManifest.bin?.['ql3-plugin-package-recover'] !==
'dist/plugin-package/recovery/pluginPackageRecoveryCli.js' ||
adminManifest.bin?.['ql3-plugin-package-manage'] !==
@@ -335,7 +398,7 @@ function assertExactExternalClosure(readFile, root, findings) {
findings.push(
finding(
'QL3_CLUSTER_PLUGIN_RECOVERY_ENTRYPOINT_MISSING',
'cluster-admin must publish the reviewed Package and Worker management and executor entrypoints',
'cluster-admin must publish the reviewed product facade, Package and Worker management and executor entrypoints',
),
);
}
@@ -465,7 +528,7 @@ function assertDockerfile(readFile, root, findings) {
'/workspace/packages/ql3-cluster-postgres/dist',
'/workspace/packages/ql3-cluster-admin/dist',
'USER 10001:10001',
'ENTRYPOINT ["node", "/opt/qinglong/node_modules/@qinglong/cluster-admin/dist/plugin-package/recovery/pluginPackageRecoveryCli.js"]',
'ENTRYPOINT ["node", "/opt/qinglong/node_modules/@qinglong/cluster-admin/dist/product-cli/cli.js"]',
];
for (const value of adminRequired) {
if (!adminDockerfile.includes(value)) {
@@ -5014,10 +5077,16 @@ function auditClusterDeployment(options = {}) {
const root = path.resolve(options.root ?? path.join(__dirname, '..'));
const readFile = options.readFile ?? fs.readFileSync;
const findings = [];
let clusterAdminImageReferences = 0;
try {
assertExactExternalClosure(readFile, root, findings);
assertDockerfile(readFile, root, findings);
assertKubernetes(readFile, root, findings);
clusterAdminImageReferences = assertClusterAdminImageCommands(
readFile,
root,
findings,
);
assertClusterAiComponent(readFile, root, findings);
assertPluginPackageManagementDeployment(readFile, root, findings);
assertWorkerCredentialManagementDeployment(readFile, root, findings);
@@ -5071,6 +5140,7 @@ function auditClusterDeployment(options = {}) {
clusterAi: 'optional-projected-authority',
clusterAiPromptOutput: 'optional-read-only-projected-keyring',
imageReleasePins: 'independent-fail-closed-digests',
clusterAdminImageReferences,
findings: Object.freeze(findings),
compatible: findings.length === 0,
});
@@ -274,6 +274,11 @@ function auditClusterImageCiWorkflow(source) {
/unexpected image contract/,
'cluster image CI must verify architecture and runtime user',
);
requirePattern(
source,
/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',
);
requirePattern(
source,
/^ image-oci:\s*$/m,
@@ -314,6 +319,7 @@ function auditClusterImageCiWorkflow(source) {
images: ['control', 'control-ai', 'admin', 'local'],
nativeArchitectures: ['amd64', 'arm64'],
runtimeInventory: true,
clusterAdminProductFacade: true,
ociAttestations: true,
osVulnerabilityScan: {
scanner: 'trivy@0.70.0',
@@ -0,0 +1,49 @@
'use strict';
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const path = require('node:path');
const { test } = require('node:test');
const {
parseArguments,
} = require('../../scripts/ql3-cluster-admin-product-live-contract.cjs');
const script = path.resolve(
__dirname,
'../../scripts/ql3-cluster-admin-product-live-contract.cjs',
);
test('accepts one bounded image reference', () => {
assert.equal(
parseArguments(['--image=registry.example.com/qinglong/admin@sha256:abc']),
'registry.example.com/qinglong/admin@sha256:abc',
);
});
test('rejects missing, duplicate, unknown, empty or unsafe image arguments', () => {
for (const argv of [
[],
['--image=one', '--image=two'],
['--profile=admin'],
['--image='],
['--image=admin image'],
[`--image=${'a'.repeat(257)}`],
]) {
assert.throws(() => parseArguments(argv), /live contract failed/);
}
});
test('fails closed before Docker without explicit opt-in', () => {
const result = spawnSync(
process.execPath,
[script, '--image=missing:latest'],
{
encoding: 'utf8',
env: { ...process.env, QL3_CLUSTER_ADMIN_PRODUCT_LIVE: '0' },
},
);
assert.equal(result.status, 1);
assert.equal(result.stdout, '');
assert.match(result.stderr, /QL3_CLUSTER_ADMIN_PRODUCT_LIVE=1 is required/);
assert.equal(result.stderr.includes('spawn'), false);
});
@@ -29,6 +29,7 @@ test('accepts the exact locked non-root multi-replica cluster deployment', () =>
'optional-read-only-projected-keyring',
);
assert.equal(report.promptOutputKeyRotation, 'caller-driven-staged-material');
assert.equal(report.clusterAdminImageReferences, 24);
assert.deepEqual(report.workspacePackages, [
'@qinglong/runtime-core',
'@qinglong/cluster-postgres',
@@ -41,6 +42,65 @@ test('accepts the exact locked non-root multi-replica cluster deployment', () =>
]);
});
test('requires every Cluster Admin Kubernetes workload to override the image command', () => {
const report = auditClusterDeployment({
root: ROOT,
readFile: intercept(
'deploy/kubernetes/ql3-cluster/operations/approval-management/base/deployment.yaml',
(source) =>
source.replace(
' command:\n - node\n - /opt/qinglong/node_modules/@qinglong/cluster-admin/dist/approval-management/approvalManagementCli.js\n',
'',
),
),
});
assert.equal(report.compatible, false);
assert.equal(report.clusterAdminImageReferences, 24);
assert.equal(
report.findings.some(
({ code }) => code === 'QL3_CLUSTER_ADMIN_IMAGE_COMMAND_IMPLICIT',
),
true,
);
});
test('requires the bounded Cluster product facade and image entrypoint', () => {
const missingBinary = auditClusterDeployment({
root: ROOT,
readFile: intercept('packages/ql3-cluster-admin/package.json', (source) => {
const manifest = JSON.parse(source);
delete manifest.bin['ql3-cluster-admin'];
return JSON.stringify(manifest);
}),
});
assert.equal(missingBinary.compatible, false);
assert.equal(
missingBinary.findings.some(
({ code }) => code === 'QL3_CLUSTER_PLUGIN_RECOVERY_ENTRYPOINT_MISSING',
),
true,
);
const legacyEntrypoint = auditClusterDeployment({
root: ROOT,
readFile: intercept(
'deploy/containers/ql3-cluster-admin/Dockerfile',
(source) =>
source.replace(
'dist/product-cli/cli.js',
'dist/plugin-package/recovery/pluginPackageRecoveryCli.js',
),
),
});
assert.equal(legacyEntrypoint.compatible, false);
assert.equal(
legacyEntrypoint.findings.some(
({ code }) => code === 'QL3_CLUSTER_ADMIN_DOCKERFILE_CONTRACT_MISSING',
),
true,
);
});
test('keeps Cluster AI optional with projected authority and an independent digest', () => {
const defaultEnabled = auditClusterDeployment({
root: ROOT,
@@ -26,6 +26,7 @@ test('accepts the reviewed native CI and digest release contracts', () => {
images: ['control', 'control-ai', 'admin', 'local'],
nativeArchitectures: ['amd64', 'arm64'],
runtimeInventory: true,
clusterAdminProductFacade: true,
ociAttestations: true,
osVulnerabilityScan: {
scanner: 'trivy@0.70.0',
@@ -98,6 +99,17 @@ test('rejects removal of the native arm64 image gate', () => {
);
});
test('rejects removal of the native Cluster Admin product facade gate', () => {
const mutated = ciSource.replace(
"QL3_CLUSTER_ADMIN_PRODUCT_LIVE: '1'",
"QL3_CLUSTER_ADMIN_PRODUCT_LIVE: '0'",
);
assert.throws(
() => auditClusterImageCiWorkflow(mutated),
/bounded product facade contract/,
);
});
test('rejects removal of the native cluster-admin image gate', () => {
const mutated = ciSource.replace(
'image_arch: arm64\n image: admin',
+2 -2
View File
@@ -340,10 +340,10 @@ test('current QL3 workspace has exactly eighteen reviewed package boundaries', (
rootSourceFileRoles: clusterAdmin.rootSourceFileRoles,
},
{
sourceFiles: 94,
sourceFiles: 96,
rootSourceFiles: 1,
rootSourceLines: 61,
nestedSourceFiles: 93,
nestedSourceFiles: 95,
rootSourceFileRoles: {
'modelInvocationMigrationCli.ts': 'binary_entry',
},