diff --git a/docs/QINGLONG_3_0_ARCHITECTURE_RFC.md b/docs/QINGLONG_3_0_ARCHITECTURE_RFC.md index 6c926e7f..7f68e17d 100644 --- a/docs/QINGLONG_3_0_ARCHITECTURE_RFC.md +++ b/docs/QINGLONG_3_0_ARCHITECTURE_RFC.md @@ -11,7 +11,7 @@ 最新增量证据(2026-08-29): -- D-421/ADR-0516(实现与本地验收已完成,阶段提交/远端 milestone 待闭合):Local Web Task mutation 不再受限于短生命周期 CLI 的进程级 active credential。`PUT /api/v3/projects/:projectId/tasks/:taskId` 把 Bearer 限定为 session credential,第一次 exact request 只在 deployment root 内 `0700` 目录发布 `0600`、当前 UID、两分钟、一次性的私有 proof file;HTTP 只返回 authorization ID、canonical request digest、expiry 与 basename,不返回 proof 或宿主绝对路径。proof 绑定 Task 全内容、credential ID/version 与 User subject;Edge/Standalone pending 上限为 8/32,按请求惰性清理,不新增 timer/watcher/daemon。验证后 principal 提升为短期 `local_console`,既有 Task administration service 重跑 Policy;SQLite runtime 为每个请求建立独立 credential-fenced repository,并在 Task append 事务内复验 credential/Identity/pepper、actor、Project/RoleBinding fence,原子提交 allowed audit 与 mutation。并发测试证明两个 User repository 不共享 ambient authority,撤销 A 不影响 B,RoleBinding 漂移仍原子拒绝;真实 loopback HTTP→proof file→SQLite create 与 Chromium 编辑器/proof ticket 已通过。Console 当前完成 command Task 创建;HTTP 与 CLI 支持完整 create/update,Web update 等待强认证 authoring read/lease,避免用不含 spec 的 bounded read 覆盖未知字段。三资产合计 62,632 bytes;Local API 56/56、18-package clean build/test 3,030 total/3,008 pass/22 条件跳过/0 fail,package/source、Local image、122-module Edge import 与 Cluster dependency audit 均 compatible。默认 Edge 为 2,736,982 bytes/329 files/83 modules,opt-in Edge/Standalone Console 为 4,040,893/4,041,037 bytes、472 files/12 packages/94 modules,均保有门禁余量;默认 headless 与 Cluster 路径不变。完整 backend 仍由原生 CI 闭合,新的可下载双架构产物必须在本阶段提交后由显式 Console milestone run 重新生成,不能沿用 D-420 的旧 archive 冒充。 +- D-421/ADR-0516(实现与本地验收已完成,阶段提交/远端 milestone 待闭合):Local Web Task mutation 不再受限于短生命周期 CLI 的进程级 active credential。`PUT /api/v3/projects/:projectId/tasks/:taskId` 把 Bearer 限定为 session credential,第一次 exact request 只在 deployment root 内 `0700` 目录发布 `0600`、当前 UID、两分钟、一次性的私有 proof file;HTTP 只返回 authorization ID、canonical request digest、expiry 与 basename,不返回 proof 或宿主绝对路径。proof 绑定 Task 全内容、credential ID/version 与 User subject;Edge/Standalone pending 上限为 8/32,按请求惰性清理,不新增 timer/watcher/daemon。验证后 principal 提升为短期 `local_console`,既有 Task administration service 重跑 Policy;SQLite runtime 为每个请求建立独立 credential-fenced repository,并在 Task append 事务内复验 credential/Identity/pepper、actor、Project/RoleBinding fence,原子提交 allowed audit 与 mutation。并发测试证明两个 User repository 不共享 ambient authority,撤销 A 不影响 B,RoleBinding 漂移仍原子拒绝;真实 loopback HTTP→proof file→SQLite create 与 Chromium 编辑器/proof ticket 已通过。Console 当前完成 command Task 创建;HTTP 与 CLI 支持完整 create/update,Web update 等待强认证 authoring read/lease,避免用不含 spec 的 bounded read 覆盖未知字段。三资产合计 62,632 bytes;Local API 56/56、18-package clean build/test 3,030 total/3,008 pass/22 条件跳过/0 fail,package/source、Local image、122-module Edge import 与 Cluster dependency audit 均 compatible。首个阶段提交 `884912d1` 暴露默认 Edge 静态加载管理 authority 的真实回归:x64 21,229,568-byte RSS 超过 20 MiB 门,failed-only 重跑再次失败;修复没有扩大预算,而是改为 proof 验证后的 request-time dynamic import。默认 Edge 恢复 2,737,205 bytes/329 files/58 modules,opt-in Edge/Standalone Console 为 4,041,294/4,041,438 bytes、472 files/12 packages/94 modules,均保有门禁余量;默认 headless 与 Cluster 路径不变。完整 backend 仍由修复提交的原生 CI 闭合,新的可下载双架构产物必须在本阶段提交后由显式 Console milestone run 重新生成,不能沿用 D-420 的旧 archive 冒充。 - D-420/ADR-0515(已实现,首份真实双架构 Console v5 Trial Kit 已交付):阶段可用的首个自动化从“原生 CI 能通过 API 看到 bounded log marker”推进为“部署者能在 Console 直接观察实际输出”。Local Run HTTP 详情只追加严格验证的 latest Attempt 低敏摘要(ID、序号、状态、时间和 `logAvailable`),不返回 executor handle、Artifact ID、路径、Worker、PID 或错误明细;共享 HIGH 风险 `executeBoundedRunReadProjection` 保持不变,避免 Local UI 字段漂移到内建 Run read/compare Tool。Console 使用既有 `artifact.read` Policy/Audit/credential re-confirm 链,每次固定读取首个 32 KiB base64 窗口,分别显示 available/pending/retired/not-found/unavailable、range 与 truncation,不新增轮询、WebSocket、timer、缓存或整文件下载。三资产增至 48,318 bytes;`edge-application-api|standalone-application-api` 为 3,960,535 / 3,960,679 bytes、467 files、12 packages、90 loaded modules,仍低于 6 MiB/640-file 门,默认 headless Edge 保持 2,669,390 bytes/325 files/58 modules,Cluster 零变化。Local API 49/49、完整 backend `1,650 total / 1,648 pass / 2 Linux conditional skip / 0 fail`、18-package clean build/test 退出 0;package/source、Local image、122-module Edge import 和 Cluster dependency audit 均 compatible。提交 `57953ec8` 的远端 QingLong 3.0 CI 为 41 success / 3 expected artifact-finalizer skip / 0 fail,独立 Kubernetes deployment live contract 为 1/1 success。现有 `task.put` 的进程级 active credential fence 不适合常驻并发 HTTP,Web Task 创建/修订必须以后续“每请求 credential fence + 同事务 Policy/Audit/mutation”切片完成,D-420 不用单因子 Bearer 绕过强认证。 diff --git a/docs/adr/ADR-0516-request-scoped-local-console-task-mutation.md b/docs/adr/ADR-0516-request-scoped-local-console-task-mutation.md index f7139028..f5cc2250 100644 --- a/docs/adr/ADR-0516-request-scoped-local-console-task-mutation.md +++ b/docs/adr/ADR-0516-request-scoped-local-console-task-mutation.md @@ -23,7 +23,7 @@ PUT /api/v3/projects/:projectId/tasks/:taskId 请求体使用既有 immutable TaskDefinition command:`expectedRevision=null` 创建,`expectedRevision=current` 修订;mutation ID、occurredAt、name/kind/spec/labels/enabled 都参与规范化。Bearer 只建立 `single_factor` session,并交付该次认证解析出的 exact credential fence;服务不设置进程级 active credential。 -SQLite runtime 以 `taskDefinitionAdministrationForCredential(fence)` 为每个请求创建独立 repository。factory 建立时先复验 credential/Identity/pepper;写事务内再次复验同一 exact fence、actor subject、Project version 与 latest RoleBinding version/state。allowed audit、Task head/revision、mutation replay 与适用的 local execution revision仍在一个事务中提交。两个同时存活的 repository 不共享可变 credential 状态。 +SQLite runtime 以异步 `taskDefinitionAdministrationForCredential(fence)` 为每个请求创建独立 repository。只有 opt-in API 已验证本机 proof 并实际调用 factory 时,才惰性加载 authenticated-management 与 Task administration authority;factory 建立时先复验 credential/Identity/pepper,写事务内再次复验同一 exact fence、actor subject、Project version 与 latest RoleBinding version/state。allowed audit、Task head/revision、mutation replay 与适用的 local execution revision仍在一个事务中提交。两个同时存活的 repository 不共享可变 credential 状态。 ### 2. 本机存在证明是第二权威,不经 HTTP 交付 secret @@ -68,7 +68,9 @@ HTTP contract 已同时支持 create/update。当前 bounded Task read 有意不 定向验证覆盖:proof 文件权限与无身份泄漏、exact request/credential/subject 绑定、一次性消费、Edge 容量与过期惰性清理;Task route 的 challenge→confirm→strong Policy→事务 mutation、内容漂移、非 User、过期和失败关闭;两个同时存活的 credential repository、单方 credential revoke、另一方继续写入与 RoleBinding 漂移原子拒绝;真实 loopback HTTP→私有 proof file→SQLite Task/audit 创建;真实 Chromium 的 Task 编辑器与 proof ticket 可访问性/布局。 -本地 18-package clean build/test 已通过:`3,030 total / 3,008 pass / 22 conditional、platform 或 external-service skip / 0 fail`;Local API 完整 loopback/SQLite/Console 回归为 `56/56`,新增双 credential request-fence 为 `1/1`,package boundary 契约为 `10/10`。package/source、Local image、122-module Edge import 与 Cluster dependency audit 均 `compatible=true`。离线 Console 三资产合计 62,632 bytes;默认 Edge 为 2,736,982 bytes/329 files/3 packages/83 loaded modules,仍低于 4 MiB/512-file/20 MiB RSS-delta 门;opt-in `edge-application-api|standalone-application-api` 为 4,040,893/4,041,037 bytes、472 files、12 packages、94 loaded modules,仍低于 6 MiB/640-file/28 MiB RSS-delta 门。真实 Chromium 的创建编辑器与 proof ticket 已完成桌面布局、键盘焦点与可访问树检查。 +本地 18-package clean build/test 已通过:`3,030 total / 3,008 pass / 22 conditional、platform 或 external-service skip / 0 fail`;Local API 完整 loopback/SQLite/Console 回归为 `56/56`,新增双 credential request-fence 为 `1/1`,package boundary 契约为 `10/10`。package/source、Local image、122-module Edge import 与 Cluster dependency audit 均 `compatible=true`。离线 Console 三资产合计 62,632 bytes;默认 Edge 为 2,737,205 bytes/329 files/3 packages/58 loaded modules,仍低于 4 MiB/512-file/20 MiB RSS-delta 门;opt-in `edge-application-api|standalone-application-api` 为 4,041,294/4,041,438 bytes、472 files、12 packages、94 loaded modules,仍低于 6 MiB/640-file/28 MiB RSS-delta 门。真实 Chromium 的创建编辑器与 proof ticket 已完成桌面布局、键盘焦点与可访问树检查。 + +阶段提交 `884912d1` 的首轮远端主 CI 为 40 success/3 expected artifact-finalizer skip/1 fail,Kubernetes deployment 与三节点 Security live 均成功。唯一失败在 x64 默认 Edge import RSS:静态引入 request-scoped administration 后 loaded modules 从历史 58 增至 83,RSS 为 21,229,568 bytes,超过 20 MiB 门 258,048 bytes;failed-only attempt 2 确认同一确定性结果。修复没有扩大预算,而是把两个 authority 改成 request-time dynamic import;默认 Edge 恢复 58 loaded modules,本机 RSS delta 为 11,157,504 bytes,Console 仍为 94 modules。最终远端 CI 与里程碑证据由后续修复提交重新闭合。 本机 2.x backend 兼容门首次运行得到 `1,348 total / 1,293 pass / 53 fail / 2 skip`,其中 52 个文件在加载 Sequelize 前统一因锁定的 `@whyour/sqlite3` 原生绑定缺失而失败,另一个 loopback 用例受当前 sandbox 拒绝;依赖重建先因 GitHub 预编译包下载超时、再因本机 C++ SDK 缺少 `` 失败,未伪装成源码回归。唯一实际源码契约漂移是 Local API 文件计数 18/17→20/19,修正后 package-boundary `10/10` 通过。完整 backend 与原生 Linux loopback 必须由阶段提交的远端 CI 闭合。 diff --git a/packages/ql3-local-api/src/task/taskPutRoute.ts b/packages/ql3-local-api/src/task/taskPutRoute.ts index 4ee4ab24..8065a6a2 100644 --- a/packages/ql3-local-api/src/task/taskPutRoute.ts +++ b/packages/ql3-local-api/src/task/taskPutRoute.ts @@ -76,7 +76,7 @@ export interface LocalApiTaskPutRouteOptions { readonly taskDefinitions: TaskDefinitionSource; readonly taskDefinitionAdministrationForCredential: ( fence: Readonly, - ) => TaskDefinitionAdministrationRepository; + ) => Promise; readonly securityAudit: SecurityAuditSink; readonly presenceProof: LocalPresenceProofManager; readonly now?: () => number; @@ -406,11 +406,13 @@ export function createLocalApiTaskPutRoute( return response(503, { code: 'authentication_unavailable' }); } try { + const mutations = + await options.taskDefinitionAdministrationForCredential( + request.authenticated.credentialFence, + ); const service = createLocalTaskDefinitionAdministrationService( options.projectPolicy, - options.taskDefinitionAdministrationForCredential( - request.authenticated.credentialFence, - ), + mutations, options.taskDefinitions, options.securityAudit, { now }, diff --git a/packages/ql3-local-api/test/taskPutRoute.test.cjs b/packages/ql3-local-api/test/taskPutRoute.test.cjs index 7893f038..87a8dca6 100644 --- a/packages/ql3-local-api/test/taskPutRoute.test.cjs +++ b/packages/ql3-local-api/test/taskPutRoute.test.cjs @@ -123,7 +123,7 @@ function fixture(t, overrides = {}) { const route = createLocalApiTaskPutRoute({ projectPolicy, taskDefinitions, - taskDefinitionAdministrationForCredential(fence) { + async taskDefinitionAdministrationForCredential(fence) { calls.push(['credential-fence', fence]); return { async appendAuthorizedTaskDefinitionRevision(mutation) { diff --git a/packages/ql3-local-sqlite/src/runtime/runtimeDatabase.ts b/packages/ql3-local-sqlite/src/runtime/runtimeDatabase.ts index 99d809cf..64c7bd98 100644 --- a/packages/ql3-local-sqlite/src/runtime/runtimeDatabase.ts +++ b/packages/ql3-local-sqlite/src/runtime/runtimeDatabase.ts @@ -51,12 +51,7 @@ import { LocalSqliteApiCredentialRepository } from '../security/apiCredentialRep import { LocalSqliteOwnerPepperRepository } from '../local-owner/ownerPepperRepository'; import { LocalSqliteOperationAuthority } from '../authority/operationAuthority'; import { LocalSqliteTaskDefinitionRepository } from '../task-definition/taskDefinitionRepository'; -import { LocalSqliteTaskDefinitionAdministrationRepository } from '../task-definition/taskDefinitionAdministration'; -import { - confirmLocalSqliteAuthenticatedUserCredentialFence, - LocalSqliteAuthenticatedManagementFenceError, - type LocalSqliteAuthenticatedUserCredentialFence, -} from '../administration/packageManagement'; +import type { LocalSqliteAuthenticatedUserCredentialFence } from '../administration/packageManagement'; import { TaskSpecSemanticRegistry, createBuiltInTaskSpecSemanticRegistry, @@ -105,7 +100,7 @@ export interface LocalSqliteRuntimeDatabase { readonly taskDefinitions: LocalSqliteTaskDefinitionRepository; taskDefinitionAdministrationForCredential( fence: Readonly, - ): TaskDefinitionAdministrationRepository; + ): Promise; readonly triggers: LocalSqliteTriggerRepository; readonly schedules: LocalSqliteScheduleRepository; readonly localDispatch: LocalDispatchStore; @@ -257,9 +252,19 @@ export async function openLocalSqliteRuntimeDatabase( readiness, runRepository, taskDefinitions, - taskDefinitionAdministrationForCredential( + async taskDefinitionAdministrationForCredential( fence: Readonly, ) { + const [administration, taskAdministration] = await Promise.all([ + import('../administration/packageManagement.js'), + import('../task-definition/taskDefinitionAdministration.js'), + ]); + const { + confirmLocalSqliteAuthenticatedUserCredentialFence, + LocalSqliteAuthenticatedManagementFenceError, + } = administration; + const { LocalSqliteTaskDefinitionAdministrationRepository } = + taskAdministration; confirmLocalSqliteAuthenticatedUserCredentialFence(authority, fence); return new LocalSqliteTaskDefinitionAdministrationRepository( authority, diff --git a/packages/ql3-local-sqlite/test/taskDefinitionRequestFence.test.cjs b/packages/ql3-local-sqlite/test/taskDefinitionRequestFence.test.cjs index 5802b3cc..c053d272 100644 --- a/packages/ql3-local-sqlite/test/taskDefinitionRequestFence.test.cjs +++ b/packages/ql3-local-sqlite/test/taskDefinitionRequestFence.test.cjs @@ -171,12 +171,10 @@ async function fixture(t) { test('keeps simultaneous Task mutation credentials request-scoped and rechecks both fences in-transaction', async (t) => { const value = await fixture(t); - const repositoryA = value.runtime.taskDefinitionAdministrationForCredential( - value.fenceA, - ); - const repositoryB = value.runtime.taskDefinitionAdministrationForCredential( - value.fenceB, - ); + const repositoryA = + await value.runtime.taskDefinitionAdministrationForCredential(value.fenceA); + const repositoryB = + await value.runtime.taskDefinitionAdministrationForCredential(value.fenceB); const policyA = await value.runtime.projectPolicy.resolve('default', { type: 'user', id: 'request-user-a',