mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
# QingLong 3.0 本机人工 Approval 检查与决定
|
||||
|
||||
`ql3-approval` 是 Edge/Standalone 的短生命周期 Owner CLI。它不会启动 daemon、监听端口、执行 migration、消费 Approved
|
||||
Action 或运行 Tool;未调用时资源占用为零。MCP 可帮助发现和查看脱敏 preview,但不能批准或拒绝。
|
||||
|
||||
## 前置条件
|
||||
|
||||
- QingLong 3.0 SQLite 已由正常部署流程完成 migration/readiness。
|
||||
- 当前 POSIX User 拥有私有 deployment root、Owner Pepper keyring、active User credential presentation 和 command file。
|
||||
- credential 的 User 在目标 Project 对检查具有 `approval.read` + `artifact.read`,对决定具有 `approval.decide`。
|
||||
- command、credential、数据库与 keyring 路径必须位于同一私有 deployment root;文件为当前 UID 的 `0600` regular file,目录为
|
||||
`0700`,不得使用 symlink。
|
||||
|
||||
## 第一步:检查 Approval
|
||||
|
||||
创建 `/opt/qinglong3/commands/approval-inspect.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "approval.inspect",
|
||||
"options": {
|
||||
"deploymentRoot": "/opt/qinglong3",
|
||||
"databasePath": "/opt/qinglong3/data/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/opt/qinglong3/owner-pepper-keyring",
|
||||
"credentialFilePath": "/opt/qinglong3/owner/credential.json",
|
||||
"busyTimeoutMs": 500
|
||||
},
|
||||
"request": {
|
||||
"projectId": "default",
|
||||
"approvalRequestId": "approval-1",
|
||||
"requestId": "owner-review-1",
|
||||
"auditEventId": "10000000-0000-4000-8000-000000000001",
|
||||
"failureAuditEventId": "10000000-0000-4000-8000-000000000002"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
chmod 0600 /opt/qinglong3/commands/approval-inspect.json
|
||||
ql3-approval run --command-file /opt/qinglong3/commands/approval-inspect.json
|
||||
```
|
||||
|
||||
检查输出的 Project、request/version/state/risk、requester、过期时间、preview 和完整 `expectedAction`。不要从 MCP 输出、日志或
|
||||
猜测重建 action reference/digest;下一步必须复制本次 Owner inspect 返回的整个 `expectedAction`。
|
||||
|
||||
## 第二步:批准或拒绝
|
||||
|
||||
创建新的私有文件,使用新的 request/audit/failure event ID;`expectedVersion` 固定为 1:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "approval.decide",
|
||||
"options": {
|
||||
"deploymentRoot": "/opt/qinglong3",
|
||||
"databasePath": "/opt/qinglong3/data/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/opt/qinglong3/owner-pepper-keyring",
|
||||
"credentialFilePath": "/opt/qinglong3/owner/credential.json",
|
||||
"busyTimeoutMs": 500
|
||||
},
|
||||
"request": {
|
||||
"projectId": "default",
|
||||
"approvalRequestId": "approval-1",
|
||||
"requestId": "owner-decision-1",
|
||||
"auditEventId": "20000000-0000-4000-8000-000000000001",
|
||||
"failureAuditEventId": "20000000-0000-4000-8000-000000000002",
|
||||
"expectedVersion": 1,
|
||||
"expectedAction": {
|
||||
"permission": "run.start",
|
||||
"actionType": "tool.invoke",
|
||||
"actionRef": "<copy-from-inspect>",
|
||||
"actionDigest": "<64-lowercase-hex-from-inspect>",
|
||||
"previewDigest": "<64-lowercase-hex-from-inspect>"
|
||||
},
|
||||
"decisionId": "approval-decision-1",
|
||||
"decision": "approved",
|
||||
"reasonCode": "reviewed"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
将 `decision` 改为 `rejected` 可拒绝;`reasonCode` 是稳定的低敏 snake_case 分类,不写自由文本、Secret 或个人信息。
|
||||
|
||||
```sh
|
||||
chmod 0600 /opt/qinglong3/commands/approval-decide.json
|
||||
ql3-approval run --command-file /opt/qinglong3/commands/approval-decide.json
|
||||
```
|
||||
|
||||
成功返回 `decided` 与 version 2 receipt。命令响应丢失时可用同一个 decision ID、decision、reason、User 与 exact action 重试;
|
||||
返回 `existing` 表示原决定已持久化。不要修改语义后复用 decision ID。
|
||||
|
||||
## 安全与故障处理
|
||||
|
||||
- action/preview digest 漂移:重新运行 inspect,确认新内容后创建新的 decision command;不要手工替换摘要绕过检查。
|
||||
- version/state/expiry 冲突:Approval 已变化或过期,不得强制回写;重新检查或创建新的 Approval request。
|
||||
- credential/Policy fence rejected:检查 credential 是否 active、User RoleBinding 是否仍允许 `approval.decide`。撤权后重试必须失败。
|
||||
- separation of duty:请求者与决定者相同时会失败;使用另一个具备强认证和权限的 User,不要复制 credential。
|
||||
- database/audit unavailable:先修复可写性、容量或锁等待。成功 audit 与决定同事务,不需要也不允许手工补写状态。
|
||||
- CLI 只作决定,不会 consume/dispatch/execute。决定成功后由对应的受信执行面按自己的 start barrier 和恢复协议处理。
|
||||
|
||||
Cluster 部署不运行本机 Owner CLI;其独立 mTLS/OIDC 管理面、一次性 client Job 和 PostgreSQL HA 恢复流程见
|
||||
[`ql3-cluster-approval-management.md`](./ql3-cluster-approval-management.md)。
|
||||
@@ -0,0 +1,162 @@
|
||||
# QingLong 3.0 Cluster Automation Management
|
||||
|
||||
该 operation 是 Cluster Task/Trigger 的人类管理入口,不适用于 Edge/Standalone。路由器继续使用
|
||||
短生命周期 `ql3-task` 与 `ql3-trigger`,不会因此增加 HTTPS listener、证书或 PostgreSQL Pool。
|
||||
|
||||
## 部署前置
|
||||
|
||||
1. 完成 PostgreSQL capability v53、54 条 control-core migration 与 readiness;
|
||||
2. 从 `deploy/kubernetes/ql3-cluster/operations/automation-management/config.example.yaml`
|
||||
创建私有 TLS、客户端 CA/CRL 与 public identity keyset Secret;
|
||||
3. 把 Pod template 的 CA/CRL digest sentinel 换成精确 bundle SHA-256;
|
||||
4. 在私有 overlay 中把 Admin image 的全零 digest 换成已验证 release digest;
|
||||
5. CloudNativePG 已创建 `ql3-postgres-automation-manager-auth` 与 `ql3-postgres-ca`。
|
||||
|
||||
```bash
|
||||
kubectl kustomize \
|
||||
deploy/kubernetes/ql3-cluster/operations/automation-management/cloudnative-pg \
|
||||
>/dev/null
|
||||
kubectl apply -k \
|
||||
deploy/kubernetes/ql3-cluster/operations/automation-management/cloudnative-pg
|
||||
kubectl -n qinglong3-system rollout status \
|
||||
deployment/ql3-automation-management
|
||||
```
|
||||
|
||||
基础清单不会被 `operations/kustomization.yaml` 自动引用。服务仅在 ClusterIP 8445 提供
|
||||
`POST /api/v3/automations/management`,只接受同 namespace 且带
|
||||
`qinglong.io/automation-management-client=true` label 的客户端 Pod。base 只有 DNS egress,
|
||||
CloudNativePG overlay 只增加 PostgreSQL 5432。
|
||||
|
||||
## 身份与请求
|
||||
|
||||
客户端必须同时提供受信 mTLS 证书与短期 OIDC 断言。断言固定:
|
||||
|
||||
- audience:`qinglong3-automation-management`
|
||||
- JWT type:`ql3-automation-management+jwt`
|
||||
- purpose claim:`automation-management`
|
||||
- subject:User
|
||||
- assurance:`multi_factor` 或 `hardware`
|
||||
|
||||
业务 body 只接受 schema v1 的六个操作:
|
||||
|
||||
- mutation:`task.publish`、`trigger.publish`;
|
||||
- inspection:`task.inspect`、`trigger.inspect`;
|
||||
- bounded list:`task.list`、`trigger.list`。
|
||||
|
||||
mutation 调用方必须复用同一 `requestId`、`mutationId` 和 command 重试无响应请求;收到明确 HTTP/业务
|
||||
拒绝后不得自动换 ID 重试。inspect/list 每次请求必须提供新的 UUID v4 `auditEventId`,分别要求
|
||||
`task.read` 或 `trigger.read`。读取已经产生 append-only allowed audit,因此响应丢失后应使用新的
|
||||
`requestId`/`auditEventId` 重新读取,不能把重复 audit identity 当作幂等读取。
|
||||
|
||||
inspect/list 与 mutation 使用相同的审计耐久性约束。HA 以 `synchronous_commit=remote_apply` 运行时,
|
||||
主库提升后若同步备库尚未恢复,读取会因 allowed audit 无法获得同步确认而超时并失败关闭。这不是只读
|
||||
服务故障,也不能临时降低 `synchronous_commit`;等待旧主 `pg_rewind` 并以同步只读备库重入后,再用
|
||||
新的 request/audit identity 重试。已经返回成功的读取,其 audit 已同步应用到备库。
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "task.inspect",
|
||||
"request": {
|
||||
"requestId": "operator-request-20260804-1",
|
||||
"auditEventId": "123e4567-e89b-42d3-a456-426614174010",
|
||||
"projectId": "default",
|
||||
"taskId": "daily-backup"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "trigger.list",
|
||||
"request": {
|
||||
"requestId": "operator-request-20260804-2",
|
||||
"auditEventId": "123e4567-e89b-42d3-a456-426614174011",
|
||||
"projectId": "default",
|
||||
"limit": 64,
|
||||
"after": { "triggerId": "hourly-cleanup" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
list 只接受 1–256 的 limit 与稳定 keyset cursor,不支持 offset。inspect 不存在时返回 `absent`;响应只
|
||||
返回 Task/Trigger current-head 低敏摘要和分页状态,不回显 name、description、labels、spec、command、
|
||||
cron expression、mutation、Principal、credential、assertion 或 audit identity。
|
||||
|
||||
仓库提供两个产品调用面:
|
||||
|
||||
- `ql3-automation-client`:从三个 owner-private `0600` 绝对路径读取 client config、command 和
|
||||
assertion,执行一次 TLS 1.3+mTLS 请求;
|
||||
- `operations/automation-management-client`:caller 创建的一次性 Kubernetes Job,不含 RBAC 或
|
||||
ServiceAccount token,`backoffLimit=0`,只访问 DNS 和 exact manager Pod 的 8445 端口。
|
||||
|
||||
本地/CI 的真实 PostgreSQL + HTTPS 产品门可通过以下标准入口运行;未同时提供两个 URL 时测试只做
|
||||
条件 skip,不会尝试隐式启动数据库:
|
||||
|
||||
```bash
|
||||
QL3_TEST_POSTGRES_URL='postgresql://ql3_migration:...@127.0.0.1:5432/ql3_contract' \
|
||||
QL3_TEST_POSTGRES_AUTOMATION_MANAGER_URL='postgresql://ql3_automation_manager:...@127.0.0.1:5432/ql3_contract' \
|
||||
pnpm test:automation-postgres-integration:ql3
|
||||
```
|
||||
|
||||
该门会启动两个独立 manager 验证同请求并发,再模拟第三实例已 COMMIT 但响应丢失并从另一实例精确
|
||||
重放;测试 identity adapter 只用于隔离验证 transport/repository,不构成生产 OIDC ceremony 证据。
|
||||
|
||||
### 三节点 Kubernetes + PostgreSQL HA live gate
|
||||
|
||||
完整 Cluster 纵切面是显式 opt-in,必须提供已下载并审查、且与 operator lock 一致的 CloudNativePG
|
||||
1.30.0 release manifest。它会创建随机命名的一个 K3s server、两个 agent 和独立 Docker network,
|
||||
拒绝复用同名资源,并在 `finally` 中只清理由本次 fixture 记录的容器、网络、临时目录与两张测试镜像:
|
||||
|
||||
```bash
|
||||
QL3_AUTOMATION_MANAGEMENT_KUBERNETES_LIVE=1 \
|
||||
QL3_CNPG_OPERATOR_MANIFEST_FILE=/owner-private/cloudnative-pg-1.30.0.yaml \
|
||||
pnpm test:automation-management-kubernetes-live:ql3
|
||||
```
|
||||
|
||||
该门使用锁定的 `rancher/k3s:v1.34.3-k3s1`、CloudNativePG 1.30.0 与 PostgreSQL 18.4,真实执行
|
||||
三实例引导、54 条 migration/capability v53、十三角色、双 manager、TLS 1.3/mTLS、identity/CRL
|
||||
轮换、primary deletion/promotion、数据库失联与 fresh-Pod 恢复、CNI ingress/egress 和 RBAC 拒绝。
|
||||
K3s 默认 Flannel 是分发内嵌组件,不存在可假定的三副本 `kube-flannel` DaemonSet;证据由锁定的 K3s
|
||||
分发、三个现场 Ready+唯一 PodCIDR 节点、server 的 Flannel VXLAN/subnet-manager annotation,以及
|
||||
真实跨节点正负网络探针共同构成。最终 stdout 只输出 exact-schema 低敏报告,任何 assertion、证书、
|
||||
私钥、DSN、kubeconfig 或 Secret 都会被审计器拒绝。
|
||||
|
||||
这是单 Docker host 上的应用/数据库故障门,不是生产基础设施 STONITH、Kubernetes control-plane HA、
|
||||
CSI/节点断电或真实外部 IdP 证据。路由器/低配设备不运行此门,也不因此新增常驻进程、workspace package
|
||||
或依赖闭包;夹具与 PKI helper 留在 `scripts/lib`,只在维护工作站或 CI 的 opt-in lifecycle 中加载。
|
||||
|
||||
Task source、command、environment recipe 等可能含敏感信息,因此 Kubernetes operation 的
|
||||
`ql3-automation-management-request` 必须是 immutable Secret,不能照搬 Worker client 使用 ConfigMap。
|
||||
从 `deploy/kubernetes/ql3-cluster/operations/automation-management-client/config.example.yaml`
|
||||
复制四个输入对象到私有 per-command overlay,替换 assertion、客户端证书、CA、Task/Trigger command
|
||||
以及全零 Admin image digest,然后执行:
|
||||
|
||||
```bash
|
||||
kubectl create -k \
|
||||
deploy/kubernetes/ql3-cluster/operations/automation-management-client
|
||||
kubectl -n qinglong3-system wait \
|
||||
--for=condition=Complete job/ql3-automation-management-client \
|
||||
--timeout=2m
|
||||
kubectl -n qinglong3-system logs job/ql3-automation-management-client
|
||||
```
|
||||
|
||||
Job 名称固定;再次执行前等待 TTL 清理或只删除该精确 Job 和对应 per-command immutable inputs。
|
||||
init container 只重试 `/readyz`,不会发送业务命令;main container 只执行一次。若客户端在收到响应前
|
||||
断线,mutation operator 必须先以完全相同的 `requestId`、`mutationId` 和 command 查询/重放,不得生成
|
||||
新 mutation 身份;inspect/list 则使用新的 request/audit identity 再执行一次有审计读取。
|
||||
|
||||
## 资源档位
|
||||
|
||||
生产 HA base 是 2 Pod,每 Pod PostgreSQL Pool 2、最多 32 个 TLS connection 与 16 个并发请求,
|
||||
Pod requests 为 100m CPU/128Mi。单节点开发 overlay 可以显式降为 1 Pod、Pool 1 并移除 PDB/required
|
||||
anti-affinity,但不属于生产支持矩阵。更大集群扩容前必须同时评审数据库角色 `connectionLimit`、全局
|
||||
限流、故障域和身份 ledger 竞争;禁止只增加 replica。
|
||||
|
||||
## 轮换与恢复
|
||||
|
||||
public keyset generation 只能递增。新旧 key 先 overlap,再把旧 `kid` 追加到 `revokedKids`;不能删除
|
||||
或复用已吊销 ID。CA/CRL 改动必须更新 Pod annotation 并完成零不可用 rollout。服务端 TLS 材料只在
|
||||
进程启动时加载,因此证书替换也需要滚动 Pod。任何数据库错误会撤销 readiness;恢复应由新 Pod
|
||||
重新完成 schema、identity ledger 和 TLS 校验,不能通过跳过 readiness 强行开放业务路由。
|
||||
@@ -0,0 +1,51 @@
|
||||
# QingLong 3.0 CloudNativePG DR 发布证据
|
||||
|
||||
正式 `v3.*` 镜像发布必须从候选提交手动运行
|
||||
`ql3-cloudnativepg-dr-live.yml`。该演练需要至少 35 GiB 可用空间,并使用仓库锁定的
|
||||
Kubernetes、CloudNativePG、PostgreSQL、Barman 与 cert-manager 版本。live runner
|
||||
必须设置 `QL3_SOURCE_REVISION` 为候选提交 SHA;产出的 JSON 报告权限必须为 `0600`。
|
||||
|
||||
35 GiB 是硬前置而不是建议值。GitHub workflow 和 Node live runner 会分别检查;Node 入口在创建
|
||||
临时目录、Docker network/container 或 K3s 数据前检查 `os.tmpdir()` 所在文件系统,空间不足立即退出。
|
||||
不得通过修改脚本、换用较小阈值或全局 `docker volume prune` 绕过:先识别并清理本次 gate 自己带
|
||||
`ql3-barman-dr-*` 前缀/label 的可重建资源,无法证明归属的共享匿名卷必须保留。容量失败不生成报告,
|
||||
也不能把静态审计或 PostgreSQL HA contract 的成功当作 latest/PITR 恢复证据。
|
||||
|
||||
报告证明的不只是 operator readiness。它必须同时覆盖:
|
||||
|
||||
- 连续 WAL 归档与完整 base backup;
|
||||
- latest restore 和位于两个持久 marker 之间的 PITR;
|
||||
- 恢复后的 schema、最小权限角色、同步提交与源集群隔离;
|
||||
- object-store writer/reader authority、版本化、不可变性与生命周期;
|
||||
- Barman client/server 证书轮换后继续完成 WAL、backup、latest restore 和 PITR;
|
||||
- 数据库与应用层 RPO/RTO 目标。
|
||||
|
||||
## 私有交接合同
|
||||
|
||||
受控流程把审计后的报告放入发布 runner 的私有挂载:
|
||||
|
||||
```text
|
||||
/run/qinglong3-release-evidence/<GITHUB_SHA>/cloudnativepg-dr-evidence.json
|
||||
```
|
||||
|
||||
目录必须为 `0700`,报告必须为普通、非符号链接、`0600` 文件。runner 使用
|
||||
`ql3-release-evidence-ephemeral` 标签和 `ql3-production-release-evidence`
|
||||
environment;执行后销毁,不缓存或上传私有证据。
|
||||
|
||||
`ql3-image-release.yml` 的 `cluster-dr-release-evidence` Job 会先重新运行
|
||||
CloudNativePG backup、Barman supply-chain 和 cert-manager selection 静态审计,再执行:
|
||||
|
||||
```bash
|
||||
pnpm gate:cloudnativepg-dr-release:ql3 -- \
|
||||
--report="/run/qinglong3-release-evidence/${GITHUB_SHA}/cloudnativepg-dr-evidence.json" \
|
||||
--source-commit="${GITHUB_SHA}" \
|
||||
--release-version="${RELEASE_VERSION}"
|
||||
```
|
||||
|
||||
门禁要求 source commit 精确相等、证据不超过 24 小时,且最多容忍 5 分钟未来时钟
|
||||
偏差。`publish` 对该 Job 使用硬 `needs`;未通过时,发布矩阵不会获得 registry package
|
||||
写权限或 GitHub OIDC token。
|
||||
|
||||
仓库内 CloudNativePG、Barman 与 cert-manager 静态锁仍保持
|
||||
`releaseReady: false`。版本/digest 锁只能证明被评审的供应链输入,不能替代当前候选
|
||||
提交上的真实备份、恢复与证书轮换证据。
|
||||
@@ -0,0 +1,183 @@
|
||||
# QingLong 3.0 Cluster Approval Management
|
||||
|
||||
这是 Cluster 中供人类检查并批准/拒绝 Approval 的独立管理面。它不是 MCP Tool、调度器或执行器,也不会 consume、dispatch 或
|
||||
执行已批准动作。Edge/Standalone 与路由器用户继续使用短生命周期 `ql3-approval`,不会加载本服务、证书、OIDC 或
|
||||
PostgreSQL Pool。
|
||||
|
||||
## 部署前置
|
||||
|
||||
1. PostgreSQL 已完成 54 条 control-core migration、capability v53 和正常 readiness;CloudNativePG 已创建
|
||||
`ql3_approval_manager` 与 `ql3-postgres-approval-manager-auth`。
|
||||
2. 从
|
||||
`deploy/kubernetes/ql3-cluster/operations/approval-management/config.example.yaml`
|
||||
复制 TLS、client CA/CRL 和 public identity keyset 到私有 overlay,替换所有 placeholder。
|
||||
3. 将 Deployment 的 client CA/CRL annotation sentinel 替换为 exact bundle SHA-256,并把 Admin image 的全零 digest 替换为
|
||||
独立验证的 release digest。
|
||||
4. 先渲染并审查,再显式部署;该 operation 不在共享 `operations/kustomization.yaml` 中。
|
||||
|
||||
```bash
|
||||
kubectl kustomize \
|
||||
deploy/kubernetes/ql3-cluster/operations/approval-management/cloudnative-pg \
|
||||
>/dev/null
|
||||
kubectl apply -k \
|
||||
deploy/kubernetes/ql3-cluster/operations/approval-management/cloudnative-pg
|
||||
kubectl -n qinglong3-system rollout status \
|
||||
deployment/ql3-approval-management
|
||||
```
|
||||
|
||||
服务仅在 ClusterIP 8447 提供 `POST /api/v3/approvals/management`。base 只允许 DNS egress;CloudNativePG overlay 只允许到
|
||||
`cnpg.io/cluster=ql3-postgres` Pod 的 TCP 5432。只有带
|
||||
`qinglong.io/approval-management-client=true` label 的同 namespace Pod 能访问 8447。
|
||||
|
||||
## 身份约束
|
||||
|
||||
客户端必须同时提供未被 CRL 吊销的 mTLS client certificate 和短期 OIDC assertion:
|
||||
|
||||
- audience:`qinglong3-approval-management`
|
||||
- JWT type:`ql3-approval-management+jwt`
|
||||
- purpose:`approval-management`
|
||||
- subject:User
|
||||
- assurance:`multi_factor` 或 `hardware`
|
||||
|
||||
不得使用 Project API bearer、浏览器 Session、Worker credential、Kubernetes ServiceAccount token 或其他 management plane 的
|
||||
assertion。服务会在进入领域服务前和提交前重新认证;身份、assurance 或 Policy fence 漂移会失败关闭并写入低敏审计。
|
||||
|
||||
## 第一步:检查 Approval
|
||||
|
||||
命令必须是 exact schema;每次检查使用新的 request、success audit 与 failure audit ID:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "approval.inspect",
|
||||
"request": {
|
||||
"projectId": "default",
|
||||
"approvalRequestId": "approval-1",
|
||||
"requestId": "cluster-review-1",
|
||||
"auditEventId": "10000000-0000-4000-8000-000000000001",
|
||||
"failureAuditEventId": "10000000-0000-4000-8000-000000000002"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
成功结果包含 Approval version/state/risk/requester、过期时间、redacted preview 和完整 `expectedAction`。复制整个
|
||||
`expectedAction` 到决定命令;不要从日志、MCP 输出或内部 ID 猜测 action reference/digest。
|
||||
|
||||
## 第二步:批准或拒绝
|
||||
|
||||
`expectedVersion` 固定为 1。决定必须使用新的 request/audit ID 和稳定 decision ID:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "approval.decide",
|
||||
"request": {
|
||||
"projectId": "default",
|
||||
"approvalRequestId": "approval-1",
|
||||
"requestId": "cluster-decision-1",
|
||||
"auditEventId": "20000000-0000-4000-8000-000000000001",
|
||||
"failureAuditEventId": "20000000-0000-4000-8000-000000000002",
|
||||
"expectedVersion": 1,
|
||||
"expectedAction": {
|
||||
"permission": "run.start",
|
||||
"actionType": "tool.invoke",
|
||||
"actionRef": "<copy-from-inspect>",
|
||||
"actionDigest": "<64-lowercase-hex-from-inspect>",
|
||||
"previewDigest": "<64-lowercase-hex-from-inspect>"
|
||||
},
|
||||
"decisionId": "approval-decision-1",
|
||||
"decision": "approved",
|
||||
"reasonCode": "reviewed"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
将 `decision` 改为 `rejected` 可拒绝。`reasonCode` 只允许稳定、低敏的 snake_case 分类,不写自由文本、Secret 或个人信息。
|
||||
成功返回 `decided` 与 version 2;同语义精确重放返回 `existing`。
|
||||
|
||||
## 使用一次性 Kubernetes Client Job
|
||||
|
||||
复制
|
||||
`deploy/kubernetes/ql3-cluster/operations/approval-management-client/config.example.yaml`
|
||||
到私有 per-command overlay,替换 client config、一个 command、短期 assertion、client certificate/key 与 server CA。不要把私有
|
||||
overlay 提交到仓库。
|
||||
|
||||
再创建一个只引用仓库 operation 并替换全零镜像 digest 的私有 Kustomization:
|
||||
|
||||
```yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- /absolute/path/to/qinglong/deploy/kubernetes/ql3-cluster/operations/approval-management-client
|
||||
|
||||
images:
|
||||
- name: registry.example.com/qinglong/qinglong3-cluster-admin
|
||||
newName: registry.example.com/qinglong/qinglong3-cluster-admin
|
||||
digest: sha256:REPLACE_WITH_REVIEWED_PRODUCTION_DIGEST
|
||||
```
|
||||
|
||||
```bash
|
||||
kubectl create -f /absolute/private/approval-command-inputs.yaml
|
||||
kubectl create -k /absolute/private/approval-client-overlay
|
||||
kubectl -n qinglong3-system wait \
|
||||
--for=condition=Complete job/ql3-approval-management-client \
|
||||
--timeout=150s
|
||||
kubectl -n qinglong3-system logs \
|
||||
job/ql3-approval-management-client --container=client
|
||||
```
|
||||
|
||||
Job 无 RBAC 和 ServiceAccount token,`backoffLimit: 0`。init container 只重试 TLS 1.3 `/readyz`;main container 只执行一次
|
||||
业务请求。固定 Job 名再次使用前,应等待 TTL 清理或只删除该精确 Job 与本次 immutable inputs。
|
||||
|
||||
也可在受控管理工作站直接运行:
|
||||
|
||||
```bash
|
||||
ql3-approval-client \
|
||||
--config=/absolute/private/client.json \
|
||||
--command=/absolute/private/command.json \
|
||||
--assertion=/absolute/private/assertion.jwt
|
||||
```
|
||||
|
||||
三个参数和它们引用的证书/key 都必须是当前 UID 拥有、不可经 symlink 到达的私有 regular file。若 endpoint 仍是 ClusterIP,
|
||||
工作站必须通过受审的私网入口;不要临时把 Service 改为公共 LoadBalancer。
|
||||
|
||||
## 重试、故障与恢复
|
||||
|
||||
- inspect 响应丢失:用新的 request/audit/failure audit ID 重读,因为 allowed read audit 已可能提交。
|
||||
- decide 响应丢失:只用同一 decision ID、decision、reason、User、expected version 与完整 action binding 重放;不要生成新决定。
|
||||
- binding/version/state/expiry 冲突:重新 inspect 并重新人工审查,不得手工替换 digest。
|
||||
- authentication/Policy fence 失败:检查 assertion lifetime、assurance、client CRL、RoleBinding 与 active credential;撤权后必须继续失败。
|
||||
- database/audit unavailable:readiness 会撤销,Pod 有界 drain 并退出。等待 CloudNativePG 恢复可写主库和同步确认后,由 fresh Pod
|
||||
完整通过 schema/role/keyset readiness;不得降低 `synchronous_commit` 或跳过 readiness。
|
||||
- CA/CRL 或 server certificate 变化:更新 annotation digest,完成 `maxUnavailable: 0` 的滚动更新。keyset generation 只前进,先
|
||||
overlap 再 append revoke,不能复用或删除已撤销 kid。
|
||||
|
||||
## 资源档位
|
||||
|
||||
生产 base 为 2 Pod,每 Pod 最多 2 条 PostgreSQL connection、32 个 TLS connection、16 个并发请求;requests 为
|
||||
50m CPU/96Mi,limits 为 1 CPU/384Mi。单节点开发 overlay 可明确降为 1 Pod/Pool 1 并移除 PDB/required anti-affinity,但该形态
|
||||
不属于 HA 支持矩阵。扩容必须同步评审 database role connection limit、transport quota、identity ledger contention 与故障域,
|
||||
不能只增加 replica。
|
||||
|
||||
## Kubernetes 多节点发布门
|
||||
|
||||
生产发布前可显式运行三节点 K3s、三实例 CloudNativePG 与双 Approval manager 的 live contract。该门不会自动进入普通 CI,也不适用于 Edge/Standalone:
|
||||
|
||||
```bash
|
||||
QL3_APPROVAL_MANAGEMENT_KUBERNETES_LIVE=1 \
|
||||
QL3_KUBECTL_BIN=/absolute/path/to/kubectl \
|
||||
QL3_CNPG_OPERATOR_MANIFEST_FILE=/absolute/path/to/cnpg-1.30.0.yaml \
|
||||
pnpm test:approval-management-kubernetes-live:ql3 \
|
||||
--report=/absolute/private/approval-management-live-report.json
|
||||
|
||||
pnpm audit:approval-management-kubernetes-live:ql3 \
|
||||
--report=/absolute/private/approval-management-live-report.json
|
||||
```
|
||||
|
||||
operator manifest 必须是已审查的 CloudNativePG 1.30.0 完整文件;CI 使用 SHA-256
|
||||
`f8bede43fe4ee0d478c2355b204a36876b2ae4faac60f2a9452280b293da3b88`。report 路径必须为已存在私有目录下的绝对新路径,runner 拒绝覆盖,并以 `0600` 写入。完整证据只存在 report,不进入 stdout。
|
||||
|
||||
门禁验证跨节点 anti-affinity、mTLS/OIDC、identity overlap/revoke/rollback、client CRL、CloudNativePG primary failover、数据库断连 readiness fence、fresh Pod recovery、CNI NetworkPolicy、RBAC deny 和 durable Approval/audit。它不证明生产 control-plane HA、外部 IdP 或基础设施 STONITH;这些限制必须和报告一起保留。
|
||||
|
||||
仓库提供手工 workflow `.github/workflows/ql3-approval-management-live.yml`。成功后还必须确认 `ql3-approval-live-*` Docker container/network 均为空。
|
||||
@@ -0,0 +1,65 @@
|
||||
# QingLong 3.0 Cluster Plugin Package Workflow
|
||||
|
||||
Cluster Workflow 入口由既有 `ql3-cluster-control` `/api/v3` mTLS listener 提供。不要启动额外 Workflow
|
||||
daemon,也不要向 `cluster-admin` 暴露 runtime PostgreSQL credential。
|
||||
|
||||
## 前置条件
|
||||
|
||||
- `cluster-control` readiness 已通过并已安装 admission;
|
||||
- 客户端通过部署受信 CA 的 mTLS 校验;
|
||||
- Bearer API credential 绑定 active User/API app/MCP client/Agent;
|
||||
- subject 在目标 Project 的当前 RoleBinding 具有 `run.read` 或 `run.start`;
|
||||
- Package install、lifecycle、publication 与目标 Workflow 均为 active,且未 quarantine/revoke。
|
||||
|
||||
Bearer 格式为 `ql3c_<credentialId>_<43-char-base64url-secret>`。不要把 token 写入命令历史、URL、日志或
|
||||
工单;示例中的 `$QL3_API_TOKEN` 应由进程私有 secret provider 注入。
|
||||
|
||||
## 检查 Workflow
|
||||
|
||||
```sh
|
||||
curl --fail-with-body \
|
||||
--cacert /run/secrets/ql3/ca.pem \
|
||||
--cert /run/secrets/ql3/client.pem \
|
||||
--key /run/secrets/ql3/client-key.pem \
|
||||
-H "Authorization: Bearer $QL3_API_TOKEN" \
|
||||
"https://cluster-control.example.test:7443/api/v3/projects/default/packages/example-automation/workflows"
|
||||
```
|
||||
|
||||
结果只包含 publication state 与 Workflow/Step/Task metadata。保存目标 Workflow 和所有 Step `id`;不要从
|
||||
Package bundle 手工拼 generation、revision 或 digest。
|
||||
|
||||
## 启动 Workflow
|
||||
|
||||
为 plan、Run 和每个 StepRun 分别生成 UUID v4;`stepRunIds` key 必须与检查结果的 Step `id` 精确一致:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "qinglong/cluster-plugin-package-workflow-start-request@v1",
|
||||
"planId": "123e4567-e89b-42d3-a456-426614174000",
|
||||
"runId": "123e4567-e89b-42d3-a456-426614174001",
|
||||
"stepRunIds": {
|
||||
"collect": "123e4567-e89b-42d3-a456-426614174002",
|
||||
"summarize": "123e4567-e89b-42d3-a456-426614174003"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
curl --fail-with-body \
|
||||
--cacert /run/secrets/ql3/ca.pem \
|
||||
--cert /run/secrets/ql3/client.pem \
|
||||
--key /run/secrets/ql3/client-key.pem \
|
||||
-H "Authorization: Bearer $QL3_API_TOKEN" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-binary @workflow-start.json \
|
||||
"https://cluster-control.example.test:7443/api/v3/projects/default/packages/example-automation/workflows/daily/runs"
|
||||
```
|
||||
|
||||
HTTP 201/`created` 表示 admission 已耐久提交,不表示 Workflow 已完成;唯一 scheduler cadence 会继续推进
|
||||
frontier 与 Task Attempt。响应丢失时必须原样重放相同 body,尤其不能重新生成 plan/Run/StepRun UUID;
|
||||
HTTP 200/`existing` 返回同一 receipt。每次 HTTP 尝试有 request audit,首次成功 mutation 只有一条与 plan
|
||||
原子提交的 `workflow.start` audit。
|
||||
|
||||
409 `authorization_fence_changed` 表示 credential、Identity、Project 或 RoleBinding 已变化;重新认证和读取
|
||||
当前授权后才能表达新请求。409 `workflow_start_conflict` 表示 UUID 已绑定其他语义或当前 Package evidence
|
||||
漂移。404 不应通过重试绕过 Package lifecycle/quarantine/revocation。
|
||||
@@ -0,0 +1,103 @@
|
||||
# QingLong 3.0 镜像 OS 漏洞门
|
||||
|
||||
本流程覆盖 `control|admin|local × amd64|arm64` 六个 release candidate 的 Debian/base image package,不替代 npm
|
||||
production dependency audit、CycloneDX、BuildKit SBOM/provenance、Cosign 或 GitHub attestation。
|
||||
|
||||
## 日常检查
|
||||
|
||||
```sh
|
||||
pnpm audit:image-os-vulnerability-policy:ql3
|
||||
pnpm audit:image-release:ql3
|
||||
pnpm audit:cluster-deployment:ql3
|
||||
```
|
||||
|
||||
默认 exception policy 位于 `deploy/containers/ql3-os-vulnerability-exceptions.json`,当前必须返回:
|
||||
|
||||
```json
|
||||
{"schemaVersion":1,"fixture":"qinglong/image-os-vulnerability-exceptions@v1","compatible":true,"exceptionCount":0,"imageExceptionCounts":{"admin":0,"control":0,"local":0}}
|
||||
```
|
||||
|
||||
真实 scan 在 QL3 CI 已有六个 native build 后执行;受保护 release 的六个 native job 各自只构建一次 OCI layout tar,
|
||||
Trivy 直接扫描该 tar。扫描成功后才解包、审计并上传同一 OCI graph;publisher 只合并 amd64/arm64 graph,不再 build。
|
||||
只有六份 artifact 和 D-236 私密 evidence job 全部成功,publisher 才会启动。scanner/DB 网络故障与漏洞命中使用相同
|
||||
的失败关闭结果,但日志会区分下载错误和 finding。
|
||||
|
||||
## Build-once 发布链
|
||||
|
||||
每个 `control|admin|local × amd64|arm64` native job 固定执行:
|
||||
|
||||
1. Buildx 用 exact Dockerfile、source revision、SBOM 与 provenance 生成
|
||||
`${RUNNER_TEMP}/ql3-native/image.oci.tar`;
|
||||
2. Trivy 以 `input` 扫描该 tar,禁止 daemon/candidate tag;
|
||||
3. 扫描成功后解包到 `layout/`,repository recorder 复验 platform、source、SBOM/provenance 和全部 digest,写入
|
||||
`evidence.json`,随后删除 tar;
|
||||
4. 以 `run_id/run_attempt/image/arch` 唯一命名上传 `layout + evidence`,保留 1 天且禁止 overwrite。
|
||||
|
||||
publisher 的固定顺序是:下载同一 run attempt 的两份 native artifact → 本地重验并确定性 merge → 生成最终 OS
|
||||
vulnerability predicate → checksum 验证 regctl → 登录 GHCR → `IMAGE@DIGEST` import → 读取远端 digest 比对 →
|
||||
Cosign/SLSA/CycloneDX/OS-vulnerability attest 与 verify → manifest/rollout verify → 最后创建 version 与完整 commit tag。
|
||||
publisher 不得安装 Buildx/QEMU、运行 Dockerfile 或在 tag promotion 后追加 step。
|
||||
|
||||
## 创建短期例外
|
||||
|
||||
优先升级 digest-pinned Node base。只有当前没有可部署修复且风险被明确接受时,才能在 central JSON 的
|
||||
`exceptions` 中增加一项:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "CVE-2026-12345",
|
||||
"images": ["admin", "control"],
|
||||
"purls": ["pkg:deb/debian/libssl3@3.0.0-1"],
|
||||
"owner": "security/platform",
|
||||
"ticket": "QLSEC-123",
|
||||
"expiresOn": "2026-08-15",
|
||||
"rationale": "Temporary exposure accepted while the fixed base image is qualified."
|
||||
}
|
||||
```
|
||||
|
||||
约束:
|
||||
|
||||
- entries 按 `id` 严格升序,ID 唯一;images/purls 各自唯一且升序;
|
||||
- 只接受 `CVE-*` 与 `pkg:apk|deb|rpm`,不能屏蔽 npm/library finding;
|
||||
- `expiresOn` 必须晚于 UTC 当天且最多 30 天,到期当天自动失败;
|
||||
- owner、ticket、rationale 缺一不可;ticket 必须先完成独立安全复核;
|
||||
- 不得使用 `ignore-unfixed=true`、裸 `.trivyignore`、skip path、allow-all Rego 或无期限 VEX 代替本策略。
|
||||
|
||||
提交前执行:
|
||||
|
||||
```sh
|
||||
node --test test/back/ql3ImageOsVulnerabilityPolicy.test.cjs
|
||||
node --test test/back/ql3ClusterImageReleaseAudit.test.cjs
|
||||
pnpm audit:image-os-vulnerability-policy:ql3
|
||||
pnpm audit:image-release:ql3
|
||||
```
|
||||
|
||||
策略生成器会为每个 image 创建临时 `.trivyignore.yaml`;Trivy action 会把该低敏 exception view 显示在 job log。
|
||||
不得在 rationale、owner 或 ticket 中写 credential、内部 URL、个人数据或 Secret。
|
||||
|
||||
## 升级 scanner 或基础镜像
|
||||
|
||||
1. 阅读 Trivy 官方 release 与 security advisory,确认目标版本不在已知暴露窗口;
|
||||
2. 解析 signed/immutable action release 到完整 commit SHA,审查 composite action 的所有 nested action pin;
|
||||
3. 固定 scanner exact version,禁止 `latest`;保持 cache false、OS-only、unfixed 不忽略;
|
||||
4. 对 Node base 的多架构 manifest digest 执行签名/来源核验;三个 production Dockerfile 必须同步更新 exact digest;
|
||||
5. 更新 ADR、静态审计 expectation 与 mutation tests;执行六个 native scan 后才能发布;
|
||||
6. 修复后删除已不需要的 exceptions,不等待 `expiresOn`。
|
||||
|
||||
## 失败恢复
|
||||
|
||||
- native job 失败或 artifact 未生成:重新运行整个 release;不要手工上传 artifact 或把旧 run artifact 改名复用;
|
||||
- artifact/evidence/layout digest 不一致:视为供应链完整性失败,从 native build 重新开始,不通过重新打 tar 修复;
|
||||
- regctl checksum 或下载失败:禁止回退到未固定 binary、`docker push` 或先推 candidate tag;
|
||||
- exact digest 已导入但证明/验证失败:不要创建 tag。保留 digest 用于调查,修复后由新 run 重建;
|
||||
- tag promotion 失败:确认目标 tag 没有指向其他 digest;不得覆盖不一致 tag来“完成”发布;
|
||||
- 清理 runner 时只删除该 job 的 `${RUNNER_TEMP}/ql3-native`、`${RUNNER_TEMP}/native` 和 merge 输出,不能删除
|
||||
workspace、D-236 owner-private source 或共享 Docker/Kind 资源。
|
||||
|
||||
## 当前证据边界
|
||||
|
||||
仓库已证明 policy、workflow、base pin、build-once/scanned-digest merge 和 fail-closed mutation contract。本机尝试
|
||||
真实 OCI attested build 时,Docker container driver 获取 BuildKit SBOM scanner 遇到网络超时并已中止,临时目录已
|
||||
删除,因此没有本地 live CVE clean 结论。首次 GitHub-hosted 六矩阵成功记录和首个 evidence-backed GHCR exact-digest
|
||||
publish 仍属于外部 Release Gate。control/admin 的 exact Dockerfile 已在本机 arm64 成功构建并确认 `10001:10001`,
|
||||
对应临时 image 随后已删除;这只证明 build/base pin,不替代漏洞数据库扫描。
|
||||
@@ -0,0 +1,178 @@
|
||||
# QingLong 3.0 本机 AI Feature 操作手册
|
||||
|
||||
本手册只适用于 `edge` 和 `standalone`。Cluster 不得使用本机 Owner command 代替
|
||||
TLS identity、平台 Policy、quota 和职责分离。
|
||||
|
||||
## 部署与启动边界
|
||||
|
||||
基础 edge/standalone application 制品不安装 `@qinglong/ai`。需要本机 AI 时,部署
|
||||
显式的 `edge-application-ai` 或 `standalone-application-ai` 制品。ADR-0178 已提供
|
||||
`ql3-local-application` headless executable 和统一 product composition,但通用 CLI
|
||||
当前只直接支持 `deployment=excluded`;installed AI 必须由受信 host 注入 provider
|
||||
authority。不要把 token 写入启动配置来绕过这道门。受审 composition 为:
|
||||
|
||||
```text
|
||||
@qinglong/local-application/ai-feature
|
||||
```
|
||||
|
||||
不要改为调用基础 `@qinglong/local-application` 后自行加载 provider。受审入口只在
|
||||
9007 head 为 `active` 且完整 AI schema/history/checksum 通过时动态加载 AI;
|
||||
deployment excluded、schema absent 和 inactive 都不会加载 provider。
|
||||
|
||||
基础进程配置、SIGTERM drain 和低敏日志见
|
||||
[本机 Headless Application 手册](./ql3-local-application-process.md)。
|
||||
|
||||
管理命令不会向常驻进程注入代码,也不包含 watcher:
|
||||
|
||||
- `inspect` 成功返回 `runtimeAction: "none"`;
|
||||
- `activate`、`deactivate` 及其 exact replay 成功返回
|
||||
`runtimeAction: "restart_required"`。
|
||||
|
||||
activate 后必须重启应用,重启成功且 AI 状态为 `active` 才表示 provider 已装配。
|
||||
deactivate 提交后数据库 admission fence 已立即生效;当前进程下一次 AI 操作会拒绝
|
||||
请求并进入 drain。完成有界 drain 后重启应用,确认 AI 状态为 `inactive` 且 provider
|
||||
loader 为零,才完成可验证卸载。
|
||||
|
||||
## 安全前置
|
||||
|
||||
1. 使用 QingLong 部署用户执行,不使用远程 HTTP、聊天输入或公共目录传递命令。
|
||||
2. command 目录权限设为 `0700`,command file 设为 `0600`。
|
||||
3. 准备当前 local-console credential presentation 和 Owner pepper keyring。
|
||||
4. 对已有数据库先完成 SQLite 文件及 `-wal`/`-shm` 一致性备份,并记录备份制品的
|
||||
SHA-256;新库可使用 `fresh_database`。
|
||||
5. 不手工修改 `QingLong3AiSchemaMigrations`、feature transition/head 或 AI 业务表。
|
||||
|
||||
以下路径必须替换成当前部署的规范化绝对路径。UUID 必须为新的 v4 UUID。
|
||||
|
||||
## 1. Inspect
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "ai-feature.inspect",
|
||||
"options": {
|
||||
"deploymentRoot": "/opt/qinglong",
|
||||
"databasePath": "/opt/qinglong/data/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/opt/qinglong/config/owner-keys",
|
||||
"credentialFilePath": "/opt/qinglong/private/owner-credential.json"
|
||||
},
|
||||
"request": {
|
||||
"requestId": "ai-feature-inspect-20260727",
|
||||
"failureAuditEventId": "00000000-0000-4000-8000-000000000001"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
ql3-ai-feature run --command-file /opt/qinglong/private/ai-feature-inspect.json
|
||||
```
|
||||
|
||||
保存输出中的 `migrationPlanDigest`。`schemaState`:
|
||||
|
||||
- `absent`:没有 AI feature schema;
|
||||
- `partial_or_drifted`:可能是中断的 reviewed migration,也可能是漂移;不要手工修表;
|
||||
- `ready`:9001–9007 history/checksum 和全部本机 AI 表已通过只读核对。
|
||||
|
||||
## 2. 首次启用
|
||||
|
||||
新库使用:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "ai-feature.activate",
|
||||
"options": {
|
||||
"deploymentRoot": "/opt/qinglong",
|
||||
"databasePath": "/opt/qinglong/data/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/opt/qinglong/config/owner-keys",
|
||||
"credentialFilePath": "/opt/qinglong/private/owner-credential.json"
|
||||
},
|
||||
"request": {
|
||||
"requestId": "ai-feature-activate-20260727",
|
||||
"failureAuditEventId": "00000000-0000-4000-8000-000000000002",
|
||||
"mutationId": "ai-feature-activate-20260727",
|
||||
"expectedGeneration": 0,
|
||||
"expectedState": null,
|
||||
"expectedMigrationDigest": "REPLACE_WITH_INSPECTED_PLAN_DIGEST",
|
||||
"safety": {
|
||||
"mode": "fresh_database",
|
||||
"backupEvidenceDigest": null
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
已有库把 safety 改为:
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "backup_verified",
|
||||
"backupEvidenceDigest": "REPLACE_WITH_64_HEX_BACKUP_EVIDENCE_DIGEST"
|
||||
}
|
||||
```
|
||||
|
||||
命令中断时,保留同一私有 command file 原样重试。不得生成新 mutation 来“绕过”
|
||||
partial schema。只有返回 `schemaState=ready` 且 `activation.state=active` 后,价格管理和
|
||||
新 ModelInvocation admission 才可用。确认响应中的
|
||||
`runtimeAction="restart_required"`,随后重启 AI application 制品;不要等待后台
|
||||
watcher,因为系统不会创建 watcher。
|
||||
|
||||
## 3. 非破坏性停用
|
||||
|
||||
先停止产生新 AI 请求并等待现有 invocation 终结。使用 inspect 返回的 generation:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "ai-feature.deactivate",
|
||||
"options": {
|
||||
"deploymentRoot": "/opt/qinglong",
|
||||
"databasePath": "/opt/qinglong/data/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/opt/qinglong/config/owner-keys",
|
||||
"credentialFilePath": "/opt/qinglong/private/owner-credential.json"
|
||||
},
|
||||
"request": {
|
||||
"requestId": "ai-feature-deactivate-20260727",
|
||||
"failureAuditEventId": "00000000-0000-4000-8000-000000000003",
|
||||
"mutationId": "ai-feature-deactivate-20260727",
|
||||
"expectedGeneration": 1,
|
||||
"expectedState": "active",
|
||||
"expectedMigrationDigest": "REPLACE_WITH_INSPECTED_PLAN_DIGEST",
|
||||
"safety": {
|
||||
"mode": "preserve_existing",
|
||||
"backupEvidenceDigest": null
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
出现 `LOCAL_AI_FEATURE_IN_FLIGHT_INVOCATION` 时不要删行;等待 completion/recovery
|
||||
收敛后原样重试。成功停用不会删除 schema、价格、usage、quota、invocation 或审计
|
||||
事实。确认 `runtimeAction="restart_required"`,等待当前进程有界 drain,再重启并
|
||||
inspect;重启后的 AI 状态必须为 `inactive`。
|
||||
|
||||
## 4. 重新启用
|
||||
|
||||
重新 inspect,使用当前 inactive generation、`expectedState: "inactive"` 和新的
|
||||
mutation/request/audit ID。已有 durable data 必须使用 `backup_verified`,不能再声明
|
||||
`fresh_database`。
|
||||
|
||||
## 5. 失败处理
|
||||
|
||||
- `...COMMAND_CONFIGURATION_INVALID`:命令 shape、路径、权限或字段不合法;
|
||||
- `...TRANSITION_CONFLICT`:plan digest、CAS、mutation replay 或身份发生漂移;
|
||||
- `...DATA_SAFETY_REJECTED`:空库/备份声明不满足当前 durable data;
|
||||
- `...FEATURE_NOT_READY`:schema/history/checksum 未通过;
|
||||
- `...FENCE_REJECTED` / `...OWNER_REJECTED`:credential/pepper/User/Owner 已漂移;
|
||||
- `...IN_FLIGHT_INVOCATION`:仍有未完成调用;
|
||||
- `...MIGRATION...UNAVAILABLE`:保留原命令和数据库,先检查磁盘、锁、完整性与审计,
|
||||
不手工补 history。
|
||||
|
||||
每次操作后再次 inspect,并执行 SQLite `integrity_check`、备份可恢复性和应用启动门。
|
||||
若 active head 存在但应用启动返回
|
||||
`LOCAL_AI_FEATURE_APPLICATION_UNAVAILABLE`,不得绕过 AI 入口启动成“部分 ready”:
|
||||
检查部署是否确实包含 `@qinglong/ai`、9001–9007 history/checksum、provider credential
|
||||
和 recovery 状态;修复后重新执行同一产品启动门。
|
||||
@@ -0,0 +1,391 @@
|
||||
# QingLong 3.0 本机 Headless Application 操作手册
|
||||
|
||||
本手册适用于 `edge` 与 `standalone`。它启动独立 QingLong 3.0 target,不会接管
|
||||
`back/app.ts`,也不会自动停止 2.x。全新安装使用
|
||||
[Fresh 初始化](./ql3-local-fresh-setup.md) 与 v2 `storage.mode=fresh`;下文保留
|
||||
2.x adopted v3 配置、legacy silence commitment 和 fence 流程。v1 adopted
|
||||
配置仍可被离线解析,但生产进程会在取得任何 runtime authority 前拒绝启动。
|
||||
|
||||
## 前置条件
|
||||
|
||||
1. 已完成 legacy SQLite inspection、side-by-side adoption 和 activation,保存
|
||||
`activationDigest`。
|
||||
2. 已显式 provision 本机 Secret keyring;runtime 不会自动生成主密钥。
|
||||
3. 若 Plugin Package install recovery 队列未收敛,必须配置 materialized recovery
|
||||
catalog;`disabled` 模式遇到 queued stage 会失败关闭。
|
||||
4. 使用与数据文件相同的专用 OS 用户运行;配置目录建议 `0700`,配置文件必须
|
||||
`0600`。
|
||||
5. 先由 `ql3-local-deploy cutover-legacy-stop` 对精确 Docker container ID 关闭 restart
|
||||
policy、停止并检查,再取得不可覆盖的 legacy silence commitment。SQLite source
|
||||
write fence 不能替代该部署证据。
|
||||
|
||||
## 配置
|
||||
|
||||
创建 `/opt/qinglong/private/local-application.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "qinglong/local-application-process@v3",
|
||||
"instanceId": "router-edge-1",
|
||||
"profile": "edge",
|
||||
"storage": {
|
||||
"mode": "adopted",
|
||||
"sourcePath": "/opt/qinglong/data/database.sqlite",
|
||||
"targetPath": "/opt/qinglong/data/qinglong3.sqlite",
|
||||
"recoveryPath": "/opt/qinglong/data/database.pre-ql3.sqlite",
|
||||
"manifestPath": "/opt/qinglong/data/qinglong3-adoption.json",
|
||||
"activationPath": "/opt/qinglong/data/qinglong3-activation.json",
|
||||
"expectedActivationDigest": "REPLACE_WITH_64_HEX_ACTIVATION_DIGEST",
|
||||
"busyTimeoutMs": 100
|
||||
},
|
||||
"cutover": {
|
||||
"cutoverId": "router-edge-1-ql3",
|
||||
"commitmentPath": "/opt/qinglong/service/cutovers/router-edge-1-ql3/0002-legacy-stopped.json",
|
||||
"expectedCommitmentDigest": "REPLACE_WITH_64_HEX_COMMITMENT_DIGEST"
|
||||
},
|
||||
"runtime": {
|
||||
"receiptRoot": "/opt/qinglong/data/receipts",
|
||||
"artifactRoot": "/opt/qinglong/data/artifacts",
|
||||
"secretKeyringPath": "/opt/qinglong/private/secret-keyring.json"
|
||||
},
|
||||
"pluginPackages": {
|
||||
"stagingRoot": "/opt/qinglong/data/plugin-staging",
|
||||
"activationRoot": "/opt/qinglong/data/plugin-activation",
|
||||
"recoverySource": {
|
||||
"mode": "disabled"
|
||||
},
|
||||
"pageSize": 4,
|
||||
"maxPages": 4,
|
||||
"taskPublicationPageSize": 4,
|
||||
"taskPublicationMaxPages": 4
|
||||
},
|
||||
"ai": {
|
||||
"deployment": "excluded"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
所有 path 必须是规范化绝对非根路径。storage、runtime、Plugin Package
|
||||
staging/activation/catalog/bundle/trust 的全部 authority path 必须互不相同;未知字段、
|
||||
symlink、非当前 UID、非普通文件、超过 16 KiB 或配置权限不是 `0600` 都会在打开
|
||||
storage 前拒绝。commitment 必须同时绑定 cutover ID、Profile、instance ID、activation
|
||||
digest、Docker endpoint/container 稳定身份和前一条 journal digest;摘要、权限、形状或
|
||||
任一绑定漂移都会在 signal subscription、SQLite、Plugin Package、Secret 和 AI 之前失败关闭。
|
||||
|
||||
```sh
|
||||
chmod 0700 /opt/qinglong/private
|
||||
chmod 0600 /opt/qinglong/private/local-application.json
|
||||
ql3-local-application --config /opt/qinglong/private/local-application.json
|
||||
```
|
||||
|
||||
## Plugin Package 恢复 catalog
|
||||
|
||||
空 install recovery 队列或明确不允许本机恢复时使用:
|
||||
|
||||
```json
|
||||
{ "mode": "disabled" }
|
||||
```
|
||||
|
||||
如果 durable queued install 需要在崩溃后继续,配置部署者已经物化的私有 catalog:
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "materialized_catalog",
|
||||
"catalogRoot": "/opt/qinglong/private/plugin-package-catalog",
|
||||
"bundleRoot": "/opt/qinglong/private/plugin-package-bundles",
|
||||
"publisherTrustFilePath": "/opt/qinglong/private/plugin-package-publisher-trust/current.json"
|
||||
}
|
||||
```
|
||||
|
||||
catalog 与 bundle root 必须是当前 UID 的非 symlink、规范真实路径 `0700` 目录,
|
||||
各最多包含 64 个 final object。catalog entry 为 `<lockDigest>.json`,bundle 为
|
||||
`<artifactDigest>.bundle`;两者都必须是当前 UID、no-follow、精确 `0600` 的
|
||||
regular file。entry 最大 256 KiB,exact schema 为:
|
||||
|
||||
```text
|
||||
schema = qinglong/local-plugin-package-recovery-source@v1
|
||||
lockDigest = durable PackageLock.lockDigest
|
||||
source = durable PackageLock.source 的完整精确副本
|
||||
bundlePath = bundleRoot/<source.artifactDigest>.bundle
|
||||
manifest = 受签名保护的 Plugin Package manifest
|
||||
signature = Plugin Package Ed25519 signature
|
||||
```
|
||||
|
||||
publisher trust 文件同样必须为当前 UID、no-follow、精确 `0600`、最大 256 KiB:
|
||||
|
||||
```text
|
||||
schema = qinglong/plugin-package-publisher-trust@v1
|
||||
keys = 受信 publisher key 定义数组
|
||||
```
|
||||
|
||||
不要直接覆盖 `current.json`。先创建精确 `0700` 的 trust root 和 deployment root
|
||||
下精确 `0600` 的候选 trust,再由当前 Owner 执行 provision:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "plugin-package.publisher-trust.provision",
|
||||
"options": {
|
||||
"deploymentRoot": "/opt/qinglong",
|
||||
"databasePath": "/opt/qinglong/data/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/opt/qinglong/private/owner-keys",
|
||||
"credentialFilePath": "/opt/qinglong/private/credential.json",
|
||||
"trustRoot": "/opt/qinglong/private/plugin-package-publisher-trust",
|
||||
"catalogRoot": "/opt/qinglong/private/plugin-package-catalog",
|
||||
"bundleRoot": "/opt/qinglong/private/plugin-package-bundles"
|
||||
},
|
||||
"request": {
|
||||
"requestId": "publisher-trust-provision-v1",
|
||||
"auditEventId": "REPLACE_WITH_UUID_V4",
|
||||
"failureAuditEventId": "REPLACE_WITH_DIFFERENT_UUID_V4",
|
||||
"mutationId": "publisher-trust-provision-v1",
|
||||
"expectedGeneration": 0,
|
||||
"trustFilePath": "/opt/qinglong/private/publisher-trust-v1.json"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
chmod 0700 /opt/qinglong/private/plugin-package-publisher-trust
|
||||
chmod 0600 /opt/qinglong/private/publisher-trust-v1.json
|
||||
chmod 0600 /opt/qinglong/private/publisher-trust-command.json
|
||||
ql3-package-trust run \
|
||||
--command-file /opt/qinglong/private/publisher-trust-command.json
|
||||
```
|
||||
|
||||
重叠轮换使用 `plugin-package.publisher-trust.rotate`、当前 generation 作为
|
||||
`expectedGeneration`、新的 mutation/audit identity,以及同时包含全部旧 key 和至少
|
||||
一个当前有效新 key 的候选文件。`rotate` 始终拒绝删除/改写旧 key。正常退休先用新
|
||||
key 发布替代 recovery entry,并 collect 所有仍由旧 key 签名的 entry,再执行
|
||||
`plugin-package.publisher-trust.retire`;命令 request 只包含
|
||||
`publisher/keyId/expectedGeneration` 和新的 mutation/audit identity,不接收候选
|
||||
trust 文件。retire 会先写 durable intent(此后旧 signer 发布立即失败),再要求
|
||||
catalog signer 引用和未决事务都为零,最后写证明和新 generation。紧急 revoke 不是
|
||||
retire,疑似泄露时不要继续此流程。若 inspect
|
||||
返回 `recoveryRequired=true`,只能精确重放造成 pending generation 的原命令,不能
|
||||
提交另一轮换。
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "plugin-package.publisher-trust.retire",
|
||||
"options": {
|
||||
"deploymentRoot": "/opt/qinglong",
|
||||
"databasePath": "/opt/qinglong/data/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/opt/qinglong/private/owner-keys",
|
||||
"credentialFilePath": "/opt/qinglong/private/credential.json",
|
||||
"trustRoot": "/opt/qinglong/private/plugin-package-publisher-trust",
|
||||
"catalogRoot": "/opt/qinglong/private/plugin-package-catalog",
|
||||
"bundleRoot": "/opt/qinglong/private/plugin-package-bundles"
|
||||
},
|
||||
"request": {
|
||||
"requestId": "publisher-trust-retire-v3",
|
||||
"auditEventId": "REPLACE_WITH_UUID_V4",
|
||||
"failureAuditEventId": "REPLACE_WITH_DIFFERENT_UUID_V4",
|
||||
"mutationId": "publisher-trust-retire-v3",
|
||||
"expectedGeneration": 2,
|
||||
"publisher": "packages.example.com",
|
||||
"keyId": "release-2026"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
疑似或确认 key 泄露时不要等待普通 retirement 的引用归零。先执行紧急提案:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "plugin-package.publisher-trust.revoke.propose",
|
||||
"options": {
|
||||
"deploymentRoot": "/opt/qinglong",
|
||||
"databasePath": "/opt/qinglong/data/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/opt/qinglong/private/owner-keys",
|
||||
"credentialFilePath": "/opt/qinglong/private/credential.json",
|
||||
"trustRoot": "/opt/qinglong/private/plugin-package-publisher-trust",
|
||||
"catalogRoot": "/opt/qinglong/private/plugin-package-catalog",
|
||||
"bundleRoot": "/opt/qinglong/private/plugin-package-bundles"
|
||||
},
|
||||
"request": {
|
||||
"requestId": "publisher-trust-revoke-v3-propose",
|
||||
"auditEventId": "REPLACE_WITH_UUID_V4",
|
||||
"failureAuditEventId": "REPLACE_WITH_DIFFERENT_UUID_V4",
|
||||
"mutationId": "publisher-trust-revoke-v3",
|
||||
"expectedGeneration": 2,
|
||||
"publisher": "packages.example.com",
|
||||
"keyId": "release-2026"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
proposal 一旦返回 `runtimeAction=stop_required`,目标 signer 的新 catalog publish 和
|
||||
queued application stage 已被持久阻断;立即停止 application,并保存响应中的
|
||||
`impactDigest`。proposal 没有取消或解除阻断操作。已经 active、staged 或
|
||||
activating 的 Package 及其 Task/Tool **不会**被本命令自动热停止。
|
||||
|
||||
默认由另一位仍为 current default Project Owner 的 subject 完成确认。确认命令必须
|
||||
复用 proposal 的 `mutationId`、`expectedGeneration`,把提案者 subject 写入
|
||||
`proposerSubjectId`,并逐字复制返回的 `impactDigest`:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "plugin-package.publisher-trust.revoke.confirm",
|
||||
"options": {
|
||||
"deploymentRoot": "/opt/qinglong",
|
||||
"databasePath": "/opt/qinglong/data/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/opt/qinglong/private/owner-keys",
|
||||
"credentialFilePath": "/opt/qinglong/private/second-owner-credential.json",
|
||||
"trustRoot": "/opt/qinglong/private/plugin-package-publisher-trust",
|
||||
"catalogRoot": "/opt/qinglong/private/plugin-package-catalog",
|
||||
"bundleRoot": "/opt/qinglong/private/plugin-package-bundles"
|
||||
},
|
||||
"request": {
|
||||
"requestId": "publisher-trust-revoke-v3-confirm",
|
||||
"auditEventId": "REPLACE_WITH_UUID_V4",
|
||||
"failureAuditEventId": "REPLACE_WITH_DIFFERENT_UUID_V4",
|
||||
"mutationId": "publisher-trust-revoke-v3",
|
||||
"expectedGeneration": 2,
|
||||
"publisher": "packages.example.com",
|
||||
"keyId": "release-2026",
|
||||
"proposerSubjectId": "OWNER_SUBJECT_FROM_PROPOSAL",
|
||||
"authorizationMode": "dual_control",
|
||||
"reasonCode": "confirmed_key_compromise",
|
||||
"expectedImpactDigest": "COPY_SHA256_FROM_PROPOSAL_RESULT"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
没有第二位 Owner 且延迟风险更高时,可显式选择
|
||||
`authorizationMode=break_glass`;这仍要求当前 Owner 强认证、精确 impact digest
|
||||
和 `suspected_key_compromise` 或 `confirmed_key_compromise` reason,不是普通单人
|
||||
撤销的别名。确认允许撤销最后一个 key;空 trust 会令 runtime registry 失败关闭,
|
||||
必须先通过受审恢复流程建立替代信任。确认返回
|
||||
`runtimeAction=restart_required` 只表示信任代已持久更新,不代表可直接启动:先审查
|
||||
`quarantinedLockCount`、替代 bundle/lock 与资源处置计划。D-174 完成前,受影响的
|
||||
active Package/Task/Tool 仍须保持 application 停机并由 operator 管理。
|
||||
|
||||
`offline` 和 digest-pinned `oci` lock 都可恢复,但 OCI bundle 必须已经由短生命周期
|
||||
部署/管理 authority 下载到私有 source path,再经发布命令写入 content-addressed
|
||||
bundle root。application 不连接 Registry、不读取
|
||||
Registry credential,也不会 watch catalog。只有 queued stage 实际发生时才读取
|
||||
entry、trust 和 bundle;空队列不产生额外加载或 I/O。
|
||||
|
||||
不要手工拼装或覆盖 entry/bundle。先用 `ql3-package` 完成
|
||||
`propose → decide → consume → dispatch`,确保 SQLite 中已有当前 durable lock;再
|
||||
准备 deployment root 下 `0600` 的 source bundle 和 publication descriptor;catalog
|
||||
命令读取上一步管理出的 `current.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "qinglong/local-plugin-package-recovery-publication@v1",
|
||||
"bundlePath": "/opt/qinglong/private/incoming/example.bundle",
|
||||
"manifest": {},
|
||||
"signature": {}
|
||||
}
|
||||
```
|
||||
|
||||
最后以同一 Owner credential 执行私有 command file:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "plugin-package.catalog.publish",
|
||||
"options": {
|
||||
"deploymentRoot": "/opt/qinglong",
|
||||
"databasePath": "/opt/qinglong/data/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/opt/qinglong/private/owner-keys",
|
||||
"credentialFilePath": "/opt/qinglong/private/credential.json",
|
||||
"catalogRoot": "/opt/qinglong/private/plugin-package-catalog",
|
||||
"bundleRoot": "/opt/qinglong/private/plugin-package-bundles",
|
||||
"trustRoot": "/opt/qinglong/private/plugin-package-publisher-trust"
|
||||
},
|
||||
"request": {
|
||||
"requestId": "package-catalog-example-v1",
|
||||
"auditEventId": "REPLACE_WITH_UUID_V4",
|
||||
"failureAuditEventId": "REPLACE_WITH_DIFFERENT_UUID_V4",
|
||||
"projectId": "default",
|
||||
"packageName": "example",
|
||||
"descriptorFilePath": "/opt/qinglong/private/publication.json"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
chmod 0700 /opt/qinglong/private/plugin-package-catalog
|
||||
chmod 0700 /opt/qinglong/private/plugin-package-bundles
|
||||
chmod 0600 /opt/qinglong/private/package-catalog-command.json
|
||||
ql3-package-catalog run \
|
||||
--command-file /opt/qinglong/private/package-catalog-command.json
|
||||
```
|
||||
|
||||
首次成功返回 `status=published`,相同 lock/command 重放返回 `status=existing`。只有此后
|
||||
才启动或重启 application。dispatch 与 filesystem publish 不是一个原子事务;中间
|
||||
崩溃时 queued recovery 会失败关闭,重放 publish 收敛,不能伪造 stage evidence。
|
||||
|
||||
`plugin-package.catalog.inspect` 的 request 必须是空对象,只返回 current/stale 与
|
||||
文件计数。`plugin-package.catalog.collect` 使用新的 request/audit/failure UUID,
|
||||
可选 `limit` 在 edge 最大为 4、standalone 最大为 16;重复执行直到
|
||||
`remaining=false`。collect 只删除已证明非当前 SQLite head 的 entry、无引用 bundle
|
||||
和识别出的临时事务。同一 catalog 的 publish/collect 应由 supervisor 串行执行。
|
||||
|
||||
看到以下低敏事实才表示 target active:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"component": "qinglong3-local-application",
|
||||
"level": "info",
|
||||
"event": "active",
|
||||
"instanceId": "router-edge-1",
|
||||
"profile": "edge",
|
||||
"aiStatus": "deployment_excluded"
|
||||
}
|
||||
```
|
||||
|
||||
## 停止
|
||||
|
||||
向进程发送一次 `SIGTERM` 或 `SIGINT`。入口只接受第一个信号,并按:
|
||||
|
||||
```text
|
||||
scheduler stop/drain → execution control drain → SQLite/source fence release
|
||||
```
|
||||
|
||||
停止。必须等待 `event=stopped` 且 `stopResult=stopped` 后再操作数据库或启动 2.x。
|
||||
`timed_out`、没有 stopped fact 或进程被 `SIGKILL` 都不能当成安全切换证据。
|
||||
|
||||
## AI 边界
|
||||
|
||||
基础路由部署使用:
|
||||
|
||||
```json
|
||||
{ "deployment": "excluded" }
|
||||
```
|
||||
|
||||
该路径不会加载 `@qinglong/ai` 或 provider。`deployment=installed` 已进入 process
|
||||
contract,但通用 CLI 尚没有 provider binding/Policy/Secret material ceremony,会以
|
||||
`QL3_LOCAL_APPLICATION_PROCESS_AI_PROVIDER_UNAVAILABLE` 在 storage 前失败关闭。不要把
|
||||
API token 写进配置或环境变量绕过该门;AI-inclusive embedded host 必须注入受信
|
||||
provider authority,完整启停流程见
|
||||
[本机 AI Feature 手册](./ql3-local-ai-feature.md)。
|
||||
|
||||
## 常见失败
|
||||
|
||||
- `PRIVATE_LOCAL_COMMAND_FILE_INVALID`:配置 path、UID、类型、大小、symlink、权限或 JSON
|
||||
不合法;
|
||||
- `QL3_LOCAL_APPLICATION_PROCESS_CONFIG_INVALID`:schema、shape、Profile、digest、path
|
||||
或预算不合法;
|
||||
- `QL3_LOCAL_APPLICATION_PLUGIN_SOURCE_UNAVAILABLE`:存在 queued Plugin Package,但
|
||||
当前进程没有受信 recovery source;
|
||||
- `QL3_LOCAL_APPLICATION_PROCESS_AI_PROVIDER_UNAVAILABLE`:声明 installed AI,但没有
|
||||
provider authority;
|
||||
- `LocalApplicationStartupRecoveryRequiredError`:Run/Attempt 证据未收敛,禁止直接删
|
||||
行或伪造 receipt;
|
||||
- `Local Secret is unavailable`:keyring 缺失、权限错误、损坏或与数据库不配对。
|
||||
|
||||
故障输出只包含 name/code。详细诊断应结合低敏 activation facts、离线 inspection 和
|
||||
备份恢复流程,不要要求常驻进程打印 path、digest、SecretRef 或 token。
|
||||
@@ -0,0 +1,56 @@
|
||||
# QingLong 3.0 Local 统一命令入口
|
||||
|
||||
Local Owner 管理制品提供统一的 `ql3` 入口,同时保留所有既有专用 binary。统一入口不
|
||||
改变权限、command-file 或输出协议,只负责从同一安装制品的静态白名单启动精确命令。
|
||||
|
||||
```sh
|
||||
ql3 --version
|
||||
ql3 --help
|
||||
ql3 task --help
|
||||
ql3 readiness --help
|
||||
ql3 deploy --help
|
||||
```
|
||||
|
||||
常用映射如下:
|
||||
|
||||
| 统一命令 | 原专用 binary | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `ql3 setup` | `ql3-local-setup` | Fresh Local 存储与 Owner material 准备 |
|
||||
| `ql3 readiness` | `ql3-local-readiness` | schema 与运行时 readiness 检查 |
|
||||
| `ql3 deploy` | `ql3-local-deploy` | systemd/OpenRC/Compose 部署 ceremony |
|
||||
| `ql3 owner` | `ql3-owner` | Owner bootstrap 与恢复 |
|
||||
| `ql3 identity` | `ql3-identity` | Identity 与 API credential 管理 |
|
||||
| `ql3 policy` | `ql3-policy` | Project 与 Policy 管理 |
|
||||
| `ql3 audit` | `ql3-audit` | 有界安全审计查询 |
|
||||
| `ql3 secret` | `ql3-secret` | Local Secret 管理 |
|
||||
| `ql3 task` | `ql3-task` | TaskDefinition 管理 |
|
||||
| `ql3 trigger` | `ql3-trigger` | Trigger 管理 |
|
||||
| `ql3 workflow` | `ql3-workflow` | Package Workflow 管理 |
|
||||
| `ql3 approval` | `ql3-approval` | 人工 Approval inspect/decide |
|
||||
| `ql3 package` | `ql3-package` | Plugin Package 生命周期 |
|
||||
| `ql3 prompt` | `ql3-prompt` | Package Prompt 查询与执行 |
|
||||
| `ql3 ai-feature` | `ql3-ai-feature` | 可选 AI schema 激活/停用 |
|
||||
|
||||
其他可发现子命令由 `ql3 --help` 列出。子命令后的参数会原样传给专用 binary,因此文档
|
||||
中的命令可以等价改写,例如:
|
||||
|
||||
```sh
|
||||
ql3 deploy prepare \
|
||||
--command-file /secure/operator/qinglong3-deployment.json
|
||||
|
||||
ql3 task run \
|
||||
--command-file /secure/operator/task-create.json
|
||||
```
|
||||
|
||||
不要用 `eval`、shell alias 或自行拼接 binary 路径包裹 `ql3`。dispatcher 自身不使用
|
||||
shell 或 `PATH`,并拒绝未知、绝对或路径穿越形式的 command name。
|
||||
|
||||
`ql3-service-bridge` 刻意不作为 `ql3` 子命令。它属于 root service-manager authority,
|
||||
必须继续由 root operator 使用 root-owned `0600` command file 显式运行:
|
||||
|
||||
```sh
|
||||
ql3-service-bridge run \
|
||||
--command-file /run/qinglong3-evidence/service-command.json
|
||||
```
|
||||
|
||||
统一入口不会隐式 sudo、读取 root 文件或把 Owner command 转换为 bridge command。
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,160 @@
|
||||
# QingLong 3.0 Fresh Edge/Standalone 初始化
|
||||
|
||||
本流程用于没有 2.x SQLite 数据的新安装。已有 2.x 数据的部署继续使用 adoption
|
||||
流程,不得把 fresh 模式当作绕过迁移审查的入口。
|
||||
|
||||
需要自动创建固定私有目录、application 配置并生成 systemd/OpenRC/Compose
|
||||
描述符时,优先使用[部署准备器](./ql3-local-deployment.md)。下文保留底层
|
||||
`ql3-local-setup` 手工流程,供恢复和逐项审计使用。
|
||||
|
||||
## 1. 准备私有目录
|
||||
|
||||
使用最终运行 QingLong 的同一个 POSIX 用户:
|
||||
|
||||
```sh
|
||||
install -d -m 0700 /opt/qinglong3
|
||||
install -d -m 0700 /opt/qinglong3/owner-peppers
|
||||
install -d -m 0700 /opt/qinglong3/owner-pepper-backup
|
||||
install -d -m 0700 /opt/qinglong3/plugin-staging
|
||||
install -d -m 0700 /opt/qinglong3/plugin-activation
|
||||
```
|
||||
|
||||
路径必须是 canonical absolute path,不能是 symlink。若平台的 `/var` 实际解析为
|
||||
`/private/var`,配置中应使用 `realpath` 后的路径。
|
||||
|
||||
## 2. 执行可重放 setup
|
||||
|
||||
创建 `/opt/qinglong3/setup.json` 并设为 `0600`:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "local.setup.prepare",
|
||||
"options": {
|
||||
"deploymentRoot": "/opt/qinglong3",
|
||||
"databasePath": "/opt/qinglong3/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/opt/qinglong3/owner-peppers",
|
||||
"ownerPepperBackupDirectory": "/opt/qinglong3/owner-pepper-backup",
|
||||
"ownerPepperKeyId": "owner-v1",
|
||||
"localSecretKeyringPath": "/opt/qinglong3/local-secret-keyring.json",
|
||||
"busyTimeoutMs": 100
|
||||
},
|
||||
"request": {
|
||||
"registerMutationId": "REPLACE_WITH_UUID",
|
||||
"activateMutationId": "REPLACE_WITH_DIFFERENT_UUID",
|
||||
"registeredAtMs": 1785254400000,
|
||||
"activatedAtMs": 1785254400001
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
chmod 0600 /opt/qinglong3/setup.json
|
||||
ql3-local-setup run --command-file /opt/qinglong3/setup.json
|
||||
```
|
||||
|
||||
首次成功返回 `prepared`;崩溃或结果未知时保留原文件并执行同一命令,成功重放返回
|
||||
`existing`。不要修改 mutation ID、时间或路径来“重试”。CLI 只输出低敏摘要,不
|
||||
输出 key、digest 或路径。
|
||||
|
||||
setup 只准备存储和密钥 authority。随后使用 `ql3-owner` 完成 Identity provision、
|
||||
challenge、Owner claim 与 delivery acknowledgement。
|
||||
|
||||
## 3. 创建 fresh application 配置
|
||||
|
||||
创建 `0600` 的 `/opt/qinglong3/local-application.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "qinglong/local-application-process@v2",
|
||||
"instanceId": "router-edge-1",
|
||||
"profile": "edge",
|
||||
"storage": {
|
||||
"mode": "fresh",
|
||||
"databasePath": "/opt/qinglong3/qinglong3.sqlite",
|
||||
"busyTimeoutMs": 100
|
||||
},
|
||||
"runtime": {
|
||||
"receiptRoot": "/opt/qinglong3/receipts",
|
||||
"artifactRoot": "/opt/qinglong3/artifacts",
|
||||
"secretKeyringPath": "/opt/qinglong3/local-secret-keyring.json"
|
||||
},
|
||||
"pluginPackages": {
|
||||
"stagingRoot": "/opt/qinglong3/plugin-staging",
|
||||
"activationRoot": "/opt/qinglong3/plugin-activation",
|
||||
"recoverySource": {
|
||||
"mode": "disabled"
|
||||
},
|
||||
"pageSize": 4,
|
||||
"maxPages": 4,
|
||||
"taskPublicationPageSize": 4,
|
||||
"taskPublicationMaxPages": 4
|
||||
},
|
||||
"ai": {
|
||||
"deployment": "excluded"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
启动:
|
||||
|
||||
```sh
|
||||
chmod 0600 /opt/qinglong3/local-application.json
|
||||
ql3-local-application --config /opt/qinglong3/local-application.json
|
||||
```
|
||||
|
||||
看到 `event=active` 后才算 admission 已开放。停止时发送 `SIGTERM`,等待
|
||||
`event=stopped` 后再维护数据库或密钥文件。
|
||||
|
||||
启动前或故障诊断时,可用只读的
|
||||
[`ql3-local-readiness`](./ql3-local-readiness.md) 复验 84 条 migration、capability、schema
|
||||
integrity 与 Profile journal mode。不要使用 legacy/Shadow schema audit 代替该门。
|
||||
|
||||
## 4. 创建第一个 TaskDefinition 与 Trigger
|
||||
|
||||
完成 Owner credential 和 Project/RoleBinding 配置后,使用短生命周期 `ql3-task` 创建、更新、
|
||||
停用、启用或查询 Task。它不会启动第二个管理 daemon,也不会在输出中回显 command 参数或
|
||||
SecretRef:
|
||||
|
||||
```sh
|
||||
chmod 0600 /opt/qinglong3/commands/task-create.json
|
||||
ql3-task run --command-file /opt/qinglong3/commands/task-create.json
|
||||
```
|
||||
|
||||
当前 production registry 只开放 `qinglong/command@v1`。完整 command schema、revision replay、
|
||||
分页和失败恢复见 [`ql3-task` 运维说明](./ql3-local-task-definition.md)。随后 inspect Task,取得
|
||||
current `revision` 与 `contentDigest`,创建 `0600` Trigger command file:
|
||||
|
||||
```sh
|
||||
chmod 0600 /opt/qinglong3/commands/trigger-create.json
|
||||
ql3-trigger run --command-file /opt/qinglong3/commands/trigger-create.json
|
||||
```
|
||||
|
||||
完整 schema、cron 约束、exact replay、停用和 Task 变化后的显式 repin 见
|
||||
[`ql3-trigger` 运维说明](./ql3-local-trigger.md)。启用的 Trigger 只能固定到当前且 enabled 的
|
||||
Task head;Task 更新、停用或重新启用后,旧 Trigger 会立即停止 admission,必须显式 repin 才会
|
||||
继续产生 Run。`ql3-task` 与 `ql3-trigger` 都是短生命周期本机入口,不增加常驻 daemon、timer
|
||||
或连接;Cluster 必须使用 PostgreSQL/RBAC 管理 transport。
|
||||
|
||||
## 5. 检查并启动 Plugin Package Workflow
|
||||
|
||||
Package 完成安装、materialize 和 automation publication 后,先使用 `ql3-workflow` 的
|
||||
`workflow.inspect` 读取当前受审 Workflow/Step metadata,再为 plan、Run 与每个 StepRun 提供
|
||||
不同的 UUID v4,执行 `workflow.start`。命令经当前 Owner credential 与 Project Policy 验证,
|
||||
只把 durable Run admission 交给已运行的 application,不同步等待 Workflow 完成:
|
||||
|
||||
```sh
|
||||
chmod 0600 /opt/qinglong3/commands/workflow-inspect.json
|
||||
ql3-workflow run --command-file /opt/qinglong3/commands/workflow-inspect.json
|
||||
|
||||
chmod 0600 /opt/qinglong3/commands/workflow-start.json
|
||||
ql3-workflow run --command-file /opt/qinglong3/commands/workflow-start.json
|
||||
```
|
||||
|
||||
完整 command schema、`run.read`/`run.start` 权限、created/existing 重放和输出边界见
|
||||
[`ql3-workflow` 运维说明](./ql3-local-plugin-package-workflow.md)。
|
||||
|
||||
AI optional feature 激活、Provider Secret 与 durable credential binding 配置完成后,可使用一次性
|
||||
`ql3-prompt` 执行已发布 Prompt;它不会启动新的 scheduler 或监听端口。参见
|
||||
[`ql3-prompt` 运维说明](./ql3-local-plugin-package-prompt.md)。
|
||||
@@ -0,0 +1,245 @@
|
||||
# `ql3-identity` 本机 Identity/Credential 管理
|
||||
|
||||
`ql3-identity` 是一次命令、一次进程的 Owner-only 管理入口。它不启动管理服务,也不把
|
||||
credential token 写入 argv、stdout、stderr、command JSON 或 SQLite。
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 已完成 Fresh Setup,当前 credential 属于 `default` Project 的 active Owner;
|
||||
- deployment root、其子目录、SQLite、Owner credential presentation 与 command
|
||||
file 均由当前 real/effective UID 拥有;
|
||||
- 目录权限为 `0700`,文件权限为 `0600`;
|
||||
- Owner pepper keyring 中存在与 SQLite active pepper 完全匹配的材料;
|
||||
- managed credential delivery 使用独立、规范化、非 symlink 的 `0700` 目录。
|
||||
|
||||
## 实例 Authority Project
|
||||
|
||||
Identity 与 API credential 是实例级对象,不属于任意单个业务 Project。
|
||||
`ql3-identity` 只接受实例 authority Project 的当前 Owner:
|
||||
|
||||
- 新部署以全库最早成功消费的 Owner bootstrap challenge 所属 Project 为 authority;
|
||||
- 没有任何 consumed bootstrap challenge 的旧库兼容回退到迁移内建的 `default`;
|
||||
- 后续给其他 Project 授予 Owner 不会转移该 authority;
|
||||
- secondary Project Owner 使用自己的 `projectId` 调用时,返回统一 authorization
|
||||
error,并记录 `instance_authority_project_required`;
|
||||
- 当前没有 authority Project 转移命令,不要通过直接编辑数据库模拟转移。
|
||||
|
||||
command 中的 `projectId` 必须填写该 authority Project。它会在服务授权和最终 SQLite
|
||||
事务各验证一次;直接调用 repository 不能绕过。
|
||||
|
||||
所有示例中的 UUID、subject 和路径都必须替换。command file 创建完成后执行:
|
||||
|
||||
```sh
|
||||
ql3-identity run --command-file /srv/qinglong3/commands/identity.json
|
||||
```
|
||||
|
||||
## 查询当前版本
|
||||
|
||||
后续 mutation 所需的 `expectedCurrentVersion` 必须通过受支持的 inspect 命令取得,
|
||||
不要直接查询 SQLite。Identity 精确查询示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "identity.inspect",
|
||||
"options": {
|
||||
"deploymentRoot": "/srv/qinglong3",
|
||||
"databasePath": "/srv/qinglong3/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/srv/qinglong3/owner-keys",
|
||||
"credentialFilePath": "/srv/qinglong3/owner-credential.json"
|
||||
},
|
||||
"request": {
|
||||
"projectId": "default",
|
||||
"target": { "type": "agent", "id": "agent-planner" },
|
||||
"requestId": "identity-inspect-agent-planner",
|
||||
"auditEventId": "10111111-1111-4111-8111-111111111111"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Credential 精确查询把 operation 改为 `credential.inspect`,request 改为:
|
||||
|
||||
```json
|
||||
{
|
||||
"projectId": "default",
|
||||
"credentialId": "agent-planner-primary",
|
||||
"requestId": "credential-inspect-agent-planner",
|
||||
"auditEventId": "10222222-2222-4222-8222-222222222222"
|
||||
}
|
||||
```
|
||||
|
||||
成功命中只返回 subject、status/state、version 和时间窗;不存在返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "credential.inspect",
|
||||
"projectId": "default",
|
||||
"found": false
|
||||
}
|
||||
```
|
||||
|
||||
`found:false` 只会在 Owner authorization 与事务内 credential/Project/RoleBinding
|
||||
围栏全部通过后返回。输出不会包含 secret digest、pepper key ID、token、数据库路径
|
||||
或交付路径。每次人工查询使用新的 `auditEventId`;inspect 不是 mutation,也不使用
|
||||
`mutationId`、`failureAuditEventId` 或 `expectedCurrentVersion`。
|
||||
|
||||
## 注册 Identity
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "identity.register",
|
||||
"options": {
|
||||
"deploymentRoot": "/srv/qinglong3",
|
||||
"databasePath": "/srv/qinglong3/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/srv/qinglong3/owner-keys",
|
||||
"credentialFilePath": "/srv/qinglong3/owner-credential.json"
|
||||
},
|
||||
"request": {
|
||||
"projectId": "default",
|
||||
"target": { "type": "agent", "id": "agent-planner" },
|
||||
"expectedCurrentVersion": 0,
|
||||
"mutationId": "11111111-1111-4111-8111-111111111111",
|
||||
"requestId": "identity-register-agent-planner",
|
||||
"failureAuditEventId": "11111111-1111-4111-8111-111111111112"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`identity.enable` 和 `identity.disable` 使用相同结构,并把
|
||||
`expectedCurrentVersion` 设为当前 Identity version。仍有 active Owner binding 的
|
||||
User Identity 不能被禁用。
|
||||
|
||||
## 签发或轮换 Credential
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "credential.issue",
|
||||
"options": {
|
||||
"deploymentRoot": "/srv/qinglong3",
|
||||
"databasePath": "/srv/qinglong3/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/srv/qinglong3/owner-keys",
|
||||
"credentialFilePath": "/srv/qinglong3/owner-credential.json",
|
||||
"credentialDeliveryDirectory": "/srv/qinglong3/managed-credentials"
|
||||
},
|
||||
"request": {
|
||||
"projectId": "default",
|
||||
"target": { "type": "agent", "id": "agent-planner" },
|
||||
"credentialId": "agent-planner-primary",
|
||||
"expectedCurrentVersion": 0,
|
||||
"lifetimeMs": 86400000,
|
||||
"mutationId": "22222222-2222-4222-8222-222222222221",
|
||||
"requestId": "credential-issue-agent-planner",
|
||||
"failureAuditEventId": "22222222-2222-4222-8222-222222222222"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
轮换时把 operation 改为 `credential.rotate`,并使用当前 credential version。
|
||||
`lifetimeMs` 范围为 60 秒至两年。
|
||||
|
||||
成功输出示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "credential.issue",
|
||||
"status": "inserted",
|
||||
"projectId": "default",
|
||||
"target": { "type": "agent", "id": "agent-planner" },
|
||||
"credentialId": "agent-planner-primary",
|
||||
"version": 1,
|
||||
"state": "active",
|
||||
"expiresAtMs": 1780000000000,
|
||||
"delivery": {
|
||||
"fileName": "managed-credential-22222222-2222-4222-8222-222222222221.ready.json",
|
||||
"digest": "64-char-lowercase-sha256"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出没有绝对路径和 token。ready 文件位于 command 指定的 delivery directory,
|
||||
权限为 `0600`,其 JSON 可以直接作为后续 `credentialFilePath`。如果 CLI 在 commit
|
||||
后中断,必须原样重放同一个 command;不要更换 mutation、request、target、version
|
||||
或 lifetime。
|
||||
|
||||
## 确认交付
|
||||
|
||||
consumer 已复制、安装并真实验证 ready credential 后,提交确认:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "credential.delivery.acknowledge",
|
||||
"options": {
|
||||
"deploymentRoot": "/srv/qinglong3",
|
||||
"databasePath": "/srv/qinglong3/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/srv/qinglong3/owner-keys",
|
||||
"credentialFilePath": "/srv/qinglong3/owner-credential.json",
|
||||
"credentialDeliveryDirectory": "/srv/qinglong3/managed-credentials"
|
||||
},
|
||||
"request": {
|
||||
"projectId": "default",
|
||||
"credentialMutationId": "22222222-2222-4222-8222-222222222221",
|
||||
"expectedDeliveryDigest": "replace-with-issue-output-digest",
|
||||
"mutationId": "33333333-3333-4333-8333-333333333331",
|
||||
"requestId": "credential-delivery-ack-agent-planner",
|
||||
"failureAuditEventId": "33333333-3333-4333-8333-333333333332"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
数据库 acknowledgement 先提交,随后 pending/ready 文件才删除。响应丢失时原样
|
||||
重放;`cleanup:"absent"` 表示数据库已经确认且文件已在前一次执行中安全删除。
|
||||
|
||||
不要在 consumer 尚未验证前 acknowledge,也不要把唯一正在用于本命令认证的
|
||||
credential presentation 提前删除。
|
||||
|
||||
## 撤销 Credential
|
||||
|
||||
`credential.revoke` 不需要 delivery directory 或 lifetime:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "credential.revoke",
|
||||
"options": {
|
||||
"deploymentRoot": "/srv/qinglong3",
|
||||
"databasePath": "/srv/qinglong3/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/srv/qinglong3/owner-keys",
|
||||
"credentialFilePath": "/srv/qinglong3/owner-credential.json"
|
||||
},
|
||||
"request": {
|
||||
"projectId": "default",
|
||||
"target": { "type": "agent", "id": "agent-planner" },
|
||||
"credentialId": "agent-planner-primary",
|
||||
"expectedCurrentVersion": 1,
|
||||
"mutationId": "44444444-4444-4444-8444-444444444441",
|
||||
"requestId": "credential-revoke-agent-planner",
|
||||
"failureAuditEventId": "44444444-4444-4444-8444-444444444442"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
撤销 Owner 当前使用的 credential 前,必须先签发并验证另一把有效 credential。
|
||||
数据库会拒绝撤销 active Owner 的最后一把凭据。
|
||||
|
||||
## 运维规则
|
||||
|
||||
- 先使用 exact inspect 取得当前 version,再创建 mutation command;不要把数据库表当作
|
||||
产品 API;
|
||||
- command file 是不可变 mutation 载体;响应不确定时只允许原样重放;
|
||||
- 不编辑 pending/ready 文件,不按 mtime 自动清理;
|
||||
- `mutation_conflict` 表示相同 mutation ID 已用于不同语义,必须人工调查;
|
||||
- `owner_continuity_required` 表示操作会造成 Owner 无法接管;
|
||||
- `credential_or_policy_fence_rejected` 表示认证后 authority 已变化,应重新认证并使用
|
||||
新 mutation;
|
||||
- managed delivery 上限是 64 个条目;长期未确认项需要人工核对,不得用通用目录
|
||||
GC 删除。
|
||||
@@ -0,0 +1,196 @@
|
||||
# QingLong 3.0 本机 MCP Task/Trigger/Run/Approval 发现、预览与运行诊断
|
||||
|
||||
`ql3-mcp` 是一个显式可选的 stdio sidecar,供同机 AI Client/Agent 发现当前 Project 的 Task、Trigger、最近 Run 与 Approval 状态,读取单个 Approval 的脱敏预览,以及读取单个 Run 的低敏状态和有界事件元数据。它不会随 Edge/Standalone application 自动启动,不监听网络端口,也不会执行 migration、任务、Shell、Approval decision 或管理操作。
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 已完成 QingLong 3.0 fresh/adopted SQLite 部署和 readiness;MCP 启动只检查 schema,不自动升级数据库。
|
||||
- 已通过 [本机 Identity Credential](./ql3-local-identity-credential.md) 签发 active API credential presentation。
|
||||
- credential 对应 subject 在目标 Project 至少具有相应的 `task.read`、`trigger.read`、`run.read` 或 `approval.read`;读取 Approval preview 还必须同时具有 `artifact.read`。调用方还需获得目标 Tool 的 exact `tool.call:<tool-name>` permission。
|
||||
- config、credential、SQLite 文件必须是当前 UID 持有的 `0600` regular file,目录必须是当前 UID 持有的私有目录;不得使用 symlink。
|
||||
- Owner Pepper keyring 必须存在并包含 credential 记录绑定的 active/retired exact key。
|
||||
|
||||
## 私有配置
|
||||
|
||||
示例 `/opt/qinglong3/mcp/mcp.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "qinglong/local-mcp-server@v1",
|
||||
"profile": "edge",
|
||||
"projectId": "default",
|
||||
"deploymentRoot": "/opt/qinglong3",
|
||||
"databasePath": "/opt/qinglong3/data/qinglong3.sqlite",
|
||||
"ownerPepperKeyringDirectory": "/opt/qinglong3/owner-pepper-keyring",
|
||||
"credentialFilePath": "/opt/qinglong3/mcp/credential.json",
|
||||
"busyTimeoutMs": 500
|
||||
}
|
||||
```
|
||||
|
||||
`profile` 只能是 `edge` 或 `standalone`。三个 authority path 必须互不相同且都是 `deploymentRoot` 的规范化后代;`busyTimeoutMs` 可省略,范围为 100–30000 ms。配置只允许上述字段。
|
||||
|
||||
credential presentation 的形状为:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"kind": "qinglong3-local-identity-credential-presentation",
|
||||
"token": "<ql3c credential token>"
|
||||
}
|
||||
```
|
||||
|
||||
设置私有权限后先做帮助检查:
|
||||
|
||||
```sh
|
||||
chmod 0700 /opt/qinglong3/mcp
|
||||
chmod 0600 /opt/qinglong3/mcp/mcp.json /opt/qinglong3/mcp/credential.json
|
||||
ql3-mcp --help
|
||||
```
|
||||
|
||||
## AI Client 配置
|
||||
|
||||
在支持本机 stdio MCP 的 Client 中,把 command/args 配为:
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "/absolute/path/to/ql3-mcp",
|
||||
"args": [
|
||||
"--config",
|
||||
"/opt/qinglong3/mcp/mcp.json"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Client 必须保持 stdin/stdout 直连。stdout 专用于 MCP JSON-RPC,不能通过会插入 banner、日志或 shell profile 输出的包装脚本启动;低敏进程错误写入 stderr。关闭 Client 的 stdin 或发送 `SIGINT`/`SIGTERM` 会关闭 MCP transport 和唯一 SQLite connection。
|
||||
|
||||
## Tool 契约
|
||||
|
||||
当前只发布七个只读 Tool。
|
||||
|
||||
`qinglong.task.list` 接受:
|
||||
|
||||
```json
|
||||
{
|
||||
"limit": 32,
|
||||
"after": {
|
||||
"taskId": "task-id-from-previous-page"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`after` 和 `limit` 都可省略;`limit` 默认 32、最大 64。响应按 `taskId` 严格递增,只返回当前 Task 的 ID/revision/name、kind、spec schema identity、enabled 与更新时间。`hasMore=true` 时原样使用 `next`。disabled Task 仍会出现,但明确返回 `enabled=false`。
|
||||
|
||||
Project ID 来自私有配置。description、spec config/command、labels、mutation/content digest、Secret reference 与数据库字段不会返回;该列表只用于发现,不构成 Task 执行授权。
|
||||
|
||||
`qinglong.trigger.list` 接受:
|
||||
|
||||
```json
|
||||
{
|
||||
"limit": 32,
|
||||
"after": {
|
||||
"triggerId": "trigger-id-from-previous-page"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`after` 和 `limit` 都可省略;`limit` 默认 32、最大 64。响应按 `triggerId` 严格递增,只返回当前 Trigger 的 ID/revision、固定绑定的 Task ID/revision、spec schema identity、enabled 与更新时间。`hasMore=true` 时原样使用 `next`。disabled Trigger 仍会出现,但明确返回 `enabled=false`。
|
||||
|
||||
Project ID 来自私有配置。cron expression、timezone、misfire/config、Task/Trigger content digest、mutation ID、Secret reference 与数据库字段不会返回;列表只说明当前调度定义和 Task 的绑定关系,不构成执行或变更授权。
|
||||
|
||||
`qinglong.approval.list` 接受:
|
||||
|
||||
```json
|
||||
{
|
||||
"limit": 32,
|
||||
"after": {
|
||||
"updatedAtMs": 1786360000000,
|
||||
"requestId": "approval-id-from-previous-page"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`after` 和 `limit` 都可省略;`limit` 默认 32、最大 64。响应按
|
||||
`updatedAtMs DESC, requestId DESC` 排列,只返回 request ID、version/state/risk/decision mode、permission/action type、
|
||||
requester type,以及请求、过期、决定、消费和更新时间。`hasMore=true` 时必须原样使用服务端 `next`。
|
||||
|
||||
Project ID、action reference、action/preview/request digest、requester/decider ID、authentication ID、reason、Policy fence、
|
||||
decision/consumption/dispatch ID 和 preview 内容不会返回。列表仅用于判断动作仍在等待、已拒绝、已批准或已消费;它不授予
|
||||
approve、reject、consume、dispatch 或 execute 权限,也不能替代强 User 的 Approval 产品流程。
|
||||
|
||||
`qinglong.approval.get` 接受:
|
||||
|
||||
```json
|
||||
{
|
||||
"requestId": "approval-id-known-by-caller"
|
||||
}
|
||||
```
|
||||
|
||||
成功响应只含 `found`;存在时返回列表已有的 request ID、version/state/risk/decision mode、permission/action type、
|
||||
requester type、请求/过期时间、`previewAvailable`,以及可用时经过 Tool redaction contract 生成的
|
||||
`title/summary/fields/warnings`。`redacted` field 不含 value。无 preview 时明确返回 `previewAvailable=false`;不存在与跨
|
||||
Project 不可区分。
|
||||
|
||||
该 Tool 必须同时通过 `approval.read` 与 `artifact.read`。MCP 不读取 input Artifact,不获得 ciphertext、key、artifact ID、
|
||||
action reference、任一 digest、主体 ID、authentication、reason/fence、decision/consumption/dispatch evidence。关联或存储
|
||||
校验失败统一返回 unavailable,不回退到未验证 JSON,也不扫描其它 Artifact。
|
||||
|
||||
`qinglong.run.list` 接受:
|
||||
|
||||
```json
|
||||
{
|
||||
"limit": 32,
|
||||
"after": {
|
||||
"createdAtMs": 1786340000000,
|
||||
"runId": "run-id-from-previous-page"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`after` 和 `limit` 都可省略;`limit` 默认 32、最大 64。响应中的 Run 按 `createdAtMs DESC, id DESC` 排列,只包含 ID、Task ID/revision、status/version/event sequence/priority、execution origin/owner 和生命周期时间。`hasMore=true` 时,下一次调用应原样使用 `next`;不要构造 offset 或猜测 cursor。
|
||||
|
||||
Project ID 来自私有 MCP 配置,不接受客户端指定。Task name/snapshot、trigger/actor/request、input/output reference、cancel/error detail、Attempt/Event payload、Artifact/Log 和 Secret 永不返回。
|
||||
|
||||
`qinglong.run.get` 接受:
|
||||
|
||||
```json
|
||||
{
|
||||
"runId": "run-id-known-by-caller"
|
||||
}
|
||||
```
|
||||
|
||||
成功响应只含 `found`,以及存在时的 Run ID、Task ID/revision、status、version、event sequence、priority、execution origin/owner 和生命周期时间。Task snapshot、command、input/output reference、credential、Principal、Policy reason、路径及内部错误不会返回。不存在和跨 Project 都返回 `{"found":false}`。
|
||||
|
||||
`qinglong.run.events.list` 接受:
|
||||
|
||||
```json
|
||||
{
|
||||
"runId": "run-id-known-by-caller",
|
||||
"afterSequence": 0,
|
||||
"limit": 32
|
||||
}
|
||||
```
|
||||
|
||||
`afterSequence` 可省略;`limit` 默认 32、最大 64。响应固定为 `found`、低敏 `events`、`hasMore` 与 `nextAfterSequence`;每个事件只含 `sequence`、`type`、`actorType` 和 `createdAtMs`。事件按 sequence 严格递增,游标只指向本页最后一条事件。Run 不存在或跨 Project 时统一返回 `found=false` 和空 events,不继续查询事件。
|
||||
|
||||
事件 payload、event ID、dedupeKey、actorId、attemptId、stepRunId、Artifact/Log reference、命令和错误详情永不返回。需要下一页时使用响应中的 `nextAfterSequence`,不要自行猜测 sequence。
|
||||
|
||||
每次调用都会重新认证 credential,解析 exact Tool Definition,执行 Tool-specific permission 与对应的 `task.read`/`trigger.read`/`run.read`/`approval.read` Policy;Approval preview 额外要求 `artifact.read`。通过后先持久写 Security Audit,再确认 credential fence,最后才做 Project-scoped 有界读取。事件 Tool 只有在 point read 确认 Run 属于当前 Project 后才查询事件。撤销 credential/RoleBinding 后无需重启 MCP 进程;后续调用会失败关闭。
|
||||
|
||||
## 资源档位
|
||||
|
||||
`edge-mcp`/`standalone-mcp` 是独立制品,不属于默认 application。MCP 单一消费者的 Task/Trigger/Run/Approval projection 位于 sidecar 自身的 `tool-projection` domain;默认 application 不加载这些文件。跨产品复用的 `run.get` projection、Profile-neutral Approval discovery/detail contract 和双方言 document-only source 保留在既有包内;完整 Tool Invocation Artifact 不进入 MCP projection。当前 Standalone 裁剪后闭包为 947 files、9,857,149 bytes、203 loaded modules,完整 import RSS 增量 40,632,320 bytes,硬门为 1,536 files、16 MiB、48 MiB。64 MiB 总内存设备不应启用;128 MiB 设备也应结合 application、内核页缓存和其他服务实测后决定。资源不足时保持 MCP 未安装/未启动,不影响 QingLong 调度。
|
||||
|
||||
可复核运行:
|
||||
|
||||
```sh
|
||||
pnpm run audit:artifact:edge-mcp:ql3
|
||||
pnpm run audit:artifact:standalone-mcp:ql3
|
||||
```
|
||||
|
||||
## 故障处理
|
||||
|
||||
- 启动立即失败:先运行本机 readiness,确认 schema 已迁移、Profile 与 journal mode 匹配,再检查 config/credential/keyring 的 owner、mode、regular-file 与 no-symlink 条件。
|
||||
- Tool 返回认证错误:检查 credential 是否 active、是否仍绑定可解析 Pepper key,以及 presentation 是否被替换。
|
||||
- Tool 返回 Policy 拒绝:按 [本机 Project Policy](./ql3-local-project-policy.md) 检查 subject 的当前 RoleBinding;不要通过复制 Owner credential 绕过授权。
|
||||
- Tool 返回 audit/database unavailable:MCP 会在 Run read 前失败关闭。修复 SQLite 可写性/容量/锁等待后重试,不要删除 audit 或自动 migration。
|
||||
- 需要 Workflow start、Approval decision、未脱敏 input、Shell、Secret、事件 payload 或写操作:当前 MCP 明确不支持;不得通过自定义 wrapper 把这些操作伪装成任一只读 Tool。
|
||||
@@ -0,0 +1,81 @@
|
||||
# 本机 Plugin Package Prompt
|
||||
|
||||
`ql3-prompt` 在 Edge/Standalone 上一次性执行已经安装、materialize 并发布的 Plugin Package
|
||||
Prompt。它不是 scheduler 或 daemon;每条命令只打开一个 SQLite authority,完成 Model Gateway
|
||||
drain 后退出。
|
||||
|
||||
## 前置条件
|
||||
|
||||
1. 本机数据库已完成 AI optional migration,且 `ql3-ai-feature` 当前为 `active`;
|
||||
2. Prompt 所属 Package 当前 active、未 quarantine,automation publication 中存在目标 Prompt;
|
||||
3. 当前强 User 同时具有 Project 的 `run.start`、`model.invoke` 与 `secret.use`;
|
||||
4. Provider token 已通过 `ql3-secret` 写入该 Project,并通过 `ql3-model-credential` 绑定到 Provider;
|
||||
5. Secret keyring 和 Owner credential 均位于 deployment root 内的私有路径;
|
||||
6. Provider endpoint/policy manifest 是 canonical JSON regular file,当前 UID 所有,完成配置后改为
|
||||
`0400` 或 `0440`,不得包含 token、SecretRef 或 authorization header。
|
||||
|
||||
Provider manifest 使用 schema `qinglong/projected-model-gateway-authority@v1`,只声明 Provider
|
||||
type/base URL、响应字节上限和 Project 模型 Policy。它不是 credential authority;实际 token 在每次
|
||||
模型请求时由 durable binding 和加密 Secret 重新解析。
|
||||
|
||||
## 执行命令
|
||||
|
||||
命令文件必须是 deployment root 的后代、当前 UID 所有且 mode `0600`:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "prompt.execute",
|
||||
"options": {
|
||||
"deploymentRoot": "/opt/qinglong3",
|
||||
"databasePath": "/opt/qinglong3/data/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/opt/qinglong3/owner-keys",
|
||||
"credentialFilePath": "/opt/qinglong3/credentials/operator.json",
|
||||
"secretKeyringPath": "/opt/qinglong3/secrets/keyring.json",
|
||||
"providerAuthorityFilePath": "/opt/qinglong3/ai/provider-authority.json",
|
||||
"busyTimeoutMs": 1000
|
||||
},
|
||||
"request": {
|
||||
"projectId": "default",
|
||||
"packageName": "example-package",
|
||||
"promptId": "summary",
|
||||
"requestId": "prompt-summary-20260804-1",
|
||||
"traceId": "trace-summary-20260804-1",
|
||||
"auditEventId": "71000000-0000-4000-8000-000000000001",
|
||||
"failureAuditEventId": "71000000-0000-4000-8000-000000000002",
|
||||
"parameters": {
|
||||
"subject": "仅存在于本次内存和 Provider 请求中的内容"
|
||||
},
|
||||
"provider": "openai-compatible",
|
||||
"model": "vendor/model-a",
|
||||
"maxOutputTokens": 512,
|
||||
"temperature": 0.2,
|
||||
"timeoutMs": 60000,
|
||||
"output": {
|
||||
"mode": "live_only"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
chmod 600 /opt/qinglong3/commands/prompt-summary.json
|
||||
ql3-prompt run --command-file /opt/qinglong3/commands/prompt-summary.json
|
||||
```
|
||||
|
||||
不要在 command 中加入 publication、publicationDigest、generation、plan、Run/StepRun/invocation
|
||||
identity、Policy fence、SecretRef 或 token;exact-shape 校验会在打开 SQLite 前拒绝这些字段。
|
||||
|
||||
## 结果与重放
|
||||
|
||||
首次成功返回 content-free receipt identity 和当前 live caller 的模型 `result`。SQLite 中的 plan、
|
||||
RunEvent、SecurityAudit、credential-use audit 与 finalization 不含参数值或模型正文。使用完全相同的
|
||||
私有 command file 重放时,返回同一 plan/admission/finalization,`status=existing`、`result=null`,
|
||||
不会再次调用 Provider。
|
||||
|
||||
当前产品入口只接受显式 `live_only`。需要长期保存正文时,不要把 stdout 重定向回数据库或日志;应在
|
||||
后续接入受支持的加密 `durable_artifact` key/output authority 后再启用该模式。
|
||||
|
||||
错误输出只包含稳定 `code`/`name`。token、SecretRef、数据库路径、credential path 和
|
||||
authentication identity 不会进入错误 JSON。
|
||||
@@ -0,0 +1,90 @@
|
||||
# QingLong 3.0 本机 Plugin Package Workflow
|
||||
|
||||
`ql3-workflow` 用于检查和启动已经由 Plugin Package 安装、materialize 并发布的 Workflow。它只适用于
|
||||
edge/standalone;Cluster 后续使用独立的受认证 API/RBAC transport。
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 已完成 Local Owner credential 与 Project RoleBinding;
|
||||
- Package generation 处于 active,未 disabled、withdrawn 或 quarantine;
|
||||
- `ql3-local-application` 正在运行并已输出 `event=active`;
|
||||
- command 目录为 `0700`,command file 为当前运行 UID 的 canonical `0600` regular file。
|
||||
|
||||
## 检查可用 Workflow
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "workflow.inspect",
|
||||
"options": {
|
||||
"deploymentRoot": "/opt/qinglong3",
|
||||
"databasePath": "/opt/qinglong3/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/opt/qinglong3/owner-peppers",
|
||||
"credentialFilePath": "/opt/qinglong3/owner-credential.json",
|
||||
"busyTimeoutMs": 100
|
||||
},
|
||||
"request": {
|
||||
"projectId": "default",
|
||||
"packageName": "example-automation",
|
||||
"requestId": "workflow-inspect-example-v1",
|
||||
"auditEventId": "REPLACE_WITH_UUID_V4",
|
||||
"failureAuditEventId": "REPLACE_WITH_DIFFERENT_UUID_V4"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
chmod 0600 /opt/qinglong3/commands/workflow-inspect.json
|
||||
ql3-workflow run --command-file /opt/qinglong3/commands/workflow-inspect.json
|
||||
```
|
||||
|
||||
结果只返回 Workflow metadata。记录目标 Workflow 的 `id` 和所有 Step `id`;不要从 Package bundle 手工
|
||||
拼 generation、revision 或 digest。
|
||||
|
||||
## 启动 Workflow
|
||||
|
||||
为 plan、Run 和每个 StepRun 分别生成 UUID v4。`stepRunIds` 的 key 必须与 inspect 返回的 Step `id`
|
||||
精确一致:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "workflow.start",
|
||||
"options": {
|
||||
"deploymentRoot": "/opt/qinglong3",
|
||||
"databasePath": "/opt/qinglong3/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/opt/qinglong3/owner-peppers",
|
||||
"credentialFilePath": "/opt/qinglong3/owner-credential.json",
|
||||
"busyTimeoutMs": 100
|
||||
},
|
||||
"request": {
|
||||
"projectId": "default",
|
||||
"packageName": "example-automation",
|
||||
"workflowId": "daily",
|
||||
"planId": "REPLACE_WITH_UUID_V4",
|
||||
"runId": "REPLACE_WITH_DIFFERENT_UUID_V4",
|
||||
"stepRunIds": {
|
||||
"collect": "REPLACE_WITH_DIFFERENT_UUID_V4",
|
||||
"summarize": "REPLACE_WITH_DIFFERENT_UUID_V4"
|
||||
},
|
||||
"requestId": "workflow-start-example-v1",
|
||||
"auditEventId": "REPLACE_WITH_DIFFERENT_UUID_V4",
|
||||
"failureAuditEventId": "REPLACE_WITH_DIFFERENT_UUID_V4"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
chmod 0600 /opt/qinglong3/commands/workflow-start.json
|
||||
ql3-workflow run --command-file /opt/qinglong3/commands/workflow-start.json
|
||||
```
|
||||
|
||||
`created` 表示 admission 已耐久提交,不表示 Workflow 已完成。application 的唯一 scheduler cadence 会继续
|
||||
推进 frontier、Task Attempt 和执行。若命令响应丢失,必须保留并原样重放同一文件;`existing` 表示同一个
|
||||
durable plan 已存在。不得重新生成 ID 来“重试”,否则会表达一个新的 Workflow Run。
|
||||
|
||||
`workflow.inspect` 需要 `run.read`;`workflow.start` 需要 `run.start`。Viewer 可以检查但不能启动,
|
||||
Operator/Admin/Owner 按当前 RoleBinding 执行。命令不会回显 credential、数据库路径、Package/plan digest、
|
||||
Task spec 或业务参数。
|
||||
@@ -0,0 +1,286 @@
|
||||
# `ql3-policy` 本机 Project 与 RoleBinding 管理
|
||||
|
||||
`ql3-policy` 是一次命令、一次进程的 Owner-only 管理入口,不启动管理 daemon,也不把
|
||||
credential token、pepper、数据库路径或 command 路径写入成功输出。
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 已完成 Fresh Setup;
|
||||
- 当前 credential 属于实例 authority Project 的 active User Owner;
|
||||
- deployment root 和子目录为当前 UID 所有、目录 `0700`、文件 `0600`;
|
||||
- SQLite 与 Owner pepper keyring 已通过 readiness;
|
||||
- 所有 UUID、Project、subject 和路径均替换为部署自己的值。
|
||||
|
||||
执行形式:
|
||||
|
||||
```sh
|
||||
ql3-policy run --command-file /srv/qinglong3/commands/project.json
|
||||
```
|
||||
|
||||
command file 必须是 deployment root 内的规范、非 symlink、当前 UID `0600` 文件。
|
||||
响应不确定时只能原样重放同一个 command。
|
||||
|
||||
## 创建 Project
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "policy.project.create",
|
||||
"options": {
|
||||
"deploymentRoot": "/srv/qinglong3",
|
||||
"databasePath": "/srv/qinglong3/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/srv/qinglong3/owner-keys",
|
||||
"credentialFilePath": "/srv/qinglong3/owner-credential.json"
|
||||
},
|
||||
"request": {
|
||||
"authorityProjectId": "default",
|
||||
"projectId": "automation",
|
||||
"name": "Automation",
|
||||
"slug": "automation",
|
||||
"expectedCurrentVersion": 0,
|
||||
"mutationId": "11111111-1111-4111-8111-111111111111",
|
||||
"requestId": "project-create-automation",
|
||||
"failureAuditEventId": "11111111-1111-4111-8111-111111111112"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
创建会在同一 SQLite 事务内建立 active Project、调用者自己的首个 active Owner
|
||||
RoleBinding、allowed audit 和 immutable mutation ledger。成功示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "policy.project.create",
|
||||
"status": "inserted",
|
||||
"projectId": "automation",
|
||||
"name": "Automation",
|
||||
"slug": "automation",
|
||||
"projectStatus": "active",
|
||||
"version": 1
|
||||
}
|
||||
```
|
||||
|
||||
Edge 最多 16 个 Project,Standalone 最多 128 个;archived Project 仍占容量。
|
||||
|
||||
## 归档与恢复
|
||||
|
||||
归档使用 Project 当前版本:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "policy.project.archive",
|
||||
"options": {
|
||||
"deploymentRoot": "/srv/qinglong3",
|
||||
"databasePath": "/srv/qinglong3/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/srv/qinglong3/owner-keys",
|
||||
"credentialFilePath": "/srv/qinglong3/owner-credential.json"
|
||||
},
|
||||
"request": {
|
||||
"authorityProjectId": "default",
|
||||
"projectId": "automation",
|
||||
"expectedCurrentVersion": 1,
|
||||
"mutationId": "22222222-2222-4222-8222-222222222221",
|
||||
"requestId": "project-archive-automation",
|
||||
"failureAuditEventId": "22222222-2222-4222-8222-222222222222"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
归档后 Project Policy 默认拒绝该 Project 的业务权限,但不删除 RoleBinding、Task、
|
||||
Run、Secret、Package 或审计历史。恢复把 operation 改为
|
||||
`policy.project.restore`,并把 `expectedCurrentVersion` 改为归档结果的版本。
|
||||
|
||||
实例 authority Project 不能归档。不要通过直接 SQL 修改其状态,也不要把 archive
|
||||
当成 hard delete。
|
||||
|
||||
## 查询 Project
|
||||
|
||||
查询一个 Project current head:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "policy.project.inspect",
|
||||
"options": {
|
||||
"deploymentRoot": "/srv/qinglong3",
|
||||
"databasePath": "/srv/qinglong3/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/srv/qinglong3/owner-keys",
|
||||
"credentialFilePath": "/srv/qinglong3/owner-credential.json"
|
||||
},
|
||||
"request": {
|
||||
"authorityProjectId": "default",
|
||||
"projectId": "automation",
|
||||
"requestId": "project-inspect-automation",
|
||||
"auditEventId": "44444444-4444-4444-8444-444444444441"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
已授权但不存在时返回 `found:false`。active 和 archived Project 都可查询;查询 archived
|
||||
Project 不会恢复其业务权限。
|
||||
|
||||
有界列出 Project:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "policy.project.list",
|
||||
"options": {
|
||||
"deploymentRoot": "/srv/qinglong3",
|
||||
"databasePath": "/srv/qinglong3/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/srv/qinglong3/owner-keys",
|
||||
"credentialFilePath": "/srv/qinglong3/owner-credential.json"
|
||||
},
|
||||
"request": {
|
||||
"authorityProjectId": "default",
|
||||
"limit": 16,
|
||||
"status": "all",
|
||||
"requestId": "project-list-first-page",
|
||||
"auditEventId": "44444444-4444-4444-8444-444444444442"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`limit` 必须为 1–64;`status` 只能为 `active`、`archived` 或 `all`。若响应中的
|
||||
`nextCursor` 非 null,下一条 command 使用新的 request/audit ID,并把该值原样放入
|
||||
request:
|
||||
|
||||
```json
|
||||
{
|
||||
"after": {
|
||||
"slug": "automation",
|
||||
"projectId": "automation"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
不要自行修改 cursor,也不要使用上一次 query command 的 audit event ID。列表按
|
||||
`slug, projectId` 正序稳定翻页,不支持 offset、任意排序或模糊搜索。
|
||||
每一页都是独立的 current-head snapshot;若翻页期间发生 create/archive/restore,
|
||||
需要严格同一时点清单时应从第一页重新查询。
|
||||
|
||||
## 管理 RoleBinding
|
||||
|
||||
授予或更新 RoleBinding:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "policy.role-binding.put",
|
||||
"options": {
|
||||
"deploymentRoot": "/srv/qinglong3",
|
||||
"databasePath": "/srv/qinglong3/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/srv/qinglong3/owner-keys",
|
||||
"credentialFilePath": "/srv/qinglong3/owner-credential.json"
|
||||
},
|
||||
"request": {
|
||||
"projectId": "automation",
|
||||
"target": { "type": "user", "id": "operator-user" },
|
||||
"role": "operator",
|
||||
"expectedCurrentVersion": 0,
|
||||
"mutationId": "33333333-3333-4333-8333-333333333331",
|
||||
"requestId": "role-put-automation-operator",
|
||||
"failureAuditEventId": "33333333-3333-4333-8333-333333333332"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
撤销时使用 `policy.role-binding.revoke`,移除 `role`,并填写 target 当前 binding
|
||||
version。Owner 只能授予 User;授予新 Owner 前,目标必须有 active Identity 和当前
|
||||
有效 credential。撤销或降级当前 Owner 时必须保留另一位可登录的 active User Owner。
|
||||
|
||||
## 查询 RoleBinding
|
||||
|
||||
取得一个 subject 的 current binding:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "policy.role-binding.inspect",
|
||||
"options": {
|
||||
"deploymentRoot": "/srv/qinglong3",
|
||||
"databasePath": "/srv/qinglong3/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/srv/qinglong3/owner-keys",
|
||||
"credentialFilePath": "/srv/qinglong3/owner-credential.json"
|
||||
},
|
||||
"request": {
|
||||
"projectId": "automation",
|
||||
"target": { "type": "user", "id": "operator-user" },
|
||||
"requestId": "role-inspect-automation-operator",
|
||||
"auditEventId": "55555555-5555-4555-8555-555555555551"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
已授权但从未存在时返回 `found:false`。当前 binding 已撤销时返回 `state:"revoked"`,
|
||||
不返回 `role`;历史 active revision 不会被当成当前状态。
|
||||
|
||||
有界列出每个 subject 的最新 binding:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "policy.role-binding.list",
|
||||
"options": {
|
||||
"deploymentRoot": "/srv/qinglong3",
|
||||
"databasePath": "/srv/qinglong3/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/srv/qinglong3/owner-keys",
|
||||
"credentialFilePath": "/srv/qinglong3/owner-credential.json"
|
||||
},
|
||||
"request": {
|
||||
"projectId": "automation",
|
||||
"limit": 32,
|
||||
"state": "active",
|
||||
"role": "all",
|
||||
"requestId": "role-list-automation-first-page",
|
||||
"auditEventId": "55555555-5555-4555-8555-555555555552"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`limit` 必须为 1–64;`state` 只能为 `active|revoked|all`;`role` 只能为
|
||||
`owner|admin|operator|viewer|all`。下一页使用新的 request/audit ID,并把响应的
|
||||
`nextCursor` 原样放入 request:
|
||||
|
||||
```json
|
||||
{
|
||||
"after": {
|
||||
"subjectType": "api_app",
|
||||
"subjectId": "deployment-bot"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
每一页都是独立的 current-head snapshot。若翻页期间发生 put/revoke,需要严格同一
|
||||
时点清单时从第一页重新查询。RoleBinding query 由目标 Project 自己的当前 Owner
|
||||
授权,不要求实例 authority Project;但 Owner 不能用一个 Project 的权限查询另一个
|
||||
Project。
|
||||
|
||||
## Authority 与失败处理
|
||||
|
||||
- Project lifecycle command 的 `authorityProjectId` 必须是 ADR-0211 定义的实例
|
||||
authority Project;
|
||||
- Project inspect/list 也必须使用该 authority Project;secondary Project Owner
|
||||
不能枚举实例 Project 拓扑;
|
||||
- secondary Project Owner 可以管理自身 RoleBinding,但不能创建、归档或恢复其他
|
||||
Project;
|
||||
- RoleBinding inspect/list 与 put/revoke 一样,只允许目标 Project 的当前 Owner;
|
||||
- `current_version_conflict`:重新取得受支持的当前版本后创建新 command;
|
||||
- `mutation_conflict`:同一 mutation ID 已用于不同语义,停止重试并人工调查;
|
||||
- `project_identity_conflict`:Project ID 或 slug 已存在;
|
||||
- `project_capacity_exceeded`:已达到 Profile 硬上限;
|
||||
- `authority_project_protected`:试图归档实例 authority Project;
|
||||
- `credential_or_policy_fence_rejected`:认证后 credential、authority 或 Owner fence
|
||||
已变化,重新认证并使用新 mutation。
|
||||
|
||||
当前没有 Project rename、authority transfer 或 hard-delete 命令。不要用直接 SQLite
|
||||
查询或修改模拟这些产品能力。
|
||||
@@ -0,0 +1,60 @@
|
||||
# QingLong 3.0 Edge/Standalone SQLite Readiness 诊断
|
||||
|
||||
本命令用于诊断已初始化的 fresh 或 adopted QingLong 3.0 SQLite authority。它不会执行 migration、
|
||||
修复 schema 或改变 journal mode。
|
||||
|
||||
## 使用
|
||||
|
||||
安装后的产品入口:
|
||||
|
||||
```sh
|
||||
ql3-local-readiness \
|
||||
--database=/opt/qinglong3/qinglong3.sqlite \
|
||||
--profile=edge \
|
||||
--busy-timeout-ms=1000
|
||||
```
|
||||
|
||||
仓库入口:
|
||||
|
||||
```sh
|
||||
pnpm audit:schema:ql3 -- \
|
||||
--database=/opt/qinglong3/qinglong3.sqlite \
|
||||
--profile=edge \
|
||||
--busy-timeout-ms=1000
|
||||
```
|
||||
|
||||
`--profile` 必须与部署配置一致:edge 要求 `DELETE` journal,standalone 要求 `WAL`。数据库必须
|
||||
是当前运行用户所有的 canonical、非 symlink、精确 `0600` regular file。
|
||||
|
||||
成功返回低敏 JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "local.readiness.inspect",
|
||||
"status": "ready",
|
||||
"profile": "edge",
|
||||
"storage": {
|
||||
"contractName": "local-control-core",
|
||||
"contractVersion": 42,
|
||||
"migrationCount": 84,
|
||||
"tableCount": 76,
|
||||
"sqliteVersion": "3.x",
|
||||
"journalMode": "delete"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`tableCount` 和 SQLite version 会随正式 schema 演进,以实际输出为准。结果不会包含数据库路径、业务行、credential、
|
||||
Secret 或 digest。失败只输出稳定错误 code/name;修复前不要通过 chmod 放宽文件权限、绕过
|
||||
symlink 检查或对用户库执行 `drizzle-kit push`。
|
||||
|
||||
旧 `back/migrations` Shadow schema 诊断必须显式使用:
|
||||
|
||||
```sh
|
||||
pnpm audit:legacy-schema:ql3 -- \
|
||||
--database=/absolute/path/to/legacy.sqlite \
|
||||
--json
|
||||
```
|
||||
|
||||
legacy 报告不能作为 fresh/adopted 3.0 readiness 证据。
|
||||
@@ -0,0 +1,153 @@
|
||||
# `ql3-audit` 本机安全审计查询与诊断压缩
|
||||
|
||||
`ql3-audit` 是一次命令、一次进程的实例 Owner 运维入口。它提供有界查询和显式的
|
||||
诊断审计压缩,不启动管理 daemon,不开放网络端口,也不返回 durable audit 中的
|
||||
`authenticationId`。
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 已完成 Fresh Setup 或采用流程;
|
||||
- 当前 credential 属于实例 authority Project 的 active User Owner;
|
||||
- deployment root 和子目录由当前 UID 所有,目录为 `0700`、文件为 `0600`;
|
||||
- SQLite、credential 和 Owner pepper keyring 已通过 readiness。
|
||||
|
||||
执行形式:
|
||||
|
||||
```sh
|
||||
ql3-audit run --command-file /srv/qinglong3/commands/audit-list.json
|
||||
```
|
||||
|
||||
command file 必须是 deployment root 内的规范、非 symlink、当前 UID `0600` 文件。
|
||||
|
||||
## 查询第一页
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "security.audit.list",
|
||||
"options": {
|
||||
"deploymentRoot": "/srv/qinglong3",
|
||||
"databasePath": "/srv/qinglong3/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/srv/qinglong3/owner-keys",
|
||||
"credentialFilePath": "/srv/qinglong3/owner-credential.json"
|
||||
},
|
||||
"request": {
|
||||
"authorityProjectId": "default",
|
||||
"query": {
|
||||
"limit": 32,
|
||||
"filter": {
|
||||
"projectId": "automation",
|
||||
"outcome": "denied"
|
||||
}
|
||||
},
|
||||
"requestId": "audit-denied-automation-page-1",
|
||||
"auditEventId": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`limit` 必须为 1–64。`filter` 只能包含以下可选精确条件:
|
||||
|
||||
- `projectId`;
|
||||
- `subject`,形如 `{"type":"user","id":"owner-user"}`;
|
||||
- `outcome`。
|
||||
|
||||
空过滤器必须写成 `"filter": {}`。不支持 offset、模糊查询、任意排序或调用方 SQL。
|
||||
|
||||
## 翻页
|
||||
|
||||
若响应的 `nextCursor` 非 null,下一条 command 使用新的 request/audit ID,并把 cursor
|
||||
原样放入 `query.before`:
|
||||
|
||||
```json
|
||||
{
|
||||
"query": {
|
||||
"limit": 32,
|
||||
"before": {
|
||||
"occurredAtMs": 1785300000000,
|
||||
"eventId": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"
|
||||
},
|
||||
"filter": {
|
||||
"projectId": "automation",
|
||||
"outcome": "denied"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
排序固定为 `occurredAtMs DESC,eventId DESC`。每页是独立授权和独立 SQLite snapshot;
|
||||
如果翻页期间有新事件,而你要求严格同一时点清单,应从第一页重新查询。
|
||||
|
||||
不要复用上一页的 `auditEventId`。查询自己的 allowed audit 在读取 snapshot 后才于同一
|
||||
事务写入,因此当前响应不会包含本次查询事件。
|
||||
|
||||
## 输出与安全边界
|
||||
|
||||
成功记录只包含 event/request/operation、nullable Project/subject、outcome、reasons、
|
||||
nullable fence 和 timestamp。输出中不存在 `authenticationId`、credential、pepper、
|
||||
Secret、路径或 command 内容。
|
||||
|
||||
只有实例 authority Project 的当前强认证 User Owner 可以使用此入口。secondary
|
||||
Project Owner 即使能管理自己的 RoleBinding,也不能枚举实例审计。不要通过直接 SQL
|
||||
绕过该边界。
|
||||
|
||||
## 显式压缩过期诊断事件
|
||||
|
||||
先保留部署所需的审计期限,再计算一个不晚于“当前受信时间减 retention”的固定
|
||||
`eligibleBeforeMs`。例如 Edge 每次最多处理 64 条:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "security.audit.compact",
|
||||
"options": {
|
||||
"deploymentRoot": "/srv/qinglong3",
|
||||
"databasePath": "/srv/qinglong3/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/srv/qinglong3/owner-keys",
|
||||
"credentialFilePath": "/srv/qinglong3/owner-credential.json"
|
||||
},
|
||||
"request": {
|
||||
"authorityProjectId": "default",
|
||||
"retentionMs": 2592000000,
|
||||
"eligibleBeforeMs": 1782710400000,
|
||||
"limit": 64,
|
||||
"mutationId": "cccccccc-cccc-4ccc-8ccc-ccccccccccc1",
|
||||
"requestId": "audit-compact-2026-06-batch-1",
|
||||
"failureAuditEventId": "cccccccc-cccc-4ccc-8ccc-ccccccccccc2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
约束:
|
||||
|
||||
- retention 必须为 30 天至 10 年;
|
||||
- `eligibleBeforeMs + retentionMs` 不得晚于执行时的受信时钟;
|
||||
- Edge `limit` 为 1–64,Standalone 为 1–512;
|
||||
- `mutationId` 和 `failureAuditEventId` 必须不同;
|
||||
- exact replay 必须复用完全相同的 command 语义;
|
||||
- 处理下一批时必须生成新的 mutation/request/failure identity。
|
||||
|
||||
响应只返回 batch receipt 的低敏投影,包括删除数量、payload bytes、首尾 cursor、
|
||||
records digest 和创建时间。重复运行新的 batch,直到 `deletedCount` 为 0。
|
||||
|
||||
## 压缩的安全边界
|
||||
|
||||
v1 只删除超过期限、没有已知引用的诊断事件:
|
||||
|
||||
- 所有非 `allowed` outcome;
|
||||
- 精确只读 operation:
|
||||
`identity.inspect`、`credential.inspect`、`policy.project.inspect`、
|
||||
`policy.project.list`、`policy.role_binding.inspect`、
|
||||
`policy.role_binding.list`、`security.audit.list`。
|
||||
|
||||
允许的 mutation、dispatch、execution、package 和 compaction evidence 不会被该命令
|
||||
删除。不要把它描述为通用 retention、合规销毁或完整日志轮转。
|
||||
|
||||
删除、allowed audit 和不可变 compaction receipt 在同一个 `BEGIN IMMEDIATE` 中提交。
|
||||
命令不执行 `VACUUM`;删除页可供 SQLite 后续复用,但数据库文件大小可能不会立即
|
||||
下降。物理缩容需要独立的离线备份/恢复维护流程。
|
||||
|
||||
查询是有界交互能力,压缩是保守的诊断回收能力;二者都不是合规 export。签名导出、
|
||||
远端归档、完整领域 retention、销毁证明、聚合和告警仍是后续独立能力。
|
||||
@@ -0,0 +1,144 @@
|
||||
# `ql3-task` 本机 TaskDefinition 管理
|
||||
|
||||
`ql3-task` 是 Edge/Standalone 的一次命令、一次进程管理入口。它不启动管理服务;成功输出
|
||||
不包含 Task spec、命令参数、SecretRef、credential、pepper 或本机路径。
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 已完成 Fresh Setup、Owner credential 交付与 Project/RoleBinding 配置;
|
||||
- 调用者是 strong User;owner、admin、operator 可 create/update,viewer 只能 inspect/list;
|
||||
- deployment root/子目录属于当前 UID,目录 `0700`、credential 与 command file 为 `0600`;
|
||||
- 当前 production Task registry 只接受 `kind=command`、`schema=qinglong/command@v1`。
|
||||
|
||||
统一执行形式:
|
||||
|
||||
```sh
|
||||
ql3-task run --command-file /srv/qinglong3/commands/task-create.json
|
||||
```
|
||||
|
||||
响应未知时只能原样重放同一个 command file。不要修改 mutation、request、时间或 Task 内容
|
||||
来“重试”。
|
||||
|
||||
## 创建 Task
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "task.put",
|
||||
"options": {
|
||||
"deploymentRoot": "/srv/qinglong3",
|
||||
"databasePath": "/srv/qinglong3/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/srv/qinglong3/owner-keys",
|
||||
"credentialFilePath": "/srv/qinglong3/owner-credential.json",
|
||||
"busyTimeoutMs": 100
|
||||
},
|
||||
"request": {
|
||||
"projectId": "default",
|
||||
"taskId": "daily-health-check",
|
||||
"expectedRevision": null,
|
||||
"mutationId": "11111111-1111-4111-8111-111111111111",
|
||||
"requestId": "task-create-daily-health-check",
|
||||
"failureAuditEventId": "11111111-1111-4111-8111-111111111112",
|
||||
"name": "Daily health check",
|
||||
"description": "Print a bounded local health marker",
|
||||
"kind": "command",
|
||||
"spec": {
|
||||
"schema": "qinglong/command@v1",
|
||||
"config": {
|
||||
"command": {
|
||||
"kind": "argv",
|
||||
"file": "/usr/bin/printf",
|
||||
"args": ["health-check\\n"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"labels": { "team": "operations" },
|
||||
"enabled": true,
|
||||
"occurredAtMs": 1785542400000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`occurredAtMs` 是 immutable command 语义的一部分,必须在创建 command file 时固定;不得用
|
||||
每次执行时的当前时间替换。首次返回 `created`,结果未知后逐字重放返回 `existing`。
|
||||
|
||||
## 更新、停用与重新启用
|
||||
|
||||
先 inspect 当前 Task,取得 `revision`。随后创建新的 `task.put` command:
|
||||
|
||||
- `expectedRevision` 填当前 revision;
|
||||
- 每次使用全新的 `mutationId`、`requestId`、`failureAuditEventId` 和 `occurredAtMs`;
|
||||
- 修改 name/description/spec/labels 或 enabled;
|
||||
- 停用设 `enabled:false`,重新启用设 `enabled:true`。
|
||||
|
||||
成功返回 `updated` 和递增 revision。不存在原地修改或 delete;历史 revision 继续解释已经
|
||||
固定到旧 revision 的 Run。`expectedRevision` 冲突时不要盲重试,重新 inspect 并人工确认差异。
|
||||
|
||||
## 查询一个 Task
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "task.inspect",
|
||||
"options": {
|
||||
"deploymentRoot": "/srv/qinglong3",
|
||||
"databasePath": "/srv/qinglong3/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/srv/qinglong3/owner-keys",
|
||||
"credentialFilePath": "/srv/qinglong3/owner-credential.json"
|
||||
},
|
||||
"request": {
|
||||
"projectId": "default",
|
||||
"taskId": "daily-health-check",
|
||||
"requestId": "task-inspect-daily-health-check",
|
||||
"auditEventId": "22222222-2222-4222-8222-222222222221",
|
||||
"failureAuditEventId": "22222222-2222-4222-8222-222222222222"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
已授权但不存在返回 `found:false`。存在时仅返回低敏摘要,不返回 `spec.config`;需要更新的
|
||||
部署者应保留自己受保护的源 command/template,不能把 inspect 当作明文配置导出。
|
||||
|
||||
## 有界列出 Task
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "task.list",
|
||||
"options": {
|
||||
"deploymentRoot": "/srv/qinglong3",
|
||||
"databasePath": "/srv/qinglong3/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/srv/qinglong3/owner-keys",
|
||||
"credentialFilePath": "/srv/qinglong3/owner-credential.json"
|
||||
},
|
||||
"request": {
|
||||
"projectId": "default",
|
||||
"limit": 32,
|
||||
"requestId": "task-list-default-first-page",
|
||||
"auditEventId": "33333333-3333-4333-8333-333333333331",
|
||||
"failureAuditEventId": "33333333-3333-4333-8333-333333333332"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`limit` 必须为 1–256。若 `nextCursor` 非 null,下一页使用新的 request/audit identity,并把
|
||||
cursor 原样放入 request:
|
||||
|
||||
```json
|
||||
{ "after": { "taskId": "daily-health-check" } }
|
||||
```
|
||||
|
||||
列表按 `taskId` 正序稳定翻页,不支持 offset、模糊搜索或无界返回。每页是独立 current-head
|
||||
snapshot;若翻页期间有更新,需要严格同一时点清单时从第一页重新查询。
|
||||
|
||||
## 事务、安全与恢复
|
||||
|
||||
- create/update 的 allowed audit、Task head、immutable revision、mutation ledger 和可执行
|
||||
command revision 在同一 SQLite 事务提交;credential 或 Policy 在授权后变化会整体回滚;
|
||||
- authentication/authorization/fence/semantic/revision/audit 冲突均失败关闭;禁止直接 SQL 修补;
|
||||
- 成功输出的 `contentDigest` 可用于变更审核,但不是可反推出 spec 的备份;
|
||||
- `ql3-task` 只适用于 Edge/Standalone。Cluster 必须使用后续 PostgreSQL 管理 transport,不得把
|
||||
本机 credential/SQLite 文件挂入集群 Pod 作为替代入口。
|
||||
@@ -0,0 +1,127 @@
|
||||
# `ql3-trigger` 本机 Trigger 管理
|
||||
|
||||
`ql3-trigger` 是 Edge/Standalone 的一次命令、一次进程管理入口。它复用现有 application
|
||||
scheduler,不启动管理服务、独立 timer 或后台连接。Cluster 不支持此入口。
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 已完成 Fresh Setup、Owner credential、Project/RoleBinding 与 TaskDefinition 配置;
|
||||
- 调用者是 strong User;owner、admin、operator 可 put,viewer 只能 inspect/list;
|
||||
- deployment root/子目录属于当前 UID,目录 `0700`、credential 与 command file 为 `0600`;
|
||||
- 先用 `ql3-task` 执行 `task.inspect` command,取得 Task 的 current `revision` 与
|
||||
`contentDigest`;
|
||||
- 当前 production Trigger registry 只接受 `schema=qinglong/cron@v1`。
|
||||
|
||||
统一执行形式:
|
||||
|
||||
```sh
|
||||
ql3-trigger run --command-file /srv/qinglong3/commands/trigger-create.json
|
||||
```
|
||||
|
||||
响应未知时只能原样重放同一个 command file。不要修改 mutation、request、时间、Task binding
|
||||
或 Trigger 内容来“重试”。
|
||||
|
||||
## 创建 Trigger
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "trigger.put",
|
||||
"options": {
|
||||
"deploymentRoot": "/srv/qinglong3",
|
||||
"databasePath": "/srv/qinglong3/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/srv/qinglong3/owner-keys",
|
||||
"credentialFilePath": "/srv/qinglong3/owner-credential.json",
|
||||
"busyTimeoutMs": 100
|
||||
},
|
||||
"request": {
|
||||
"projectId": "default",
|
||||
"triggerId": "daily-health-check-cron",
|
||||
"expectedRevision": null,
|
||||
"mutationId": "44444444-4444-4444-8444-444444444441",
|
||||
"requestId": "trigger-create-daily-health-check",
|
||||
"failureAuditEventId": "44444444-4444-4444-8444-444444444442",
|
||||
"taskId": "daily-health-check",
|
||||
"taskRevision": 1,
|
||||
"taskContentDigest": "REPLACE_WITH_TASK_INSPECT_CONTENT_DIGEST",
|
||||
"spec": {
|
||||
"schema": "qinglong/cron@v1",
|
||||
"config": {
|
||||
"expression": "*/5 * * * *",
|
||||
"timezone": "Etc/UTC",
|
||||
"misfirePolicy": "skip"
|
||||
}
|
||||
},
|
||||
"enabled": true,
|
||||
"occurredAtMs": 1785542401000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`expression` 接受有界五段或六段 cron,`timezone` 必须是运行时支持的 IANA timezone,
|
||||
`misfirePolicy` 只能是 `skip` 或 `fire_once`。首次返回 `created`;相同文件逐字重放返回
|
||||
`existing`。
|
||||
|
||||
## 更新、停用、启用与 repin
|
||||
|
||||
先 inspect 当前 Trigger,取得 `revision`。每次更新都提交新的 `trigger.put`:
|
||||
|
||||
- `expectedRevision` 填当前 Trigger revision;
|
||||
- 每次使用新的 `mutationId`、`requestId`、`failureAuditEventId` 和 `occurredAtMs`;
|
||||
- 修改 schedule 或 `enabled` 会追加新 revision,不存在原地修改;
|
||||
- 启用 Trigger 时,`taskRevision`/`taskContentDigest` 必须精确匹配当前且 enabled 的 Task head;
|
||||
- Task 更新、停用或重新启用后,旧 Trigger 会自动停止 admission。先 inspect Task,再以新的
|
||||
Trigger revision 显式 repin;系统不会静默改写绑定;
|
||||
- Task 已变化时仍允许提交 `enabled:false` 的 Trigger revision,保证 operator 能撤权。此 revision
|
||||
可以保留历史 Task pin,但再次启用必须改为 current pin。
|
||||
|
||||
扫描取得候选后若 Task 发生变化,最终 Run commit 仍会失败关闭;不需要直接清理 schedule 表。
|
||||
|
||||
## 查询一个 Trigger
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "trigger.inspect",
|
||||
"options": {
|
||||
"deploymentRoot": "/srv/qinglong3",
|
||||
"databasePath": "/srv/qinglong3/qinglong3.sqlite",
|
||||
"profile": "edge",
|
||||
"ownerPepperKeyringDirectory": "/srv/qinglong3/owner-keys",
|
||||
"credentialFilePath": "/srv/qinglong3/owner-credential.json"
|
||||
},
|
||||
"request": {
|
||||
"projectId": "default",
|
||||
"triggerId": "daily-health-check-cron",
|
||||
"requestId": "trigger-inspect-daily-health-check",
|
||||
"auditEventId": "55555555-5555-4555-8555-555555555551",
|
||||
"failureAuditEventId": "55555555-5555-4555-8555-555555555552"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
存在时只返回低敏摘要,不返回 cron expression、timezone 或 misfire policy。部署者应保留受保护
|
||||
的源 command/template,不要把 inspect 当作配置导出。
|
||||
|
||||
## 有界列出 Trigger
|
||||
|
||||
`trigger.list` request 包含 `projectId`、`limit`、`requestId`、`auditEventId` 与
|
||||
`failureAuditEventId`。`limit` 必须为 1–256;若 `nextCursor` 非 null,下一页使用新的
|
||||
request/audit identity,并原样加入:
|
||||
|
||||
```json
|
||||
{ "after": { "triggerId": "daily-health-check-cron" } }
|
||||
```
|
||||
|
||||
列表按 `triggerId` 正序稳定翻页,不支持 offset、模糊搜索或无界返回。每页是独立
|
||||
current-head snapshot。
|
||||
|
||||
## 事务、安全与恢复
|
||||
|
||||
- put 的 allowed audit、Trigger head/revision、mutation ledger 与 schedule reset 在同一 SQLite
|
||||
事务提交;credential、Policy 或 Task fence 变化会整体回滚;
|
||||
- exact replay 绑定 actor、Policy fence、Task binding、Trigger spec 和 immutable time;
|
||||
- 失败后禁止直接 SQL 修补 Trigger、schedule 或 audit;revision conflict 应重新 inspect 并人工
|
||||
确认;
|
||||
- `ql3-trigger` 只适用于 Edge/Standalone。Cluster 必须使用 PostgreSQL/RBAC 管理 transport。
|
||||
@@ -0,0 +1,127 @@
|
||||
# QingLong 3.0 Cluster Plugin Package 管理客户端
|
||||
|
||||
`ql3-plugin-package-client` 是短生命周期 operator 命令。每次启动只向私有
|
||||
Plugin Package management Service 发送一个命令,然后退出;它不签发身份 assertion,
|
||||
不自动重试,也不保存 server response。
|
||||
|
||||
直接 client 只适用于已经能够解析并访问 management Service DNS 的受控私有网络。
|
||||
Kubernetes ClusterIP 从 workstation 访问时,使用后文的 tunnel client;不要把 Service
|
||||
改成公共 LoadBalancer。
|
||||
|
||||
## 准备文件
|
||||
|
||||
创建三个由当前 UID 拥有、mode 为 `0600` 的 canonical regular file。不要使用 symlink,
|
||||
也不要把 assertion 放进 shell 参数、环境变量或 stdin。
|
||||
|
||||
`client.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"endpoint": "https://ql3-plugin-package-management.qinglong3-system.svc:8443/api/v3/plugin-packages/management",
|
||||
"servername": "ql3-plugin-package-management.qinglong3-system.svc",
|
||||
"caFile": "/absolute/path/management-ca.pem",
|
||||
"requestTimeoutMs": 5000
|
||||
}
|
||||
```
|
||||
|
||||
CA 文件必须是 canonical、非 symlink、当前 UID 或 root 拥有且 group/world 不可写。
|
||||
|
||||
`command.json` 示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"operation": "plugin-package.inspect",
|
||||
"request": {
|
||||
"actionRef": "package:example:1",
|
||||
"approvalRequestId": "approval-example-1",
|
||||
"inspectionId": "inspection-example-1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`assertion.jwt` 只包含外部 IdP ceremony 签发的短期 compact JWT,不带换行或其他字段。
|
||||
|
||||
```sh
|
||||
chmod 600 /absolute/path/client.json \
|
||||
/absolute/path/command.json \
|
||||
/absolute/path/assertion.jwt
|
||||
```
|
||||
|
||||
## 执行
|
||||
|
||||
```sh
|
||||
ql3-plugin-package-client \
|
||||
--config=/absolute/path/client.json \
|
||||
--command=/absolute/path/command.json \
|
||||
--assertion=/absolute/path/assertion.jwt
|
||||
```
|
||||
|
||||
可用 operation 只有以下九个:
|
||||
|
||||
- `plugin-package.propose|decide|inspect`
|
||||
- `plugin-package.publisher-revocation.propose|decide|inspect`
|
||||
- `plugin-package.publisher-trust-transition.propose|decide|inspect`
|
||||
|
||||
成功时 stdout 只有服务端审查过的低敏 result。失败时 stderr 只有稳定 code,以及可能的
|
||||
HTTP status、response code、request ID 和 Retry-After;不会输出 assertion、路径、
|
||||
endpoint、command、证书、Error message 或 stack。
|
||||
|
||||
## 结果不确定与重放
|
||||
|
||||
连接中断或 timeout 不能证明服务端没有提交。客户端不会自动重试,也不会生成新的 ID。
|
||||
operator 应先保留原 command 文件并检查 durable 状态;需要重放时,使用原文件再次显式
|
||||
执行。服务端以原 action/decision/inspection identity 的 exact replay 收敛。
|
||||
|
||||
不要在结果不确定时修改 command 中的 ID、expected version 或业务字段后重试;那会形成
|
||||
新的操作语义,而不是恢复原操作。
|
||||
|
||||
## Kubernetes ClusterIP tunnel
|
||||
|
||||
`ql3-plugin-package-client-kubernetes` 从 workstation 使用 Kubernetes port-forward
|
||||
subresource 建立一次 raw tunnel,随后仍用 management CA 和 Service DNS 执行端到端
|
||||
TLS 1.3。它不创建 Pod、Job、Secret、ConfigMap 或本地 listener。
|
||||
|
||||
新增 `kubernetes.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"kubeconfigFile": "/absolute/private/ql3-operator.kubeconfig.json",
|
||||
"context": "production",
|
||||
"namespace": "qinglong3-system",
|
||||
"apiTimeoutMs": 5000
|
||||
}
|
||||
```
|
||||
|
||||
该文件和 kubeconfig 都必须是当前 UID 拥有、canonical、非 symlink 的 `0600` 文件。
|
||||
kubeconfig 必须是专用 exact JSON,而不是 ambient `~/.kube/config`:只能有一个
|
||||
cluster、一个 user、一个 context,`current-context` 必须等于 `production`。cluster
|
||||
只允许 HTTPS server 和内嵌 `certificate-authority-data`;user 只允许内嵌短期 token,
|
||||
或一对匹配的内嵌 `client-certificate-data`/`client-key-data`。proxy、skip TLS、
|
||||
token/certificate/key 文件引用、exec/auth-provider、basic auth 和 impersonation 都会
|
||||
失败关闭。
|
||||
|
||||
operator Kubernetes identity 需要目标 namespace 的:
|
||||
|
||||
- `list` `pods`
|
||||
- `create` `pods/portforward`
|
||||
|
||||
port-forward 是 Kubernetes RBAC 控制的独立私有入口,不是 labelled-Pod NetworkPolicy
|
||||
证据;它仍须通过 QingLong User assertion、Project Policy、双人审批和 durable quota。
|
||||
|
||||
执行:
|
||||
|
||||
```sh
|
||||
ql3-plugin-package-client-kubernetes \
|
||||
--config=/absolute/path/client.json \
|
||||
--command=/absolute/path/command.json \
|
||||
--assertion=/absolute/path/assertion.jwt \
|
||||
--kubernetes=/absolute/path/kubernetes.json
|
||||
```
|
||||
|
||||
client 只接受
|
||||
`ql3-plugin-package-management.qinglong3-system.svc:8443`,只选择最多两个
|
||||
Running/Ready、无 token mount 的 management Pod,并且不会在失败后自动切换 Pod 或
|
||||
重建 tunnel。
|
||||
@@ -0,0 +1,125 @@
|
||||
# QingLong 3.0 caller-driven Worker credential execution
|
||||
|
||||
This operation consumes one already approved Worker credential action. It is a
|
||||
short-lived Kubernetes `Job`, not a scheduler, controller or rotation loop.
|
||||
The management Deployment never receives this Job's PostgreSQL credential,
|
||||
pepper or Kubernetes issuer token.
|
||||
|
||||
## Preconditions
|
||||
|
||||
- PostgreSQL is migrated through `pg-0050` and the dedicated
|
||||
`ql3_worker_credential_executor` role is ready.
|
||||
- The Worker `credential-admin` overlay is installed. Its delivery
|
||||
ServiceAccount has the reviewed 8-allow/20-deny RBAC matrix.
|
||||
- Two distinct strong Users created and approved the exact immutable plan.
|
||||
- The caller has the resulting action, approval, consumption, dispatch and
|
||||
audit IDs without any credential token.
|
||||
- The independently verified cluster-admin image digest is available.
|
||||
|
||||
Copy
|
||||
`deploy/kubernetes/ql3-cluster/operations/worker-credential-executor/config.example.yaml`
|
||||
to a private per-dispatch overlay. Replace every placeholder. Keep
|
||||
`command.json` at schema version 1 with exactly these six fields:
|
||||
|
||||
```json
|
||||
{"schemaVersion":1,"actionRef":"…","approvalRequestId":"…","consumptionId":"…","dispatchId":"…","auditEventId":"…"}
|
||||
```
|
||||
|
||||
Create the pepper through a Secret manager. It must be a distinct canonical
|
||||
32-byte base64url value and must not appear in the command, argv, logs, Git or
|
||||
the management Deployment. Treat the immutable command ConfigMap as
|
||||
per-dispatch input; do not rewrite and reuse it for a different approval.
|
||||
|
||||
## Private network and image overlay
|
||||
|
||||
The committed base deliberately permits only DNS. The CloudNativePG overlay
|
||||
adds only the exact `ql3-postgres` Pods on TCP 5432. Copy
|
||||
`api-server-egress-patch.example.yaml` into the private overlay and replace its
|
||||
TEST-NET address and port with the exact Kubernetes API destination observed by
|
||||
Pods under the deployed CNI. Some CNIs enforce policy before Service DNAT and
|
||||
others after it; verify the enforced destination rather than assuming the
|
||||
`kubernetes.default` ClusterIP is sufficient. Never use `0.0.0.0/0`, an empty
|
||||
egress peer, or a namespace-wide API destination.
|
||||
|
||||
The private Kustomization must also replace the all-zero
|
||||
`qinglong3-cluster-admin` digest with the independently verified digest. Keep
|
||||
the Job `backoffLimit` at zero: a caller inspects durable execution state before
|
||||
explicitly deciding whether to retry the same command.
|
||||
|
||||
Render and audit before creating anything:
|
||||
|
||||
```bash
|
||||
pnpm audit:cluster-deployment:ql3
|
||||
kubectl kustomize /absolute/path/to/private-worker-executor-overlay >/tmp/ql3-worker-executor.yaml
|
||||
```
|
||||
|
||||
Review the rendered file for exactly one `Job`, no `CronJob` or `Deployment`,
|
||||
the executor-only PostgreSQL Secret, a 600-second projected ServiceAccount
|
||||
token and the exact TokenRequest RoleBinding. Then create and observe it:
|
||||
|
||||
```bash
|
||||
kubectl create -k /absolute/path/to/private-worker-executor-overlay
|
||||
kubectl -n qinglong3-system wait \
|
||||
--for=condition=Complete job/ql3-worker-credential-executor \
|
||||
--timeout=10m
|
||||
kubectl -n qinglong3-system logs job/ql3-worker-credential-executor
|
||||
```
|
||||
|
||||
Successful stdout contains only low-sensitive execution and delivery status.
|
||||
It never contains the issued Worker token. On failure, inspect the durable
|
||||
approval dispatch and execution receipt before recreating the same exact Job;
|
||||
do not generate a new command identity or broaden RBAC to force progress.
|
||||
|
||||
## Live evidence
|
||||
|
||||
The `test:worker-kubernetes-rollout-live:ql3` gate runs current-source
|
||||
cluster-admin, cluster-control and Worker images in K3s `v1.34.3+k3s1`
|
||||
against digest-bound PostgreSQL 18.4. Its first
|
||||
caller-driven Job completed a third credential rotation with
|
||||
`deliveryStatus=published` and `tokenRequestUsed=true`. A second independent
|
||||
Job replayed the exact same command and completed with
|
||||
`deliveryStatus=existing` and `tokenRequestUsed=false`. The durable database
|
||||
facts converged to four plans, consumed approvals, dispatches, successful
|
||||
executions, credentials and published deliveries plus sixteen management
|
||||
security audit events. Three host-side fresh execution/exact-replay pairs
|
||||
perform exactly nine authorization rechecks; the separate Job keeps its own
|
||||
authorization boundary.
|
||||
|
||||
The product phase then composes the real Cluster Worker ingress and production
|
||||
Worker over TLS 1.3 mutual authentication. Credential generation 4 and a new
|
||||
client identity each force a `Recreate` replacement on the same RWO PVC. The
|
||||
three distinct Worker Sessions must each persist `online`, at least one
|
||||
heartbeat audit, `draining` and `offline`; their generations must increase
|
||||
strictly. The final scale-to-zero must complete graceful drain before the
|
||||
360-second Kubernetes termination grace expires.
|
||||
|
||||
That gate also demonstrates why private overlays must account for the active
|
||||
CNI enforcement point: K3s-in-Docker required exact egress to both the
|
||||
`kubernetes.default` Service `/32:443` and its post-DNAT API backend `/32:6443`.
|
||||
This is evidence for exact destination discovery, not permission to copy those
|
||||
fixture addresses into another cluster or widen the destination CIDR.
|
||||
|
||||
Run this destructive fixture only with explicit opt-in and a new private report
|
||||
path. The parent directory must already exist, must not be a symlink and should
|
||||
be mode `0700`:
|
||||
|
||||
```bash
|
||||
QL3_WORKER_KUBERNETES_ROLLOUT_LIVE=1 \
|
||||
QL3_KUBECTL_BIN=/absolute/path/to/kubectl \
|
||||
pnpm test:worker-kubernetes-rollout-live:ql3 \
|
||||
--report=/absolute/private/worker-kubernetes-report.json
|
||||
|
||||
pnpm audit:worker-kubernetes-rollout-live:ql3 \
|
||||
--report=/absolute/private/worker-kubernetes-report.json
|
||||
```
|
||||
|
||||
The producer refuses to overwrite a report and writes it with mode `0600`.
|
||||
The auditor is process-independent, rejects non-exact schemas and secret
|
||||
material, and must return `compatible=true`. The corresponding GitHub Actions
|
||||
workflow is manual-only; it does not add K3s, PostgreSQL or Cluster packages to
|
||||
an Edge/Standalone deployment.
|
||||
|
||||
After retaining the required audit evidence, delete the per-dispatch command
|
||||
ConfigMap and pepper Secret according to the deployment retention policy. The
|
||||
Job TTL removes only the finished Job; it does not remove those caller-owned
|
||||
inputs.
|
||||
@@ -0,0 +1,123 @@
|
||||
# QingLong 3.0 Worker 管理客户端 CA 轮换
|
||||
|
||||
本流程只适用于 Cluster Profile 的 Worker credential management 客户端信任根。它不轮换服务端证书,
|
||||
不改变 Worker-purpose OIDC、Project Policy 或双人审批,也不允许用客户端证书替代 User authority。
|
||||
|
||||
## 固定边界
|
||||
|
||||
- `ca.crt` 必须是严格 UTF-8,只含 1–16 张唯一、当前有效且 Basic Constraints 为 CA 的 PEM 证书;
|
||||
- `client.crl` 必须只含 1–16 份唯一、OpenSSL 可加载的 PEM CRL;生产 PKI operator 必须为 overlap 中每个
|
||||
issuer 发布当前 CRL,并在真实请求门中分别验证;
|
||||
- 两个文件都只在新进程启动时读取。禁止原地 watcher、动态 TLS context、第二 listener 或 sidecar;
|
||||
- 每一阶段都必须同时计算 CA/CRL 原始 bundle 的 SHA-256,更新 Pod-template 两个摘要注解,并完成
|
||||
`maxUnavailable=0` rollout;只更新 Secret 而不推进 Deployment generation 不算生效;
|
||||
- 证据 collector 保持只读,PKI/Deployment operator 才能更新 Secret 和 Deployment。
|
||||
|
||||
## 阶段 1:old
|
||||
|
||||
1. 盘点全部仍可能调用管理面的 old client identity,并确认 old CRL 当前有效;
|
||||
2. Secret 只投影 old CA 与 old CRL;摘要注解分别绑定这两个文件;
|
||||
3. 完整 rollout 后,从两个新 Pod 分别证明 old client 业务请求 200,未知 CA 请求 401,健康探针无证书 200;
|
||||
4. 保存 Deployment generation、Secret revision、Pod 世代与低敏请求结果,不保存私钥、JWT 或 Secret 内容。
|
||||
|
||||
## 阶段 2:overlap
|
||||
|
||||
1. 在外部 PKI 中签发 new CA 和 new client identity;先分发 new client 私钥,后扩大服务端 trust;
|
||||
2. `ca.crt` 精确变为 `old CA + new CA`,`client.crl` 同时包含 old/new issuer 的当前 CRL;
|
||||
3. 更新两个 bundle 摘要注解并完成完整 rollout,确认旧 Pod 全部退役;
|
||||
4. 在每个新 Pod 上分别证明 old client 200、new client 200、未知 CA 401;继续运行 D-229 身份、D-230
|
||||
durable audit 与 D-232 CRL 证据所需的 inspect 检查;
|
||||
5. 只有所有合法 caller 都已观测 new client 成功,且 overlap 达到组织规定的最小传播窗口,才允许退休 old。
|
||||
|
||||
## 阶段 3:new
|
||||
|
||||
1. 停止签发 old CA,吊销或禁用残留 old identity,并先发布最终 old CRL;
|
||||
2. `ca.crt` 精确收敛为 new CA,`client.crl` 收敛为 new issuer 的当前 CRL;
|
||||
3. 更新两个摘要注解并完成完整 rollout,确认 overlap Pod 全部退役;
|
||||
4. 在每个新 Pod 上证明 old client 401 `client_certificate_required`、new client 200、健康探针 200;
|
||||
5. 保留 old CA/CRL 与变更证据到审计保留期结束,但不得再把 old CA 加回运行时 trust。
|
||||
|
||||
## D-234 三阶段证据命令
|
||||
|
||||
三个阶段必须由同一只读 collector、同一外部 OIDC operator subject、同一 endpoint/server trust 和同一
|
||||
`worker-credential.inspect` command 采集。`--client-ca-bundle` 是服务端用来验证客户端证书的 CA 集合;client
|
||||
config 内的 `caFile` 是客户端用来验证管理 API 服务端证书的 trust bundle,两者互相独立,不要求同一 issuer。
|
||||
|
||||
所有输入都必须是 canonical absolute path。assertion、client config、private key、Kubeconfig 和阶段/最终报告使用
|
||||
owner-private 文件;输出路径必须尚不存在,runner 以 `0600` no-replace 创建。先在阶段 1 执行:
|
||||
|
||||
```sh
|
||||
QL3_WORKER_CREDENTIAL_MANAGEMENT_CA_ROLLOVER_EVIDENCE=1 \
|
||||
pnpm evidence:worker-management-ca-rollover:ql3 -- \
|
||||
--phase=old \
|
||||
--old-config=/secure/ql3/old-client.json \
|
||||
--new-config=/secure/ql3/new-client.json \
|
||||
--assertion=/secure/ql3/operator.jwt \
|
||||
--command=/secure/ql3/inspect-command.json \
|
||||
--kubernetes=/secure/ql3/evidence.kubeconfig \
|
||||
--client-ca-bundle=/secure/ql3/old-client-ca.pem \
|
||||
--client-crl-bundle=/secure/ql3/old-client-crl.pem \
|
||||
--output=/secure/ql3/old-state.json
|
||||
```
|
||||
|
||||
完成 exact old+new rollout 后执行阶段 2;`--previous` 摘要绑定 old-state,不能复制或改写其 JSON:
|
||||
|
||||
```sh
|
||||
QL3_WORKER_CREDENTIAL_MANAGEMENT_CA_ROLLOVER_EVIDENCE=1 \
|
||||
pnpm evidence:worker-management-ca-rollover:ql3 -- \
|
||||
--phase=overlap \
|
||||
--old-config=/secure/ql3/old-client.json \
|
||||
--new-config=/secure/ql3/new-client.json \
|
||||
--assertion=/secure/ql3/operator.jwt \
|
||||
--command=/secure/ql3/inspect-command.json \
|
||||
--kubernetes=/secure/ql3/evidence.kubeconfig \
|
||||
--client-ca-bundle=/secure/ql3/overlap-client-ca.pem \
|
||||
--client-crl-bundle=/secure/ql3/overlap-client-crl.pem \
|
||||
--previous=/secure/ql3/old-state.json \
|
||||
--output=/secure/ql3/overlap-state.json
|
||||
```
|
||||
|
||||
完成 new-only rollout 后执行阶段 3。D-229 ceremony 与 D-230 durable audit 报告必须来自本次受审变更,durable
|
||||
report 必须已经摘要绑定 ceremony report:
|
||||
|
||||
```sh
|
||||
QL3_WORKER_CREDENTIAL_MANAGEMENT_CA_ROLLOVER_EVIDENCE=1 \
|
||||
pnpm evidence:worker-management-ca-rollover:ql3 -- \
|
||||
--phase=new \
|
||||
--old-config=/secure/ql3/old-client.json \
|
||||
--new-config=/secure/ql3/new-client.json \
|
||||
--assertion=/secure/ql3/operator.jwt \
|
||||
--command=/secure/ql3/inspect-command.json \
|
||||
--kubernetes=/secure/ql3/evidence.kubeconfig \
|
||||
--client-ca-bundle=/secure/ql3/new-client-ca.pem \
|
||||
--client-crl-bundle=/secure/ql3/new-client-crl.pem \
|
||||
--old=/secure/ql3/old-state.json \
|
||||
--previous=/secure/ql3/overlap-state.json \
|
||||
--ceremony-report=/secure/ql3/live-ceremony.json \
|
||||
--durable-audit-report=/secure/ql3/durable-audit.json \
|
||||
--output=/secure/ql3/ca-rollover-evidence.json
|
||||
```
|
||||
|
||||
最终报告必须再由不需要集群凭据的离线门重判:
|
||||
|
||||
```sh
|
||||
pnpm audit:worker-management-ca-rollover:ql3 -- \
|
||||
--report=/secure/ql3/ca-rollover-evidence.json
|
||||
```
|
||||
|
||||
兼容输出必须同时包含 `"compatible":true`,最终报告的 `gates.passed` 必须为 `true`。D-232 同 CA 单证书 CRL
|
||||
吊销证据仍可按组织策略独立运行,但不是把 old/new 两个 CA 合并成同 issuer 的前置条件。
|
||||
|
||||
## 失败与回退
|
||||
|
||||
- 新 Pod 因 PEM、有效期、CA 用途、重复或 CRL 解析失败而不能 Ready:停止 rollout,保留仍 Ready 的旧 Pod,
|
||||
修复候选 bundle 后推进新的 generation;
|
||||
- overlap 中 new client 失败:保持 old+new,不得退休 old;检查 client key/certificate、EKU、issuer CRL 与
|
||||
NetworkPolicy,修复后重新完整 rollout;
|
||||
- new-only 阶段出现未迁移 caller:只能经新的受审变更回到 exact overlap,并重新计算两个摘要、完整 rollout;
|
||||
禁止只重启单 Pod或把旧 CA 临时塞入某个副本;
|
||||
- 紧急 old CA compromise 时可缩短 overlap,但仍必须先使 new client 可用、发布 CRL、推进完整 generation,
|
||||
并运行 D-232 的旧证书拒绝证据。可用性压力不能把未知结果解释成成功退休。
|
||||
|
||||
该流程不增加 workspace package、第三方依赖、migration、controller、timer、watcher、listener、Pool 或连接;
|
||||
Edge、Standalone 与 Worker Profile 不装配该能力,低配部署没有稳态成本。
|
||||
@@ -0,0 +1,156 @@
|
||||
# QingLong 3.0 Worker Credential Management Client Operation
|
||||
|
||||
This operation lets an authorized operator workstation submit one reviewed
|
||||
Worker credential management command through the in-cluster TLS endpoint. The
|
||||
workstation drives Kubernetes object creation; the request itself runs in one
|
||||
short-lived Pod that has no Kubernetes API token or RBAC.
|
||||
|
||||
It is not a controller, scheduler, sidecar or credential executor. It can call
|
||||
only the management transport (`plan`, `propose`, `decide` or `inspect`) and
|
||||
cannot consume an approval, request a ServiceAccount token or write a Worker
|
||||
Secret.
|
||||
|
||||
## Preconditions
|
||||
|
||||
- the opt-in `ql3-worker-credential-management` Deployment has two Ready Pods;
|
||||
- the manager certificate covers
|
||||
`ql3-worker-credential-management.qinglong3-system.svc`;
|
||||
- a reviewed CA bundle is available outside the repository;
|
||||
- a short-lived client certificate and matching private key issued by the
|
||||
manager's reviewed client CA are available outside the repository and are
|
||||
absent from the current CRL;
|
||||
- the external identity provider has minted a short-lived, strong-User
|
||||
assertion for the exact operator and command, with
|
||||
`aud=qinglong3-worker-credential-management`,
|
||||
`typ=ql3-worker-credential-management+jwt` and
|
||||
`ql3_purpose=worker-credential-management`;
|
||||
- the production Admin image digest has been independently verified.
|
||||
|
||||
Do not use a Project API bearer, browser session, Worker credential or
|
||||
Kubernetes ServiceAccount token as the assertion. A Plugin Package management
|
||||
assertion is also invalid even when it uses the same issuer and signing key;
|
||||
there is no legacy-purpose compatibility window in QingLong 3.0.
|
||||
|
||||
## Prepare one private command
|
||||
|
||||
Copy
|
||||
`deploy/kubernetes/ql3-cluster/operations/worker-credential-management-client/config.example.yaml`
|
||||
to a private directory that is excluded from source control. Replace every
|
||||
placeholder. The four input objects have fixed names and are immutable:
|
||||
|
||||
- `ql3-worker-credential-management-request` contains only `client.json` and
|
||||
the one reviewed `command.json`;
|
||||
- `ql3-worker-credential-management-client-trust` contains the reviewed manager
|
||||
CA certificate;
|
||||
- `ql3-worker-credential-management-assertion` contains one short-lived strong
|
||||
User assertion.
|
||||
- `ql3-worker-credential-management-client-identity` contains only the
|
||||
short-lived client certificate chain and matching private key. It is a
|
||||
separate possession factor and does not replace the User assertion.
|
||||
|
||||
`client.json` must reference `/tmp/ca.crt`, `/tmp/client.crt` and
|
||||
`/tmp/client.key`. The production client verifies the certificate/private-key
|
||||
match before opening a connection. Do not reuse the manager server key, an
|
||||
assertion-signing key or a Worker execution certificate.
|
||||
|
||||
The example command is read-only `worker-credential.inspect`. For a mutation,
|
||||
produce a new command through the approved plan → propose → decide ceremony;
|
||||
never edit an existing immutable input object in place. Run only one instance
|
||||
of this fixed-name operation at a time.
|
||||
|
||||
Create a private Kustomize overlay that references the repository operation and
|
||||
replaces the all-zero image digest with the reviewed production digest:
|
||||
|
||||
```yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- /absolute/path/to/qinglong/deploy/kubernetes/ql3-cluster/operations/worker-credential-management-client
|
||||
|
||||
images:
|
||||
- name: registry.example.com/qinglong/qinglong3-cluster-admin
|
||||
newName: registry.example.com/qinglong/qinglong3-cluster-admin
|
||||
digest: sha256:REPLACE_WITH_REVIEWED_PRODUCTION_DIGEST
|
||||
```
|
||||
|
||||
Render and inspect both private inputs and the Job before creation. The checked
|
||||
in all-zero digest is deliberately unusable and must never be replaced by a
|
||||
mutable tag.
|
||||
|
||||
## Execute and observe
|
||||
|
||||
```bash
|
||||
kubectl create -f /absolute/private/worker-credential-command-inputs.yaml
|
||||
kubectl create -k /absolute/private/worker-credential-client-overlay
|
||||
|
||||
kubectl -n qinglong3-system wait \
|
||||
--for=condition=Complete \
|
||||
job/ql3-worker-credential-management-client \
|
||||
--timeout=150s
|
||||
|
||||
kubectl -n qinglong3-system logs \
|
||||
job/ql3-worker-credential-management-client \
|
||||
--container=client
|
||||
```
|
||||
|
||||
The init container may retry only the authenticated TLS 1.3 `/readyz` probe so
|
||||
that DNS and NetworkPolicy can converge. The main container invokes the
|
||||
production client exactly once. `backoffLimit: 0` and `restartPolicy: Never`
|
||||
prevent Kubernetes from replaying an ambiguous business request.
|
||||
|
||||
If the init container fails, no business request was sent. If the main
|
||||
container starts and its outcome is unavailable, inspect the durable plan,
|
||||
approval and audit facts before replaying the exact semantic command. Do not
|
||||
mint new idempotency identities or assume that a missing client response means
|
||||
rollback.
|
||||
|
||||
After evidence has been retained, remove the completed operation and its
|
||||
short-lived inputs before preparing the next command:
|
||||
|
||||
```bash
|
||||
kubectl -n qinglong3-system delete \
|
||||
job/ql3-worker-credential-management-client \
|
||||
networkpolicy/ql3-worker-credential-management-client \
|
||||
serviceaccount/ql3-worker-credential-management-client
|
||||
|
||||
kubectl -n qinglong3-system delete \
|
||||
configmap/ql3-worker-credential-management-request \
|
||||
configmap/ql3-worker-credential-management-client-trust \
|
||||
secret/ql3-worker-credential-management-assertion \
|
||||
secret/ql3-worker-credential-management-client-identity
|
||||
```
|
||||
|
||||
## Fixed security and resource envelope
|
||||
|
||||
- no ServiceAccount token, RBAC, environment-sourced credential or host access;
|
||||
- deny-all ingress; egress only to kube-system DNS and same-namespace manager
|
||||
Pods on TCP 8444;
|
||||
- request, assertion, client identity, trust and memory-backed scratch are
|
||||
separate projections;
|
||||
- non-root UID/GID 10001, read-only root filesystem, RuntimeDefault seccomp and
|
||||
all Linux capabilities dropped;
|
||||
- effective Pod request is 25 millicores/48 MiB and the limit is 250
|
||||
millicores/128 MiB because init and main containers run sequentially;
|
||||
- no steady-state CPU, memory, connection, timer or process cost after the Job
|
||||
exits.
|
||||
|
||||
The repository gate is:
|
||||
|
||||
```bash
|
||||
pnpm test:worker-management-kubernetes-live:ql3
|
||||
```
|
||||
|
||||
It loads the checked-in ServiceAccount, NetworkPolicy and Job into a disposable
|
||||
three-node K3s cluster, substitutes only the locally built production image,
|
||||
and proves successful init/main exit, zero client restarts, immutable inputs,
|
||||
no projected ServiceAccount token, health probes without client identity,
|
||||
business-route rejection without a certificate, CRL rejection after a
|
||||
zero-unavailable rollout, acceptance of the replacement certificate and the
|
||||
full two-manager availability and OIDC identity-rotation matrix.
|
||||
|
||||
For a real external-identity separation-of-duty report that intentionally stops
|
||||
before approval consumption or credential delivery, use
|
||||
[`ql3-worker-credential-management-live-ceremony.md`](./ql3-worker-credential-management-live-ceremony.md).
|
||||
The same operation guide includes the independent, short-lived read-only
|
||||
PostgreSQL durable-audit collector required after the ceremony.
|
||||
@@ -0,0 +1,145 @@
|
||||
# QingLong 3.0 Worker Credential Management External OIDC Ceremony
|
||||
|
||||
This operation records one external-identity, two-User management ceremony. It creates and approves a
|
||||
secret-free Worker credential plan, then inspects it without consuming the approval or executing credential
|
||||
delivery.
|
||||
|
||||
It is a short-lived operator command, not a controller. Run it only against an already reviewed Worker
|
||||
management endpoint. A successful local/K3s fixture is not a substitute for this external report.
|
||||
|
||||
## Private inputs
|
||||
|
||||
Prepare five canonical absolute paths:
|
||||
|
||||
- mode 0600 production client config using the exact Worker endpoint path, TLS server name, CA file, client
|
||||
certificate file, matching private-key file and a 1–30 second timeout;
|
||||
- mode 0600 requester assertion;
|
||||
- mode 0600 reviewer assertion for a different User from the same external issuer;
|
||||
- mode 0600 ceremony JSON;
|
||||
- an unused output path in a canonical private directory.
|
||||
|
||||
Both assertions must use:
|
||||
|
||||
```text
|
||||
aud=qinglong3-worker-credential-management
|
||||
typ=ql3-worker-credential-management+jwt
|
||||
ql3_purpose=worker-credential-management
|
||||
```
|
||||
|
||||
The client certificate is a separate transport-possession factor. It must be issued by the manager's reviewed
|
||||
client CA, be absent from the current CRL and must not be reused as an assertion-signing key or Worker execution
|
||||
identity. A valid certificate never substitutes for either external User assertion.
|
||||
|
||||
The ceremony JSON has this exact top-level shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"planRequest": {
|
||||
"actionRef": "worker-credential:REVIEWED_WORKER:REVIEWED_GENERATION",
|
||||
"authorityProjectId": "REVIEWED_AUTHORITY_PROJECT",
|
||||
"action": "rotate",
|
||||
"deliveryId": "REVIEWED_UUID",
|
||||
"workerId": "REVIEWED_WORKER",
|
||||
"credentialId": "REVIEWED_NEW_CREDENTIAL",
|
||||
"previousCredentialId": "REVIEWED_PREVIOUS_CREDENTIAL",
|
||||
"credentialNotBeforeAtMs": 0,
|
||||
"credentialExpiresAtMs": 0,
|
||||
"deploymentTargetDigest": "REVIEWED_64_LOWERCASE_HEX_DIGEST",
|
||||
"deploymentGeneration": "REVIEWED_GENERATION"
|
||||
},
|
||||
"approvalRequestId": "REVIEWED_APPROVAL_ID",
|
||||
"approvalAuditEventId": "REVIEWED_UUID",
|
||||
"requesterDecisionId": "REVIEWED_SELF_DENY_PROBE_ID",
|
||||
"requesterDecisionAuditEventId": "REVIEWED_UUID",
|
||||
"reviewerDecisionId": "REVIEWED_REVIEWER_DECISION_ID",
|
||||
"reviewerDecisionAuditEventId": "REVIEWED_UUID",
|
||||
"decisionReasonCode": "reviewed",
|
||||
"inspectionId": "REVIEWED_INSPECTION_ID"
|
||||
}
|
||||
```
|
||||
|
||||
Replace both timestamps with valid future millisecond values accepted by the Worker management plan contract.
|
||||
Use new identifiers dedicated to evidence; do not reuse a production delivery that an executor may consume.
|
||||
|
||||
## Run and audit
|
||||
|
||||
```bash
|
||||
export QL3_WORKER_CREDENTIAL_MANAGEMENT_LIVE_CEREMONY=1
|
||||
|
||||
pnpm evidence:worker-management-live-ceremony:ql3 -- \
|
||||
--config=/absolute/private/client.json \
|
||||
--requester-assertion=/absolute/private/requester.jwt \
|
||||
--reviewer-assertion=/absolute/private/reviewer.jwt \
|
||||
--ceremony=/absolute/private/ceremony.json \
|
||||
--output=/absolute/private/worker-management-ceremony.json
|
||||
|
||||
unset QL3_WORKER_CREDENTIAL_MANAGEMENT_LIVE_CEREMONY
|
||||
|
||||
pnpm audit:worker-management-live-ceremony:ql3 -- \
|
||||
--report=/absolute/private/worker-management-ceremony.json
|
||||
```
|
||||
|
||||
The runner performs exactly five calls: requester plan, requester propose, requester self-decision rejection,
|
||||
reviewer decision and reviewer inspect. It fails if self-decision is accepted, if the resulting approval is not
|
||||
approved by the reviewer, or if inspect observes a dispatch/consumption.
|
||||
|
||||
The report contains no raw assertion, subject, JTI, request ID, Worker identifier, Project identifier, token,
|
||||
Secret, DSN or private key. Retain it with the IdP/operator change record and the independent durable-audit
|
||||
evidence.
|
||||
|
||||
## Collect independent durable-audit evidence
|
||||
|
||||
Create a short-lived PostgreSQL login role outside the QingLong migration stream. It must not inherit or be
|
||||
granted any QingLong runtime role:
|
||||
|
||||
```sql
|
||||
CREATE ROLE ql3_worker_management_evidence
|
||||
LOGIN NOINHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE
|
||||
NOREPLICATION NOBYPASSRLS;
|
||||
|
||||
GRANT CONNECT ON DATABASE qinglong
|
||||
TO ql3_worker_management_evidence;
|
||||
GRANT USAGE ON SCHEMA ql3
|
||||
TO ql3_worker_management_evidence;
|
||||
GRANT SELECT ON
|
||||
ql3.worker_credential_management_plans,
|
||||
ql3.approval_requests,
|
||||
ql3.security_audit_events
|
||||
TO ql3_worker_management_evidence;
|
||||
```
|
||||
|
||||
Set its password through the deployment's private credential mechanism, not a checked-in SQL file. Prepare a
|
||||
mode 0600 libpq service file and an independently protected passfile. The service entry should use
|
||||
`sslmode=verify-full`, the reviewed hostname and an absolute CA path; do not put a DSN on the command line.
|
||||
|
||||
Run the second collector only after the ceremony report has passed its offline audit:
|
||||
|
||||
```bash
|
||||
export QL3_WORKER_CREDENTIAL_MANAGEMENT_DURABLE_AUDIT_EVIDENCE=1
|
||||
|
||||
pnpm evidence:worker-management-durable-audit:ql3 -- \
|
||||
--ceremony-report=/absolute/private/worker-management-ceremony.json \
|
||||
--ceremony=/absolute/private/ceremony.json \
|
||||
--pg-service-file=/absolute/private/pg_service.conf \
|
||||
--pg-service=ql3_worker_management_evidence \
|
||||
--output=/absolute/private/worker-management-durable-audit.json
|
||||
|
||||
unset QL3_WORKER_CREDENTIAL_MANAGEMENT_DURABLE_AUDIT_EVIDENCE
|
||||
|
||||
pnpm audit:worker-management-durable-audit:ql3 -- \
|
||||
--report=/absolute/private/worker-management-durable-audit.json
|
||||
```
|
||||
|
||||
The collector rejects a role that can read any other `ql3` table, mutate any `ql3` table, or become a
|
||||
privileged QingLong role. It observes exactly two durable audit rows: proposal and reviewer decision. The
|
||||
requester's rejected self-decision must have no audit row because separation-of-duty fails before the database
|
||||
update/audit insert transaction can commit. The v1 evidence contract requires PostgreSQL 18.4 or a later
|
||||
security patch in the reviewed 18.x major.
|
||||
|
||||
After retaining the report, revoke the three SELECT grants, schema usage and database connect grant, then drop
|
||||
the short-lived role according to the deployment's credential revocation procedure. Do not retain its passfile
|
||||
with the low-sensitive evidence reports.
|
||||
|
||||
If the runner stops after creating durable facts, inspect them before using any new identifiers. Do not infer
|
||||
rollback from a missing response and do not hand this evidence-only approval to the credential executor.
|
||||
@@ -0,0 +1,125 @@
|
||||
# QingLong 3.0 Worker 管理 PKI 吊销轮换证据
|
||||
|
||||
本流程用两个短生命周期阶段证明生产 Worker management 客户端证书吊销已经跨全部副本生效。runner 只观察
|
||||
指定 Deployment/Pod 并发起 `worker-credential.inspect`;它不会读取 Secret、更新 CRL、patch Deployment、
|
||||
执行 credential delivery 或消费审批。
|
||||
|
||||
## 前置条件
|
||||
|
||||
准备以下 canonical absolute paths:
|
||||
|
||||
- 旧、新两份 mode 0600 production client config;两者必须使用同一 HTTPS endpoint、TLS server name 和
|
||||
服务端 TLS trust bundle,
|
||||
但引用不同、当前有效且具备 clientAuth EKU 的 certificate/private-key pair;
|
||||
- 一份显式 client issuer CA 文件,必须只含一张当前有效 CA,并同时签发旧/新客户端证书;它用于验证客户端
|
||||
issuer 与 CRL issuer,不要求等于 client config `caFile` 指向的服务端 TLS trust;
|
||||
- mode 0600、短生命周期、强认证的 external User assertion,绑定 Worker audience/type/purpose;
|
||||
- mode 0600 的同一份 `worker-credential.inspect` command;
|
||||
- mode 0600 Kubernetes evidence config 和它引用的独立 kubeconfig;
|
||||
- 当前 CRL 文件;文件可以被组读取,但不能 group/other writable;
|
||||
- 已通过 audit 的 D-229 ceremony report 与 D-230 durable audit report;
|
||||
- canonical private directory 中两个尚不存在的输出路径。
|
||||
|
||||
client config 的 exact schema 与 production client 相同:
|
||||
|
||||
```json
|
||||
{"schemaVersion":1,"endpoint":"https://management.example.org/api/v3/worker-credentials/management","servername":"management.example.org","caFile":"/absolute/private/server-ca.crt","clientCertificateFile":"/absolute/private/old-client.crt","clientPrivateKeyFile":"/absolute/private/old-client.key","requestTimeoutMs":15000}
|
||||
```
|
||||
|
||||
`caFile` 只验证管理 API 的服务端证书。`--client-issuer-ca` 只验证客户端证书和 CRL;生产部署可以也通常应该
|
||||
使用不同的 server PKI 与 client PKI。D-232 v1 曾错误要求前者签发客户端证书,v2 报告已拒绝该耦合。
|
||||
|
||||
Kubernetes evidence config 固定观察生产对象:
|
||||
|
||||
```json
|
||||
{"schemaVersion":1,"kubeconfigFile":"/absolute/private/evidence-kubeconfig.json","context":"production-evidence","namespace":"qinglong3-system","deployment":"ql3-worker-credential-management","labelSelector":"app.kubernetes.io/name=ql3-worker-credential-management,app.kubernetes.io/component=worker-credential-management","apiTimeoutMs":15000}
|
||||
```
|
||||
|
||||
kubeconfig 必须是单 cluster、单 context、单 static user 的严格 v1 JSON,只允许 embedded CA 加 static token,
|
||||
或 embedded client certificate/key;禁止 exec/auth-provider、外部文件引用和多 context。给该身份的 RBAC 只允许:
|
||||
|
||||
```text
|
||||
get deployments.apps/ql3-worker-credential-management
|
||||
list pods
|
||||
```
|
||||
|
||||
runner 会用 `kubectl auth can-i` 验证这两项允许,并验证 Secret/ConfigMap/Deployment 列表、所有相关 mutation、
|
||||
Pod exec/port-forward 和 ServiceAccount TokenRequest 均拒绝。RBAC 仍应通过 `resourceNames` 约束 Deployment;
|
||||
Pod list 的保密边界必须由专用 namespace 与 admission policy 配合,因为 Kubernetes RBAC 不能按 label selector
|
||||
限制 list。
|
||||
|
||||
## 阶段一:吊销前
|
||||
|
||||
先计算当前 CRL 原始字节摘要,并由独立 deployment operator 在私有 overlay 中替换提交态的全零占位注解:
|
||||
|
||||
```bash
|
||||
openssl dgst -sha256 /absolute/private/current-client.crl
|
||||
kubectl -n qinglong3-system patch deployment/ql3-worker-credential-management \
|
||||
--type=merge \
|
||||
--patch='{"spec":{"template":{"metadata":{"annotations":{"qinglong.io/worker-credential-management-client-crl-sha256":"sha256:REPLACE_WITH_64_LOWERCASE_HEX"}}}}}'
|
||||
kubectl -n qinglong3-system rollout status deployment/ql3-worker-credential-management
|
||||
```
|
||||
|
||||
只有 operator 使用上面的 mutation authority;不要授予 evidence kubeconfig。Deployment 的注解必须位于 Pod
|
||||
template,因此改变摘要会创建新 generation。随后运行:
|
||||
|
||||
```bash
|
||||
export QL3_WORKER_CREDENTIAL_MANAGEMENT_PKI_ROTATION_EVIDENCE=1
|
||||
|
||||
pnpm evidence:worker-management-pki-rotation:ql3 -- \
|
||||
--phase=before \
|
||||
--old-config=/absolute/private/old-client.json \
|
||||
--new-config=/absolute/private/new-client.json \
|
||||
--assertion=/absolute/private/operator.jwt \
|
||||
--command=/absolute/private/inspect-command.json \
|
||||
--kubernetes=/absolute/private/kubernetes-evidence.json \
|
||||
--client-issuer-ca=/absolute/private/client-issuer-ca.pem \
|
||||
--crl=/absolute/private/current-client.crl \
|
||||
--output=/absolute/private/pki-rotation-before.json
|
||||
```
|
||||
|
||||
成功条件是同一 Deployment 已收敛到两个不同 Node 上的 Ready Pod,注解等于 CRL SHA-256,collector 权限精确
|
||||
只读,且旧、新证书调用同一 inspect command 均为 200。before-state 仅包含摘要和低敏事实,mode 为 0600。
|
||||
|
||||
## 操作暂停:由独立 PKI/Deployment operator 完成吊销
|
||||
|
||||
停止 runner。使用生产 PKI 流程吊销旧证书,发布同一 issuer 的新 CRL,确保 CRL number 和 lastUpdate 增加;
|
||||
更新 `ql3-worker-credential-management-tls` 中的 `client.crl`,把新 CRL 原始字节摘要写入 Pod template 注解,
|
||||
并完成 `maxUnavailable=0` rollout。保留 PKI change ticket、Secret revision 和 rollout 日志,但不要把 Secret
|
||||
内容并入证据报告。
|
||||
|
||||
不要只重启一个 Pod,也不要在旧 Pod 存活时进入 after 阶段。替代证书不得被吊销或更换,旧/new client config、
|
||||
assertion、inspect command、kubeconfig 和 endpoint 必须与 before 阶段相同。
|
||||
`--client-issuer-ca` 也必须保持同一 canonical 文件内容;如需轮换 CA,应改用 D-234 三阶段 CA rollover 证据,
|
||||
不能把 D-232 的单 issuer 吊销流程扩成隐式 overlap。
|
||||
|
||||
## 阶段二:吊销后
|
||||
|
||||
```bash
|
||||
pnpm evidence:worker-management-pki-rotation:ql3 -- \
|
||||
--phase=after \
|
||||
--before=/absolute/private/pki-rotation-before.json \
|
||||
--old-config=/absolute/private/old-client.json \
|
||||
--new-config=/absolute/private/new-client.json \
|
||||
--assertion=/absolute/private/operator.jwt \
|
||||
--command=/absolute/private/inspect-command.json \
|
||||
--kubernetes=/absolute/private/kubernetes-evidence.json \
|
||||
--client-issuer-ca=/absolute/private/client-issuer-ca.pem \
|
||||
--crl=/absolute/private/rotated-client.crl \
|
||||
--ceremony-report=/absolute/private/worker-management-ceremony.json \
|
||||
--durable-audit-report=/absolute/private/worker-management-durable-audit.json \
|
||||
--output=/absolute/private/worker-management-pki-rotation.json
|
||||
|
||||
unset QL3_WORKER_CREDENTIAL_MANAGEMENT_PKI_ROTATION_EVIDENCE
|
||||
|
||||
pnpm audit:worker-management-pki-rotation:ql3 -- \
|
||||
--report=/absolute/private/worker-management-pki-rotation.json
|
||||
```
|
||||
|
||||
after 阶段要求 CRL 单调前进、Deployment UID 不变但 generation/resourceVersion 改变、旧 Pod UID 全部退役、
|
||||
两个新 Pod 在不同 Node Ready,并观察旧证书 401 `client_certificate_required`、新证书 200。当前 assertion
|
||||
subject 必须与 D-229/D-230 的 requester 或 reviewer 摘要一致。
|
||||
|
||||
最终报告不包含证书、私钥、JWT、Kubernetes token、Pod/Node/Deployment 原始 UID、Secret 或 DSN。将报告与
|
||||
D-229/D-230 报告、PKI ticket 和 rollout 日志一起保存;不要把 private input 文件或 evidence kubeconfig 放进
|
||||
低敏报告归档。完成后按部署凭据流程撤销 evidence 身份。
|
||||
@@ -0,0 +1,101 @@
|
||||
# QingLong 3.0 Worker 管理统一发布证据
|
||||
|
||||
本流程把四个已经独立完成的生产证据协议收敛为一个可离线重判的 release gate。聚合器只读取本地报告并创建
|
||||
`0600` 摘要清单,不访问 Kubernetes、PostgreSQL、IdP、PKI 或管理 API,也不会修改任何生产对象。
|
||||
|
||||
## 必需输入
|
||||
|
||||
必须准备同一发布边界的四份 canonical、owner-private、最大 1 MiB 报告:
|
||||
|
||||
1. D-229 `worker-credential-management-live-ceremony@v1`;
|
||||
2. D-230 `worker-credential-management-durable-audit-evidence@v1`;
|
||||
3. D-232 `worker-credential-management-pki-rotation-evidence@v2`;
|
||||
4. D-234 `worker-credential-management-ca-rollover-evidence@v1`。
|
||||
|
||||
D-232 v1 不再兼容,因为它错误耦合了 server TLS trust CA 与 client issuer CA。四份报告必须使用相同 external
|
||||
issuer/profile;D-232 与 D-234 必须由同一已审 operator subject 采集,并绑定同一 endpoint、servername、server
|
||||
trust bundle、inspect command、cluster、collector 和 Deployment UID。
|
||||
|
||||
D-232 与 D-234 可以来自不同维护窗口,不要求两代 CRL rollout 与三代 CA rollover 的 generation 连续;但各自
|
||||
报告内部的 generation/resourceVersion/Pod replacement 门必须已经通过。
|
||||
|
||||
## 生成 release evidence
|
||||
|
||||
输出路径必须尚不存在,父目录必须 canonical 且由 operator 管理:
|
||||
|
||||
```sh
|
||||
pnpm evidence:worker-management-release:ql3 -- \
|
||||
--ceremony-report=/secure/ql3/worker-management-ceremony.json \
|
||||
--durable-audit-report=/secure/ql3/worker-management-durable-audit.json \
|
||||
--pki-rotation-report=/secure/ql3/worker-management-pki-rotation-v2.json \
|
||||
--ca-rollover-report=/secure/ql3/worker-management-ca-rollover.json \
|
||||
--output=/secure/ql3/worker-management-release-evidence.json
|
||||
```
|
||||
|
||||
成功输出只表示聚合器已经重新验证四份 source、摘要链和交叉绑定,CLI 会返回:
|
||||
|
||||
```json
|
||||
{"schemaVersion":1,"fixture":"qinglong/worker-credential-management-release-evidence@v1","compatible":true}
|
||||
```
|
||||
|
||||
## 独立离线审计
|
||||
|
||||
不要只把最终 JSON 交给 auditor。必须同时提供生成时的四份 source;auditor 会重新计算文件摘要、重跑四个原始
|
||||
validator,并重建完整 release report:
|
||||
|
||||
```sh
|
||||
pnpm audit:worker-management-release:ql3 -- \
|
||||
--report=/secure/ql3/worker-management-release-evidence.json \
|
||||
--ceremony-report=/secure/ql3/worker-management-ceremony.json \
|
||||
--durable-audit-report=/secure/ql3/worker-management-durable-audit.json \
|
||||
--pki-rotation-report=/secure/ql3/worker-management-pki-rotation-v2.json \
|
||||
--ca-rollover-report=/secure/ql3/worker-management-ca-rollover.json
|
||||
```
|
||||
|
||||
只有退出码 0、`compatible=true` 且最终报告 `gates.passed=true` 才能进入后续 release approval。修改 source、替换
|
||||
final 字段、使用另一 operator/cluster/Deployment、时间倒置或缺失任一文件都会失败。
|
||||
|
||||
## D-236 镜像发布接入
|
||||
|
||||
生产发布不上传五份 JSON。为目标 tag commit 创建一次性 self-hosted JIT runner,并为它附加精确标签
|
||||
`self-hosted,linux,ql3-release-evidence-ephemeral`;`ql3-production-release-evidence` GitHub Environment 必须启用
|
||||
required reviewers。runner provisioner 在启动 job 前完成:
|
||||
|
||||
1. 确认 dispatch 选择的 tag 受保护,并且 tag commit 的 release workflow/gate script 已审;
|
||||
2. 以 runner 实际 UID 创建 canonical `0700` 目录 `/run/qinglong3-release-evidence/<40-or-64-hex-commit>`;
|
||||
3. 以 `0600`、非 symlink、最大 1 MiB 的 regular file 挂载以下固定名称:
|
||||
`worker-management-release-evidence.json`、`worker-management-ceremony.json`、
|
||||
`worker-management-durable-audit.json`、`worker-management-pki-rotation-v2.json`、
|
||||
`worker-management-ca-rollover.json`;
|
||||
4. 禁止 runner 用户访问同目录之外的 PKI key、JWT、Kubeconfig、DSN 或证据归档;
|
||||
5. job 完成后注销并销毁 runner/临时挂载,不复用工作目录或 runner registration token。
|
||||
|
||||
随后在 GitHub Actions 手动 dispatch `QingLong 3.0 Image Release`,输入不带 `v` 的 exact version,并选择
|
||||
`v<version>` tag 作为 ref。gate 会执行等价命令:
|
||||
|
||||
```sh
|
||||
pnpm gate:worker-management-release:ql3 -- \
|
||||
--report=/run/qinglong3-release-evidence/$GITHUB_SHA/worker-management-release-evidence.json \
|
||||
--ceremony-report=/run/qinglong3-release-evidence/$GITHUB_SHA/worker-management-ceremony.json \
|
||||
--durable-audit-report=/run/qinglong3-release-evidence/$GITHUB_SHA/worker-management-durable-audit.json \
|
||||
--pki-rotation-report=/run/qinglong3-release-evidence/$GITHUB_SHA/worker-management-pki-rotation-v2.json \
|
||||
--ca-rollover-report=/run/qinglong3-release-evidence/$GITHUB_SHA/worker-management-ca-rollover.json \
|
||||
--source-commit=$GITHUB_SHA \
|
||||
--release-version=3.0.0
|
||||
```
|
||||
|
||||
final 必须在 runner 当前时间前 24 小时内生成,允许的未来时钟偏差最多 5 分钟。成功日志只包含低敏 fixture、
|
||||
commit、version、final SHA-256 和 freshness 上限;workflow 不使用 artifact/cache,也不把证据路径传给 publish job。
|
||||
三个镜像 publisher 只有在该 job 成功后才获得 GHCR/OIDC/attestation 写权限。
|
||||
|
||||
`ql3-release-evidence-ephemeral` 标签本身不能证明 runner 真正一次性;若 provisioner 未销毁 runner,必须停止发布、
|
||||
隔离该主机并轮换 registration credential。不得为了“自动清理”让 workflow 删除外部不可变证据归档。
|
||||
|
||||
## 归档与保留
|
||||
|
||||
- final report 与四份 source 必须作为同一不可变归档单元保存;final 不能替代 source;
|
||||
- PKI ticket、Deployment rollout log、IdP/审批记录可在外部审计系统按自身 retention 保存,但不得把原始 JWT、
|
||||
private key、certificate/CRL 内容、Kube token、Secret 或 DSN 填入 release JSON;
|
||||
- 重新采集任一 source 后必须生成新的 final output,不得覆盖旧文件;
|
||||
- 本门证明观察窗口内的 Worker management identity、durable review、leaf revocation、CA rollover 和部署一致性,
|
||||
不自动证明未来 CRL SLO、PKI compromise 响应、IdP 可用性或所有 caller 已永久销毁旧 material。
|
||||
Reference in New Issue
Block a user