feat(ql3): observe package installations in console

This commit is contained in:
whyour
2026-08-20 14:42:54 +08:00
parent 344680d64a
commit ffa4b4e7cb
29 changed files with 1259 additions and 36 deletions
+31 -2
View File
@@ -125,6 +125,15 @@ and private key, and issue a short-lived strong User assertion with only
the D-374 canonical `/api/v3/workers/management`; the Console cannot accept the
legacy credential-management path or a credential mutation command file.
To enable Plugin Package installation observation, copy
`package-management-client-config.example.json` to
`package-management-client.json`, install its CA, and issue a short-lived
strong User assertion with only `package.manage` into
`package-management-assertion.jwt`. Its endpoint is fixed to the canonical
`/api/v3/plugin-packages/management`. This authority is independent of the
Project, Run and Worker files; the Console exposes no Package command file or
lifecycle mutation.
Create an independent 256-bit browser session key without placing its value in
argv or an environment variable:
@@ -143,6 +152,9 @@ Apply the same rule to `worker-management-client.json`,
`worker-management-ca.pem`, `worker-management-client.crt`,
`worker-management-client.key` and `worker-management-assertion.jwt` when
Worker observation is enabled.
Apply the same rule to `package-management-client.json`,
`package-management-ca.pem` and `package-management-assertion.jwt` when Package
observation is enabled.
Every file must be a current-owner, non-symlink, canonical regular file. The
session file contains exactly 43 base64url characters and no newline. It is a
@@ -176,6 +188,13 @@ Worker observation uses its own pair:
--worker-management-assertion /absolute/private/ql3-copilot-console/worker-management-assertion.jwt
```
Package observation also uses an independent pair:
```sh
--package-management-config /absolute/private/ql3-copilot-console/package-management-client.json \
--package-management-assertion /absolute/private/ql3-copilot-console/package-management-assertion.jwt
```
It validates every configured private authority and performs one unauthenticated
TLS 1.3 `GET /readyz`. It does not open the Console listener or reveal paths,
endpoint, credential, Project or Cluster identity.
@@ -222,6 +241,14 @@ read; each listed Worker can be inspected only by another explicit click. The
projection contains bounded lifecycle, compatibility, architecture, protocol
and capacity facts, but no credential, raw capability, label or Secret.
Explicit Package management authority adds `package_list|package_inspect`, for
a maximum vocabulary of twenty operations when every optional authority is
enabled. The list is fixed at 16 installations with click-only pagination and
click-only inspection. The product projection includes Package version,
installation state, availability and bounded recovery codes, but omits
installation IDs, locks, record digests, transport identity and every Package
mutation. Package authority remains disabled by default.
## Export a redacted evidence bundle
After at least one successful read, **Export redacted bundle** creates one
@@ -276,8 +303,10 @@ read-only private mount; all certificate paths in the config must point into
that mount. `disabled` is the only default and unknown values fail closed.
Worker observation follows the independent
`QL3_COPILOT_CONSOLE_WORKER_MANAGEMENT=enabled` switch and reads only its
Worker config/assertion pair. Enabling one management authority does not enable
the other.
Worker config/assertion pair. Package observation follows
`QL3_COPILOT_CONSOLE_PACKAGE_MANAGEMENT=enabled` and reads only its Package
config/assertion pair. Enabling one management authority does not enable either
of the others.
| Resource class | Memory | CPU | PIDs | Console reads |
| --- | ---: | ---: | ---: | ---: |
@@ -26,6 +26,7 @@ port=${QL3_COPILOT_CONSOLE_PORT-}
resource_class=${QL3_COPILOT_CONSOLE_RESOURCE_CLASS-compact}
run_management=${QL3_COPILOT_CONSOLE_RUN_MANAGEMENT-disabled}
worker_management=${QL3_COPILOT_CONSOLE_WORKER_MANAGEMENT-disabled}
package_management=${QL3_COPILOT_CONSOLE_PACKAGE_MANAGEMENT-disabled}
printf '%s' "$image" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9._/-]{0,191}@sha256:[0-9a-f]{64}$' || fail
printf '%s' "$network" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$' || fail
@@ -67,6 +68,10 @@ case "$worker_management" in
disabled|enabled) ;;
*) fail ;;
esac
case "$package_management" in
disabled|enabled) ;;
*) fail ;;
esac
set -- docker run --rm --pull never --init --read-only \
--network "$network" \
@@ -103,6 +108,12 @@ if [ "$worker_management" = enabled ]; then
--worker-management-assertion /var/run/secrets/qinglong3/copilot-console/worker-management-assertion.jwt
fi
if [ "$package_management" = enabled ]; then
set -- "$@" \
--package-management-config /var/run/secrets/qinglong3/copilot-console/package-management-client.json \
--package-management-assertion /var/run/secrets/qinglong3/copilot-console/package-management-assertion.jwt
fi
if [ "$mode" = check ]; then
set -- "$@" --check
fi
@@ -5,5 +5,6 @@
"QL3_COPILOT_CONSOLE_PORT": "5701",
"QL3_COPILOT_CONSOLE_RESOURCE_CLASS": "compact",
"QL3_COPILOT_CONSOLE_RUN_MANAGEMENT": "disabled",
"QL3_COPILOT_CONSOLE_WORKER_MANAGEMENT": "disabled"
"QL3_COPILOT_CONSOLE_WORKER_MANAGEMENT": "disabled",
"QL3_COPILOT_CONSOLE_PACKAGE_MANAGEMENT": "disabled"
}
@@ -0,0 +1,7 @@
{
"schemaVersion": 1,
"endpoint": "https://replace-cluster-api.example.com:8443/api/v3/plugin-packages/management",
"servername": "replace-cluster-api.example.com",
"caFile": "/absolute/private/ql3-copilot-console/package-management-ca.pem",
"requestTimeoutMs": 5000
}
@@ -89,6 +89,8 @@ COPY --chmod=0444 deploy/console/ql3-cluster-copilot/run-management-client-confi
share/ql3-copilot-console/run-management-client-config.example.json
COPY --chmod=0444 deploy/console/ql3-cluster-copilot/worker-management-client-config.example.json \
share/ql3-copilot-console/worker-management-client-config.example.json
COPY --chmod=0444 deploy/console/ql3-cluster-copilot/package-management-client-config.example.json \
share/ql3-copilot-console/package-management-client-config.example.json
COPY --chmod=0444 deploy/console/ql3-cluster-copilot/host-environment.example.json \
share/ql3-copilot-console/host-environment.example.json
+18
View File
@@ -11,6 +11,24 @@
最新增量证据(2026-08-20):
- D-376/ADR-0469(已接受):在既有 operator-workstation、loopback-only Copilot Console 内增加可选 Plugin Package installation
观察,而不新增 workspace package、服务、端口或集群工作负载。Browser/BFF 只增加固定 `package_list|package_inspect`
`/api/v1/package-management/installations|installation`;上游复用 canonical `/api/v3/plugin-packages/management` client,独立
`--package-management-config|assertion` 必须成对提供,launcher 仅在
`QL3_COPILOT_CONSOLE_PACKAGE_MANAGEMENT=enabled` 时启用,默认报告 `packageManagementAuthority=disabled`。list 固定 16 项、只接受
nullable `afterPackageName` 并由用户点击翻页;inspect 只读取用户选中的 canonical Package。没有 propose、decide、install、reinstall、
upgrade、rollback、disable、uninstall、caller limit/filter、自动翻页、poller、retry、queue、cache、watcher、WebSocket/SSE 或后台 timer。
产品投影只保留 Package/version、install operation/state、target generation、availability、recovery/failure/quarantine code 与时间/version
丢弃 installation/lock/record digest 和 transport request identity;浏览器证据继续使用 bundle-local typed alias。实现只在现有
`@qinglong/cluster-admin``plugin-package/management``copilot-console` 内聚目录扩展,workspace 仍为 18 packages、没有新增依赖,
`singleSourcePackages=[]``shallowSourcePackages=[]`。专项回归 `56/56`Cluster Admin 全量
`438 total / 435 pass / 3 conditional skip / 0 fail`legacy backend 当前工作树全量与 18-package clean build/逐包测试均单次退出 0。
package boundary、Cluster dependency、Edge import、Cluster/Worker deployment、Console 与 Console distribution 七项审计全部
compatible/passedCluster Admin 为 `129 source / 128 nested`。14 档 Local artifact audit 全部 compatible,基础 Edge/Standalone 仍为
`2,598,669 / 2,598,747` bytes、57 loaded modulesApplication+AI 为 `4,501,822 / 4,501,954` bytesMCP 为
`7,324,601 / 7,324,709` bytes,证明 Package Console authority 未进入低配路由设备闭包。本切片不改变 schema、ACL、repository、role、
Pool、连接或 failover 语义,因此不重跑且不重新占有 PostgreSQL HA 证明;D-373/D-374 PostgreSQL 18.6 arm64 HA `146/146`、timeline
`1→2` 仅作为相邻既有基线,后续数据库语义变化必须重跑。
- D-375/ADR-0468(已接受):在既有 operator-workstation、loopback-only Copilot Console 内接入 D-374 通用 Worker
management 的两个只读产品操作,而不是新增包、服务或集群工作负载。Browser/BFF 只新增固定
`worker_list|worker_inspect``/api/v1/worker-management/workers|worker`,上游只走 canonical
@@ -0,0 +1,77 @@
# ADR-0469Copilot Console 显式可选 Package Installation 只读观察
- 状态:Accepted
- 日期:2026-08-20
- 关联 RFCQL-RFC-0001 D-376、D-14、D-16、D-107
- 关联 ADRADR-0142、ADR-0191、ADR-0462、ADR-0468
## 上下文
Cluster Plugin Package management 已提供经过认证的 canonical HTTPS/client 边界和有界 installation inventory。运维人员需要在现有
Copilot Console 中把 Run、Worker 与 Package 安装状态放在同一个只读现场账本中观察,但浏览器不能持有 Package assertion、选择上游
command/path,也不能把管理生命周期变成持续轮询或默认常驻能力。
新建 Package Console 服务或 workspace package 会复制 session、TLS、镜像、分发和资源生命周期;把完整 Package management command
file 暴露给 Console 又会把 propose、decide、install、upgrade、rollback 和 lifecycle mutation 带入只读诊断面。低配路由设备也不应因
Cluster 工作站能力增加任何 importer、常驻内存或连接成本。
## 决策
1. 在既有 `@qinglong/cluster-admin/copilot-console` 增加 `package_list``package_inspect`,分别只接受固定 BFF route
`/api/v1/package-management/installations``/api/v1/package-management/installation`。浏览器不能提供上游 path、method、command
file 或 caller limit。
2. Package authority 使用独立 `--package-management-config``--package-management-assertion`;二者必须成对存在,config 必须指向
canonical `/api/v3/plugin-packages/management`。它不复用 Project、Run 或 Worker credential/assertion。
3. 未提供该 authority 时原有 Console 行为不变并报告 `packageManagementAuthority=disabled`。宿主 launcher 只有在
`QL3_COPILOT_CONSOLE_PACKAGE_MANAGEMENT=enabled` 时添加这对 owner-private 文件;三个可选 management authority 互不隐式启用。
4. `package_list` 固定最多 16 项,只接受 nullable `afterPackageName`,下一页必须由用户点击。`package_inspect` 只读取用户明确选择的
canonical Package。禁止自动翻页、批量 inspect、poller、retry、queue、cache、watcher、WebSocket/SSE 和后台 timer。
5. BFF 复用现有严格 Plugin Package management client validator,并增加 in-memory one-shot command 入口;一次点击只产生一次上游 POST。
caller request ID 只作为 inspection identitymanagement transport request ID 不返回浏览器。
6. 产品投影只包含 Package name/version、install operation/state、target generation、availability、recovery/failure/quarantine code、record
version 和时间。installation ID、active/previous lock digest、record digest、assertion、authentication 与 transport identity 均不进入产品响应。
7. 浏览器证据包对 Project、Package、request 与 digest 使用 bundle-local typed alias,只保留固定枚举、数字、布尔和容器字段;未知字段与
free text 继续删除,且不声明 server signature、durable audit 或 action authority。
8. 本切片不新增 workspace package、external dependency、binary、监听端口、Kubernetes workload、Ingress、数据库 schema/role/Pool 或
持久状态。实现留在现有 `plugin-package/management``copilot-console` 内聚目录;Edge/Standalone 不导入 Cluster Admin。
## 被拒绝的替代方案
### 新建 Package Console package 或服务
拒绝。两个 caller-driven 只读操作没有独立部署、版本或资源生命周期,不足以承担新的 package/daemon。复用现有工作站 Console 可保持包数、
镜像和 session 边界稳定。
### 将完整 Package command file 暴露给浏览器
拒绝。现有 command vocabulary 同时包含高风险 mutation。固定 list/inspect command builder 和固定 BFF route 才能从结构上证明只读,而不是
依赖 UI 隐藏按钮。
### 默认启用 Package authority
拒绝。Project observation 与 `package.manage` 是不同权限域。默认 disabled、成对私有文件和独立 launcher switch 让未启用部署不读取 assertion
也不打开 Package connection。
### 自动刷新或自动遍历全部 installations
拒绝。持续 inventory 会隐藏数据库和网络负载,在小型管理节点上尤其不合适。固定 16 项与点击翻页使每次 authority use 都可见、可限界。
## 升级与回滚
- 旧启动方式不传 Package 参数时行为不变。启用者先安装 canonical config、CA 与短期 assertion,再显式打开 launcher switch。
- 回滚到 ADR-0468 只移除 Package tab、两条 BFF route 和可选参数;canonical manager/client、installation repository 与数据均不变化,无迁移。
- 未来若加入 install/upgrade/rollback、跨 Project inventory、历史指标或实时流,必须另立 mutation authority、审计、配额、retention 和资源预算
ADR,不能在本只读 BFF 上渐进扩大。
## 验证与证据
- Console/CLI/product/evidence/launcher 专项回归 `56/56`,覆盖 exact route、固定 16 项、click-only cursor、canonical TLS 1.3 request、独立
authority 默认关闭、transport/durable identity 隔离、证据脱敏和 mutation/remote-listener/ambient-authority 拒绝。
- `@qinglong/cluster-admin` 全量 `438 total / 435 pass / 3 conditional skip / 0 fail`legacy backend 当前工作树全量与 18-package clean
build/逐包测试均单次退出 0。
- package boundary、Cluster dependency、Edge import、Cluster/Worker deployment、Console 与 distribution 七项审计全部 compatible/passed。
workspace 保持 18 packages、`singleSourcePackages=[]``shallowSourcePackages=[]`Cluster Admin 为 `129 source / 128 nested`,无新增依赖。
- 14 档 Local artifact audit 全部 compatible。基础 Edge/Standalone 为 `2,598,669 / 2,598,747` bytes、57 loaded modules
Application+AI 为 `4,501,822 / 4,501,954` bytesMCP 为 `7,324,601 / 7,324,709` bytes。Package Console authority 未进入低配制品。
- 本切片没有 PostgreSQL schema、ACL、repository、role、Pool、连接或 failover 变化,不重跑也不重新占有物理 HA 证明;仅引用 D-373/D-374
PostgreSQL 18.6 arm64 HA `146/146`、timeline `1→2` 相邻基线。数据库语义变化时必须重跑。
+1
View File
@@ -472,6 +472,7 @@
| [ADR-0466](./ADR-0466-bounded-worker-session-compatibility-observation.md) | 有界 Worker Session 兼容性观察 | Accepted |
| [ADR-0467](./ADR-0467-generic-worker-management-product-entry.md) | 通用 Worker Management 产品入口与兼容路径 | Accepted |
| [ADR-0468](./ADR-0468-optional-console-worker-observation.md) | 可选 Console Worker 只读观察 | Accepted |
| [ADR-0469](./ADR-0469-optional-console-package-installation-observation.md) | 可选 Console Package Installation 只读观察 | Accepted |
## 规则
+4 -2
View File
@@ -8,8 +8,10 @@ Console is a loopback-only read BFF serving digest-bound static assets. Neither
opens database or Kubernetes authority, enters the legacy 2.x Web application,
or resides in `cluster-control`.
The Console accepts thirteen exact Run, Task, Workflow and Copilot reads. The
browser cannot provide an upstream path or HTTP method, and every list page and
The Console accepts thirteen default Run, Task, Workflow and Copilot reads,
plus independently enabled Run cancellation, Worker and Plugin Package
installation observations, for at most twenty exact operations. The browser
cannot provide an upstream path or HTTP method, and every list page and
detail/evidence read requires an explicit click. Its Cluster API credential
stays in a canonical owner-private file and is reread for each upstream
request; browser JavaScript receives only a separate session token which cannot
@@ -11,6 +11,8 @@
run_cancellation_inspect: '/api/v1/run-management/cancellation-inspect',
worker_list: '/api/v1/worker-management/workers',
worker_inspect: '/api/v1/worker-management/worker',
package_list: '/api/v1/package-management/installations',
package_inspect: '/api/v1/package-management/installation',
run_list: '/api/v1/observe/run-list',
run_read: '/api/v1/observe/run',
run_event_list: '/api/v1/observe/run-events',
@@ -31,6 +33,8 @@
run_cancellation_inspect: '取消诊断',
worker_list: 'Worker 目录',
worker_inspect: 'Worker 详情',
package_list: 'Package 安装目录',
package_inspect: 'Package 安装详情',
run_list: 'Run 目录',
run_read: 'Run 详情',
run_event_list: 'Run Events',
@@ -131,6 +135,10 @@
result.afterWorkerId = null;
} else if (operation === 'worker_inspect') {
result.workerId = value('worker-id');
} else if (operation === 'package_list') {
result.afterPackageName = null;
} else if (operation === 'package_inspect') {
result.packageName = value('installation-package-name');
} else if (operation === 'run_list') {
result.afterCreatedAtMs = null;
result.afterRunId = null;
@@ -188,6 +196,11 @@
typeof fact.nextAfterWorkerId === 'string'
) {
next.afterWorkerId = fact.nextAfterWorkerId;
} else if (
operation === 'package_list' &&
typeof fact.nextAfterPackageName === 'string'
) {
next.afterPackageName = fact.nextAfterPackageName;
} else if (operation === 'run_list' && fact.hasMore === true && fact.next) {
next.afterCreatedAtMs = fact.next.createdAtMs;
next.afterRunId = fact.next.runId;
@@ -260,6 +273,23 @@
});
return;
}
if (operation === 'package_list' && Array.isArray(fact.installations)) {
fact.installations.forEach(function (installation) {
if (!installation || typeof installation.packageName !== 'string') {
return;
}
const button = document.createElement('button');
button.type = 'button';
button.textContent = '显式检查 ' + installation.packageName;
button.addEventListener('click', function () {
document.getElementById('installation-package-name').value =
installation.packageName;
void execute('package_inspect');
});
entry.append(button);
});
return;
}
if (
operation !== 'run_cancellation_blocked_list' ||
!Array.isArray(fact.items)
@@ -33,6 +33,8 @@
'run_cancellation_inspect',
'worker_list',
'worker_inspect',
'package_list',
'package_inspect',
'run_list',
'run_read',
'run_event_list',
@@ -54,6 +56,8 @@
run_cancellation_inspect: ['projectId', 'requestId', 'runId'],
worker_list: ['afterWorkerId', 'projectId', 'requestId'],
worker_inspect: ['projectId', 'requestId', 'workerId'],
package_list: ['afterPackageName', 'projectId', 'requestId'],
package_inspect: ['packageName', 'projectId', 'requestId'],
run_list: [
'afterCreatedAtMs',
'afterRunId',
@@ -122,6 +126,7 @@
afterStepRunId: 'step',
afterTaskId: 'task',
afterWorkerId: 'worker',
afterPackageName: 'package',
artifactId: 'artifact',
attemptId: 'attempt',
contentDigest: 'digest',
@@ -131,6 +136,7 @@
id: 'identifier',
modelId: 'model',
nextAfterWorkerId: 'worker',
nextAfterPackageName: 'package',
outputRef: 'artifact',
packageName: 'package',
projectId: 'project',
@@ -170,6 +176,8 @@
'runtimes',
'worker',
'workers',
'installation',
'installations',
'usage',
'workflow',
'workflows',
@@ -207,6 +215,12 @@
'status',
'supportTier',
'operatingSystem',
'availability',
'failureReason',
'installOperation',
'quarantineReason',
'recoveryAction',
'state',
]);
const safeEnumValues = new Set([
'accepted',
@@ -302,13 +316,31 @@
'ok',
'unavailable',
'workflow',
'not_active',
'install',
'reinstall',
'upgrade',
'rollback',
'resume_stage',
'resume_activation',
'inspect_activation',
'source_unavailable',
'source_mismatch',
'stage_failed',
'activation_failed',
'activation_fact_conflict',
'approval_expired',
'policy_fence_changed',
'resource_exhausted',
'suspected_key_compromise',
'confirmed_key_compromise',
]);
const sensitiveKey =
/credential|token|authorization|secret|session|password|cookie|private|keyring/iu;
const freeTextKey =
/text|content|stdout|stderr|command|input|output|environment|reason|error|message|description|name|path|url|uri|host|endpoint/iu;
const numericKey =
/^(?:schemaVersion|version|revision|sequence|attempt|priority|limit|offset|size|total|count|exitCode|pending|leased|retryWait|dispatched|blocked|due|expiredLease|identityMismatch|pidMismatch|unsupported|invalid|availableSlots|maxConcurrentRuns|cpuCores|[A-Za-z0-9_]*(?:AtMs|TimeMs|DurationMs|Bytes|Tokens|Micros|Sequence|Version|Count|Limit|Offset|Size|Total))$/u;
/^(?:schemaVersion|version|revision|sequence|attempt|priority|limit|offset|size|total|count|exitCode|pending|leased|retryWait|dispatched|blocked|due|expiredLease|identityMismatch|pidMismatch|unsupported|invalid|availableSlots|maxConcurrentRuns|cpuCores|targetGeneration|[A-Za-z0-9_]*(?:AtMs|TimeMs|DurationMs|Bytes|Tokens|Micros|Sequence|Version|Count|Limit|Offset|Size|Total))$/u;
const schemaValue = /^[a-z0-9][a-z0-9./_-]{0,126}@[a-z0-9._-]{1,16}$/u;
class ClusterConsoleEvidenceBundleError extends TypeError {
@@ -24,7 +24,7 @@
<div class="boundary" aria-label="当前权限边界">
<span class="boundary-dot" aria-hidden="true"></span>
<span>本机只读 BFF</span>
<strong>Run · Task · Workflow · Worker · Copilot</strong>
<strong>Run · Task · Workflow · Worker · Package · Copilot</strong>
</div>
</header>
@@ -33,7 +33,7 @@
<div class="section-heading">
<p class="eyebrow">Observation coordinates</p>
<h2 id="control-title">选择要读取的事实</h2>
<p>每次按钮点击只发起一次有界 GET。页面不创建、取消、重试、轮询或缓存任何任务。</p>
<p>每次按钮点击只发起一次有界读取。页面不创建、安装、升级、回滚、取消、重试、轮询或缓存任何任务。</p>
</div>
<form id="session-form" class="session-gate" autocomplete="off">
@@ -53,6 +53,7 @@
<button type="button" class="mode-tab active" data-panel="runtime-panel" aria-pressed="true">运行态</button>
<button type="button" class="mode-tab" data-panel="management-panel" aria-pressed="false">取消可用性</button>
<button type="button" class="mode-tab" data-panel="worker-panel" aria-pressed="false">Worker</button>
<button type="button" class="mode-tab" data-panel="package-panel" aria-pressed="false">Package</button>
<button type="button" class="mode-tab" data-panel="workflow-panel" aria-pressed="false">工作流</button>
<button type="button" class="mode-tab" data-panel="copilot-panel" aria-pressed="false">Copilot</button>
</nav>
@@ -81,6 +82,20 @@
</div>
</section>
<section id="package-panel" class="mode-panel" hidden>
<div class="control-group">
<div class="control-title"><span>01</span><strong>Package 安装目录</strong></div>
<button type="button" class="primary" data-read="package_list">读取首屏 Installations</button>
<p class="field-note">只有启动进程显式提供独立 Package management 配置与短期 assertion 时可用;固定 16 项,下一页必须再次点击。</p>
</div>
<div class="control-group">
<div class="control-title"><span>02</span><strong>单 Package 安装状态</strong></div>
<input id="installation-package-name" type="text" maxlength="63" autocomplete="off" spellcheck="false" placeholder="ops-package" />
<button type="button" data-read="package_inspect">读取 Package 安装详情</button>
<p class="field-note">只投影版本、安装状态、可用性与恢复提示;没有 propose、decide、install、upgrade、rollback、disable 或 uninstall mutation。</p>
</div>
</section>
<section id="worker-panel" class="mode-panel" hidden>
<div class="control-group">
<div class="control-title"><span>01</span><strong>Worker 目录</strong></div>
@@ -145,7 +160,7 @@
<aside class="trust-note">
<span>Authority boundary</span>
<p>Project credential、可选 Run authority 与可选 Worker authority 只由本机进程从彼此独立的私有文件读取。浏览器无法提交任意路径,也没有 start、stop、retry、rearm、drain、revoke 或 diagnose 权限入口。脱敏导出只处理本页已读事实,不补读或上传。</p>
<p>Project credential、可选 Run、Worker 与 Package authority 只由本机进程从彼此独立的私有文件读取。浏览器无法提交任意路径,也没有 start、stop、retry、rearm、drain、revoke、propose、decide、install、upgrade、rollback、disable、uninstall 或 diagnose 权限入口。脱敏导出只处理本页已读事实,不补读或上传。</p>
</aside>
</aside>
@@ -176,7 +191,7 @@
<footer>
<span>Loopback only · explicit reads · zero polling</span>
<span>QingLong 3.0 incubation / D-375</span>
<span>QingLong 3.0 incubation / D-376</span>
</footer>
</div>
</body>
@@ -24,7 +24,7 @@ const ASSETS = Object.freeze([
name: 'index.html',
field: 'html',
maximumBytes: 32 * 1024,
digest: '363fcf2d52ff86e5b2a1c9ed8b7810226920a9270b6d0c41a1f61ce24f957832',
digest: '429d7b3dd2da4989865be6ac07180cc9c3ebdcbbac5028054cddaac870ad520c',
}),
Object.freeze({
name: 'app.css',
@@ -36,13 +36,13 @@ const ASSETS = Object.freeze([
name: 'evidence-bundle.js',
field: 'evidenceBundle',
maximumBytes: 32 * 1024,
digest: 'ae4a08572cfc3296284c56549a3850f530474151573e2705401996730bf0466e',
digest: '83d17dfa815c175161b35c1aca5f270b15005a16cb79be41c2884b490f617783',
}),
Object.freeze({
name: 'app.js',
field: 'javascript',
maximumBytes: 32 * 1024,
digest: '4b13da1a85d59e29a606da3b1a3327419926a496a498a06ddc323365ce5230c1',
digest: '365ccd43ae2aa4b11a0ab3d142cd04e89ec0b96711bef253f63584e8182589ee',
}),
] as const);
@@ -10,7 +10,16 @@ import {
validateClusterCopilotClientCredentialFile,
} from '../copilot-client/client';
import { readCanonicalFile } from '../management-support/managementClientConfiguration';
import { validateClusterAuthenticatedManagementClientConfiguration } from '../management-support/pluginPackageManagementClient';
import {
executeClusterPluginPackageManagementCommand,
validateClusterAuthenticatedManagementClientConfiguration,
} from '../management-support/pluginPackageManagementClient';
import {
createPluginPackageInstallationInspectionCommand,
createPluginPackageInstallationListCommand,
projectPluginPackageInstallationInspection,
projectPluginPackageInstallationList,
} from '../plugin-package/management/pluginPackageInstallationProduct';
import {
createRunCancellationBlockedListCommand,
projectRunCancellationBlockedList,
@@ -50,6 +59,7 @@ const USAGE = [
' ql3-copilot-console --container-published-loopback --port=1024..65535 --config /absolute/client.json --credential /absolute/credential --session /absolute/session [--check]',
' Optional Run reads: --run-management-config /absolute/run-client.json --run-management-assertion /absolute/assertion.jwt',
' Optional Worker reads: --worker-management-config /absolute/worker-client.json --worker-management-assertion /absolute/assertion.jwt',
' Optional Package reads: --package-management-config /absolute/package-client.json --package-management-assertion /absolute/assertion.jwt',
'',
'Native mode binds 127.0.0.1. Container mode requires host-loopback port publication.',
'The browser session key remains in a separate owner-private 0600 file.',
@@ -60,6 +70,8 @@ interface ClusterCopilotConsoleCliArguments {
readonly configFile: string;
readonly credentialFile: string;
readonly networkBoundary: 'host-loopback' | 'container-published-loopback';
readonly packageManagementAssertionFile?: string;
readonly packageManagementConfigFile?: string;
readonly runManagementAssertionFile?: string;
readonly runManagementConfigFile?: string;
readonly workerManagementAssertionFile?: string;
@@ -78,6 +90,10 @@ const RUN_MANAGEMENT_OPERATIONS = new Set([
'run_cancellation_inspect',
]);
const WORKER_MANAGEMENT_OPERATIONS = new Set(['worker_list', 'worker_inspect']);
const PACKAGE_MANAGEMENT_OPERATIONS = new Set([
'package_list',
'package_inspect',
]);
function usageFailure(): never {
process.stderr.write(USAGE + '\n');
@@ -118,6 +134,8 @@ export function parseClusterCopilotConsoleCliArguments(
let runManagementAssertionFile: string | undefined;
let workerManagementConfigFile: string | undefined;
let workerManagementAssertionFile: string | undefined;
let packageManagementConfigFile: string | undefined;
let packageManagementAssertionFile: string | undefined;
let port = 0;
let portSeen = false;
let containerPublishedLoopback = false;
@@ -201,6 +219,28 @@ export function parseClusterCopilotConsoleCliArguments(
index += workerManagementAssertion.consumed;
continue;
}
const packageManagementConfig = argumentValue(
argv,
index,
'--package-management-config',
);
if (packageManagementConfig) {
if (packageManagementConfigFile !== undefined) return usageFailure();
packageManagementConfigFile = packageManagementConfig.value;
index += packageManagementConfig.consumed;
continue;
}
const packageManagementAssertion = argumentValue(
argv,
index,
'--package-management-assertion',
);
if (packageManagementAssertion) {
if (packageManagementAssertionFile !== undefined) return usageFailure();
packageManagementAssertionFile = packageManagementAssertion.value;
index += packageManagementAssertion.consumed;
continue;
}
const portArgument = argumentValue(argv, index, '--port');
if (portArgument) {
if (portSeen || !/^(?:0|[1-9][0-9]{0,4})$/.test(portArgument.value)) {
@@ -227,6 +267,8 @@ export function parseClusterCopilotConsoleCliArguments(
(runManagementAssertionFile === undefined) ||
(workerManagementConfigFile === undefined) !==
(workerManagementAssertionFile === undefined) ||
(packageManagementConfigFile === undefined) !==
(packageManagementAssertionFile === undefined) ||
(containerPublishedLoopback && port === 0) ||
(!containerPublishedLoopback && check && port !== 0)
) {
@@ -247,6 +289,10 @@ export function parseClusterCopilotConsoleCliArguments(
workerManagementAssertionFile !== undefined
? { workerManagementConfigFile, workerManagementAssertionFile }
: {}),
...(packageManagementConfigFile !== undefined &&
packageManagementAssertionFile !== undefined
? { packageManagementConfigFile, packageManagementAssertionFile }
: {}),
sessionFile,
port,
});
@@ -255,7 +301,7 @@ export function parseClusterCopilotConsoleCliArguments(
function validateManagementAuthority(
configFile: string | undefined,
assertionFile: string | undefined,
kind: 'run' | 'worker',
kind: 'package' | 'run' | 'worker',
): boolean {
if (configFile === undefined || assertionFile === undefined) return false;
validateClusterAuthenticatedManagementClientConfiguration(configFile, kind);
@@ -281,12 +327,15 @@ function validateManagementAuthority(
function availableOperations(
runManagementAuthority: boolean,
workerManagementAuthority: boolean,
packageManagementAuthority: boolean,
) {
return CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS.filter(
(operation) =>
(runManagementAuthority || !RUN_MANAGEMENT_OPERATIONS.has(operation)) &&
(workerManagementAuthority ||
!WORKER_MANAGEMENT_OPERATIONS.has(operation)),
!WORKER_MANAGEMENT_OPERATIONS.has(operation)) &&
(packageManagementAuthority ||
!PACKAGE_MANAGEMENT_OPERATIONS.has(operation)),
);
}
@@ -393,6 +442,48 @@ async function executeConsoleRead(
result: projected as unknown as Readonly<Record<string, unknown>>,
});
}
if (
request.operation === 'package_list' ||
request.operation === 'package_inspect'
) {
if (
parsed.packageManagementConfigFile === undefined ||
parsed.packageManagementAssertionFile === undefined
) {
throw new Error('Package management authority is disabled');
}
const createUuid = commandIdSource(request.requestId);
const command =
request.operation === 'package_list'
? createPluginPackageInstallationListCommand(
request.projectId,
request.afterPackageName ?? undefined,
createUuid,
)
: createPluginPackageInstallationInspectionCommand(
request.projectId,
request.packageName,
createUuid,
);
const result = await executeClusterPluginPackageManagementCommand({
configFile: parsed.packageManagementConfigFile,
assertionFile: parsed.packageManagementAssertionFile,
command,
});
const projected =
request.operation === 'package_list'
? projectPluginPackageInstallationList(request.projectId, result)
: projectPluginPackageInstallationInspection(
request.projectId,
request.packageName,
result,
);
return Object.freeze({
schemaVersion: 1 as const,
requestId: request.requestId,
result: projected as unknown as Readonly<Record<string, unknown>>,
});
}
return executeClusterProjectApiRead({
configFile: parsed.configFile,
credentialFile: parsed.credentialFile,
@@ -439,9 +530,15 @@ async function main(): Promise<void> {
parsed.workerManagementAssertionFile,
'worker',
);
const packageManagementAuthority = validateManagementAuthority(
parsed.packageManagementConfigFile,
parsed.packageManagementAssertionFile,
'package',
);
const operations = availableOperations(
runManagementAuthority,
workerManagementAuthority,
packageManagementAuthority,
);
const sessionDigest = readSessionDigest(parsed.sessionFile);
if (parsed.check) {
@@ -465,6 +562,9 @@ async function main(): Promise<void> {
workerManagementAuthority: workerManagementAuthority
? 'server_only'
: 'disabled',
packageManagementAuthority: packageManagementAuthority
? 'server_only'
: 'disabled',
operations,
mutation: false,
}) + '\n',
@@ -504,6 +604,9 @@ async function main(): Promise<void> {
workerManagementAuthority: workerManagementAuthority
? 'server_only'
: 'disabled',
packageManagementAuthority: packageManagementAuthority
? 'server_only'
: 'disabled',
operations,
mutation: false,
}) + '\n',
@@ -16,6 +16,8 @@ export const CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS = Object.freeze([
'run_cancellation_inspect',
'worker_list',
'worker_inspect',
'package_list',
'package_inspect',
'run_list',
'run_read',
'run_event_list',
@@ -51,6 +53,9 @@ export type ClusterCopilotConsoleReadRequest =
| (BaseReadRequest<'worker_list'> &
Readonly<{ afterWorkerId: string | null }>)
| (BaseReadRequest<'worker_inspect'> & Readonly<{ workerId: string }>)
| (BaseReadRequest<'package_list'> &
Readonly<{ afterPackageName: string | null }>)
| (BaseReadRequest<'package_inspect'> & Readonly<{ packageName: string }>)
| (BaseReadRequest<'run_list'> &
Readonly<{
afterCreatedAtMs: number | null;
@@ -267,6 +272,33 @@ export function normalizeClusterCopilotConsoleReadRequest(
workerId: record.workerId,
});
}
if (op === 'package_list') {
exact(record, op, ['afterPackageName']);
if (
record.afterPackageName !== null &&
(typeof record.afterPackageName !== 'string' ||
!PACKAGE_NAME.test(record.afterPackageName))
)
invalid();
return Object.freeze({
...common(record),
operation: op,
afterPackageName: record.afterPackageName as string | null,
});
}
if (op === 'package_inspect') {
exact(record, op, ['packageName']);
if (
typeof record.packageName !== 'string' ||
!PACKAGE_NAME.test(record.packageName)
)
invalid();
return Object.freeze({
...common(record),
operation: op,
packageName: record.packageName,
});
}
if (op === 'run_list') {
exact(record, op, ['afterCreatedAtMs', 'afterRunId', 'limit']);
if (
@@ -502,7 +534,9 @@ export function clusterCopilotConsoleProjectReadPath(
normalized.operation === 'run_cancellation_blocked_list' ||
normalized.operation === 'run_cancellation_inspect' ||
normalized.operation === 'worker_list' ||
normalized.operation === 'worker_inspect'
normalized.operation === 'worker_inspect' ||
normalized.operation === 'package_list' ||
normalized.operation === 'package_inspect'
)
invalid();
const project = '/api/v3/projects/' + encoded(normalized.projectId);
@@ -35,6 +35,8 @@ const OPERATIONS = Object.freeze([
'run_cancellation_inspect',
'worker_list',
'worker_inspect',
'package_list',
'package_inspect',
'run_list',
'run_read',
'run_event_list',
@@ -66,6 +68,8 @@ const REQUEST_FIELDS: Readonly<Record<EvidenceOperation, readonly string[]>> =
]),
worker_list: Object.freeze(['afterWorkerId', 'projectId', 'requestId']),
worker_inspect: Object.freeze(['projectId', 'requestId', 'workerId']),
package_list: Object.freeze(['afterPackageName', 'projectId', 'requestId']),
package_inspect: Object.freeze(['packageName', 'projectId', 'requestId']),
run_list: Object.freeze([
'afterCreatedAtMs',
'afterRunId',
@@ -139,6 +143,7 @@ const IDENTIFIER_DOMAINS: Readonly<Record<string, string>> = Object.freeze({
afterStepRunId: 'step',
afterTaskId: 'task',
afterWorkerId: 'worker',
afterPackageName: 'package',
artifactId: 'artifact',
attemptId: 'attempt',
contentDigest: 'digest',
@@ -148,6 +153,7 @@ const IDENTIFIER_DOMAINS: Readonly<Record<string, string>> = Object.freeze({
id: 'identifier',
modelId: 'model',
nextAfterWorkerId: 'worker',
nextAfterPackageName: 'package',
outputRef: 'artifact',
packageName: 'package',
projectId: 'project',
@@ -187,6 +193,8 @@ const SAFE_CONTAINERS = new Set([
'runtimes',
'worker',
'workers',
'installation',
'installations',
'usage',
'workflow',
'workflows',
@@ -224,6 +232,12 @@ const SAFE_ENUM_KEYS = new Set([
'status',
'supportTier',
'operatingSystem',
'availability',
'failureReason',
'installOperation',
'quarantineReason',
'recoveryAction',
'state',
]);
const SAFE_ENUM_VALUES = new Set([
'accepted',
@@ -319,9 +333,27 @@ const SAFE_ENUM_VALUES = new Set([
'ok',
'unavailable',
'workflow',
'not_active',
'install',
'reinstall',
'upgrade',
'rollback',
'resume_stage',
'resume_activation',
'inspect_activation',
'source_unavailable',
'source_mismatch',
'stage_failed',
'activation_failed',
'activation_fact_conflict',
'approval_expired',
'policy_fence_changed',
'resource_exhausted',
'suspected_key_compromise',
'confirmed_key_compromise',
]);
const NUMERIC_KEY =
/^(?:schemaVersion|version|revision|sequence|attempt|priority|limit|offset|size|total|count|exitCode|pending|leased|retryWait|dispatched|blocked|due|expiredLease|identityMismatch|pidMismatch|unsupported|invalid|availableSlots|maxConcurrentRuns|cpuCores|[A-Za-z0-9_]*(?:AtMs|TimeMs|DurationMs|Bytes|Tokens|Micros|Sequence|Version|Count|Limit|Offset|Size|Total))$/u;
/^(?:schemaVersion|version|revision|sequence|attempt|priority|limit|offset|size|total|count|exitCode|pending|leased|retryWait|dispatched|blocked|due|expiredLease|identityMismatch|pidMismatch|unsupported|invalid|availableSlots|maxConcurrentRuns|cpuCores|targetGeneration|[A-Za-z0-9_]*(?:AtMs|TimeMs|DurationMs|Bytes|Tokens|Micros|Sequence|Version|Count|Limit|Offset|Size|Total))$/u;
const SCHEMA_VALUE = /^[a-z0-9][a-z0-9./_-]{0,126}@[a-z0-9._-]{1,16}$/u;
const SHA256 = /^[0-9a-f]{64}$/u;
const CONTROL = /[\0-\x1f\x7f]/u;
@@ -197,6 +197,8 @@ const READ_ROUTES: Readonly<
'/api/v1/run-management/cancellation-inspect': 'run_cancellation_inspect',
'/api/v1/worker-management/workers': 'worker_list',
'/api/v1/worker-management/worker': 'worker_inspect',
'/api/v1/package-management/installations': 'package_list',
'/api/v1/package-management/installation': 'package_inspect',
'/api/v1/observe/run-list': 'run_list',
'/api/v1/observe/run': 'run_read',
'/api/v1/observe/run-events': 'run_event_list',
@@ -598,16 +598,18 @@ function validateSecretBindingPlanSummary(
}
if (
summary.actionRef !== command.request.actionRef ||
command.operation === 'plugin-package.secret-binding.plan' &&
(summary.projectId !== command.request.projectId ||
summary.packageName !== command.request.packageName ||
summary.entries.length !== command.request.assignments.length ||
command.request.assignments.some((assignment) => {
const responseEntry = (summary.entries as JsonObject[]).find(
(entry) => entry.name === assignment.name,
);
return !responseEntry || responseEntry.secretRef !== assignment.secretRef;
}))
(command.operation === 'plugin-package.secret-binding.plan' &&
(summary.projectId !== command.request.projectId ||
summary.packageName !== command.request.packageName ||
summary.entries.length !== command.request.assignments.length ||
command.request.assignments.some((assignment) => {
const responseEntry = (summary.entries as JsonObject[]).find(
(entry) => entry.name === assignment.name,
);
return (
!responseEntry || responseEntry.secretRef !== assignment.secretRef
);
})))
) {
throw new ClusterPluginPackageManagementClientRequestError();
}
@@ -707,7 +709,9 @@ function validateSecretBindingTransitionPlanSummary(
throw new ClusterPluginPackageManagementClientRequestError();
}
} catch (error) {
if (error instanceof ClusterPluginPackageManagementClientRequestError) {
if (
error instanceof ClusterPluginPackageManagementClientRequestError
) {
throw error;
}
throw new ClusterPluginPackageManagementClientRequestError();
@@ -758,7 +762,9 @@ function validateResult(
result as unknown as ClusterPluginPackageManagementTransportResult,
);
}
if (command.operation === 'plugin-package.secret-binding.transition.propose') {
if (
command.operation === 'plugin-package.secret-binding.transition.propose'
) {
const result = exactResponseObject(value, [
'schemaVersion',
'operation',
@@ -789,7 +795,9 @@ function validateResult(
result as unknown as ClusterPluginPackageManagementTransportResult,
);
}
if (command.operation === 'plugin-package.secret-binding.transition.inspect') {
if (
command.operation === 'plugin-package.secret-binding.transition.inspect'
) {
const result = exactResponseObject(value, [
'schemaVersion',
'operation',
@@ -893,7 +901,7 @@ function validateResult(
result.schemaVersion !== 1 ||
result.operation !== command.operation ||
typeof result.stale !== 'boolean' ||
result.plan === null && result.approval === null
(result.plan === null && result.approval === null)
) {
throw new ClusterPluginPackageManagementClientRequestError();
}
@@ -903,8 +911,7 @@ function validateResult(
if (result.approval !== null) {
validateScalarSummary(result.approval, APPROVAL_KEYS);
if (
(result.approval as JsonObject).id !==
command.request.approvalRequestId
(result.approval as JsonObject).id !== command.request.approvalRequestId
) {
throw new ClusterPluginPackageManagementClientRequestError();
}
@@ -1486,3 +1493,14 @@ export async function executeClusterPluginPackageManagementClient(
connectionOptions,
);
}
export async function executeClusterPluginPackageManagementCommand(
execution: ClusterAuthenticatedManagementCommandExecution<ClusterPluginPackageManagementCommand>,
connectionOptions?: ClusterPluginPackageManagementClientConnectionOptions,
): Promise<Readonly<ClusterPluginPackageManagementClientResult>> {
return executeClusterAuthenticatedManagementClient(
execution,
PLUGIN_PACKAGE_MANAGEMENT_CLIENT_PROTOCOL,
connectionOptions,
);
}
@@ -0,0 +1,255 @@
/** Bounded commands and low-sensitive product projections for Package installations. */
import { randomUUID } from 'node:crypto';
import type { ClusterPluginPackageManagementClientResult } from '../../management-support/pluginPackageManagementClient';
import type {
ClusterPluginPackageManagementCommand,
ClusterPluginPackageManagementTransportResult,
} from './pluginPackageManagementTransport';
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const PACKAGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
const PACKAGE_VERSION = /^[0-9A-Za-z](?:[0-9A-Za-z.+-]{0,126}[0-9A-Za-z])?$/;
const PAGE_SIZE = 16;
const INSTALL_OPERATIONS = new Set([
'install',
'reinstall',
'upgrade',
'rollback',
]);
const INSTALL_STATES = new Set([
'queued',
'staged',
'activating',
'active',
'failed',
]);
const RECOVERY_ACTIONS = new Set([
'resume_stage',
'resume_activation',
'inspect_activation',
'none',
]);
const AVAILABILITY = new Set(['active', 'not_active', 'quarantined']);
const FAILURE_REASONS = new Set([
'source_unavailable',
'source_mismatch',
'stage_failed',
'activation_failed',
'activation_fact_conflict',
'approval_expired',
'policy_fence_changed',
'resource_exhausted',
]);
const QUARANTINE_REASONS = new Set([
'suspected_key_compromise',
'confirmed_key_compromise',
]);
type InspectCommand = Extract<
ClusterPluginPackageManagementCommand,
{ readonly operation: 'plugin-package.installation.inspect' }
>;
type ListCommand = Extract<
ClusterPluginPackageManagementCommand,
{ readonly operation: 'plugin-package.installation.list' }
>;
type InspectResult = Extract<
ClusterPluginPackageManagementTransportResult,
{ readonly operation: 'plugin-package.installation.inspect' }
>;
type ListResult = Extract<
ClusterPluginPackageManagementTransportResult,
{ readonly operation: 'plugin-package.installation.list' }
>;
type Installation = NonNullable<InspectResult['installation']>;
export interface PluginPackageInstallationObservation {
readonly packageName: string;
readonly packageVersion: string;
readonly installOperation: Installation['operation'];
readonly state: Installation['state'];
readonly targetGeneration: number;
readonly recoveryAction: Installation['recoveryAction'];
readonly availability: Installation['availability'];
readonly quarantineReason: Installation['quarantineReason'];
readonly failureReason: Installation['failureReason'];
readonly version: number;
readonly createdAtMs: number;
readonly updatedAtMs: number;
}
export interface PluginPackageInstallationInspection {
readonly schema: 'qinglong/plugin-package-installation-inspection@v1';
readonly projectId: string;
readonly packageName: string;
readonly found: boolean;
readonly installation: Readonly<PluginPackageInstallationObservation> | null;
}
export interface PluginPackageInstallationList {
readonly schema: 'qinglong/plugin-package-installation-list@v1';
readonly projectId: string;
readonly count: number;
readonly installations: readonly Readonly<PluginPackageInstallationObservation>[];
readonly truncated: boolean;
readonly nextAfterPackageName: string | null;
}
export class ClusterPluginPackageInstallationProductError extends TypeError {
readonly code = 'QL3_PLUGIN_PACKAGE_INSTALLATION_PRODUCT_INPUT_INVALID';
constructor() {
super('Plugin Package installation product input is invalid');
this.name = 'ClusterPluginPackageInstallationProductError';
}
}
function identifier(value: string): string {
if (!IDENTIFIER.test(value)) {
throw new ClusterPluginPackageInstallationProductError();
}
return value;
}
function packageName(value: string): string {
if (!PACKAGE_NAME.test(value)) {
throw new ClusterPluginPackageInstallationProductError();
}
return value;
}
function packageVersion(value: string): string {
if (!PACKAGE_VERSION.test(value)) {
throw new ClusterPluginPackageInstallationProductError();
}
return value;
}
function enumValue<T extends string>(value: T, values: ReadonlySet<string>): T {
if (!values.has(value)) {
throw new ClusterPluginPackageInstallationProductError();
}
return value;
}
function nullableEnum<T extends string>(
value: T | null,
values: ReadonlySet<string>,
): T | null {
return value === null ? null : enumValue(value, values);
}
function safeInteger(value: number): number {
if (!Number.isSafeInteger(value) || value < 0) {
throw new ClusterPluginPackageInstallationProductError();
}
return value;
}
function inspectionId(createId: () => string): string {
return identifier(createId());
}
export function createPluginPackageInstallationInspectionCommand(
projectId: string,
name: string,
createId: () => string = randomUUID,
): Readonly<InspectCommand> {
return Object.freeze({
schemaVersion: 1,
operation: 'plugin-package.installation.inspect',
request: Object.freeze({
projectId: identifier(projectId),
packageName: packageName(name),
inspectionId: inspectionId(createId),
}),
});
}
export function createPluginPackageInstallationListCommand(
projectId: string,
afterPackageName?: string,
createId: () => string = randomUUID,
): Readonly<ListCommand> {
return Object.freeze({
schemaVersion: 1,
operation: 'plugin-package.installation.list',
request: Object.freeze({
projectId: identifier(projectId),
limit: PAGE_SIZE,
...(afterPackageName === undefined
? {}
: {
after: Object.freeze({
packageName: packageName(afterPackageName),
}),
}),
inspectionId: inspectionId(createId),
}),
});
}
function projectInstallation(
installation: Installation | ListResult['installations'][number],
): Readonly<PluginPackageInstallationObservation> {
return Object.freeze({
packageName: packageName(installation.packageName),
packageVersion: packageVersion(installation.packageVersion),
installOperation: enumValue(installation.operation, INSTALL_OPERATIONS),
state: enumValue(installation.state, INSTALL_STATES),
targetGeneration: safeInteger(installation.targetGeneration),
recoveryAction: enumValue(installation.recoveryAction, RECOVERY_ACTIONS),
availability: enumValue(installation.availability, AVAILABILITY),
quarantineReason: nullableEnum(
installation.quarantineReason,
QUARANTINE_REASONS,
),
failureReason: nullableEnum(installation.failureReason, FAILURE_REASONS),
version: safeInteger(installation.version),
createdAtMs: safeInteger(installation.createdAtMs),
updatedAtMs: safeInteger(installation.updatedAtMs),
});
}
export function projectPluginPackageInstallationInspection(
projectId: string,
name: string,
response: Readonly<ClusterPluginPackageManagementClientResult>,
): Readonly<PluginPackageInstallationInspection> {
if (response.result.operation !== 'plugin-package.installation.inspect') {
throw new ClusterPluginPackageInstallationProductError();
}
const installation = response.result.installation;
if (installation !== null && installation.packageName !== name) {
throw new ClusterPluginPackageInstallationProductError();
}
return Object.freeze({
schema: 'qinglong/plugin-package-installation-inspection@v1',
projectId: identifier(projectId),
packageName: packageName(name),
found: installation !== null,
installation:
installation === null ? null : projectInstallation(installation),
});
}
export function projectPluginPackageInstallationList(
projectId: string,
response: Readonly<ClusterPluginPackageManagementClientResult>,
): Readonly<PluginPackageInstallationList> {
if (response.result.operation !== 'plugin-package.installation.list') {
throw new ClusterPluginPackageInstallationProductError();
}
const installations = Object.freeze(
response.result.installations.map(projectInstallation),
);
return Object.freeze({
schema: 'qinglong/plugin-package-installation-list@v1',
projectId: identifier(projectId),
count: installations.length,
installations,
truncated: response.result.truncated,
nextAfterPackageName: response.result.next?.packageName ?? null,
});
}
@@ -265,6 +265,23 @@ test('normalizes Copilot and fixed Project observation operations without arbitr
assert.throws(() => clusterCopilotConsoleProjectReadPath(workerList), {
code: 'QL3_CLUSTER_COPILOT_CONSOLE_READ_REQUEST_INVALID',
});
const packageList = normalizeClusterCopilotConsoleReadRequest({
schema: CLUSTER_COPILOT_CONSOLE_READ_REQUEST_SCHEMA,
operation: 'package_list',
projectId: 'project-main',
requestId: 'console-package-list',
afterPackageName: 'ops-package',
});
assert.deepEqual(packageList, {
schema: CLUSTER_COPILOT_CONSOLE_READ_REQUEST_SCHEMA,
operation: 'package_list',
projectId: 'project-main',
requestId: 'console-package-list',
afterPackageName: 'ops-package',
});
assert.throws(() => clusterCopilotConsoleProjectReadPath(packageList), {
code: 'QL3_CLUSTER_COPILOT_CONSOLE_READ_REQUEST_INVALID',
});
assert.throws(
() =>
normalizeClusterCopilotConsoleReadRequest({
@@ -690,6 +707,62 @@ test('routes only fixed Worker list and inspect reads with a bounded cursor', as
assert.equal(reads.length, 2);
});
test('routes only fixed Package installation list and inspect reads', async (t) => {
const reads = [];
const { server, headers } = await fixture(async (read) => {
reads.push(read);
return {
schemaVersion: 1,
requestId: read.requestId,
result: { operation: read.operation },
};
});
t.after(() => server.close());
const cases = [
[
'/api/v1/package-management/installations',
{
schema: CLUSTER_COPILOT_CONSOLE_READ_REQUEST_SCHEMA,
operation: 'package_list',
projectId: 'project-main',
requestId: 'console-package-list-1',
afterPackageName: null,
},
],
[
'/api/v1/package-management/installation',
{
schema: CLUSTER_COPILOT_CONSOLE_READ_REQUEST_SCHEMA,
operation: 'package_inspect',
projectId: 'project-main',
requestId: 'console-package-inspect-1',
packageName: 'ops-package',
},
],
];
for (const [path, body] of cases) {
const response = await request(server.origin, {
method: 'POST',
path,
headers,
body,
});
assert.equal(response.statusCode, 200);
}
assert.deepEqual(
reads,
cases.map(([, body]) => body),
);
const invalidCursor = await request(server.origin, {
method: 'POST',
path: '/api/v1/package-management/installations',
headers,
body: { ...cases[0][1], afterPackageName: 'Contains_underscore' },
});
assert.equal(invalidCursor.statusCode, 400);
assert.equal(reads.length, 2);
});
test('returns model text as JSON data only after an explicit output read', async (t) => {
const { server, headers } = await fixture(async (command) => {
assert.equal(command.operation, 'output');
@@ -40,6 +40,7 @@ const runManagementOperations = [
'run_cancellation_inspect',
];
const workerManagementOperations = ['worker_list', 'worker_inspect'];
const packageManagementOperations = ['package_list', 'package_inspect'];
function workerSummary(workerId = 'worker-a') {
return {
@@ -63,6 +64,36 @@ function workerSummary(workerId = 'worker-a') {
};
}
function packageInstallationSummary(packageName = 'ops-package') {
return {
installationId: 'installation-transport-hidden',
projectId: 'project-main',
packageName,
packageVersion: '3.1.0',
operation: 'upgrade',
state: 'active',
targetGeneration: 4,
activeLockDigest: 'a'.repeat(64),
previousActiveLockDigest: 'b'.repeat(64),
recoveryAction: 'none',
availability: 'active',
quarantineReason: null,
quarantineAuthorizationMode: null,
quarantineEventDigest: null,
quarantinedAtMs: null,
withdrawalStatus: null,
withdrawalReceiptDigest: null,
withdrawalCommittedAtMs: null,
failureReason: null,
failedFrom: null,
failedAtMs: null,
version: 7,
createdAtMs: 1_000,
updatedAtMs: 1_100,
recordDigest: 'c'.repeat(64),
};
}
function privateFile(directory, name, contents) {
const filePath = path.join(directory, name);
fs.writeFileSync(filePath, contents, { mode: 0o600 });
@@ -201,7 +232,29 @@ async function fixture(t) {
command,
});
const body =
command?.operation === 'worker-session.list'
command?.operation === 'plugin-package.installation.list'
? {
schemaVersion: 1,
requestId: 'package-transport-hidden',
result: {
schemaVersion: 1,
operation: 'plugin-package.installation.list',
installations: [packageInstallationSummary()],
truncated: true,
next: { packageName: 'ops-package' },
},
}
: command?.operation === 'plugin-package.installation.inspect'
? {
schemaVersion: 1,
requestId: 'package-transport-hidden',
result: {
schemaVersion: 1,
operation: 'plugin-package.installation.inspect',
installation: packageInstallationSummary(),
},
}
: command?.operation === 'worker-session.list'
? {
schemaVersion: 1,
requestId: 'worker-transport-hidden',
@@ -341,6 +394,19 @@ async function fixture(t) {
requestTimeoutMs: 2_000,
}),
);
const packageManagementConfigFile = privateFile(
directory,
'package-client.json',
JSON.stringify({
schemaVersion: 1,
endpoint: `https://localhost:${
server.address().port
}/api/v3/plugin-packages/management`,
servername: 'localhost',
caFile,
requestTimeoutMs: 2_000,
}),
);
const sessionToken = randomBytes(32).toString('base64url');
return {
requests,
@@ -360,6 +426,12 @@ async function fixture(t) {
'worker-assertion.jwt',
'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJvcGVyYXRvci0xIn0.c2lnbmF0dXJl',
),
packageManagementConfigFile,
packageManagementAssertionFile: privateFile(
directory,
'package-assertion.jwt',
'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJvcGVyYXRvci0xIn0.c2lnbmF0dXJl',
),
};
}
@@ -371,6 +443,7 @@ test('CLI exposes deterministic help and a low-sensitive failure surface', async
' ql3-copilot-console --container-published-loopback --port=1024..65535 --config /absolute/client.json --credential /absolute/credential --session /absolute/session [--check]',
' Optional Run reads: --run-management-config /absolute/run-client.json --run-management-assertion /absolute/assertion.jwt',
' Optional Worker reads: --worker-management-config /absolute/worker-client.json --worker-management-assertion /absolute/assertion.jwt',
' Optional Package reads: --package-management-config /absolute/package-client.json --package-management-assertion /absolute/assertion.jwt',
'',
'Native mode binds 127.0.0.1. Container mode requires host-loopback port publication.',
'The browser session key remains in a separate owner-private 0600 file.',
@@ -426,6 +499,7 @@ test('preflight proves private authority and unauthenticated TLS 1.3 readiness',
clusterCredential: 'server_only',
runManagementAuthority: 'disabled',
workerManagementAuthority: 'disabled',
packageManagementAuthority: 'disabled',
operations: consoleOperations,
mutation: false,
});
@@ -521,6 +595,47 @@ test('preflight enables exactly two Worker reads only with canonical config and
assert.equal(incomplete.status, 64);
});
test('preflight enables exactly two Package reads only with canonical config and assertion', async (t) => {
const value = await fixture(t);
const result = await runCli([
'--check',
'--config',
value.configFile,
'--credential',
value.credentialFile,
'--session',
value.sessionFile,
'--package-management-config',
value.packageManagementConfigFile,
'--package-management-assertion',
value.packageManagementAssertionFile,
]);
assert.equal(result.status, 0, result.stderr);
const fact = JSON.parse(result.stdout);
assert.equal(fact.runManagementAuthority, 'disabled');
assert.equal(fact.workerManagementAuthority, 'disabled');
assert.equal(fact.packageManagementAuthority, 'server_only');
assert.deepEqual(fact.operations, [
'inspect',
'output',
...packageManagementOperations,
...consoleOperations.slice(2),
]);
assert.equal(value.requests.length, 1);
const incomplete = await runCli([
'--config',
value.configFile,
'--credential',
value.credentialFile,
'--session',
value.sessionFile,
'--package-management-config',
value.packageManagementConfigFile,
]);
assert.equal(incomplete.status, 64);
});
test('serve mode starts an ephemeral loopback origin and shuts down cleanly', async (t) => {
const value = await fixture(t);
const child = spawn(
@@ -548,6 +663,7 @@ test('serve mode starts an ephemeral loopback origin and shuts down cleanly', as
assert.equal(started.mutation, false);
assert.equal(started.runManagementAuthority, 'disabled');
assert.equal(started.workerManagementAuthority, 'disabled');
assert.equal(started.packageManagementAuthority, 'disabled');
assert.equal(started.networkBoundary, 'host-loopback');
assert.equal(started.publishedHostAddress, '127.0.0.1');
const shell = await get(started.origin);
@@ -687,6 +803,76 @@ test('serve mode performs one canonical Worker page read without exposing transp
assert.deepEqual(exit, { status: 0, signal: null });
});
test('serve mode performs one canonical Package page read without exposing durable identity', async (t) => {
const value = await fixture(t);
const child = spawn(
process.execPath,
[
cliPath,
'--config',
value.configFile,
'--credential',
value.credentialFile,
'--session',
value.sessionFile,
'--package-management-config',
value.packageManagementConfigFile,
'--package-management-assertion',
value.packageManagementAssertionFile,
'--port=0',
],
{ cwd: packageRoot, stdio: ['ignore', 'pipe', 'pipe'] },
);
t.after(() => {
if (child.exitCode === null && child.signalCode === null)
child.kill('SIGKILL');
});
const started = JSON.parse(await firstLine(child.stdout));
assert.equal(started.packageManagementAuthority, 'server_only');
const response = await post(
started.origin,
value.sessionToken,
'/api/v1/package-management/installations',
{
schema: 'qinglong/cluster-copilot-console-read-request@v1',
operation: 'package_list',
projectId: 'project-main',
requestId: 'console-package-list-1',
afterPackageName: null,
},
);
assert.equal(response.statusCode, 200);
assert.equal(
response.body.result.result.schema,
'qinglong/plugin-package-installation-list@v1',
);
assert.equal(response.body.result.result.count, 1);
assert.equal(response.body.result.result.nextAfterPackageName, 'ops-package');
const encoded = JSON.stringify(response.body);
assert.equal(encoded.includes('package-transport-hidden'), false);
assert.equal(encoded.includes('installation-transport-hidden'), false);
assert.equal(encoded.includes('recordDigest'), false);
const management = value.requests.find(
(request) => request.path === '/api/v3/plugin-packages/management',
);
assert.equal(management.method, 'POST');
assert.equal(
management.command.operation,
'plugin-package.installation.list',
);
assert.equal(
management.command.request.inspectionId,
'console-package-list-1',
);
assert.equal(management.command.request.limit, 16);
child.kill('SIGTERM');
const exit = await new Promise((resolve, reject) => {
child.once('error', reject);
child.once('close', (status, signal) => resolve({ status, signal }));
});
assert.deepEqual(exit, { status: 0, signal: null });
});
test('container mode requires an explicit publish port before any authority read', async () => {
const result = await runCli([
'--container-published-loopback',
@@ -294,6 +294,65 @@ test('redacts Worker identity and preserves only bounded placement and capacity
);
});
test('redacts Package transport identity and keeps bounded installation state', async () => {
const bundle = await createClusterConsoleEvidenceBundle(
[
{
operation: 'package_list',
observedAtMs: 1_700_000_005_000,
request: {
schema: requestSchema,
operation: 'package_list',
afterPackageName: null,
projectId: 'project-sensitive',
requestId: 'package-request-sensitive',
},
fact: {
schema: 'qinglong/plugin-package-installation-list@v1',
projectId: 'project-sensitive',
count: 1,
installations: [
{
packageName: 'ops-package-sensitive',
packageVersion: '3.1.0',
installOperation: 'upgrade',
state: 'active',
targetGeneration: 4,
recoveryAction: 'none',
availability: 'active',
quarantineReason: null,
failureReason: null,
version: 7,
createdAtMs: 1_700_000_001_000,
updatedAtMs: 1_700_000_004_000,
installationId: 'must-not-export',
recordDigest: 'a'.repeat(64),
},
],
truncated: true,
nextAfterPackageName: 'ops-package-sensitive',
},
},
],
1_700_000_006_000,
webcrypto,
);
const entry = bundle.entries[0];
assert.equal(entry.operation, 'package_list');
assert.equal(entry.fact.installations[0].packageName, 'package-001');
assert.equal(entry.fact.nextAfterPackageName, 'package-001');
assert.equal(entry.fact.installations[0].installOperation, 'upgrade');
assert.equal(entry.fact.installations[0].state, 'active');
assert.equal(entry.fact.installations[0].targetGeneration, 4);
assert.equal(entry.fact.installations[0].installationId, undefined);
assert.equal(entry.fact.installations[0].recordDigest, 'digest-001');
assert.equal(entry.fact.installations[0].packageVersion, undefined);
assert.doesNotMatch(
JSON.stringify(bundle),
/ops-package-sensitive|package-request-sensitive|project-sensitive|must-not-export/,
);
});
test('fails closed on widened records, unsafe JSON and every capacity ceiling', async () => {
const error = { code: 'QL3_CLUSTER_CONSOLE_EVIDENCE_BUNDLE_INVALID' };
assert.throws(() => measureClusterConsoleEvidenceRecord(null), error);
@@ -147,6 +147,16 @@ test('cross-verifies every fixed Console read operation', async (t) => {
requestId: 'q-worker-inspect',
workerId: 'worker-1',
},
package_list: {
afterPackageName: null,
projectId: 'p-package',
requestId: 'q-package-list',
},
package_inspect: {
packageName: 'ops-package',
projectId: 'p-package',
requestId: 'q-package-inspect',
},
run_list: {
afterCreatedAtMs: null,
afterRunId: null,
@@ -247,7 +257,7 @@ test('cross-verifies every fixed Console read operation', async (t) => {
assert.equal(result.status, 'verified');
verified += result.bundle.entryCount;
}
assert.equal(verified, 18);
assert.equal(verified, 20);
});
test('CLI is secret-free on success, invalid input and usage errors', async (t) => {
@@ -0,0 +1,170 @@
'use strict';
const assert = require('node:assert/strict');
const test = require('node:test');
const {
ClusterPluginPackageInstallationProductError,
createPluginPackageInstallationInspectionCommand,
createPluginPackageInstallationListCommand,
projectPluginPackageInstallationInspection,
projectPluginPackageInstallationList,
} = require('../dist/plugin-package/management/pluginPackageInstallationProduct.js');
function installation(overrides = {}) {
return {
installationId: 'installation-sensitive',
projectId: 'project-main',
packageName: 'ops-package',
packageVersion: '3.1.0',
operation: 'upgrade',
state: 'active',
targetGeneration: 4,
activeLockDigest: 'a'.repeat(64),
previousActiveLockDigest: 'b'.repeat(64),
recoveryAction: 'none',
availability: 'active',
quarantineReason: null,
quarantineAuthorizationMode: null,
quarantineEventDigest: null,
quarantinedAtMs: null,
withdrawalStatus: null,
withdrawalReceiptDigest: null,
withdrawalCommittedAtMs: null,
failureReason: null,
failedFrom: null,
failedAtMs: null,
version: 7,
createdAtMs: 100,
updatedAtMs: 200,
recordDigest: 'c'.repeat(64),
...overrides,
};
}
test('builds fixed bounded installation observation commands', () => {
assert.deepEqual(
createPluginPackageInstallationListCommand(
'project-main',
'after-package',
() => 'inspection-list',
),
{
schemaVersion: 1,
operation: 'plugin-package.installation.list',
request: {
projectId: 'project-main',
limit: 16,
after: { packageName: 'after-package' },
inspectionId: 'inspection-list',
},
},
);
assert.deepEqual(
createPluginPackageInstallationInspectionCommand(
'project-main',
'ops-package',
() => 'inspection-one',
),
{
schemaVersion: 1,
operation: 'plugin-package.installation.inspect',
request: {
projectId: 'project-main',
packageName: 'ops-package',
inspectionId: 'inspection-one',
},
},
);
});
test('projects installation facts without transport or durable identifiers', () => {
const fact = projectPluginPackageInstallationList('project-main', {
schemaVersion: 1,
requestId: 'transport-request-sensitive',
result: {
operation: 'plugin-package.installation.list',
installations: [installation()],
truncated: true,
next: { packageName: 'ops-package' },
},
});
assert.deepEqual(fact, {
schema: 'qinglong/plugin-package-installation-list@v1',
projectId: 'project-main',
count: 1,
installations: [
{
packageName: 'ops-package',
packageVersion: '3.1.0',
installOperation: 'upgrade',
state: 'active',
targetGeneration: 4,
recoveryAction: 'none',
availability: 'active',
quarantineReason: null,
failureReason: null,
version: 7,
createdAtMs: 100,
updatedAtMs: 200,
},
],
truncated: true,
nextAfterPackageName: 'ops-package',
});
const encoded = JSON.stringify(fact);
for (const secret of [
'installation-sensitive',
'transport-request-sensitive',
'activeLockDigest',
'recordDigest',
]) {
assert.equal(encoded.includes(secret), false);
}
});
test('inspection binds the selected package and fails closed on drift', () => {
const response = {
schemaVersion: 1,
requestId: 'request-one',
result: {
operation: 'plugin-package.installation.inspect',
installation: installation(),
},
};
assert.equal(
projectPluginPackageInstallationInspection(
'project-main',
'ops-package',
response,
).found,
true,
);
assert.throws(
() =>
projectPluginPackageInstallationInspection(
'project-main',
'other-package',
response,
),
ClusterPluginPackageInstallationProductError,
);
assert.throws(
() =>
projectPluginPackageInstallationInspection(
'project-main',
'ops-package',
{
...response,
result: {
...response.result,
installation: installation({
recoveryAction: 'run-arbitrary-code',
}),
},
},
),
ClusterPluginPackageInstallationProductError,
);
});
@@ -15,6 +15,7 @@ const REQUIRED_FILES = Object.freeze([
CONSOLE_ROOT + '/evidenceVerifierCli.ts',
CONSOLE_ROOT + '/server.ts',
'packages/ql3-cluster-admin/src/run-management/runCancellationInspection.ts',
'packages/ql3-cluster-admin/src/plugin-package/management/pluginPackageInstallationProduct.ts',
CLIENT_FILE,
ASSET_ROOT + '/index.html',
ASSET_ROOT + '/app.css',
@@ -24,6 +25,7 @@ const REQUIRED_FILES = Object.freeze([
DEPLOYMENT_ROOT + '/client-config.example.json',
DEPLOYMENT_ROOT + '/run-management-client-config.example.json',
DEPLOYMENT_ROOT + '/worker-management-client-config.example.json',
DEPLOYMENT_ROOT + '/package-management-client-config.example.json',
'deploy/containers/ql3-cluster-admin/Dockerfile',
'scripts/ql3-cluster-admin-product-live-contract.cjs',
]);
@@ -111,6 +113,8 @@ function auditClusterCopilotConsole(options = {}) {
"'run_cancellation_inspect'",
"'worker_list'",
"'worker_inspect'",
"'package_list'",
"'package_inspect'",
"'task_read'",
"'workflow_step_list'",
]);
@@ -140,6 +144,7 @@ function auditClusterCopilotConsole(options = {}) {
"'/api/v1/copilot/inspect': 'inspect'",
"'/api/v1/run-management/cancellation-status': 'run_cancellation_status'",
"'/api/v1/worker-management/workers': 'worker_list'",
"'/api/v1/package-management/installations': 'package_list'",
"'/api/v1/observe/run-list': 'run_list'",
"'/api/v1/observe/task-list': 'task_list'",
"'/api/v1/observe/workflow-list': 'workflow_list'",
@@ -165,6 +170,8 @@ function auditClusterCopilotConsole(options = {}) {
'--run-management-assertion /absolute/assertion.jwt',
'--worker-management-config /absolute/worker-client.json',
'--worker-management-assertion /absolute/assertion.jwt',
'--package-management-config /absolute/package-client.json',
'--package-management-assertion /absolute/assertion.jwt',
'readCanonicalFile(',
"'private'",
'validateClusterCopilotClientCredentialFile',
@@ -173,6 +180,7 @@ function auditClusterCopilotConsole(options = {}) {
"publishedHostAddress: '127.0.0.1'",
'runManagementAuthority: runManagementAuthority',
'workerManagementAuthority: workerManagementAuthority',
'packageManagementAuthority: packageManagementAuthority',
'mutation: false',
]);
rejectFragments(CONSOLE_ROOT + '/cli.ts', [
@@ -233,6 +241,8 @@ function auditClusterCopilotConsole(options = {}) {
'读取首屏 Blocked Runs',
'读取首屏 Workers',
'读取 Worker 详情',
'读取首屏 Installations',
'读取 Package 安装详情',
'该只读面没有 rearm',
'模型文本是不可信内容',
'导出脱敏包',
@@ -301,8 +311,10 @@ function auditClusterCopilotConsole(options = {}) {
'Run, Task, Workflow',
'thirteen exact operations',
'available vocabulary to sixteen',
'maximum vocabulary of twenty operations',
'QL3_COPILOT_CONSOLE_RUN_MANAGEMENT=enabled',
'QL3_COPILOT_CONSOLE_WORKER_MANAGEMENT=enabled',
'QL3_COPILOT_CONSOLE_PACKAGE_MANAGEMENT=enabled',
'--port=0',
'TLS 1.3 `GET /readyz`',
'excluded from small router Edge/Standalone artifacts',
@@ -451,6 +463,8 @@ function auditClusterCopilotConsole(options = {}) {
'run_cancellation_inspect',
'worker_list',
'worker_inspect',
'package_list',
'package_inspect',
'run_list',
'run_read',
'run_event_list',
@@ -15,6 +15,8 @@ const FILES = Object.freeze({
'deploy/console/ql3-cluster-copilot/run-management-client-config.example.json',
workerManagementExample:
'deploy/console/ql3-cluster-copilot/worker-management-client-config.example.json',
packageManagementExample:
'deploy/console/ql3-cluster-copilot/package-management-client-config.example.json',
image: 'deploy/containers/ql3-cluster-admin/Dockerfile',
workflow: '.github/workflows/ql3-image-release.yml',
candidate: 'scripts/ql3-release-candidate-contract.cjs',
@@ -92,6 +94,9 @@ function auditClusterCopilotConsoleDistribution(options = {}) {
'QL3_COPILOT_CONSOLE_WORKER_MANAGEMENT-disabled',
'--worker-management-config /var/run/secrets/qinglong3/copilot-console/worker-management-client.json',
'--worker-management-assertion /var/run/secrets/qinglong3/copilot-console/worker-management-assertion.jwt',
'QL3_COPILOT_CONSOLE_PACKAGE_MANAGEMENT-disabled',
'--package-management-config /var/run/secrets/qinglong3/copilot-console/package-management-client.json',
'--package-management-assertion /var/run/secrets/qinglong3/copilot-console/package-management-assertion.jwt',
],
'QL3_COPILOT_CONSOLE_LAUNCHER_CONTRACT_DRIFT',
);
@@ -105,6 +110,15 @@ function auditClusterCopilotConsoleDistribution(options = {}) {
],
'QL3_COPILOT_CONSOLE_WORKER_MANAGEMENT_EXAMPLE_DRIFT',
);
requireFragments(
'packageManagementExample',
[
'"schemaVersion": 1',
'/api/v3/plugin-packages/management',
'package-management-ca.pem',
],
'QL3_COPILOT_CONSOLE_PACKAGE_MANAGEMENT_EXAMPLE_DRIFT',
);
rejectFragments(
'launcher',
['--privileged', '--network host', '/var/run/docker.sock', '--pull always'],
@@ -222,6 +236,7 @@ function auditClusterCopilotConsoleDistribution(options = {}) {
QL3_COPILOT_CONSOLE_RESOURCE_CLASS: 'compact',
QL3_COPILOT_CONSOLE_RUN_MANAGEMENT: 'disabled',
QL3_COPILOT_CONSOLE_WORKER_MANAGEMENT: 'disabled',
QL3_COPILOT_CONSOLE_PACKAGE_MANAGEMENT: 'disabled',
};
if (
environment &&
@@ -249,6 +264,8 @@ function auditClusterCopilotConsoleDistribution(options = {}) {
'share/ql3-copilot-console/run-management-client-config.example.json',
'COPY --chmod=0444 deploy/console/ql3-cluster-copilot/worker-management-client-config.example.json',
'share/ql3-copilot-console/worker-management-client-config.example.json',
'COPY --chmod=0444 deploy/console/ql3-cluster-copilot/package-management-client-config.example.json',
'share/ql3-copilot-console/package-management-client-config.example.json',
],
'QL3_COPILOT_CONSOLE_IMAGE_DISTRIBUTION_DRIFT',
);
@@ -343,6 +360,7 @@ function auditClusterCopilotConsoleDistribution(options = {}) {
additionalWorkspacePackages: 0,
runManagementAuthorityDefault: 'disabled',
workerManagementAuthorityDefault: 'disabled',
packageManagementAuthorityDefault: 'disabled',
externalWorkstationCeremony: 'source-tag-private-report',
ceremonyStatus: 'implementation-ready-public-release-pending',
findings: Object.freeze(findings),
@@ -33,6 +33,8 @@ test('keeps the QingLong 3.0 Copilot Console independent and read-only', () => {
'run_cancellation_inspect',
'worker_list',
'worker_inspect',
'package_list',
'package_inspect',
'run_list',
'run_read',
'run_event_list',
@@ -29,6 +29,7 @@ test('accepts the signed multi-architecture Admin OCI workstation distribution',
additionalWorkspacePackages: 0,
runManagementAuthorityDefault: 'disabled',
workerManagementAuthorityDefault: 'disabled',
packageManagementAuthorityDefault: 'disabled',
externalWorkstationCeremony: 'source-tag-private-report',
ceremonyStatus: 'implementation-ready-public-release-pending',
findings: [],
@@ -156,6 +156,26 @@ test('adds optional Worker management files only after its independent enabled s
assert.equal(args.includes('--run-management-config'), false);
});
test('adds optional Package management files only after its independent enabled switch', (t) => {
const value = fixture(t);
const result = invoke('check', {
...value.env,
QL3_COPILOT_CONSOLE_PACKAGE_MANAGEMENT: 'enabled',
});
assert.equal(result.status, 0, result.stderr);
const args = fs.readFileSync(value.capture, 'utf8').trimEnd().split('\n');
assert.equal(
args[args.indexOf('--package-management-config') + 1],
'/var/run/secrets/qinglong3/copilot-console/package-management-client.json',
);
assert.equal(
args[args.indexOf('--package-management-assertion') + 1],
'/var/run/secrets/qinglong3/copilot-console/package-management-assertion.jwt',
);
assert.equal(args.includes('--run-management-config'), false);
assert.equal(args.includes('--worker-management-config'), false);
});
test('rejects mutable, ambient and malformed host inputs before Docker', (t) => {
const value = fixture(t);
for (const environment of [
@@ -169,6 +189,7 @@ test('rejects mutable, ambient and malformed host inputs before Docker', (t) =>
{ ...value.env, QL3_COPILOT_CONSOLE_RESOURCE_CLASS: 'unbounded' },
{ ...value.env, QL3_COPILOT_CONSOLE_RUN_MANAGEMENT: 'ambient' },
{ ...value.env, QL3_COPILOT_CONSOLE_WORKER_MANAGEMENT: 'ambient' },
{ ...value.env, QL3_COPILOT_CONSOLE_PACKAGE_MANAGEMENT: 'ambient' },
]) {
const rejected = invoke('serve', environment);
assert.equal(rejected.status, 78);