feat(ql3): expose worker observations in console

This commit is contained in:
whyour
2026-08-20 14:02:01 +08:00
parent af5d5bfc0b
commit 344680d64a
25 changed files with 840 additions and 49 deletions
+36 -1
View File
@@ -12,6 +12,12 @@ by default. Supplying a separate Run management mTLS config and short-lived
User assertion adds only status, blocked-list and inspect reads to the same
loopback process; rearm, stop and retry remain absent.
Worker observation is a third, independent authority and is also disabled by
default. Supplying its generic Worker management mTLS config and short-lived
User assertion adds only a fixed 16-item list and point inspect to the same
loopback process. Credential mutation, drain, revoke, background polling and
automatic pagination remain absent.
Use `ql3-cluster-admin` from the same independently verified Admin release as
the Cluster deployment. D-328 also supports the image-carried
`docker-loopback.sh`: it uses an explicit container-only listener but publishes
@@ -111,6 +117,14 @@ credential. Supplying only one of config/assertion is rejected before a
listener or Cluster request is created. The assertion is reread for every
explicit click so it can be rotated or removed while the Console is running.
To enable Worker observation, copy
`worker-management-client-config.example.json` to
`worker-management-client.json`, install its separate CA, client certificate
and private key, and issue a short-lived strong User assertion with only
`worker.manage` into `worker-management-assertion.jwt`. Its endpoint must be
the D-374 canonical `/api/v3/workers/management`; the Console cannot accept the
legacy credential-management path or a credential mutation command file.
Create an independent 256-bit browser session key without placing its value in
argv or an environment variable:
@@ -125,6 +139,10 @@ If optional Run management reads are enabled, apply the same owner-private,
canonical, non-symlink `0600` rule to `run-management-client.json`,
`run-management-ca.pem`, `run-management-client.crt`,
`run-management-client.key` and `run-management-assertion.jwt`.
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.
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
@@ -151,7 +169,14 @@ intended:
--run-management-assertion /absolute/private/ql3-copilot-console/run-management-assertion.jwt
```
It validates all three private authorities and performs one unauthenticated
Worker observation uses its own pair:
```sh
--worker-management-config /absolute/private/ql3-copilot-console/worker-management-client.json \
--worker-management-assertion /absolute/private/ql3-copilot-console/worker-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.
@@ -191,6 +216,12 @@ offers a user-clicked blocked-list step; each returned Run offers a user-clicked
inspect step. Pagination is also click-only. There is no polling, automatic
page traversal, bulk inspection or mutation route.
Explicit Worker management authority adds `worker_list|worker_inspect`. The
list is fixed at 16 items and exposes a next cursor only as a new user-clicked
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.
## Export a redacted evidence bundle
After at least one successful read, **Export redacted bundle** creates one
@@ -243,6 +274,10 @@ The image launcher keeps Run management disabled unless
`run-management-client.json` and `run-management-assertion.jwt` from the same
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.
| Resource class | Memory | CPU | PIDs | Console reads |
| --- | ---: | ---: | ---: | ---: |
@@ -25,6 +25,7 @@ network=${QL3_COPILOT_CONSOLE_NETWORK-}
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}
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
@@ -62,6 +63,10 @@ case "$run_management" in
disabled|enabled) ;;
*) fail ;;
esac
case "$worker_management" in
disabled|enabled) ;;
*) fail ;;
esac
set -- docker run --rm --pull never --init --read-only \
--network "$network" \
@@ -92,6 +97,12 @@ if [ "$run_management" = enabled ]; then
--run-management-assertion /var/run/secrets/qinglong3/copilot-console/run-management-assertion.jwt
fi
if [ "$worker_management" = enabled ]; then
set -- "$@" \
--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
fi
if [ "$mode" = check ]; then
set -- "$@" --check
fi
@@ -4,5 +4,6 @@
"QL3_COPILOT_CONSOLE_NETWORK": "qinglong3-copilot-console-egress",
"QL3_COPILOT_CONSOLE_PORT": "5701",
"QL3_COPILOT_CONSOLE_RESOURCE_CLASS": "compact",
"QL3_COPILOT_CONSOLE_RUN_MANAGEMENT": "disabled"
"QL3_COPILOT_CONSOLE_RUN_MANAGEMENT": "disabled",
"QL3_COPILOT_CONSOLE_WORKER_MANAGEMENT": "disabled"
}
@@ -0,0 +1,9 @@
{
"schemaVersion": 1,
"endpoint": "https://replace-cluster-api.example.com:8448/api/v3/workers/management",
"servername": "replace-cluster-api.example.com",
"caFile": "/absolute/private/ql3-copilot-console/worker-management-ca.pem",
"clientCertificateFile": "/absolute/private/ql3-copilot-console/worker-management-client.crt",
"clientPrivateKeyFile": "/absolute/private/ql3-copilot-console/worker-management-client.key",
"requestTimeoutMs": 5000
}
@@ -87,6 +87,8 @@ COPY --chmod=0444 deploy/console/ql3-cluster-copilot/client-config.example.json
share/ql3-copilot-console/client-config.example.json
COPY --chmod=0444 deploy/console/ql3-cluster-copilot/run-management-client-config.example.json \
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/host-environment.example.json \
share/ql3-copilot-console/host-environment.example.json
+21
View File
@@ -11,6 +11,27 @@
最新增量证据(2026-08-20):
- 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
`/api/v3/workers/management`Worker config/assertion 必须成对显式提供,宿主启动器也只在
`QL3_COPILOT_CONSOLE_WORKER_MANAGEMENT=enabled` 时只读挂载,默认输出 `workerManagementAuthority=disabled`
list 固定 16 项并只在用户点击时携带 `afterWorkerId`,inspect 只由用户选定一个 Worker;没有 credential mutation、drain、
revoke、caller limit/filter、自动翻页、poller、retry、queue、cache、watcher 或后台 timer。浏览器不接触 mTLS key/assertion
Console 返回 caller request ID 并丢弃 management transport request ID;证据 bundle 对 Worker/Project/request identity 使用域内
alias,只保留有界 lifecycle、compatibility、support tier、architecture、OS、runtime 和 capacity 事实。实现继续内聚在现有
`@qinglong/cluster-admin/copilot-console`,复用 `worker-management/` client/productworkspace 保持 18 packages,没有新增依赖、
binary、端口、数据库对象、Deployment、Ingress 或常驻连接。专项门 `48/48`Cluster Admin 全量为
`431 total / 428 pass / 3 conditional skip / 0 fail`backend 为
`1,504 total / 1,502 pass / 2 conditional skip / 0 fail`18-package clean build/逐包测试单次退出 0。package boundary、Cluster
dependency、Edge import、Cluster/Worker deployment、Console 与 Console distribution 审计全部 compatible;仍为
`singleSourcePackages=[]``shallowSourcePackages=[]`Cluster Admin `128 source / 127 nested`。14 档 Local artifact audit 全部
compatible;基础 Edge/Standalone 为 `2,598,669 / 2,598,747` bytes、57 loaded modules、RSS 增量
`11,272,192 / 11,223,040` bytesApplication+AI 为 `4,501,822 / 4,501,954` bytesMCP 为
`7,324,601 / 7,324,709` bytes,证明 Cluster Console/Worker authority 没有进入低配路由设备闭包。本切片不改变 schema、ACL、
repository、role、Pool、连接或 failover 语义,不重跑并不重新占有 PostgreSQL HA 证明;D-373/D-374 的 PostgreSQL 18.6
arm64 HA `146/146`、timeline `1→2` 仅作为相邻既有基线,任何后续数据库语义变化必须重新执行 HA 门。
- D-374/ADR-0467(已接受):完成 D-373 留下的 Worker 产品命名债务,同时保持零新增常驻服务。通用 canonical path 为
`/api/v3/workers/management`;旧 `/api/v3/worker-credentials/management` 作为精确兼容 alias 继续由同一个 TLS 1.3/mTLS/
OIDC listener、transport、quota、rate limiter 和连接集合处理。共享 host 只允许这一对 alias,任意跨 Run/Automation/
@@ -0,0 +1,78 @@
# ADR-0468Copilot Console 显式可选 Worker 只读观察
- 状态:Accepted
- 日期:2026-08-20
- 关联 RFCQL-RFC-0001 D-375、D-14、D-16、D-107
- 关联 ADRADR-0462、ADR-0463、ADR-0466、ADR-0467
## 上下文
ADR-0466/0467 已在现有 Worker management authority 上提供通用 `worker-session.inspect|list` 产品入口。集群运维仍需要在
Copilot Console 中把 Run 异常与 Worker 在线状态、兼容性和剩余 slot 放在同一个只读工作台观察,但这不能让浏览器持有 mTLS
key/OIDC assertion,也不能把 Cluster 诊断能力带入低配 Edge/Standalone 常驻闭包。
新建 Worker UI 服务或 workspace package 会复制 Console session、TLS、分发、端口和生命周期边界。把 Worker authority 默认并入
Console 又会让普通 Project API 观察面静默获得更强的管理身份,并可能诱发自动轮询和不可控分页负载。
## 决策
1. 在既有 `@qinglong/cluster-admin/copilot-console` 增加 `worker_list``worker_inspect`,分别只接受固定 BFF route
`/api/v1/worker-management/workers``/api/v1/worker-management/worker`。浏览器不能提供上游 path、method 或任意 command。
2. Worker authority 使用独立的 `--worker-management-config``--worker-management-assertion`,两者必须成对存在且 config 必须指向
canonical `/api/v3/workers/management`。它不复用普通 Cluster credential,也不接受 legacy credential mutation path。
3. 未提供 Worker authority 时,Console 正常提供原有操作并报告 `workerManagementAuthority=disabled`。宿主容器启动器仅在
`QL3_COPILOT_CONSOLE_WORKER_MANAGEMENT=enabled` 时挂载两份 owner-private 文件并添加参数;Run 与 Worker 两个可选 authority
独立开关,不能互相隐式启用。
4. `worker_list` 每次固定最多 16 项,只接受 nullable `afterWorkerId`;下一页必须由用户点击。`worker_inspect` 只观察用户明确选中的
canonical Worker ID。禁止 caller limit/filter、自动翻页、poller、retry、queue、cache、watcher、WebSocket/SSE 和后台 timer。
5. BFF 复用 ADR-0467 的 generic Worker client、严格 transport validator 与产品投影。一次浏览器请求只产生一次上游 POST;Console
使用 caller request ID 作为关联身份,不向浏览器返回 management transport request ID、inspection ID、assertion 或 credential。
6. UI 只展示有界 Session、lifecycle、compatibility、support tier、architecture、OS、runtime、capacity 与下一页事实;所有文本继续以
data/textContent 渲染。证据 bundle 对 Worker、Project 和 request identity 使用 bundle-local 域内 alias,且不声明服务器签名或
action authority。
7. 本切片不新增 workspace package、external dependency、binary、监听端口、Kubernetes workload、Ingress、数据库 schema/role/Pool
或持久化状态。Console 仍只在受信 operator workstation 回环生命周期运行,Edge/Standalone 不导入 Cluster Admin。
## 被拒绝的替代方案
### 独立 Worker Console 服务或 package
拒绝。两个 caller-driven 只读操作不足以承担第二套 session、TLS、镜像、发布和运维生命周期;代码应留在已有 Console 与
Worker management 内聚目录。
### 默认启用 Worker authority
拒绝。普通 Project 观察与 Worker management mTLS/OIDC 是不同权限域。显式成对文件与独立启动开关让部署者可以证明未启用路径
不会读取 assertion、打开额外连接或展示 Worker 操作。
### 自动刷新、自动翻页或实时 Worker dashboard
拒绝。它会把一次诊断变成持续数据库、网络和浏览器成本,并掩盖低配管理节点上的真实压力。固定 16 项页面与用户驱动 inspect/
next 是本阶段唯一接受的负载模型。
### 把 assertion 或 transport identity 发给浏览器
拒绝。浏览器只应持有短期 Console session;上游身份、私钥和传输关联信息必须留在 BFF authority 内。
## 升级与回滚
- 旧启动方式不传 Worker 参数时行为不变。需要观察 Worker 的部署者先提供 canonical generic config 与专用 assertion,再显式启用
launcher switch。
- 回滚到 ADR-0467 时只失去 Console 中两个 Worker tab/routegeneric CLI、manager、数据库与 Worker Session 不变,无数据迁移。
- 若未来增加 Worker mutation、历史指标、label/filter、跨 Project inventory 或实时流,必须另立 authority、索引、retention、隐私和
资源预算 ADR,不能在本只读 BFF 上渐进偷渡。
## 验证与证据
- Console/CLI/evidence/launcher 专项门 `48/48`;覆盖 exact route、固定分页、mTLS canonical read、authority 默认关闭、独立开关、
transport identity 隔离、证据脱敏和 mutation/remote-listener/ambient-authority 拒绝。
- `@qinglong/cluster-admin` 全量 `431 total / 428 pass / 3 conditional skip / 0 fail`backend
`1,504 total / 1,502 pass / 2 conditional skip / 0 fail`18-package clean build/逐包测试单次退出 0。
- package boundary、Cluster dependency、Edge import、Cluster/Worker deployment、Console 与 distribution 审计全部 compatible。
workspace 保持 18 packages、`singleSourcePackages=[]``shallowSourcePackages=[]`Cluster Admin 为
`128 source / 127 nested`,没有新增 dependency。
- 14 档 Local artifact audit 全部 compatible。基础 Edge/Standalone 为 `2,598,669 / 2,598,747` bytes、57 loaded modulesRSS
增量 `11,272,192 / 11,223,040` bytesApplication+AI 为 `4,501,822 / 4,501,954` bytesMCP 为
`7,324,601 / 7,324,709` bytes。Console/Worker authority 未进入低配路由设备制品。
- 本切片没有 PostgreSQL schema、ACL、repository、role、Pool、连接或 failover 变化,因此不重跑并不重新占有物理 HA 证明;仅引用
D-373/D-374 PostgreSQL 18.6 arm64 HA `146/146`、timeline `1→2` 相邻基线。数据库语义一旦改变必须重新运行 HA 门。
+1
View File
@@ -471,6 +471,7 @@
| [ADR-0465](./ADR-0465-versioned-worker-support-tier-admission.md) | 版本化 Worker 支持等级准入 | Accepted |
| [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 |
## 规则
@@ -8,8 +8,9 @@
run_cancellation_status: '/api/v1/run-management/cancellation-status',
run_cancellation_blocked_list:
'/api/v1/run-management/blocked-cancellations',
run_cancellation_inspect:
'/api/v1/run-management/cancellation-inspect',
run_cancellation_inspect: '/api/v1/run-management/cancellation-inspect',
worker_list: '/api/v1/worker-management/workers',
worker_inspect: '/api/v1/worker-management/worker',
run_list: '/api/v1/observe/run-list',
run_read: '/api/v1/observe/run',
run_event_list: '/api/v1/observe/run-events',
@@ -28,6 +29,8 @@
run_cancellation_status: '取消可用性',
run_cancellation_blocked_list: 'Blocked Cancellations',
run_cancellation_inspect: '取消诊断',
worker_list: 'Worker 目录',
worker_inspect: 'Worker 详情',
run_list: 'Run 目录',
run_read: 'Run 详情',
run_event_list: 'Run Events',
@@ -124,6 +127,10 @@
result.cursor = null;
} else if (operation === 'run_cancellation_inspect') {
result.runId = value('cancellation-run-id');
} else if (operation === 'worker_list') {
result.afterWorkerId = null;
} else if (operation === 'worker_inspect') {
result.workerId = value('worker-id');
} else if (operation === 'run_list') {
result.afterCreatedAtMs = null;
result.afterRunId = null;
@@ -176,6 +183,11 @@
typeof fact.nextCursor === 'string'
) {
next.cursor = fact.nextCursor;
} else if (
operation === 'worker_list' &&
typeof fact.nextAfterWorkerId === 'string'
) {
next.afterWorkerId = fact.nextAfterWorkerId;
} else if (operation === 'run_list' && fact.hasMore === true && fact.next) {
next.afterCreatedAtMs = fact.next.createdAtMs;
next.afterRunId = fact.next.runId;
@@ -234,6 +246,20 @@
entry.append(button);
return;
}
if (operation === 'worker_list' && Array.isArray(fact.workers)) {
fact.workers.forEach(function (worker) {
if (!worker || typeof worker.workerId !== 'string') return;
const button = document.createElement('button');
button.type = 'button';
button.textContent = '显式检查 ' + worker.workerId;
button.addEventListener('click', function () {
document.getElementById('worker-id').value = worker.workerId;
void execute('worker_inspect');
});
entry.append(button);
});
return;
}
if (
operation !== 'run_cancellation_blocked_list' ||
!Array.isArray(fact.items)
@@ -31,6 +31,8 @@
'run_cancellation_status',
'run_cancellation_blocked_list',
'run_cancellation_inspect',
'worker_list',
'worker_inspect',
'run_list',
'run_read',
'run_event_list',
@@ -50,6 +52,8 @@
run_cancellation_status: ['projectId', 'requestId'],
run_cancellation_blocked_list: ['cursor', 'projectId', 'requestId'],
run_cancellation_inspect: ['projectId', 'requestId', 'runId'],
worker_list: ['afterWorkerId', 'projectId', 'requestId'],
worker_inspect: ['projectId', 'requestId', 'workerId'],
run_list: [
'afterCreatedAtMs',
'afterRunId',
@@ -117,6 +121,7 @@
afterStepKey: 'step',
afterStepRunId: 'step',
afterTaskId: 'task',
afterWorkerId: 'worker',
artifactId: 'artifact',
attemptId: 'attempt',
contentDigest: 'digest',
@@ -125,6 +130,7 @@
executionId: 'execution',
id: 'identifier',
modelId: 'model',
nextAfterWorkerId: 'worker',
outputRef: 'artifact',
packageName: 'package',
projectId: 'project',
@@ -160,6 +166,10 @@
'target',
'task',
'tasks',
'declaredCapacity',
'runtimes',
'worker',
'workers',
'usage',
'workflow',
'workflows',
@@ -170,6 +180,7 @@
'available',
'cancelRequested',
'enabled',
'found',
'hasMore',
'outputAvailable',
'ready',
@@ -183,6 +194,9 @@
'assessment',
'cancelReason',
'kind',
'architecture',
'compatibility',
'lifecycle',
'lastResult',
'operation',
'operatorAction',
@@ -191,6 +205,8 @@
'severity',
'stage',
'status',
'supportTier',
'operatingSystem',
]);
const safeEnumValues = new Set([
'accepted',
@@ -198,6 +214,13 @@
'admission',
'attention_required',
'available',
'amd64',
'arm64',
'ppc64le',
's390x',
'arm/v7',
'arm/v6',
'386',
'blocked',
'cancelled',
'completed',
@@ -208,6 +231,22 @@
'dispatch',
'dispatching',
'disabled',
'default_placement',
'explicit_placement_required',
'protocol_incompatible',
'online',
'draining',
'offline',
'lease_expired',
'tier1',
'candidate',
'experimental',
'legacy-only',
'linux',
'darwin',
'win32',
'freebsd',
'aix',
'enabled',
'execution',
'failed',
@@ -269,7 +308,7 @@
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|[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|[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 · Copilot · Optional management</strong>
<strong>Run · Task · Workflow · Worker · Copilot</strong>
</div>
</header>
@@ -52,6 +52,7 @@
<nav class="mode-tabs" aria-label="观察面">
<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="workflow-panel" aria-pressed="false">工作流</button>
<button type="button" class="mode-tab" data-panel="copilot-panel" aria-pressed="false">Copilot</button>
</nav>
@@ -80,6 +81,20 @@
</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>
<button type="button" class="primary" data-read="worker_list">读取首屏 Workers</button>
<p class="field-note">只有启动进程显式提供独立 Worker management 配置与短期 assertion 时可用;固定 16 项,下一页必须再次点击。</p>
</div>
<div class="control-group">
<div class="control-title"><span>02</span><strong>单 Worker 状态</strong></div>
<input id="worker-id" type="text" maxlength="128" autocomplete="off" spellcheck="false" placeholder="worker-id" />
<button type="button" data-read="worker_inspect">读取 Worker 详情</button>
<p class="field-note">只投影在线状态、兼容性、架构与容量;没有 credential、drain、revoke 或调度 mutation。</p>
</div>
</section>
<section id="management-panel" class="mode-panel" hidden>
<div class="control-group">
<div class="control-title"><span>01</span><strong>Project 状态</strong></div>
@@ -130,7 +145,7 @@
<aside class="trust-note">
<span>Authority boundary</span>
<p>Project credential可选 Run management authority 只由本机进程从彼此独立的私有文件读取。浏览器无法提交任意路径,也没有 start、stop、retry、rearm 或 diagnose 权限入口。脱敏导出只处理本页已读事实,不补读或上传。</p>
<p>Project credential可选 Run authority 与可选 Worker authority 只由本机进程从彼此独立的私有文件读取。浏览器无法提交任意路径,也没有 start、stop、retry、rearm、drain、revoke 或 diagnose 权限入口。脱敏导出只处理本页已读事实,不补读或上传。</p>
</aside>
</aside>
@@ -161,7 +176,7 @@
<footer>
<span>Loopback only · explicit reads · zero polling</span>
<span>QingLong 3.0 incubation / D-369</span>
<span>QingLong 3.0 incubation / D-375</span>
</footer>
</div>
</body>
@@ -24,7 +24,7 @@ const ASSETS = Object.freeze([
name: 'index.html',
field: 'html',
maximumBytes: 32 * 1024,
digest: 'a5a3d46a8493a27b53bd4a253ef38ebaf00d204a1454f4b47a1f1ceff668855f',
digest: '363fcf2d52ff86e5b2a1c9ed8b7810226920a9270b6d0c41a1f61ce24f957832',
}),
Object.freeze({
name: 'app.css',
@@ -36,13 +36,13 @@ const ASSETS = Object.freeze([
name: 'evidence-bundle.js',
field: 'evidenceBundle',
maximumBytes: 32 * 1024,
digest: '739ff786b651de23876fc5f4df5073e211085dfdfa1d2ecb79f53d5c871c6c1d',
digest: 'ae4a08572cfc3296284c56549a3850f530474151573e2705401996730bf0466e',
}),
Object.freeze({
name: 'app.js',
field: 'javascript',
maximumBytes: 32 * 1024,
digest: '7ed994d8f2f5b151a247c5dec1d2841d45d30ff05b14dd1f41c12c5582acf9e6',
digest: '4b13da1a85d59e29a606da3b1a3327419926a496a498a06ddc323365ce5230c1',
}),
] as const);
@@ -24,6 +24,13 @@ import {
projectRunCancellationStatus,
} from '../run-management/runCancellationStatus';
import { executeClusterRunManagementCommand } from '../run-management/runManagementClient';
import { executeClusterWorkerManagementClient } from '../worker-management/workerManagementClient';
import {
createWorkerSessionInspectionCommand,
createWorkerSessionListCommand,
projectWorkerSessionInspection,
projectWorkerSessionList,
} from '../worker-management/workerManagementProduct';
import { loadClusterCopilotConsoleAssets } from './assets';
import {
CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS,
@@ -42,6 +49,7 @@ const USAGE = [
' ql3-copilot-console --check --config /absolute/client.json --credential /absolute/credential --session /absolute/session',
' 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',
'',
'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.',
@@ -54,6 +62,8 @@ interface ClusterCopilotConsoleCliArguments {
readonly networkBoundary: 'host-loopback' | 'container-published-loopback';
readonly runManagementAssertionFile?: string;
readonly runManagementConfigFile?: string;
readonly workerManagementAssertionFile?: string;
readonly workerManagementConfigFile?: string;
readonly sessionFile: string;
readonly port: number;
}
@@ -67,6 +77,7 @@ const RUN_MANAGEMENT_OPERATIONS = new Set([
'run_cancellation_blocked_list',
'run_cancellation_inspect',
]);
const WORKER_MANAGEMENT_OPERATIONS = new Set(['worker_list', 'worker_inspect']);
function usageFailure(): never {
process.stderr.write(USAGE + '\n');
@@ -105,6 +116,8 @@ export function parseClusterCopilotConsoleCliArguments(
let sessionFile: string | undefined;
let runManagementConfigFile: string | undefined;
let runManagementAssertionFile: string | undefined;
let workerManagementConfigFile: string | undefined;
let workerManagementAssertionFile: string | undefined;
let port = 0;
let portSeen = false;
let containerPublishedLoopback = false;
@@ -166,6 +179,28 @@ export function parseClusterCopilotConsoleCliArguments(
index += runManagementAssertion.consumed;
continue;
}
const workerManagementConfig = argumentValue(
argv,
index,
'--worker-management-config',
);
if (workerManagementConfig) {
if (workerManagementConfigFile !== undefined) return usageFailure();
workerManagementConfigFile = workerManagementConfig.value;
index += workerManagementConfig.consumed;
continue;
}
const workerManagementAssertion = argumentValue(
argv,
index,
'--worker-management-assertion',
);
if (workerManagementAssertion) {
if (workerManagementAssertionFile !== undefined) return usageFailure();
workerManagementAssertionFile = workerManagementAssertion.value;
index += workerManagementAssertion.consumed;
continue;
}
const portArgument = argumentValue(argv, index, '--port');
if (portArgument) {
if (portSeen || !/^(?:0|[1-9][0-9]{0,4})$/.test(portArgument.value)) {
@@ -190,6 +225,8 @@ export function parseClusterCopilotConsoleCliArguments(
sessionFile === undefined ||
(runManagementConfigFile === undefined) !==
(runManagementAssertionFile === undefined) ||
(workerManagementConfigFile === undefined) !==
(workerManagementAssertionFile === undefined) ||
(containerPublishedLoopback && port === 0) ||
(!containerPublishedLoopback && check && port !== 0)
) {
@@ -206,28 +243,26 @@ export function parseClusterCopilotConsoleCliArguments(
runManagementAssertionFile !== undefined
? { runManagementConfigFile, runManagementAssertionFile }
: {}),
...(workerManagementConfigFile !== undefined &&
workerManagementAssertionFile !== undefined
? { workerManagementConfigFile, workerManagementAssertionFile }
: {}),
sessionFile,
port,
});
}
function validateRunManagementAuthority(
parsed: Readonly<ClusterCopilotConsoleCliArguments>,
function validateManagementAuthority(
configFile: string | undefined,
assertionFile: string | undefined,
kind: 'run' | 'worker',
): boolean {
if (
parsed.runManagementConfigFile === undefined ||
parsed.runManagementAssertionFile === undefined
) {
return false;
}
validateClusterAuthenticatedManagementClientConfiguration(
parsed.runManagementConfigFile,
'run',
);
if (configFile === undefined || assertionFile === undefined) return false;
validateClusterAuthenticatedManagementClientConfiguration(configFile, kind);
let bytes: Buffer | undefined;
try {
bytes = readCanonicalFile(
parsed.runManagementAssertionFile,
assertionFile,
MAXIMUM_MANAGEMENT_ASSERTION_BYTES,
'private',
);
@@ -235,7 +270,7 @@ function validateRunManagementAuthority(
bytes.some((byte) => byte > 0x7f) ||
!MANAGEMENT_ASSERTION.test(bytes.toString('ascii'))
) {
throw new Error('invalid Run management assertion');
throw new Error('invalid management assertion');
}
return true;
} finally {
@@ -243,12 +278,16 @@ function validateRunManagementAuthority(
}
}
function availableOperations(runManagementAuthority: boolean) {
return runManagementAuthority
? CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS
: CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS.filter(
(operation) => !RUN_MANAGEMENT_OPERATIONS.has(operation),
);
function availableOperations(
runManagementAuthority: boolean,
workerManagementAuthority: boolean,
) {
return CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS.filter(
(operation) =>
(runManagementAuthority || !RUN_MANAGEMENT_OPERATIONS.has(operation)) &&
(workerManagementAuthority ||
!WORKER_MANAGEMENT_OPERATIONS.has(operation)),
);
}
function commandIdSource(requestId: string): () => string {
@@ -312,7 +351,45 @@ async function executeConsoleRead(
: projectRunCancellationInspection(result);
return Object.freeze({
schemaVersion: 1 as const,
requestId: result.requestId,
requestId: request.requestId,
result: projected as unknown as Readonly<Record<string, unknown>>,
});
}
if (
request.operation === 'worker_list' ||
request.operation === 'worker_inspect'
) {
if (
parsed.workerManagementConfigFile === undefined ||
parsed.workerManagementAssertionFile === undefined
) {
throw new Error('Worker management authority is disabled');
}
const createUuid = commandIdSource(request.requestId);
const command =
request.operation === 'worker_list'
? createWorkerSessionListCommand(
request.projectId,
request.afterWorkerId ?? undefined,
createUuid,
)
: createWorkerSessionInspectionCommand(
request.projectId,
request.workerId,
createUuid,
);
const result = await executeClusterWorkerManagementClient({
configFile: parsed.workerManagementConfigFile,
assertionFile: parsed.workerManagementAssertionFile,
command,
});
const projected =
request.operation === 'worker_list'
? projectWorkerSessionList(request.projectId, result)
: projectWorkerSessionInspection(request.projectId, result);
return Object.freeze({
schemaVersion: 1 as const,
requestId: request.requestId,
result: projected as unknown as Readonly<Record<string, unknown>>,
});
}
@@ -352,8 +429,20 @@ async function main(): Promise<void> {
const assets = loadClusterCopilotConsoleAssets(__dirname);
validateClusterCopilotClientConfiguration(parsed.configFile);
validateClusterCopilotClientCredentialFile(parsed.credentialFile);
const runManagementAuthority = validateRunManagementAuthority(parsed);
const operations = availableOperations(runManagementAuthority);
const runManagementAuthority = validateManagementAuthority(
parsed.runManagementConfigFile,
parsed.runManagementAssertionFile,
'run',
);
const workerManagementAuthority = validateManagementAuthority(
parsed.workerManagementConfigFile,
parsed.workerManagementAssertionFile,
'worker',
);
const operations = availableOperations(
runManagementAuthority,
workerManagementAuthority,
);
const sessionDigest = readSessionDigest(parsed.sessionFile);
if (parsed.check) {
try {
@@ -373,6 +462,9 @@ async function main(): Promise<void> {
runManagementAuthority: runManagementAuthority
? 'server_only'
: 'disabled',
workerManagementAuthority: workerManagementAuthority
? 'server_only'
: 'disabled',
operations,
mutation: false,
}) + '\n',
@@ -409,6 +501,9 @@ async function main(): Promise<void> {
runManagementAuthority: runManagementAuthority
? 'server_only'
: 'disabled',
workerManagementAuthority: workerManagementAuthority
? 'server_only'
: 'disabled',
operations,
mutation: false,
}) + '\n',
@@ -14,6 +14,8 @@ export const CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS = Object.freeze([
'run_cancellation_status',
'run_cancellation_blocked_list',
'run_cancellation_inspect',
'worker_list',
'worker_inspect',
'run_list',
'run_read',
'run_event_list',
@@ -46,6 +48,9 @@ export type ClusterCopilotConsoleReadRequest =
| (BaseReadRequest<'run_cancellation_blocked_list'> &
Readonly<{ cursor: string | null }>)
| (BaseReadRequest<'run_cancellation_inspect'> & Readonly<{ runId: string }>)
| (BaseReadRequest<'worker_list'> &
Readonly<{ afterWorkerId: string | null }>)
| (BaseReadRequest<'worker_inspect'> & Readonly<{ workerId: string }>)
| (BaseReadRequest<'run_list'> &
Readonly<{
afterCreatedAtMs: number | null;
@@ -243,6 +248,25 @@ export function normalizeClusterCopilotConsoleReadRequest(
runId: record.runId,
});
}
if (op === 'worker_list') {
exact(record, op, ['afterWorkerId']);
if (record.afterWorkerId !== null && !identifier(record.afterWorkerId))
invalid();
return Object.freeze({
...common(record),
operation: op,
afterWorkerId: record.afterWorkerId as string | null,
});
}
if (op === 'worker_inspect') {
exact(record, op, ['workerId']);
if (!identifier(record.workerId)) invalid();
return Object.freeze({
...common(record),
operation: op,
workerId: record.workerId,
});
}
if (op === 'run_list') {
exact(record, op, ['afterCreatedAtMs', 'afterRunId', 'limit']);
if (
@@ -476,7 +500,9 @@ export function clusterCopilotConsoleProjectReadPath(
normalized.operation === 'output' ||
normalized.operation === 'run_cancellation_status' ||
normalized.operation === 'run_cancellation_blocked_list' ||
normalized.operation === 'run_cancellation_inspect'
normalized.operation === 'run_cancellation_inspect' ||
normalized.operation === 'worker_list' ||
normalized.operation === 'worker_inspect'
)
invalid();
const project = '/api/v3/projects/' + encoded(normalized.projectId);
@@ -33,6 +33,8 @@ const OPERATIONS = Object.freeze([
'run_cancellation_status',
'run_cancellation_blocked_list',
'run_cancellation_inspect',
'worker_list',
'worker_inspect',
'run_list',
'run_read',
'run_event_list',
@@ -62,6 +64,8 @@ const REQUEST_FIELDS: Readonly<Record<EvidenceOperation, readonly string[]>> =
'requestId',
'runId',
]),
worker_list: Object.freeze(['afterWorkerId', 'projectId', 'requestId']),
worker_inspect: Object.freeze(['projectId', 'requestId', 'workerId']),
run_list: Object.freeze([
'afterCreatedAtMs',
'afterRunId',
@@ -134,6 +138,7 @@ const IDENTIFIER_DOMAINS: Readonly<Record<string, string>> = Object.freeze({
afterStepKey: 'step',
afterStepRunId: 'step',
afterTaskId: 'task',
afterWorkerId: 'worker',
artifactId: 'artifact',
attemptId: 'attempt',
contentDigest: 'digest',
@@ -142,6 +147,7 @@ const IDENTIFIER_DOMAINS: Readonly<Record<string, string>> = Object.freeze({
executionId: 'execution',
id: 'identifier',
modelId: 'model',
nextAfterWorkerId: 'worker',
outputRef: 'artifact',
packageName: 'package',
projectId: 'project',
@@ -177,6 +183,10 @@ const SAFE_CONTAINERS = new Set([
'target',
'task',
'tasks',
'declaredCapacity',
'runtimes',
'worker',
'workers',
'usage',
'workflow',
'workflows',
@@ -187,6 +197,7 @@ const SAFE_BOOLEANS = new Set([
'available',
'cancelRequested',
'enabled',
'found',
'hasMore',
'outputAvailable',
'ready',
@@ -199,8 +210,11 @@ const SAFE_ENUM_KEYS = new Set([
'assessment',
'cancelReason',
'finishReason',
'architecture',
'compatibility',
'kind',
'lastResult',
'lifecycle',
'operation',
'operatorAction',
'outcome',
@@ -208,6 +222,8 @@ const SAFE_ENUM_KEYS = new Set([
'severity',
'stage',
'status',
'supportTier',
'operatingSystem',
]);
const SAFE_ENUM_VALUES = new Set([
'accepted',
@@ -215,6 +231,13 @@ const SAFE_ENUM_VALUES = new Set([
'admission',
'attention_required',
'available',
'amd64',
'arm64',
'ppc64le',
's390x',
'arm/v7',
'arm/v6',
'386',
'blocked',
'cancelled',
'completed',
@@ -225,6 +248,22 @@ const SAFE_ENUM_VALUES = new Set([
'dispatch',
'dispatching',
'disabled',
'default_placement',
'explicit_placement_required',
'protocol_incompatible',
'online',
'draining',
'offline',
'lease_expired',
'tier1',
'candidate',
'experimental',
'legacy-only',
'linux',
'darwin',
'win32',
'freebsd',
'aix',
'enabled',
'execution',
'failed',
@@ -282,7 +321,7 @@ const SAFE_ENUM_VALUES = new Set([
'workflow',
]);
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|[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|[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;
@@ -195,6 +195,8 @@ const READ_ROUTES: Readonly<
'/api/v1/run-management/blocked-cancellations':
'run_cancellation_blocked_list',
'/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/observe/run-list': 'run_list',
'/api/v1/observe/run': 'run_read',
'/api/v1/observe/run-events': 'run_event_list',
@@ -248,6 +248,23 @@ test('normalizes Copilot and fixed Project observation operations without arbitr
}),
{ code: 'QL3_CLUSTER_COPILOT_CONSOLE_READ_REQUEST_INVALID' },
);
const workerList = normalizeClusterCopilotConsoleReadRequest({
schema: CLUSTER_COPILOT_CONSOLE_READ_REQUEST_SCHEMA,
operation: 'worker_list',
projectId: 'project-main',
requestId: 'console-worker-list',
afterWorkerId: 'worker-16',
});
assert.deepEqual(workerList, {
schema: CLUSTER_COPILOT_CONSOLE_READ_REQUEST_SCHEMA,
operation: 'worker_list',
projectId: 'project-main',
requestId: 'console-worker-list',
afterWorkerId: 'worker-16',
});
assert.throws(() => clusterCopilotConsoleProjectReadPath(workerList), {
code: 'QL3_CLUSTER_COPILOT_CONSOLE_READ_REQUEST_INVALID',
});
assert.throws(
() =>
normalizeClusterCopilotConsoleReadRequest({
@@ -617,6 +634,62 @@ test('routes only the three fixed Run management reads and validates their curso
assert.equal(reads.length, 3);
});
test('routes only fixed Worker list and inspect reads with a bounded cursor', 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/worker-management/workers',
{
schema: CLUSTER_COPILOT_CONSOLE_READ_REQUEST_SCHEMA,
operation: 'worker_list',
projectId: 'project-main',
requestId: 'console-worker-list-1',
afterWorkerId: null,
},
],
[
'/api/v1/worker-management/worker',
{
schema: CLUSTER_COPILOT_CONSOLE_READ_REQUEST_SCHEMA,
operation: 'worker_inspect',
projectId: 'project-main',
requestId: 'console-worker-inspect-1',
workerId: 'worker-a',
},
],
];
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/worker-management/workers',
headers,
body: { ...cases[0][1], afterWorkerId: 'contains space' },
});
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');
@@ -39,6 +39,29 @@ const runManagementOperations = [
'run_cancellation_blocked_list',
'run_cancellation_inspect',
];
const workerManagementOperations = ['worker_list', 'worker_inspect'];
function workerSummary(workerId = 'worker-a') {
return {
workerId,
sessionId: 'session-a',
generation: 2,
sessionVersion: 5,
lifecycle: 'online',
compatibility: 'default_placement',
architecture: 'arm64',
supportTier: 'tier1',
protocolVersion: '1.0.0',
operatingSystem: 'linux',
maxConcurrentRuns: 2,
availableSlots: 1,
registeredAtMs: 900,
lastHeartbeatAtMs: 1_050,
leaseExpiresAtMs: 2_000,
updatedAtMs: 1_050,
observedAtMs: 1_100,
};
}
function privateFile(directory, name, contents) {
const filePath = path.join(directory, name);
@@ -156,6 +179,9 @@ async function fixture(t) {
{
key: fs.readFileSync(path.join(tlsFixture, 'server-key.pem')),
cert: fs.readFileSync(path.join(tlsFixture, 'server-cert.pem')),
ca: fs.readFileSync(path.join(tlsFixture, 'ca-cert.pem')),
requestCert: true,
rejectUnauthorized: false,
minVersion: 'TLSv1.3',
maxVersion: 'TLSv1.3',
},
@@ -175,7 +201,39 @@ async function fixture(t) {
command,
});
const body =
command?.operation === 'run.cancellation.summary'
command?.operation === 'worker-session.list'
? {
schemaVersion: 1,
requestId: 'worker-transport-hidden',
result: {
schemaVersion: 1,
operation: 'worker-session.list',
observedAtMs: 1_100,
workers: [workerSummary()],
nextCursor: 'worker-a',
},
}
: command?.operation === 'worker-session.inspect'
? {
schemaVersion: 1,
requestId: 'worker-transport-hidden',
result: {
schemaVersion: 1,
operation: 'worker-session.inspect',
observedAtMs: 1_100,
worker: {
...workerSummary(),
runtimes: [{ name: 'node', version: '24.18.0' }],
declaredCapacity: {
cpuCores: 1,
memoryBytes: 268_435_456,
diskBytes: 1_073_741_824,
gpuCount: 0,
},
},
},
}
: command?.operation === 'run.cancellation.summary'
? {
schemaVersion: 1,
requestId: command.request.requestId,
@@ -268,6 +326,21 @@ async function fixture(t) {
requestTimeoutMs: 2_000,
}),
);
const workerManagementConfigFile = privateFile(
directory,
'worker-client.json',
JSON.stringify({
schemaVersion: 1,
endpoint: `https://localhost:${
server.address().port
}/api/v3/workers/management`,
servername: 'localhost',
caFile,
clientCertificateFile,
clientPrivateKeyFile,
requestTimeoutMs: 2_000,
}),
);
const sessionToken = randomBytes(32).toString('base64url');
return {
requests,
@@ -281,6 +354,12 @@ async function fixture(t) {
'run-assertion.jwt',
'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJvcGVyYXRvci0xIn0.c2lnbmF0dXJl',
),
workerManagementConfigFile,
workerManagementAssertionFile: privateFile(
directory,
'worker-assertion.jwt',
'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJvcGVyYXRvci0xIn0.c2lnbmF0dXJl',
),
};
}
@@ -291,6 +370,7 @@ test('CLI exposes deterministic help and a low-sensitive failure surface', async
' ql3-copilot-console --check --config /absolute/client.json --credential /absolute/credential --session /absolute/session',
' 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',
'',
'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.',
@@ -345,6 +425,7 @@ test('preflight proves private authority and unauthenticated TLS 1.3 readiness',
browserCredential: 'forbidden',
clusterCredential: 'server_only',
runManagementAuthority: 'disabled',
workerManagementAuthority: 'disabled',
operations: consoleOperations,
mutation: false,
});
@@ -400,6 +481,46 @@ test('preflight enables exactly three optional Run management reads only when bo
assert.doesNotMatch(incomplete.stderr, /ql3-copilot-console-cli-/);
});
test('preflight enables exactly two Worker 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,
'--worker-management-config',
value.workerManagementConfigFile,
'--worker-management-assertion',
value.workerManagementAssertionFile,
]);
assert.equal(result.status, 0, result.stderr);
const fact = JSON.parse(result.stdout);
assert.equal(fact.runManagementAuthority, 'disabled');
assert.equal(fact.workerManagementAuthority, 'server_only');
assert.deepEqual(fact.operations, [
'inspect',
'output',
...workerManagementOperations,
...consoleOperations.slice(2),
]);
assert.equal(value.requests.length, 1);
const incomplete = await runCli([
'--config',
value.configFile,
'--credential',
value.credentialFile,
'--session',
value.sessionFile,
'--worker-management-config',
value.workerManagementConfigFile,
]);
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(
@@ -426,6 +547,7 @@ test('serve mode starts an ephemeral loopback origin and shuts down cleanly', as
assert.deepEqual(started.operations, consoleOperations);
assert.equal(started.mutation, false);
assert.equal(started.runManagementAuthority, 'disabled');
assert.equal(started.workerManagementAuthority, 'disabled');
assert.equal(started.networkBoundary, 'host-loopback');
assert.equal(started.publishedHostAddress, '127.0.0.1');
const shell = await get(started.origin);
@@ -499,6 +621,72 @@ test('serve mode forwards one explicit status click through the optional mTLS Ru
assert.deepEqual(exit, { status: 0, signal: null });
});
test('serve mode performs one canonical Worker page read without exposing transport identity', async (t) => {
const value = await fixture(t);
const child = spawn(
process.execPath,
[
cliPath,
'--config',
value.configFile,
'--credential',
value.credentialFile,
'--session',
value.sessionFile,
'--worker-management-config',
value.workerManagementConfigFile,
'--worker-management-assertion',
value.workerManagementAssertionFile,
'--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.workerManagementAuthority, 'server_only');
const response = await post(
started.origin,
value.sessionToken,
'/api/v1/worker-management/workers',
{
schema: 'qinglong/cluster-copilot-console-read-request@v1',
operation: 'worker_list',
projectId: 'project-main',
requestId: 'console-worker-list-1',
afterWorkerId: null,
},
);
assert.equal(response.statusCode, 200);
assert.equal(
response.body.result.result.schema,
'qinglong/worker-session-list@v1',
);
assert.equal(response.body.result.result.count, 1);
assert.equal(response.body.result.result.nextAfterWorkerId, 'worker-a');
assert.equal(
JSON.stringify(response.body).includes('worker-transport-hidden'),
false,
);
const management = value.requests.find(
(request) => request.path === '/api/v3/workers/management',
);
assert.equal(management.method, 'POST');
assert.equal(management.command.operation, 'worker-session.list');
assert.equal(
management.command.request.inspectionId,
'console-worker-list-1',
);
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',
@@ -233,6 +233,67 @@ test('redacts optional Run management observations while preserving fixed availa
assert.doesNotMatch(JSON.stringify(bundle), /project-sensitive/);
});
test('redacts Worker identity and preserves only bounded placement and capacity facts', async () => {
const bundle = await createClusterConsoleEvidenceBundle(
[
{
operation: 'worker_list',
observedAtMs: 1_700_000_003_000,
request: {
schema: requestSchema,
operation: 'worker_list',
afterWorkerId: null,
projectId: 'project-sensitive',
requestId: 'worker-request-sensitive',
},
fact: {
schema: 'qinglong/worker-session-list@v1',
projectId: 'project-sensitive',
observedAtMs: 1_700_000_003_000,
count: 1,
workers: [
{
workerId: 'worker-sensitive',
sessionId: 'session-must-not-export',
generation: 2,
sessionVersion: 5,
lifecycle: 'online',
compatibility: 'default_placement',
architecture: 'arm64',
supportTier: 'tier1',
protocolVersion: '1.0.0',
operatingSystem: 'linux',
maxConcurrentRuns: 2,
availableSlots: 1,
lastHeartbeatAtMs: 1_700_000_002_000,
leaseExpiresAtMs: 1_700_000_004_000,
},
],
nextAfterWorkerId: 'worker-sensitive',
},
},
],
1_700_000_004_000,
webcrypto,
);
const entry = bundle.entries[0];
assert.equal(entry.operation, 'worker_list');
assert.equal(entry.target.afterWorkerId, null);
assert.equal(entry.fact.workers[0].workerId, 'worker-001');
assert.equal(entry.fact.nextAfterWorkerId, 'worker-001');
assert.equal(entry.fact.workers[0].lifecycle, 'online');
assert.equal(entry.fact.workers[0].compatibility, 'default_placement');
assert.equal(entry.fact.workers[0].architecture, 'arm64');
assert.equal(entry.fact.workers[0].supportTier, 'tier1');
assert.equal(entry.fact.workers[0].availableSlots, 1);
assert.equal(entry.fact.workers[0].sessionId, undefined);
assert.equal(entry.fact.workers[0].protocolVersion, undefined);
assert.doesNotMatch(
JSON.stringify(bundle),
/worker-sensitive|session-must-not-export|project-sensitive/,
);
});
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);
@@ -137,6 +137,16 @@ test('cross-verifies every fixed Console read operation', async (t) => {
requestId: 'q-management-inspect',
runId: 'r-management',
},
worker_list: {
afterWorkerId: null,
projectId: 'p-worker',
requestId: 'q-worker-list',
},
worker_inspect: {
projectId: 'p-worker',
requestId: 'q-worker-inspect',
workerId: 'worker-1',
},
run_list: {
afterCreatedAtMs: null,
afterRunId: null,
@@ -221,18 +231,23 @@ test('cross-verifies every fixed Console read operation', async (t) => {
},
}),
);
const bundle = await createClusterConsoleEvidenceBundle(
records,
1_700_000_001_000,
webcrypto,
);
const { filePath } = fixture(
t,
serializeClusterConsoleEvidenceBundle(bundle),
);
const result = verifyClusterConsoleEvidenceBundleFile(filePath);
assert.equal(result.status, 'verified');
assert.equal(result.bundle.entryCount, 16);
let verified = 0;
for (let index = 0; index < records.length; index += 16) {
const bundle = await createClusterConsoleEvidenceBundle(
records.slice(index, index + 16),
1_700_000_001_000 + index,
webcrypto,
);
const { filePath } = fixture(
t,
serializeClusterConsoleEvidenceBundle(bundle),
`operations-${index}.json`,
);
const result = verifyClusterConsoleEvidenceBundleFile(filePath);
assert.equal(result.status, 'verified');
verified += result.bundle.entryCount;
}
assert.equal(verified, 18);
});
test('CLI is secret-free on success, invalid input and usage errors', async (t) => {
@@ -23,6 +23,7 @@ const REQUIRED_FILES = Object.freeze([
DEPLOYMENT_ROOT + '/README.md',
DEPLOYMENT_ROOT + '/client-config.example.json',
DEPLOYMENT_ROOT + '/run-management-client-config.example.json',
DEPLOYMENT_ROOT + '/worker-management-client-config.example.json',
'deploy/containers/ql3-cluster-admin/Dockerfile',
'scripts/ql3-cluster-admin-product-live-contract.cjs',
]);
@@ -108,6 +109,8 @@ function auditClusterCopilotConsole(options = {}) {
"'run_cancellation_status'",
"'run_cancellation_blocked_list'",
"'run_cancellation_inspect'",
"'worker_list'",
"'worker_inspect'",
"'task_read'",
"'workflow_step_list'",
]);
@@ -136,6 +139,7 @@ function auditClusterCopilotConsole(options = {}) {
'maximumConcurrentRequests: 2',
"'/api/v1/copilot/inspect': 'inspect'",
"'/api/v1/run-management/cancellation-status': 'run_cancellation_status'",
"'/api/v1/worker-management/workers': 'worker_list'",
"'/api/v1/observe/run-list': 'run_list'",
"'/api/v1/observe/task-list': 'task_list'",
"'/api/v1/observe/workflow-list': 'workflow_list'",
@@ -159,6 +163,8 @@ function auditClusterCopilotConsole(options = {}) {
'--session /absolute/session',
'--run-management-config /absolute/run-client.json',
'--run-management-assertion /absolute/assertion.jwt',
'--worker-management-config /absolute/worker-client.json',
'--worker-management-assertion /absolute/assertion.jwt',
'readCanonicalFile(',
"'private'",
'validateClusterCopilotClientCredentialFile',
@@ -166,6 +172,7 @@ function auditClusterCopilotConsole(options = {}) {
'networkBoundary: parsed.networkBoundary',
"publishedHostAddress: '127.0.0.1'",
'runManagementAuthority: runManagementAuthority',
'workerManagementAuthority: workerManagementAuthority',
'mutation: false',
]);
rejectFragments(CONSOLE_ROOT + '/cli.ts', [
@@ -224,6 +231,8 @@ function auditClusterCopilotConsole(options = {}) {
'显式读取诊断内容',
'读取取消可用性',
'读取首屏 Blocked Runs',
'读取首屏 Workers',
'读取 Worker 详情',
'该只读面没有 rearm',
'模型文本是不可信内容',
'导出脱敏包',
@@ -293,6 +302,7 @@ function auditClusterCopilotConsole(options = {}) {
'thirteen exact operations',
'available vocabulary to sixteen',
'QL3_COPILOT_CONSOLE_RUN_MANAGEMENT=enabled',
'QL3_COPILOT_CONSOLE_WORKER_MANAGEMENT=enabled',
'--port=0',
'TLS 1.3 `GET /readyz`',
'excluded from small router Edge/Standalone artifacts',
@@ -439,6 +449,8 @@ function auditClusterCopilotConsole(options = {}) {
'run_cancellation_status',
'run_cancellation_blocked_list',
'run_cancellation_inspect',
'worker_list',
'worker_inspect',
'run_list',
'run_read',
'run_event_list',
@@ -13,6 +13,8 @@ const FILES = Object.freeze({
'deploy/console/ql3-cluster-copilot/host-environment.example.json',
runManagementExample:
'deploy/console/ql3-cluster-copilot/run-management-client-config.example.json',
workerManagementExample:
'deploy/console/ql3-cluster-copilot/worker-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',
@@ -87,9 +89,22 @@ function auditClusterCopilotConsoleDistribution(options = {}) {
'QL3_COPILOT_CONSOLE_RUN_MANAGEMENT-disabled',
'--run-management-config /var/run/secrets/qinglong3/copilot-console/run-management-client.json',
'--run-management-assertion /var/run/secrets/qinglong3/copilot-console/run-management-assertion.jwt',
'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_LAUNCHER_CONTRACT_DRIFT',
);
requireFragments(
'workerManagementExample',
[
'"schemaVersion": 1',
'/api/v3/workers/management',
'worker-management-client.crt',
'worker-management-client.key',
],
'QL3_COPILOT_CONSOLE_WORKER_MANAGEMENT_EXAMPLE_DRIFT',
);
rejectFragments(
'launcher',
['--privileged', '--network host', '/var/run/docker.sock', '--pull always'],
@@ -206,6 +221,7 @@ function auditClusterCopilotConsoleDistribution(options = {}) {
QL3_COPILOT_CONSOLE_PORT: '5701',
QL3_COPILOT_CONSOLE_RESOURCE_CLASS: 'compact',
QL3_COPILOT_CONSOLE_RUN_MANAGEMENT: 'disabled',
QL3_COPILOT_CONSOLE_WORKER_MANAGEMENT: 'disabled',
};
if (
environment &&
@@ -231,6 +247,8 @@ function auditClusterCopilotConsoleDistribution(options = {}) {
'share/ql3-copilot-console/host-environment.example.json',
'COPY --chmod=0444 deploy/console/ql3-cluster-copilot/run-management-client-config.example.json',
'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',
],
'QL3_COPILOT_CONSOLE_IMAGE_DISTRIBUTION_DRIFT',
);
@@ -324,6 +342,7 @@ function auditClusterCopilotConsoleDistribution(options = {}) {
kubernetesResident: false,
additionalWorkspacePackages: 0,
runManagementAuthorityDefault: 'disabled',
workerManagementAuthorityDefault: 'disabled',
externalWorkstationCeremony: 'source-tag-private-report',
ceremonyStatus: 'implementation-ready-public-release-pending',
findings: Object.freeze(findings),
@@ -31,6 +31,8 @@ test('keeps the QingLong 3.0 Copilot Console independent and read-only', () => {
'run_cancellation_status',
'run_cancellation_blocked_list',
'run_cancellation_inspect',
'worker_list',
'worker_inspect',
'run_list',
'run_read',
'run_event_list',
@@ -28,6 +28,7 @@ test('accepts the signed multi-architecture Admin OCI workstation distribution',
kubernetesResident: false,
additionalWorkspacePackages: 0,
runManagementAuthorityDefault: 'disabled',
workerManagementAuthorityDefault: 'disabled',
externalWorkstationCeremony: 'source-tag-private-report',
ceremonyStatus: 'implementation-ready-public-release-pending',
findings: [],
@@ -137,6 +137,25 @@ test('adds optional Run management files only after an explicit enabled switch',
);
});
test('adds optional Worker management files only after its independent enabled switch', (t) => {
const value = fixture(t);
const result = invoke('check', {
...value.env,
QL3_COPILOT_CONSOLE_WORKER_MANAGEMENT: 'enabled',
});
assert.equal(result.status, 0, result.stderr);
const args = fs.readFileSync(value.capture, 'utf8').trimEnd().split('\n');
assert.equal(
args[args.indexOf('--worker-management-config') + 1],
'/var/run/secrets/qinglong3/copilot-console/worker-management-client.json',
);
assert.equal(
args[args.indexOf('--worker-management-assertion') + 1],
'/var/run/secrets/qinglong3/copilot-console/worker-management-assertion.jwt',
);
assert.equal(args.includes('--run-management-config'), false);
});
test('rejects mutable, ambient and malformed host inputs before Docker', (t) => {
const value = fixture(t);
for (const environment of [
@@ -149,6 +168,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' },
]) {
const rejected = invoke('serve', environment);
assert.equal(rejected.status, 78);