# 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 为 `.json`,bundle 为 `.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/.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。