Files
qinglong/docs/operations/ql3-local-application-process.md
T

16 KiB
Raw Blame History

QingLong 3.0 本机 Headless Application 操作手册

本手册适用于 edgestandalone。它启动独立 QingLong 3.0 target,不会接管 back/app.ts,也不会自动停止 2.x。全新安装使用 Fresh 初始化 与 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 keyringruntime 不会自动生成主密钥。
  3. 若 Plugin Package install recovery 队列未收敛,必须配置 materialized recovery catalogdisabled 模式遇到 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

{
  "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 之前失败关闭。

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 队列或明确不允许本机恢复时使用:

{ "mode": "disabled" }

如果 durable queued install 需要在崩溃后继续,配置部署者已经物化的私有 catalog:

{
  "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>.jsonbundle 为 <artifactDigest>.bundle;两者都必须是当前 UID、no-follow、精确 0600 的 regular file。entry 最大 256 KiBexact schema 为:

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

schema = qinglong/plugin-package-publisher-trust@v1
keys = 受信 publisher key 定义数组

不要直接覆盖 current.json。先创建精确 0700 的 trust root 和 deployment root 下精确 0600 的候选 trust,再由当前 Owner 执行 provision

{
  "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"
  }
}
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 的原命令,不能 提交另一轮换。

{
  "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 的引用归零。先执行紧急提案:

{
  "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 的 mutationIdexpectedGeneration,把提案者 subject 写入 proposerSubjectId,并逐字复制返回的 impactDigest

{
  "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_compromiseconfirmed_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 descriptorcatalog 命令读取上一步管理出的 current.json

{
  "schema": "qinglong/local-plugin-package-recovery-publication@v1",
  "bundlePath": "/opt/qinglong/private/incoming/example.bundle",
  "manifest": {},
  "signature": {}
}

最后以同一 Owner credential 执行私有 command file

{
  "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"
  }
}
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

{
  "schemaVersion": 1,
  "component": "qinglong3-local-application",
  "level": "info",
  "event": "active",
  "instanceId": "router-edge-1",
  "profile": "edge",
  "aiStatus": "deployment_excluded"
}

停止

向进程发送一次 SIGTERMSIGINT。入口只接受第一个信号,并按:

scheduler stop/drain → execution control drain → SQLite/source fence release

停止。必须等待 event=stoppedstopResult=stopped 后再操作数据库或启动 2.x。 timed_out、没有 stopped fact 或进程被 SIGKILL 都不能当成安全切换证据。

AI 边界

基础路由部署使用:

{ "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 手册

常见失败

  • PRIVATE_LOCAL_COMMAND_FILE_INVALID:配置 path、UID、类型、大小、symlink、权限或 JSON 不合法;
  • QL3_LOCAL_APPLICATION_PROCESS_CONFIG_INVALIDschema、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
  • LocalApplicationStartupRecoveryRequiredErrorRun/Attempt 证据未收敛,禁止直接删 行或伪造 receipt;
  • Local Secret is unavailable:keyring 缺失、权限错误、损坏或与数据库不配对。

故障输出只包含 name/code。详细诊断应结合低敏 activation facts、离线 inspection 和 备份恢复流程,不要要求常驻进程打印 path、digest、SecretRef 或 token。