diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc69d4a..aa3607b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,8 +81,14 @@ jobs: - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: node-version: ${{ matrix.node }} + - name: Install locked dependencies for M6 Host integration + run: npm ci --ignore-scripts --no-audit --no-fund - name: Prove cross-UID model worker cannot read Host key or SQLite DB run: node tests/security/host-isolation.mjs + - name: Prove M6 Host and isolated worker integration + env: + FLOWKIT_M6_ISOLATION: '1' + run: node --test tests/integration/m6-e2e.test.mjs website: runs-on: ubuntu-24.04 diff --git a/docs/FlowKit-PR16-PR17-Local-Verification-Final-20261112.md b/docs/FlowKit-PR16-PR17-Local-Verification-Final-20261112.md new file mode 100644 index 0000000..da07d68 --- /dev/null +++ b/docs/FlowKit-PR16-PR17-Local-Verification-Final-20261112.md @@ -0,0 +1,302 @@ +# FlowKit PR #16 / #17 本地验收最终报告(2026-11-12) + +本报告是对两个 Draft PR 的本地源码核对、测试复现、最小修复和独立复审记录。它不宣布 Phase 2、M6、P0-A、C12 或 S13 验收完成,也不代替 GitHub Actions 的 Ubuntu/Windows × Node 22/24 矩阵或真实 Grok 原生 Host。 + +合并建议: + +- PR #16(文档审查):建议合并。它是审查记录,不是 Phase 2 通过结论。 +- PR #17(预期失败的回归测试):不建议合并。两个 R04 用例已稳定失败,缺陷属实。 +- PR #18(修复):建议合并。竞态已消除,本地回归已通过,范围内复审没有新的 P0/P1。CI 矩阵仍是 `queued`,不能把本机结果写成在线矩阵通过。 +- PR #19(本报告):建议作为证据记录入库。它没有产品代码。同一次复核时它自己的 Actions 也仍是 `queued`。 + +## 1. PR HEAD 与基线 + +| PR | 状态 | Head | Base | 范围 | +|---|---|---|---|---| +| [#16](https://github.com/devcodex-labs/flowkit/pull/16) | OPEN, Draft, MERGEABLE | `90cc0de7e4f0d5971207a85221c32e9f79d9c9b5` | `13d573c5ddc4a0ed3502401589c581799c96c5d2` | 只新增两份审查文档,+259 / −0 | +| [#17](https://github.com/devcodex-labs/flowkit/pull/17) | OPEN, Draft, MERGEABLE | `ada01c933553206948ffcf2f944dfc5604e44da2` | 同上 | 只改 `tests/interactive/coordinator.test.mjs`,+31 / −0 | +| [#18](https://github.com/devcodex-labs/flowkit/pull/18) | OPEN, Draft, MERGEABLE | `452ac387b1f3da97d38bfb27b8582953ab1b818e` | 同上 | 修复 + 与 #17 相同的两个测试,+45 / −3 | +| [#19](https://github.com/devcodex-labs/flowkit/pull/19) | OPEN, Draft, MERGEABLE | 见第 14 节;本段写入时为 `3175a0794a8fb1305ffa0f06d1ae426c0c2b4ae9` | 同上 | 本报告与脱敏日志,无产品代码 | + +PR #16 / #17 的 HEAD 与任务开始时给出的历史 SHA 一致,执行前已用 `gh pr view` 重新读取,不是沿用旧记录。2026-11-12 13:15 +08 再次 `gh pr view`:四条 PR 仍是 OPEN Draft,`mergedAt` 为 null,base 仍是 `13d573c5ddc4a0ed3502401589c581799c96c5d2`,#16/#17/#18 的 head 未变。本地 `D:\Worker\flowkit` 的 `main` 仍是该 SHA,`git status --porcelain=v1` 为空。原始输出在 `docs/evidence/20261112-pr16-pr17/actions-reconfirm-20261112T1315+0800.txt`。 + +检出: + +- `D:\Worker\flowkit`:用户原工作区,仍在 `main`,未改。 +- `D:\Worker\flowkit-pr16-verify`:detached `90cc0de`。 +- `D:\Worker\flowkit-pr17-verify`:detached `ada01c9`。 +- `D:\Worker\flowkit-pr17-fix`:分支 `fix/r04-start-input-snapshot-20261112`。 +- `D:\Worker\flowkit-pr16-pr17-review`:分支 `review/pr16-pr17-local-verification-20261112`,本报告所在。 +- 已有 worktree `D:\Worker\flowkit-acceptance-grok-20261111` 未使用。 + +`git diff --stat 13d573c 90cc0de` 只有: + +- `docs/interactive-phase2-conformance-audit-r1-20261112.md` +- `docs/interactive-phase2-conformance-audit-r2-20261112.md` + +没有生产代码。`git diff ada01c9 -- tests/interactive/coordinator.test.mjs` 在修复分支上为空,说明纳入的回归测试与 PR #17 逐字相同。 + +## 2. 环境与依赖 + +| 项 | 值 | +|---|---| +| 操作系统 | Microsoft Windows NT 10.0.26200.0 | +| 默认 Node | v24.22.2,x64,Volta `C:\Program Files\Volta\node.exe` | +| 另一次运行 | Volta `node@22.22.2` | +| npm | 11.19.1 | +| 引擎要求 | `package.json` `engines.node` 为 `>=22.13.0`。两个实际版本都满足 | +| 根依赖 | lockfile v3;`acorn` 8.19.0;dev `tar` 7.5.22、`typescript` 5.0.4 | +| 安装 | 两个 PR worktree 均 `npm ci --ignore-scripts --no-audit --no-fund`,exit 0,`added 8 packages` | +| 认证 | `gh auth status`:`rockyshi1993`,HTTPS,scopes `gist, read:org, repo, workflow`。本报告不记录令牌 | + +CI 指定的 Node 22.13.0 与 24.22.1 没有逐版本重跑。本机跑的是 22.22.2 与 24.22.2。Linux 未跑。 + +## 3. 命令与日志位置 + +原始日志目录(未入库的完整输出,含 npm pack 文件清单):`D:\Worker\flowkit-verify-logs\`。 + +随本审查分支提交的是脱敏后的决定性日志,位于 `docs/evidence/20261112-pr16-pr17/`。PowerShell `*>` 捕获的中文示例输出有代码页乱码;JSON 里的 `passed:true` 和英文断言不受影响。乱码不记为产品失败。 + +| 命令 | 目录 | 结果 | 日志 | +|---|---|---|---| +| `npm ci --ignore-scripts --no-audit --no-fund` | PR16、PR17 worktree | PASS | 控制台 `added 8 packages` | +| `npm run verify` | PR16 `90cc0de`,Node 24.22.2 | PASS,369 tests / 369 pass / 0 fail,随后 types、package check、package consumer 均 exit 0 | `pr16-verify-node24.txt` | +| `npm run docs:examples` | PR16 | PASS,`FlowkitDocsConsumerV1.passed=true`,E01–E13 | `pr16-docs-examples-node24.txt` | +| `npm --prefix website ci --ignore-scripts --no-audit --no-fund` | PR16 | PASS,`added 333 packages` | `pr16-website-ci.txt` | +| `npm run docs:check` | PR16,autocrlf 工作区 CRLF | FAIL,见第 4 节 | `pr16-docs-check-node24.txt` | +| `npm run docs:check` | PR16,按索引强制 LF 后 | PASS,`{"pages":48,"full":true,"passed":true}`,16 tests / 0 fail | `pr16-docs-check-lf-node24.txt` | +| `npm run docs:build` | PR16 | PASS,`{"pages":48,"html":49,"full":true,"passed":true}` | `pr16-docs-build-node24.txt` | +| `node --test tests/interactive/coordinator.test.mjs` | PR17,Node 24.22.2 | FAIL,23 tests / 21 pass / 2 fail | `pr17-coordinator-node24.txt` | +| 同上 | PR17,Node 22.22.2 | FAIL,同样 21 pass / 2 fail | `pr17-coordinator-node22.22.2.txt` | +| 同上 | 修复提交前,Node 24.22.2 | PASS,23 / 0 | `fix-coordinator-node24-before-full.txt` | +| `npm run verify` | 修复分支,Node 24.22.2 | PASS,371 / 0 | `fix-verify-node24.txt` | +| `npm run verify` | 修复分支,Node 22.22.2 | PASS,371 / 0,package check `passed:true` | `fix-verify-node22.22.2.txt` | +| impact tests(coordinator、store、crash、s07、trusted-host、stage2) | 修复分支,Node 22.22.2 | PASS,71 / 0 | `fix-impact-node22.22.2.txt` | +| `npm run docs:examples` / `docs:check` / `docs:build` | 修复分支,LF 工作区,Node 24.22.2 | PASS | `fix-docs-*.txt` | +| 副作用探针 | 修复分支 | PASS | `probe-r04-side-effects-node24.txt` | +| Trusted Host 探针 | 修复分支 | PASS | `probe-r04-host-node24.txt` | +| CF-08 探针 | PR17 源码(与 main 的 `compile.mjs` 相同) | 竞态复现成功(脚本断言 exit 0) | `probe-cf08-node24.txt` | + +`core.autocrlf=true`。`README.md` 在强制 LF 之前是 `i/lf w/crlf`。`docs:check` 第一次失败是工作区 CRLF 与生成的 quick-start 片段 LF 不一致,断言原文是 `Run npm run docs:sync`。按索引强制 LF 后,同一命令通过。这不是 PR #16 的文档内容缺陷。没有为了让检查变绿而改 README。 + +## 4. PR #16 审查结论:建议合并 + +结论:**PASS(作为审查记录可合并)**。不是 Phase 2 符合性 PASS。 + +已核对: + +- 两份文档都写明不是最终 PASS。T01–T34、R01–R22、S01–S14 使用 `P-PARTIAL`、`DEFERRED`、`BLOCKED`、`GAP-P0-A`、`DESIGN-ONLY/NOT-FULLY-TESTED`。正文里的 “PASS” 都是在否定“已经通过”。 +- R2 把证据分成 `SOURCE-CONFIRMED`、`SOURCE-INFERRED`、`CONTRACT-CONFIRMED-V03`、`V06-R1-EXTRACT`、`RUN-BLOCKED`,并写明这些不是测试 PASS 等级。 +- R2 第 5 节把 PR #17 标成预期失败复现,并写明 CI `queued` 不等于测试 FAIL。这一点与当时的写法一致。本报告之后已经补上本地 FAIL,见第 5 节。R2 没有把这次尚未发生的运行写成通过。 +- 五个关键 blob 在 `ae5fc5a` 与 `13d573c` 上由 `git rev-parse :` 复核,与 R2 表一致: + +| 文件 | blob | +|---|---| +| `src/interactive/coordinator.mjs` | `1ce6460d58c20a38580a30c2966ba8187f1382b7` | +| `src/interactive/trusted-host.mjs` | `315e22662e80c4f1561c2ab8d717a168251c1ed5` | +| `src/interactive/sqlite-store.mjs` | `0f2db54c145649525bcd77b0c501f083dfeb54ee` | +| `src/interactive/compile.mjs` | `43f6dfa86908a486446dc5193c7a4d2895926617` | +| `types/interactive.d.ts` | `a178b2adbff6b18345a7e1b63e3370269ac2425f` | + +Findings: + +| ID | 本轮结论 | 证据 | +|---|---|---| +| CF-01 | 源码缺口属实,P0-A `adminTerminate` 缺失。不因此改 API | `src/` 与 `types/` 无 `adminTerminate`。`coordinator.mjs` 返回的是 `start/complete/resume/load/policyHash`(main 第 657 行,修复后第 668 行) | +| CF-02 | factory 与具名函数的差异属实。未改公共导出 | `src/interactive/index.mjs` 导出的是 `compileInteractive`、`createInteractiveCoordinator`、`createTrustedInteractiveHost` 等 factory | +| CF-05 | 终态帧携带完整 `state` 属实。未改投影 | `asPublicFrame` 在 `coordinator.mjs` 第 144–151 行。类型 `InteractiveRunFrame.state?` 在 `types/interactive.d.ts` 第 83–92 行 | +| CF-06 | 公开 Store 接口是同步的,属实。未改 Store | `types/interactive.d.ts` 第 106–141 行;`sqlite-store.mjs` `createRun` 从第 290 行起同步返回 | +| CF-07 | R2 当时标为 `SOURCE-INFERRED`。本轮已升级为稳定复现的实现缺陷,并由 PR #18 修复。R2 没有把它写成测试 PASS | 见第 5–6 节 | +| CF-08 | 编译选项竞态属实,当前会被 P0-B 门禁拒绝,没有越权启用新阶段。本轮复现,不放进 CF-07 修复 | `compile.mjs` 第 40–44 行。探针见第 10 节 | + +非阻断的引用精度: + +- R1 把 `asPublicFrame` 写成约第 140–150 行。同一 blob 上函数实际从第 144 行开始。R2 已改成第 144–151 行。行为描述正确。 +- R1 说闭包 API 只有 `start/complete/resume/load`。返回对象里还有 `policyHash`。这不改变“没有 `adminTerminate`”的结论。 +- 本机没有找到 V0.6-R5 原文文件,因此没有独立重读 §4/§5/§8/§16 的措辞。R2 已把这部分标成 `V06-R1-EXTRACT`。CF-02、CF-05、CF-06 继续保持“不未经确认就改 API”。源码事实已在本轮重读。 + +本地适用测试全部通过(第 3 节)。R1 与 R2 的结论方向一致:R2 校准了行号和五个 blob,并新增 CF-07/CF-08,没有把 R1 的 PARTIAL/BLOCKED 改成 PASS。 + +## 5. PR #17 竞态复现:FAIL,根因属实 + +结论:**FAIL / 缺陷确认**。不是测试写错,也不是依赖或环境故障。其余 21 个 coordinator 测试在同一进程里通过。Node 24.22.2 与 Node 22.22.2 的失败形态相同。 + +提交:`ada01c933553206948ffcf2f944dfc5604e44da2`。 + +根因在修复前的 `src/interactive/coordinator.mjs` `start()`(当时第 534–552 行): + +1. `programBytes` 与 `programIdentity` 在第一次 `await` 之前已经复制。 +2. `await loadInteractiveProgram(...)` 一定会让出。`load.mjs` 第 66 行还有 `await import(...)`。即使 Promise 已经完成,`await` 仍把后续代码排到微任务。 +3. `initialCheckpoint` 里的 `copy(state)` 和随后的 `copy(startIntent)` 发生在这次让出之后。 +4. 调用方在 `start()` 返回 Promise 之后、第一次 `await` 完成之前修改原对象,Checkpoint 和 `startIntentHash` 会看到修改后的值。 + +`copy` 是 `safeJSONSnapshot`(`canonical.mjs` 第 111–112 行),本身是深拷贝,但调用得太晚。 + +### 用例 1:State,含嵌套对象 + +命令:`node --test tests/interactive/coordinator.test.mjs`。 + +预期:`store.load(frame.runId).state` 等于调用瞬间的 `{ready:true, existing:'hello', context:{request:'before-await'}}`。 + +实际:AssertionError `persisted State must be a deep entry-time snapshot`。 + +```text +actual: { context: { request: 'modified-after-start' }, existing: 'modified-after-start', ready: true } +expected: { ready: true, existing: 'hello', context: { request: 'before-await' } } +at tests/interactive/coordinator.test.mjs:417 +``` + +顶层 `existing` 和嵌套 `context.request` 都变成了调用之后写入的值。Store 已保存错误内容。Node 24 与 Node 22 的 actual/expected 相同。 + +### 用例 2:startIntent + +预期:用原始 `{request:'before-await'}` 对同一 `startKey` 重放,得到同一个 `runId`。 + +实际: + +```text +Error: FK_I_START_CONFLICT: startKey belongs to different intent +at sqlite-store.mjs:313 +at coordinator.mjs:551 +at tests/interactive/coordinator.test.mjs:430 +code: FK_I_START_CONFLICT +``` + +第一次 `start()` 把突变后的 intent 写进了幂等身份。原始 intent 的重放被当成冲突。这是 Store 副作用,不是断言文案问题。 + +两次运行都是 2 fail / 21 pass,没有抖动。因此不建议合并 PR #17:它会把已知失败测试送进 `main`。同等测试已在 PR #18 中与修复一起通过。 + +## 6. 修复 + +分支 `fix/r04-start-input-snapshot-20261112`,提交 `452ac387b1f3da97d38bfb27b8582953ab1b818e`,Draft PR https://github.com/devcodex-labs/flowkit/pull/18 。 + +`start()` 在第一次 `await` 之前: + +- 用现有 `copy()`(有界、拒绝 getter / proxy / 循环 / 非 JSON)复制 State。 +- 超限 State 仍映射为 `FK_I_CHECKPOINT_CAPACITY` / `initial State exceeds Checkpoint hard limit`,与 `initialCheckpoint` 原来的映射相同。 +- 同样复制 `startIntent`。非法 intent 仍是原来的 `TypeError` / `RangeError`,发生在 `putArtifact` 之前。 +- 之后的 `initialCheckpoint(..., stateSnapshot)`、`createRun({startIntent:intentSnapshot})` 不再读取调用方对象。 + +没有 `Object.freeze` 调用方对象。没有只修 State 或只修 `startIntent`。没有改 CAS、Receipt、Journal、Program 身份或安全规则。 + +`initialCheckpoint` 仍会对已复制的 State 再做一次 canonical 拷贝。第二次拷贝的输入是第一次快照,不是调用方对象。`safeJSONSnapshot` 经 `JSON.parse` 产生新对象,不与调用方共享嵌套引用。持久化结果由下面的测试证明与调用瞬间一致。 + +非法输入与合法输入同时存在时,State/intent 错误会早于 Program 加载错误出现。这是“副作用之前拒绝非法输入”的结果。现有单故障测试(NaN State、缺失 binding、超限初始 State)仍通过。 + +## 7. 修复前后对比 + +| 运行 | 修复前 `ada01c9` | 修复后 `452ac38` | +|---|---|---| +| coordinator,Node 24.22.2 | 21 pass / 2 fail | 23 pass / 0 fail | +| coordinator,Node 22.22.2 | 21 pass / 2 fail | 含在下方 71 与 371 中 | +| `npm run verify`,Node 24.22.2 | PR16 基线 369 pass(无这两个测试) | 371 pass / 0 fail | +| `npm run verify`,Node 22.22.2 | 未对失败提交跑全量 | 371 pass / 0 fail | +| Node 22.22.2 impact(6 个文件) | 未跑 | 71 pass / 0 fail | + +副作用探针(修复后,Node 24.22.2)对 NaN、getter、循环 State 和 NaN `startIntent` 计数:这些调用没有增加 `putArtifact` 或 `createRun`。随后一次成功启动、一次原始值重放、一次冲突重放合计 `puts: 3`、`creates: 3`。保存的 State 为: + +```json +{ "existing": "before", "nested": { "request": "before" }, "ready": true } +``` + +调用后把顶层、嵌套和 `startIntent` 都改成 `after`,没有进入 Checkpoint。原始 intent 重放命中同一 Run;不同 intent 抛出 `FK_I_START_CONFLICT`。 + +Trusted Host 探针:`host.start()` 在同步进入 `coordinator.start()` 时完成快照。保存的 `existing` 与 `nested.request` 都是 `before`。`getPublicAgentTask` 的 `input` 是 `{existing:'before'}`,任务 JSON 不含 `after`。 + +## 8. 修复 PR + +- URL:https://github.com/devcodex-labs/flowkit/pull/18 +- Draft:是 +- Head:`452ac387b1f3da97d38bfb27b8582953ab1b818e` +- Base:`13d573c5ddc4a0ed3502401589c581799c96c5d2` +- 未合并,未推 `main`,未发布 npm,未创建 Release + +## 9. 测试状态 + +| 项 | 状态 | +|---|---| +| PR #16 `npm run verify`(Node 24.22.2) | PASS | +| PR #16 `docs:examples` | PASS | +| PR #16 `docs:check`(CRLF 工作区) | FAIL,环境行尾,非文档内容 | +| PR #16 `docs:check`(索引 LF) | PASS | +| PR #16 `docs:build` | PASS | +| PR #16 GitHub Actions run `43156062422` | BLOCKED,`status=queued`,`conclusion` 空,7 个 job pending / 0 秒,`gh pr checks` exit 8 | +| PR #17 两个 R04 测试 | FAIL,已复现 | +| PR #17 同文件其余测试 | PASS | +| PR #18 `npm run verify` Node 24.22.2 与 22.22.2 | PASS,各 371 | +| PR #18 docs examples / check / build | PASS | +| PR #17 Actions run `43155961638` | BLOCKED,`status=queued`,`conclusion` 空,7 个 job pending / 0 秒,`gh pr checks` exit 8 | +| PR #18 Actions run `43159273722` | BLOCKED,同上 | +| PR #19 Actions run `43159631042` | BLOCKED,同上。这是本报告自己的运行,不能当成报告已通过在线 CI | +| Ubuntu | BLOCKED,本机未跑 | +| CI 精确 Node 22.13.0、24.22.1 | BLOCKED,未跑 | +| 真实 Grok 原生 Host、C12、S13、T27 | BLOCKED | +| Windows ACL / 独立 OS 用户 | BLOCKED | +| CF-08 修复 | DEFERRED,已复现,未混入 PR #18 | + +Standard 模式、Interactive 阶段顺序、Checkpoint CAS、startKey 冲突、两进程测试、Program 身份、失败恢复和类型/包导出都包含在两次 `npm run verify` 的 371 个测试里。这些通过不等于原生 Host 验收。 + +## 10. 与冻结契约的符合性 + +本次代码只提前做了 R04 要求的入口快照,使用仓库里已有的 canonical JSON。没有修改 Requirements V0.3、Technical Design V0.6-R5、Program ABI、SQLite schema 或公开类型。 + +仍未关闭、且本轮故意不改的契约项: + +- CF-01:P0-A `adminTerminate` 仍缺失。 +- CF-02:公共 SDK 形态差异仍待 V0.6 §4 与批准记录裁决。 +- CF-05:终态公开帧仍带完整 State。修复没有新增泄露路径;`getPublicAgentTask` 的 allowlist 行为未改。 +- CF-06:同步 Store 与异步五操作规范的差异仍在。 +- CF-08:`compile.mjs` 第 44 行在两次 `await` 之后把 `options.featureStage` 传给 `lowerInteractive`,而前面的检查用的是 `snapshotOptions`。探针在合法 `P0-A` 调用返回前把选项改成 `P0-B`,结果从 7632 字节变成 0 字节,诊断为 `FK_I_UNSUPPORTED_GRAPH: This build implements only P0-A; caller cannot enable P0-B`。这是非确定性失败,不是权限提升。未修。 + +## 11. 独立复审 + +复审对象是提交 `452ac38` 的 diff,不是实施过程中的口头结论。 + +| 问题 | 结论 | +|---|---| +| 是否只让测试变绿 | 否。快照发生在 `coordinator.mjs` 第 545–553 行,早于第 554 行的 `await`。第 560 与 562 行使用这两份拷贝 | +| 是否只冻结外部对象 | 否。没有冻结调用方对象。拷贝来自 `canonicalBytes`,拒绝 getter | +| 新的调用方竞态 | 未发现。调用方要等到 `start()` 让出才能继续,而让出点已经在快照之后。Host `start()`(`trusted-host.mjs` 第 147–156 行)在自己的第一次让出之前同步调用 `coordinator.start`。探针已覆盖 | +| Store / CAS / 历史 Run | diff 不改 `sqlite-store.mjs`。旧 Checkpoint 不迁移。`createRun` 的哈希公式未改,只是输入改为入口快照 | +| 完整 State 泄露 | `asPublicFrame` 未改。Host 探针里的模型任务只有 allowlist 字段 | +| 额外不可逆副作用 | 非法输入在 `putArtifact` 之前抛出。合法路径的 `putArtifact` 仍在校验和 `assertRuntime` 之后,顺序与原来一致 | +| 已确认契约 | 未改 ABI、schema、错误码集合或 P0-A 范围。超限 State 的 `FK_I_CHECKPOINT_CAPACITY` 映射保留 | +| 新的 P0/P1 | 本 diff 内未发现。双故障时非法 State 会先于 Program 错误抛出,现有单故障测试覆盖了原先后果 | + +CF-08 保持独立,不在这个 diff 里。 + +## 12. 合并建议 + +- **建议合并 PR #16。** 文档与源码证据一致,没有把未完成验收标成通过,没有生产代码。合并它只表示审查记录可以入库。 +- **不建议合并 PR #17。** 测试失败是真的。PR #18 已包含同一测试。两个 PR 都合并会造成重复。修复合并后可以由后续复审关闭 #17。 +- **建议合并 PR #18。** 竞态已在入口快照处消除,本地 Windows 的 Node 22.22.2 与 24.22.2 回归已通过,独立复审没有发现本变更范围内的新 P0/P1。这三项是本建议的依据。GitHub Actions 仍是 `queued`,不计入通过,也不把排队写成已证实的故障。 +- **建议合并 PR #19。** 它只提交本报告和脱敏日志。合并它只表示证据记录可以入库。 +- 本会话不执行合并、不推 `main`、不发布 npm、不创建 Release。最终复审和授权范围内的合并由后续复审方执行。 + +## 13. 未完成项 + +- GitHub Actions:PR #16 run `43156062422`、PR #17 run `43155961638`、PR #18 run `43159273722`、PR #19 run `43159631042` 在 2026-11-12 13:15 +08 仍是 `queued`,`conclusion` 为空。`gh pr checks` 对各 PR 的退出码都是 8。排队原因未判定。exit 8 在这里是 CLI 对未完成检查的退出码,不是产品测试失败。 +- 未跑 Ubuntu,未跑 CI 锁定的 Node 22.13.0 与 24.22.1。 +- 未做真实 Grok 原生工具调用、C12、S13、T27、Windows 独立账户 / ACL。脚本 Host 测试不能代替这些。 +- CF-01、CF-02、CF-05、CF-06、CF-08 仍打开。CF-02/05/06 需要正式契约裁决后才能改 API。 +- 本机未重新打开 V0.6-R5 原文,相关措辞仍以 R1 摘录和 R2 的披露为准。 + +## 14. 2026-11-12 13:15 +08 复核 + +命令在 `D:\Worker\flowkit` 执行。原始输出:`docs/evidence/20261112-pr16-pr17/actions-reconfirm-20261112T1315+0800.txt`。 + +| 对象 | 结果 | +|---|---| +| 本地 `main` | `13d573c5ddc4a0ed3502401589c581799c96c5d2`,`git status --porcelain=v1` 为空 | +| PR #16 | OPEN,Draft,MERGEABLE,`mergedAt` null,head `90cc0de7e4f0d5971207a85221c32e9f79d9c9b5` | +| PR #17 | OPEN,Draft,MERGEABLE,`mergedAt` null,head `ada01c933553206948ffcf2f944dfc5604e44da2` | +| PR #18 | OPEN,Draft,MERGEABLE,`mergedAt` null,head `452ac387b1f3da97d38bfb27b8582953ab1b818e` | +| PR #19 | OPEN,Draft,MERGEABLE,`mergedAt` null,head `3175a0794a8fb1305ffa0f06d1ae426c0c2b4ae9`(本复核记录提交之前) | +| run `43156062422` | `status=queued`,`conclusion` 空,`createdAt` `2026-11-12T04:37:52Z`,`updatedAt` `2026-11-12T04:37:54Z`,head `90cc0de` | +| run `43155961638` | `status=queued`,`conclusion` 空,`createdAt` `2026-11-12T04:36:58Z`,`updatedAt` `2026-11-12T04:36:59Z`,head `ada01c9` | +| run `43159273722` | `status=queued`,`conclusion` 空,`createdAt` 与 `updatedAt` 都是 `2026-11-12T05:07:42Z`,head `452ac38` | +| run `43159631042` | `status=queued`,`conclusion` 空,`createdAt` 与 `updatedAt` 都是 `2026-11-12T05:10:51Z`,head `3175a07` | + +四个 run 各有 7 个 job,`gh pr checks` 都显示 `pending`、耗时 `0`。没有 job 进入 `in_progress`、`success` 或 `failure`。因此不能写在线 CI 通过,也不能写 GitHub Actions 已无法运行。本机 Windows 结果仍然只覆盖 Node 22.22.2 与 24.22.2。 + +本段本身的提交会让 PR #19 再排一次 Actions。那一次运行不在上表四个 run 里。本报告不预写它的 `status` 或 `conclusion`。 diff --git a/docs/evidence/20261112-pr16-pr17/actions-reconfirm-20261112T1315+0800.txt b/docs/evidence/20261112-pr16-pr17/actions-reconfirm-20261112T1315+0800.txt new file mode 100644 index 0000000..57ca677 --- /dev/null +++ b/docs/evidence/20261112-pr16-pr17/actions-reconfirm-20261112T1315+0800.txt @@ -0,0 +1,48 @@ +Reconfirm at 2026-11-12 13:15:03 +08:00 +Working directory: D:\Worker\flowkit +Commands: git status --porcelain=v1; git rev-parse --abbrev-ref HEAD; git rev-parse HEAD; + gh pr view 16,17,18,19 --json number,state,isDraft,mergedAt,mergeable,headRefOid,baseRefOid,url,title + gh pr checks 16,17,18,19 + gh run view 43156062422,43155961638,43159273722,43159631042 --json databaseId,status,conclusion,createdAt,updatedAt,event,headSha,url,displayTitle + +--- git --- +porcelain: empty +branch: main +HEAD: 13d573c5ddc4a0ed3502401589c581799c96c5d2 + +--- gh pr view --- +16 OPEN draft MERGEABLE mergedAt=null head=90cc0de7e4f0d5971207a85221c32e9f79d9c9b5 base=13d573c5ddc4a0ed3502401589c581799c96c5d2 + https://github.com/devcodex-labs/flowkit/pull/16 +17 OPEN draft MERGEABLE mergedAt=null head=ada01c933553206948ffcf2f944dfc5604e44da2 base=13d573c5ddc4a0ed3502401589c581799c96c5d2 + https://github.com/devcodex-labs/flowkit/pull/17 +18 OPEN draft MERGEABLE mergedAt=null head=452ac387b1f3da97d38bfb27b8582953ab1b818e base=13d573c5ddc4a0ed3502401589c581799c96c5d2 + https://github.com/devcodex-labs/flowkit/pull/18 +19 OPEN draft MERGEABLE mergedAt=null head=3175a0794a8fb1305ffa0f06d1ae426c0c2b4ae9 base=13d573c5ddc4a0ed3502401589c581799c96c5d2 + https://github.com/devcodex-labs/flowkit/pull/19 + +--- gh pr checks --- +Each of PR 16, 17, 18, 19 printed seven jobs, all "pending" with duration 0. +gh pr checks exit code: 8 for each PR. +Exit 8 here is the CLI code for checks that have not finished. It is not a recorded test failure and not a success. + +PR 16 run https://github.com/devcodex-labs/flowkit/actions/runs/43156062422 +PR 17 run https://github.com/devcodex-labs/flowkit/actions/runs/43155961638 +PR 18 run https://github.com/devcodex-labs/flowkit/actions/runs/43159273722 +PR 19 run https://github.com/devcodex-labs/flowkit/actions/runs/43159631042 + +Job names on every run: +Host OS-user isolation / Node 22.13.0 +Host OS-user isolation / Node 24.22.1 +ubuntu-24.04 / Node 22.13.0 +ubuntu-24.04 / Node 24.22.1 +website +windows-2025 / Node 22.13.0 +windows-2025 / Node 24.22.1 + +--- gh run view --- +43156062422 status=queued conclusion= createdAt=2026-11-12T04:37:52Z updatedAt=2026-11-12T04:37:54Z event=pull_request headSha=90cc0de7e4f0d5971207a85221c32e9f79d9c9b5 +43155961638 status=queued conclusion= createdAt=2026-11-12T04:36:58Z updatedAt=2026-11-12T04:36:59Z event=pull_request headSha=ada01c933553206948ffcf2f944dfc5604e44da2 +43159273722 status=queued conclusion= createdAt=2026-11-12T05:07:42Z updatedAt=2026-11-12T05:07:42Z event=pull_request headSha=452ac387b1f3da97d38bfb27b8582953ab1b818e +43159631042 status=queued conclusion= createdAt=2026-11-12T05:10:51Z updatedAt=2026-11-12T05:10:51Z event=pull_request headSha=3175a0794a8fb1305ffa0f06d1ae426c0c2b4ae9 + +No run has a success or failure conclusion. Queued is not recorded as an outage and not recorded as a green matrix. diff --git a/docs/evidence/20261112-pr16-pr17/fix-coordinator-node24-before-full.txt b/docs/evidence/20261112-pr16-pr17/fix-coordinator-node24-before-full.txt new file mode 100644 index 0000000..8f7f19d --- /dev/null +++ b/docs/evidence/20261112-pr16-pr17/fix-coordinator-node24-before-full.txt @@ -0,0 +1,31 @@ +鉁?M4 pure Terminal passes without Agent bindings, then resume is stable (60.8125ms) +鉁?M4 generated Program routes Condition before Agent, publishes immutable Pending, then commits verified State (69.443ms) +鉁?M4 false Condition never emits an Agent Pending and returns failed Terminal (38.7358ms) +鉁?M4 Standard-compatible missing when input is a durable errored FK_R002 (never a fake waiting) (34.377ms) +鉁?M4 duplicate equal receipt returns durable acceptedRevision after multiple later CAS stages (54.7375ms) +鉁?M4 wrong node, attempt, requestId, different digest and untrusted event kind all reject without State mutation (55.6257ms) +鉁?M4 retries invalid Agent outputs at most retries+1, preserving State and FK_R004 on exhaustion (59.6097ms) +鉁?M4 synchronous validator that throws or returns undefined becomes terminal contract error without retry (128.5878ms) +鉁?M4 host-reported Agent call failure retries once and eventually stops without executing a model internally (52.1236ms) +鉁?M4 run start fingerprint rejects drift but same startKey+same intent returns original run (44.78ms) +鉁?M4 restart with no workflow source uses retained compiler artifact and immutable policy (65.3015ms) +鉁?M4 stage budget=1 checkpoints every step, never returns an empty fake waiting, resumes to the same result (80.6356ms) +鉁?M4 changing client-owned input object while waiting does not mutate persisted State (73.5977ms) +鉁?M4 rejects unsupported checkStep and missing runtime bindings before start writes (27.6211ms) +鉁?M4 concurrent complete from two independent Store connections accepts one receipt but returns same Ack (64.7732ms) +鉁?M4 rejects other tenant runId without exposing checkpoint, even with same compiled workflow (56.7495ms) +鉁?M4 forbids invalid JSON and preserve __proto__ as State data after verified merge (54.1332ms) +鉁?M4 malformed Host failure message is rejected before receipt CAS and leaves waiting replayable (64.418ms) +鉁?M4 an overly permissive validator cannot strand a primitive candidate at commit (60.8871ms) +鉁?M4 a sparse validator reasons array fails closed without committing invalid State (42.0403ms) +鉁?M4 a successful receipt CAS followed by a lost response is recovered by durable duplicate Ack (61.1731ms) +鉁?R04 regression: start snapshots caller-owned State before first await (38.0448ms) +鉁?R04 regression: start snapshots caller-owned startIntent before first await (45.259ms) +鈩?tests 23 +鈩?suites 0 +鈩?pass 23 +鈩?fail 0 +鈩?cancelled 0 +鈩?skipped 0 +鈩?todo 0 +鈩?duration_ms 1454.8789 diff --git a/docs/evidence/20261112-pr16-pr17/pr17-coordinator-node22.22.2.txt b/docs/evidence/20261112-pr16-pr17/pr17-coordinator-node22.22.2.txt new file mode 100644 index 0000000..fe90075 --- /dev/null +++ b/docs/evidence/20261112-pr16-pr17/pr17-coordinator-node22.22.2.txt @@ -0,0 +1,196 @@ +TAP version 13 +# (node:34416) ExperimentalWarning: SQLite is an experimental feature and might change at any time +# (Use `node --trace-warnings ...` to show where the warning was created) +# Subtest: M4 pure Terminal passes without Agent bindings, then resume is stable +ok 1 - M4 pure Terminal passes without Agent bindings, then resume is stable + --- + duration_ms: 152.39 + type: 'test' + ... +# Subtest: M4 generated Program routes Condition before Agent, publishes immutable Pending, then commits verified State +ok 2 - M4 generated Program routes Condition before Agent, publishes immutable Pending, then commits verified State + --- + duration_ms: 96.897 + type: 'test' + ... +# Subtest: M4 false Condition never emits an Agent Pending and returns failed Terminal +ok 3 - M4 false Condition never emits an Agent Pending and returns failed Terminal + --- + duration_ms: 101.0779 + type: 'test' + ... +# Subtest: M4 Standard-compatible missing when input is a durable errored FK_R002 (never a fake waiting) +ok 4 - M4 Standard-compatible missing when input is a durable errored FK_R002 (never a fake waiting) + --- + duration_ms: 67.8472 + type: 'test' + ... +# Subtest: M4 duplicate equal receipt returns durable acceptedRevision after multiple later CAS stages +ok 5 - M4 duplicate equal receipt returns durable acceptedRevision after multiple later CAS stages + --- + duration_ms: 87.8788 + type: 'test' + ... +# Subtest: M4 wrong node, attempt, requestId, different digest and untrusted event kind all reject without State mutation +ok 6 - M4 wrong node, attempt, requestId, different digest and untrusted event kind all reject without State mutation + --- + duration_ms: 222.6651 + type: 'test' + ... +# Subtest: M4 retries invalid Agent outputs at most retries+1, preserving State and FK_R004 on exhaustion +ok 7 - M4 retries invalid Agent outputs at most retries+1, preserving State and FK_R004 on exhaustion + --- + duration_ms: 271.6269 + type: 'test' + ... +# Subtest: M4 synchronous validator that throws or returns undefined becomes terminal contract error without retry +ok 8 - M4 synchronous validator that throws or returns undefined becomes terminal contract error without retry + --- + duration_ms: 411.5546 + type: 'test' + ... +# Subtest: M4 host-reported Agent call failure retries once and eventually stops without executing a model internally +ok 9 - M4 host-reported Agent call failure retries once and eventually stops without executing a model internally + --- + duration_ms: 88.0933 + type: 'test' + ... +# Subtest: M4 run start fingerprint rejects drift but same startKey+same intent returns original run +ok 10 - M4 run start fingerprint rejects drift but same startKey+same intent returns original run + --- + duration_ms: 68.5111 + type: 'test' + ... +# Subtest: M4 restart with no workflow source uses retained compiler artifact and immutable policy +ok 11 - M4 restart with no workflow source uses retained compiler artifact and immutable policy + --- + duration_ms: 110.8433 + type: 'test' + ... +# Subtest: M4 stage budget=1 checkpoints every step, never returns an empty fake waiting, resumes to the same result +ok 12 - M4 stage budget=1 checkpoints every step, never returns an empty fake waiting, resumes to the same result + --- + duration_ms: 146.1063 + type: 'test' + ... +# Subtest: M4 changing client-owned input object while waiting does not mutate persisted State +ok 13 - M4 changing client-owned input object while waiting does not mutate persisted State + --- + duration_ms: 176.2838 + type: 'test' + ... +# Subtest: M4 rejects unsupported checkStep and missing runtime bindings before start writes +ok 14 - M4 rejects unsupported checkStep and missing runtime bindings before start writes + --- + duration_ms: 168.1791 + type: 'test' + ... +# Subtest: M4 concurrent complete from two independent Store connections accepts one receipt but returns same Ack +ok 15 - M4 concurrent complete from two independent Store connections accepts one receipt but returns same Ack + --- + duration_ms: 130.0283 + type: 'test' + ... +# Subtest: M4 rejects other tenant runId without exposing checkpoint, even with same compiled workflow +ok 16 - M4 rejects other tenant runId without exposing checkpoint, even with same compiled workflow + --- + duration_ms: 99.6333 + type: 'test' + ... +# Subtest: M4 forbids invalid JSON and preserve __proto__ as State data after verified merge +ok 17 - M4 forbids invalid JSON and preserve __proto__ as State data after verified merge + --- + duration_ms: 142.4165 + type: 'test' + ... +# Subtest: M4 malformed Host failure message is rejected before receipt CAS and leaves waiting replayable +ok 18 - M4 malformed Host failure message is rejected before receipt CAS and leaves waiting replayable + --- + duration_ms: 118.2922 + type: 'test' + ... +# Subtest: M4 an overly permissive validator cannot strand a primitive candidate at commit +ok 19 - M4 an overly permissive validator cannot strand a primitive candidate at commit + --- + duration_ms: 124.1347 + type: 'test' + ... +# Subtest: M4 a sparse validator reasons array fails closed without committing invalid State +ok 20 - M4 a sparse validator reasons array fails closed without committing invalid State + --- + duration_ms: 93.9348 + type: 'test' + ... +# Subtest: M4 a successful receipt CAS followed by a lost response is recovered by durable duplicate Ack +ok 21 - M4 a successful receipt CAS followed by a lost response is recovered by durable duplicate Ack + --- + duration_ms: 105.876 + type: 'test' + ... +# Subtest: R04 regression: start snapshots caller-owned State before first await +not ok 22 - R04 regression: start snapshots caller-owned State before first await + --- + duration_ms: 111.4347 + type: 'test' + location: 'D:\\Worker\\flowkit-pr17-verify\\tests\\interactive\\coordinator.test.mjs:407:1' + failureType: 'testCodeFailure' + error: |- + persisted State must be a deep entry-time snapshot + + actual - expected + + { + context: { + + request: 'modified-after-start' + - request: 'before-await' + }, + + existing: 'modified-after-start', + - existing: 'hello', + ready: true + } + + code: 'ERR_ASSERTION' + name: 'AssertionError' + expected: + ready: true + existing: 'hello' + context: + request: 'before-await' + actual: + context: + request: 'modified-after-start' + existing: 'modified-after-start' + ready: true + operator: 'deepStrictEqual' + stack: |- + TestContext. (file:///D:/Worker/flowkit-pr17-verify/tests/interactive/coordinator.test.mjs:417:10) + async Test.run (node:internal/test_runner/test:1054:7) + async Test.processPendingSubtests (node:internal/test_runner/test:744:7) + ... +# Subtest: R04 regression: start snapshots caller-owned startIntent before first await +not ok 23 - R04 regression: start snapshots caller-owned startIntent before first await + --- + duration_ms: 104.4814 + type: 'test' + location: 'D:\\Worker\\flowkit-pr17-verify\\tests\\interactive\\coordinator.test.mjs:421:1' + failureType: 'testCodeFailure' + error: 'FK_I_START_CONFLICT: startKey belongs to different intent' + code: 'FK_I_START_CONFLICT' + stack: |- + failure (file:///D:/Worker/flowkit-pr17-verify/src/interactive/sqlite-store.mjs:24:24) + file:///D:/Worker/flowkit-pr17-verify/src/interactive/sqlite-store.mjs:313:19 + transaction (file:///D:/Worker/flowkit-pr17-verify/src/interactive/sqlite-store.mjs:142:22) + Object.createRun (file:///D:/Worker/flowkit-pr17-verify/src/interactive/sqlite-store.mjs:308:14) + Object.start (file:///D:/Worker/flowkit-pr17-verify/src/interactive/coordinator.mjs:551:24) + async TestContext. (file:///D:/Worker/flowkit-pr17-verify/tests/interactive/coordinator.test.mjs:430:24) + async Test.run (node:internal/test_runner/test:1054:7) + async Test.processPendingSubtests (node:internal/test_runner/test:744:7) + ... +1..23 +# tests 23 +# suites 0 +# pass 21 +# fail 2 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms 3495.5286 diff --git a/docs/evidence/20261112-pr16-pr17/pr17-coordinator-node24.txt b/docs/evidence/20261112-pr16-pr17/pr17-coordinator-node24.txt new file mode 100644 index 0000000..d827f06 --- /dev/null +++ b/docs/evidence/20261112-pr16-pr17/pr17-coordinator-node24.txt @@ -0,0 +1,73 @@ +鉁?M4 pure Terminal passes without Agent bindings, then resume is stable (75.0133ms) +鉁?M4 generated Program routes Condition before Agent, publishes immutable Pending, then commits verified State (69.0836ms) +鉁?M4 false Condition never emits an Agent Pending and returns failed Terminal (38.6616ms) +鉁?M4 Standard-compatible missing when input is a durable errored FK_R002 (never a fake waiting) (38.3901ms) +鉁?M4 duplicate equal receipt returns durable acceptedRevision after multiple later CAS stages (52.8343ms) +鉁?M4 wrong node, attempt, requestId, different digest and untrusted event kind all reject without State mutation (55.4604ms) +鉁?M4 retries invalid Agent outputs at most retries+1, preserving State and FK_R004 on exhaustion (64.6791ms) +鉁?M4 synchronous validator that throws or returns undefined becomes terminal contract error without retry (128.5816ms) +鉁?M4 host-reported Agent call failure retries once and eventually stops without executing a model internally (51.927ms) +鉁?M4 run start fingerprint rejects drift but same startKey+same intent returns original run (43.0537ms) +鉁?M4 restart with no workflow source uses retained compiler artifact and immutable policy (65.0977ms) +鉁?M4 stage budget=1 checkpoints every step, never returns an empty fake waiting, resumes to the same result (77.9976ms) +鉁?M4 changing client-owned input object while waiting does not mutate persisted State (72.4723ms) +鉁?M4 rejects unsupported checkStep and missing runtime bindings before start writes (33.6865ms) +鉁?M4 concurrent complete from two independent Store connections accepts one receipt but returns same Ack (64.7803ms) +鉁?M4 rejects other tenant runId without exposing checkpoint, even with same compiled workflow (50.4427ms) +鉁?M4 forbids invalid JSON and preserve __proto__ as State data after verified merge (49.1118ms) +鉁?M4 malformed Host failure message is rejected before receipt CAS and leaves waiting replayable (48.8353ms) +鉁?M4 an overly permissive validator cannot strand a primitive candidate at commit (62.5123ms) +鉁?M4 a sparse validator reasons array fails closed without committing invalid State (39.514ms) +鉁?M4 a successful receipt CAS followed by a lost response is recovered by durable duplicate Ack (53.1683ms) +鉁?R04 regression: start snapshots caller-owned State before first await (46.7853ms) +鉁?R04 regression: start snapshots caller-owned startIntent before first await (54.4232ms) +鈩?tests 23 +鈩?suites 0 +鈩?pass 21 +鈩?fail 2 +鈩?cancelled 0 +鈩?skipped 0 +鈩?todo 0 +鈩?duration_ms 1478.7332 + +鉁?failing tests: + +test at tests\interactive\coordinator.test.mjs:407:1 +鉁?R04 regression: start snapshots caller-owned State before first await (46.7853ms) + AssertionError [ERR_ASSERTION]: persisted State must be a deep entry-time snapshot + + actual - expected + + { + context: { + + request: 'modified-after-start' + - request: 'before-await' + }, + + existing: 'modified-after-start', + - existing: 'hello', + ready: true + } + + at TestContext. (file:///D:/Worker/flowkit-pr17-verify/tests/interactive/coordinator.test.mjs:417:10) + at async Test.run (node:internal/test_runner/test:1409:7) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: { context: { request: 'modified-after-start' }, existing: 'modified-after-start', ready: true }, + expected: { ready: true, existing: 'hello', context: { request: 'before-await' } }, + operator: 'deepStrictEqual', + diff: 'simple' + } + +test at tests\interactive\coordinator.test.mjs:421:1 +鉁?R04 regression: start snapshots caller-owned startIntent before first await (54.4232ms) + Error: FK_I_START_CONFLICT: startKey belongs to different intent + at failure (file:///D:/Worker/flowkit-pr17-verify/src/interactive/sqlite-store.mjs:24:24) + at file:///D:/Worker/flowkit-pr17-verify/src/interactive/sqlite-store.mjs:313:19 + at transaction (file:///D:/Worker/flowkit-pr17-verify/src/interactive/sqlite-store.mjs:142:22) + at Object.createRun (file:///D:/Worker/flowkit-pr17-verify/src/interactive/sqlite-store.mjs:308:14) + at Object.start (file:///D:/Worker/flowkit-pr17-verify/src/interactive/coordinator.mjs:551:24) + at async TestContext. (file:///D:/Worker/flowkit-pr17-verify/tests/interactive/coordinator.test.mjs:430:24) + at async Test.run (node:internal/test_runner/test:1409:7) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + code: 'FK_I_START_CONFLICT' + } diff --git a/docs/evidence/20261112-pr16-pr17/probe-cf08-node24.txt b/docs/evidence/20261112-pr16-pr17/probe-cf08-node24.txt new file mode 100644 index 0000000..1fec22b --- /dev/null +++ b/docs/evidence/20261112-pr16-pr17/probe-cf08-node24.txt @@ -0,0 +1,8 @@ +{ + "controlBytes": 7632, + "racedBytes": 0, + "racedDiagnostics": [ + "FK_I_UNSUPPORTED_GRAPH:This build implements only P0-A; caller cannot enable P0-B" + ], + "node": "v24.22.2" +} diff --git a/docs/evidence/20261112-pr16-pr17/probe-r04-host-node24.txt b/docs/evidence/20261112-pr16-pr17/probe-r04-host-node24.txt new file mode 100644 index 0000000..33b59e3 --- /dev/null +++ b/docs/evidence/20261112-pr16-pr17/probe-r04-host-node24.txt @@ -0,0 +1,13 @@ +{ + "runId": "host-r04", + "savedState": { + "existing": "before", + "nested": { + "request": "before" + } + }, + "taskInput": { + "existing": "before" + }, + "node": "v24.22.2" +} diff --git a/docs/evidence/20261112-pr16-pr17/probe-r04-side-effects-node24.txt b/docs/evidence/20261112-pr16-pr17/probe-r04-side-effects-node24.txt new file mode 100644 index 0000000..7ff1413 --- /dev/null +++ b/docs/evidence/20261112-pr16-pr17/probe-r04-side-effects-node24.txt @@ -0,0 +1,15 @@ +{ + "puts": 3, + "creates": 3, + "runId": "ok-run", + "savedState": { + "existing": "before", + "nested": { + "request": "before" + }, + "ready": true + }, + "journalEntries": 6, + "node": "v24.22.2", + "platform": "win32" +} diff --git a/docs/interactive-complex-acceptance-c01-c12.md b/docs/interactive-complex-acceptance-c01-c12.md new file mode 100644 index 0000000..f2375d2 --- /dev/null +++ b/docs/interactive-complex-acceptance-c01-c12.md @@ -0,0 +1,51 @@ +# FlowKit Interactive P0-A — 复杂验收矩阵 C01–C12(冻结版) + +**范围与判断标准**:此矩阵只测试 P0-A 的 `Condition / Agent / Terminal` 和可信 Host 的 CAS、回执、恢复、安全。不得把 Standard 的 Human/Action/Verification/Loop/Parallel 实现或脚本 Worker 的结果当作 Interactive 已支持或真实模型验证。不得把 GitHub CI 的测试数量当成真实模型会话次数。 + +**证据必须分层**: +- **Protocol / Scripted(自动化)**:实际 FlowKit Compiler → JavaScript Program → SQLite Store / Trusted Host 执行,但 Agent 结果由脚本提供。证实协议,不证实模型遵循指令。 +- **Live Model(真实模型)**:真实 ChatGPT/Grok 逐次读取已生成的 Pending 后生成答案;每次的 requestId、taskDigest、输出、工具操作必须有独立证据。ChatGPT → GitHub Actions 桥接只能证明真实参与和受控处理,不代表原生宿主/会话来源认证。 +- **Native Host(S13)**:单个受控 Grok Build / Codex 原生会话模型→JS 工具→Host→回执,原生工具日志及 Host 独立验证的会话/来源证据;不能用脚本模拟、GitHub 上传身份替代。 + +## 场景、可观测预期和证据 + +| ID | 场景 | 冻结验收要求 | 自动化证据范围 | 真实模型门禁 | +|---|---|---|---|---| +| C01 | 4 个串行 Agent + 2 个 Condition | 原生 Program 顺序、后续节点只接收合法上游状态,未跳过/重复执行;最终 State 包含各节点输出 | 新增复杂链+Host 签名测试 | **待实测**:至少四次真实模型回合 | +| C02 | 动态路线与异常条件 | ready=true/false、审查通过/拒绝、缺失 when 输入分别进入正确路径;未选 Agent 不调度 | 新增多路径正反测试 | **待实测**:至少两条真实分支 | +| C03 | Schema 拒绝与修复 | 第一次输出无效不污染 State;下一 Pending requestId/attempt 更新,合法结果可以成功 | 新增接纳/重试负例 | **待实测**:真实模型两次输出 | +| C04 | 失败/重试耗尽 | retries+1 限制,最后 `FK_R004`,State 不含未认可结果 | 新增失败序列 | 至少一个可信 Host 失败路径(真实模型待实测) | +| C05 | Host 崩溃恢复 | Pending 发布、接纳前、校验及提交各切点 SIGKILL;请求 identity/State/Journal 一致 | 已有 `tests/interactive/crash.test.mjs` 三项真子进程测试;当前 main 的 tests/integration/m6-e2e.test.mjs 另有 Host+脚本 Worker(原 PR #7 的相关工作由 PR #13 纳入) | 与真实模型耦合的 kill/resume **待实测** | +| C06 | 双进程竞争、丢响应、重复回执 | CAS 只接纳一次;后续始终返回原始 `acceptedRevision`;不声明外部 Provider 恰好执行一次 | 已有 `crash.test.mjs` + `coordinator.test.mjs` | 原生模型多会话竞争待实测 | +| C07 | Program / Skill / Policy 漂移 | 变更后未发生额外调度/CAS;旧依赖恢复不误放行 | main M5 policy 测试;当前 main 的 tests/integration/m6-e2e.test.mjs S06 覆盖脚本 Host 的 source/Skill/Program drift(由 PR #13 纳入) | 原生 Host/模型待实测 | +| C08 | 权限、签名与泄漏 | 错租户/伪签/敏感字段/UID 隔离 fail closed;模型不能直接操作私有 Store | main M5 `trusted-host` + Linux 2 项 OS 用户隔离任务 | 原生 Grok Build 私钥/SQLite 不可达待实测 | +| C09 | 容量与超限 | 2MiB hard,1.75MiB active 预留;有界结果可成功;错误原子退出;未认证超大回执不能杀 Run | main S07 `s07-capacity` + `host-oversize` | 真实模型大输出路径待实测 | +| C10 | 长链与账本 | 至少 30 个串行 Agent,Checkpoint 近期摘要 ≤16,SQLite 全量历史回执可查;Journal 连续,恢复历史 Ack | 新增长链与 SQLite 回执索引测试(现有 21 节点基础) | 不要求 30 次模型计费;脚本覆盖为准 | +| C11 | 阶段预算一致性 | `maxStagesPerCall=1` 与 128 在相同 Program/结果序列下最终 State **字节相等**,Journal 合法;不要求 revision 一致 | 新增复杂链跨预算用例,当前 main 的 tests/integration/m6-e2e.test.mjs 还有脚本 Worker 验收 | 模型侧不同调度路径待实测 | +| C12 | 原生真实模型宿主 | Grok Build 等原生 JS 工具调用原始日志、真实会话身份、Host 独立来源认证、隔离、终态 | **不能用脚本或 GitHub ChatGPT 桥接替代** | **BLOCKED**:需 Dot/Grok Build 实际宿主权限与真实日志 | + +## 每个可执行测试的最小证据格式 + +```json +{ + "scenarioId": "C01", + "execution": "SCRIPTED_HOST", + "programHash": "sha256:...", + "runId": "...", + "nodeTrace": ["requirements", "design", "plan", "review"], + "checkpointStatus": "succeeded", + "journalRevisions": 0, + "assertions": ["..."], + "result": "PASS" +} +``` + +测试完成后补充**精确提交/CI URL、各环境完成状态、实际模型回合次数、失败/未覆盖限制**。即使自动化 7/7 通过,C01–C05 只应在「协议层」标记 PASS;真实模型层未取得证据必须为 PARTIAL/BLOCKED。 + +## 阶段门禁 + +1. 只开发测试与验证报告,发现真实缺陷才另外起小范围修复 PR;不擅自重构 FlowKit Core。 +2. PR #7 已关闭且未合并,其适用 M6 测试工作由 PR #13 纳入 main;不得把历史 PR #7 当作仍可合并的分支,也不得因绿灯宣称 Grok/Codex 原生模型来源验收通过。 +3. 在复杂矩阵中允许使用脚本结果注入来做 30 Agent、并发、容量、安全负例,但必须标明执行类型和模型调用次数(脚本为零)。 +4. M6 所需的冻结需求 V0.3 / 技术 V0.6-R5 合规审查仍独立存在,不能被此矩阵替代。 +5. 本轮不引入 P0-B、P1、P2、Context、通用业务产物存储或 GC。 diff --git a/docs/interactive-complex-acceptance-c13-c24-results-2026-11-11.md b/docs/interactive-complex-acceptance-c13-c24-results-2026-11-11.md new file mode 100644 index 0000000..15bca73 --- /dev/null +++ b/docs/interactive-complex-acceptance-c13-c24-results-2026-11-11.md @@ -0,0 +1,58 @@ +# FlowKit Interactive — C13–C24 补充复杂验收结果(2026-11-11) + +## 结论 + +**P0-A 复杂协议补充用例 C13–C20:8/8 PASS(脚本可信 Host);原 C01–C12 测试继续通过。原生模型宿主 C12/S13 仍 BLOCKED,不能据此合并 M6 / 发布。** + +- 仓库:devcodex-labs/flowkit(private) +- PR:[Draft #12](https://github.com/devcodex-labs/flowkit/pull/12),基于 main,不修改生产 Core +- 验证测试提交:**09e14e82b72277ef58b656bf129c9c4bc0b4b779** +- 完成的 CI:[42968295106 — 7/7 SUCCESS](https://github.com/devcodex-labs/flowkit/actions/runs/42968295106) +- 关联矩阵:[C01–C12 原矩阵](./interactive-complex-acceptance-c01-c12.md)、[C13–C24 增量矩阵](./interactive-complex-acceptance-extension-c13-c24.md) +- 执行类型:**SCRIPTED_TRUSTED_HOST**,本次新增用例模型调用次数 **0**;不能计入真实模型调用次数 + +### 精确跨平台测试清单 + +| GitHub Actions Job | Test | Pass | Fail | Skip | Job | +|---|---:|---:|---:|---:|---| +| Ubuntu 24.04 / Node 22.13.0 | 372 | 372 | 0 | 0 | SUCCESS | +| Ubuntu 24.04 / Node 24.22.1 | 372 | 372 | 0 | 0 | SUCCESS | +| Windows 2025 / Node 22.13.0 | 372 | 372 | 0 | 0 | SUCCESS | +| Windows 2025 / Node 24.22.1 | 372 | 372 | 0 | 0 | SUCCESS | +| Host OS-user isolation / Node 22.13.0 | — | — | — | — | SUCCESS | +| Host OS-user isolation / Node 24.22.1 | — | — | — | — | SUCCESS | +| Website/docs | — | — | — | — | SUCCESS | + +以上从 GitHub Actions 的实际 job 日志/结论核验,非 PR 描述中的主观声称。372 是每一个环境内的全部测试数量,包含现有回归;新增复杂测试函数为 8 个,不能解释成 372 个真实 Agent 回合。 + +## C13–C24 逐项裁决 + +| ID | 协议层 P | 真实模型 M / 原生宿主 N | 可观测证据 / 缺口 | +|---|---|---|---| +| C13 分支×阶段预算 | **PASS** | M 未单独测 | 预算 1/128 下四个分支结果相同,已核对两个终态 State Hash | +| C14 链中伪造/超限回执 | **PASS** | N 模型对抗攻击待测 | 5 个不同类型的未认证/错误回执被拒且 Checkpoint 和 Journal 字节保持一致 | +| C15 Host 重建后旧 Ack | **PASS** | N 未测 | 下游等待态及 Terminal 后重放历史 receipt,不额外提交 State,Ack 原 acceptedRevision | +| C16 Provider 可信失败回执 | **PASS** | N 真 Provider 失败待测 | signed agent-call-failed 使末端 Agent 重试一次,旧回执重复 Ack,合法第二回合成功 | +| C17 链中 Host policy drift | **PASS** | N 实际宿主升级待测 | 3 条不同入口拒绝政策漂移并证明无 CAS 写入,旧 Host 继续成功 | +| C18 多 Run 交错、重复启动 | **PASS** | N 多模型 Session 未测 | 两 Run 状态隔离;重复 startKey 幂等,同 key 不同 State 冲突 | +| C19 36 Agent 中段失败 | **PASS** | 不要求 36 次模型计费 | 第 18 个脚本结果无效,终态 FK_R004,余下 18 个未分发,State 无非法字段 | +| C20 双有效签名并发竞争 | **PASS(同进程 Host 对象)** | N 真进程/多模型竞争待测 | 两个不同签名内容仅一个 CAS 成功、另一个 FK_I_RECEIPT_CONFLICT;真正双进程测试已在 C06/M4 另验 | +| C21 崩溃/跨版本恢复 | **PARTIAL** | N 未测 | M4 有真 SIGKILL 与同环境恢复;同一物理 DB 跨 OS/Node 迁移未执行,是增强项不是原 P0-A 硬门禁 | +| C22 Prompt 注入/宿主权限 | **PASS(既有 M5 + OS UID)** | **N BLOCKED** | 白名单、拒签、跨 UID 隔离已有证据;Grok Build 原生模型权限分离无证据 | +| C23 真实模型异常组合 | 不可用脚本替代 | **M/N BLOCKED** | 真实 false 分支、持续错误、与 SIGKILL 耦合恢复及多模型竞争未完成 | +| C24 兼容/发行/原生来源门禁 | **PASS(CI 部分)** | **N BLOCKED** | 7 job 全绿不意味着 C12/S13 原生认证已通过 | + +## 首轮失败与复审记录 + +新增 C13–C20 首次提交 5be1abb(CI 42967554504)曾 370 PASS / 2 FAIL,失败集中在 **C16 和 C20 的测试断言**:在状态为 waiting 时,测试错误地读取了公开 Frame 中被隐藏的 State。FlowKit 此时不公开私有 State 是预期安全设计。第二次提交 ac49e3e(CI 42968021802)修复了 C20,留下 C16 一处相同的测试问题(371 PASS / 1 FAIL)。最终测试提交 09e14e8 将 C16 内部断言改为从受信 SQLite Store 读取,四环境 372/372 PASS。 + +这两轮失败**不是 FlowKit Core 缺陷的证据**;本轮修复未修改生产源码、签名器、Coordinator、SQLite Schema 或 Program ABI。PR #12 changed-files 范围仅 docs、tests、fixtures、示例工作流。 + +## 仍不得放行的明确缺口 + +1. **C12/S13:原生 Grok Build/Codex 工具调用、原生 Session/Provider 来源认证、Host 独立可信证明和模型进程权限隔离**。GitHub Actions + ChatGPT 五回合桥接不满足该条件。 +2. **真实模型负例**:C02 的 false 分支、C04 持续错误、C05 实际模型执行中 SIGKILL/恢复、C06 多原生会话竞争、C08/C09 原生输出与权限攻击。 +3. **C07 综合漂移**:原 Draft PR #7 已关闭且未合并;其适用的 source/Program/Skill drift 测试已由 PR #13 纳入当前 main 的 tests/integration/m6-e2e.test.mjs S06。该证据仍是脚本 Host 协议层,不是实时模型或原生 Host 漂移验收。 +4. 未证明的 Provider exactly-once、模型遵循任意 Skill 指令、同 UID 混合进程隔离不能假定成立;这些必须通过宿主信任设计而非脚本测试补足。 + +**裁决:C13–C20 协议 PASS,C01–C12 原先已验证结果继续有效;整体 M6 最终验收仍 BLOCKED。PR #12 保持 Draft 待更新/复审;PR #7 已关闭且未合并。当前阶段不修改 main、不发布新版本。** diff --git a/docs/interactive-complex-acceptance-extension-c13-c24.md b/docs/interactive-complex-acceptance-extension-c13-c24.md new file mode 100644 index 0000000..35940d9 --- /dev/null +++ b/docs/interactive-complex-acceptance-extension-c13-c24.md @@ -0,0 +1,51 @@ +# FlowKit Interactive P0-A — 复杂验收补充 C13–C24 + +> 2026-11-11。追加于[冻结 C01–C12](./interactive-complex-acceptance-c01-c12.md),不修改原冻结用例和通过条件。只针对 P0-A 的 Condition / Agent / Terminal、JavaScript Program / SQLite / Trusted Host。C12/S13 原生模型宿主门禁独立有效。 + +## 0. 范围与证据等级 + +- **P — protocol/scripted**:真实编译器、生成 JS Program、SQLite WAL/FULL、Host 回执/签名/CAS、脚本 Agent 回答;证实程序约束,不证实模型行为。 +- **M — live model**:实际模型读取真实 Pending 后生成回答,有独立 requestId + taskDigest + tool trace;不等于原生宿主来源认证。 +- **N — native host**:Grok Build/Codex 等原生受控宿主,会话/工具原始日志、受信传输的独立来源证明和模型进程隔离。只有这一级可放行 C12/S13。 +- **X — out of scope**:Human、Action、Verification、Loop、Parallel、P0-B/P1/P2、Context、通用产物存储、Provider 恰好执行一次。不得用本矩阵引入未批准的功能。 + +## 1. 追加用例和可观察不变式 + +| ID | 组合场景 | 正反例和核心断言 | 执行入口 | 验收层 | +|---|---|---|---|---| +| **C13** | 分支真值表 × 阶段预算 | ready=false 不分发 Agent;ready 缺失 FK_R002;reviewApproved=false 去 declined;true 去 done。预算 1/128 的终态 State Hash 一致、Journal 合法 | complex-c01-c12.test.mjs C13 | P | +| **C14** | 第 2 个 Agent 等待时恶意回执 | 错 body、attempt、requestId、伪签、超过 2MiB 的未认证大回执全部拒绝;Checkpoint 字节、Journal、Ack 无变化;合法回执后继续成功 | 同文件 C14 | P | +| **C15** | 下游运行时旧回执重传 | Host 对象重建与终态后重传均返回原始 acceptedRevision,无额外 Journal、无状态重写 | 同文件 C15,交叉 C06/C10 | P | +| **C16** | 最后 Agent 可信 Provider 报错 | 只有签名的 Host call-failed 能触发重试;新 requestId,原状态不污染;旧失败回执仅返回旧 Ack;合法第二次响应路由成功 | 同文件 C16 | P | +| **C17** | 多 Agent 链中途策略漂移 | 修改绑定版本后 load/resume/getPublicAgentTask 均拒绝;Checkpoint/Journal 字节不变;旧 Host 可续接 | 同文件 C17 | P | +| **C18** | 两个 Run 交错与重复启动 | 同 startKey+intent 幂等,变更 State 报冲突;不同 Run 的分支、State、Journal 不串 | 同文件 C18 | P | +| **C19** | 36 Agent 链第 18 步失败 | 无效结果触发 FK_R004;余下 18 个 Agent 不再调度,State 不出现未验证字段,终态 resume 幂等 | 同文件 C19 | P | +| **C20** | 两个独立有效签名但冲突的结果 | 同一 Pending 同时提交两个不同结果,恰好一个 CAS 成功,另一个 FK_I_RECEIPT_CONFLICT;后续仍可正常完成 | 同文件 C20;真 OS 多进程由 crash.test.mjs 另验 | P | +| **C21** | SIGKILL 与跨版本恢复 | Pending、校验、接纳、Commit 切点的真正杀进程恢复;同物理 SQLite 文件在不同 Node 版本/OS 重新打开仍能保留身份、账本 | main crash.test.mjs 已有同环境 SIGKILL;跨 Node/OS 文件转移未验证 | P:部分;跨版本增强项 | +| **C22** | Prompt injection / 模型声称拥有权限 | 未签名的模型输出绝不成为 Host 命令;公开 Task 只包含白名单输入;模型无法获取私钥/私有 SQLite;真实模型中仍需攻击实测 | M5 trusted-host 与 Linux 跨 UID;原生模型进程未验证 | P PASS;N BLOCKED | +| **C23** | 真实模型异常组合 | 真实拒绝分支、真实重试耗尽、实际中途 SIGKILL+恢复、两个真实模型会话竞争都有独立原生执行记录;不能复用脚本 fixture | 原 C02/C04/C05/C06 的 M/N 缺口 | M/N BLOCKED | +| **C24** | 安装兼容、证据与发布门禁 | Program ABI、SDK exports/types、CLI、Node 22/24 × Windows/Linux、docs/site/package、宿主原始工具调用来源;只有全部适用硬门禁通过才可宣称 M6 成功 | CI 7 jobs + C12/S13 | CI P 已历史通过;N BLOCKED | + +**裁决**:C13–C20 全部通过只代表复杂协议层 PASS,不等于 24 个真实模型场景全部通过。C21 跨版本迁移是增强项,不擅自升格为 P0-A 原硬门禁;C22–C24 的 N 缺口仍属于原 M6 S13 门禁。 + +## 2. 端到端风险族覆盖索引 + +| 风险族 | 关键验收 | 测试位置 | 判定要点 | +|---|---|---|---| +| 编译/SDK | 真正 Analyzer、TS/JS、固定 Stage/Graph、Program ABI、hash、打包/types/docs | compiler-front, ir, emitter, stage2-integration, stage2-program-cache, test:package, test:types, docs:examples | 不得将非法图降级通过,不能以模型代码冒充受信 Program | +| 条件与数据流 | 公开字段、Condition 真/假/缺失、串行 Agent、长链早停、预算等价 | C01/C02/C10/C11/C13/C19 + coordinator | 比较 State/Trace,不只比较 terminal status | +| 失败/重试 | Schema 错误、validator throw、Provider failure、retries+1、容量失败退出 | C03/C04/C09/C16/C19 + coordinator, host-oversize, s07-capacity | 无假 Ack、无无限 waiting、无错误数据合并 | +| 身份/幂等 | runId/requestId/attempt/startKey、重复/冲突回执、并发 | C06/C10/C15/C18/C20 + crash, store | State commit 至多一次,不保证外部模型执行恰好一次 | +| 原子性/恢复 | WAL/FULL、真 SIGKILL、Journal hash 链、回执索引、2 MiB hard、active 余量 | C05/C09/C10/C19/C21 + store, crash, s07-capacity | 崩溃可恢复、损坏 fail-closed | +| 安全/漂移 | Program/Skill/Policy hash、签名与租户、秘密披露、跨 UID | C07/C08/C14/C17/C22 + trusted-host, stage2, host-isolation | Host-only 密钥/DB、不可泄漏、不可被 Prompt 自报权限 | +| 模型/宿主 | 多回合、真实异常路径、原生来源及会话日志 | C01–C06/C12/C22/C23/C24 | P/M/N 分层,禁止用 CI 重放充当模型调用 | + +## 3. 判停和证据 + +每个测试必须记录场景编号、执行类别、模型调用次数、实际 Program 身份、Run/Request 身份(有时)、终态、Revision、Journal/Checkpoint 不变式和断言。敏感值、Host 私钥、数据库内容不得写入公开任务或日志。 + +测试通过标准:Ubuntu 24.04 / Windows 2025 × Node 22.13.0 / 24.22.1 的精确 HEAD 回归,另有 Ubuntu Node 22/24 OS 用户隔离、网站、打包、类型与 docs 消费。新提交取消的旧 CI 不得作为新 HEAD 证据。 + +阻断(不得合并 M6):伪造或越权 Ack、双 CAS 成功、State/Journal 不一致、未披露字段泄漏、合法输入无限 waiting、真实宿主来源证明缺失。故障属于 Core 时另开最小修复 PR,不得在验收分支悄然改动生产逻辑。 + +历史 C01–C12 结果:[2026-11-11 原报告](./interactive-complex-acceptance-results-2026-11-11.md)。M6 独立整体门禁继续按当前主线路线图与验收台账执行;历史 PR #7 已关闭且未合并,其适用测试由 PR #13 纳入 main。C13–C20 精确结果以本分支最新 GitHub Actions 为准,不继承 364/364 的历史测试计数。 diff --git a/docs/interactive-complex-acceptance-results-2026-11-11.md b/docs/interactive-complex-acceptance-results-2026-11-11.md new file mode 100644 index 0000000..5f64b4c --- /dev/null +++ b/docs/interactive-complex-acceptance-results-2026-11-11.md @@ -0,0 +1,86 @@ +# FlowKit Interactive P0-A 复杂验收 C01–C12 — 实测结果与缺口 + +**日期:2026-11-11。仓库:devcodex-labs/flowkit。** +**被测试分支:test/interactive-complex-c01-c12-acceptance。** +**历史测试基线 main:ea2bb10893a9be2e5de3561fc9a660a6977ae15c。** +**代码+真实 ChatGPT 结果 HEAD:abe592e888a4ea77357e31bd5d26ab14b4dac1dd。** + +**结论:复杂 P0-A 自动化回归通过,真实 ChatGPT 4 Agent / 5 回合的非原生工具桥接通过,Grok Build 等原生宿主 C12/S13 仍 BLOCKED。不是“C01–C12 全部完成”。** + +## 1. 精确提交 / CI 证据 + +- [GitHub Actions 42963110678](https://github.com/devcodex-labs/flowkit/actions/runs/42963110678):7/7 jobs completed / success,Windows 2025 与 Ubuntu 24.04 × Node 22.13.0 / 24.22.1 四组合各 **364 tests / 364 pass / 0 fail / 0 skip**,并且四个组合均输出了 C01_CHATGPT_RECORDED_RESPONSES_ACCEPTED 及 C01/C02/C03/C04/C10/C11 的 FLOWKIT_COMPLEX_ACCEPTANCE 结构化记录。 +- CI 同时覆盖打包、类型、Standard Regression、文档消费、网站构建以及两项 Linux Host 跨 UID 权限隔离。 +- 完成前的多轮 GitHub Actions 因 PR 新提交被取消,**不能**把它们作为精确最终 HEAD 的 7/7 通过证据;每个模型回合的原始 Pending / SHA 仍可在对应 Job 日志核查。 +- Program 代码来源:真正的 FlowKit Analyzer/Interactive Lowerer/Emitter,运行时使用实际生成的 JavaScript Program、默认 SQLite Store 与 Ed25519 Trusted Host;除真实模型桥接外,Agent 回答由脚本确定性提供,**模型调用数为零**。 + +## 2. 每项单独裁决 + +| ID | 协议/脚本验收 | 当前 ChatGPT 桥接 | 原生 Grok Build/Codex Host | 主要证据与限制 | +|---|---|---|---|---| +| C01 串行 4 Agent + 2 Conditions | **PASS** | **PASS(5 次真实模型输出)** | BLOCKED | 脚本 4 个签名回执、35 Journal;真实桥接 4 个不同 Agent / 5 回合,39 Journal,正常 succeeded | +| C02 动态条件与缺失值 | **PASS** | **PARTIAL** | BLOCKED | 脚本分别覆盖 ready=false 直接 declined、ready 缺失 FK_R002、reviewApproved=false 走 declined;真实桥接只覆盖 ready=true + reviewApproved=true | +| C03 Schema 拒绝与修复 | **PASS** | **PASS(受控故障注入)** | BLOCKED | 第一次 reviewApproved 为字符串,被校验拒绝;新 requestId、attempt=2、reasons 中出现类型错误;第二次真正布尔 true 成功,未污染已确认 State | +| C04 重试耗尽 | **PASS** | BLOCKED | BLOCKED | 两次错误结构导致 FK_R004;真实模型只执行了一次拒绝然后修正,没有执行真实持续失败 | +| C05 Host SIGKILL 恢复 | **PASS(既有真子进程)** | PARTIAL | BLOCKED | main tests/interactive/crash.test.mjs 覆盖 Pending 发布、接纳、校验、State 提交前后 SIGKILL;真实 ChatGPT 只在单次 CI 内重新打开 SQLite,并非模型执行中途 SIGKILL | +| C06 并发与重复回执 | **PASS(既有真子进程)** | PARTIAL | BLOCKED | main crash.test.mjs 双进程 CAS 竞争及 coordinator.test.mjs 丢响应恢复;真实 ChatGPT 连续提交不是并发模型会话 | +| C07 Program/Skill/Policy 漂移 | **PASS(脚本协议范围)** | PARTIAL | BLOCKED | main Stage2 policyHash 版本漂移、M5 Program 字节/策略篡改拒绝已测试;PR #13 已将 M6 S06 source/Program/Skill drift 脚本 Host 用例纳入 main。真实模型升级漂移与原生 Host 认证仍未测试 | +| C08 签名、隔离与泄漏 | **PASS(已有指定边界)** | PARTIAL | BLOCKED | main TrustedHost 错签、错租户、输入披露白名单与 Linux 跨 UID 密钥/SQLite 读取拒绝;未证明 Windows ACL 或原生 Grok Build 模型进程隔离 | +| C09 Checkpoint 容量 | **PASS(脚本)** | BLOCKED | BLOCKED | main s07-capacity / host-oversize:2 MiB hard、1.75 MiB active、可接受的 256 KiB 结果、不可接纳的合法回执原子退出;未认证 >2 MiB 拒绝;无真实模型大输出事件 | +| C10 36 Agent 长链 | **PASS(脚本)** | 非必需 | BLOCKED | SQLite Journal 255 条,近期回执缓存 16,全部 36 个 acceptedRevision 可恢复查询,最早回执重放 Ack 不变 | +| C11 budget=1 与 128 | **PASS(脚本)** | 非必需 | BLOCKED | 最终 State 哈希 sha256:e69d3bc210a864cab2801e1c479c5cbc6db7bf492e65256078bafc53be1f7c25,两个预算 Journal 分别 85 与 35 条,符合阶段预算造成不同迁移记录的预期 | +| C12 原生真实模型宿主 | **不适合脚本替代** | ChatGPT GitHub 桥接 PASS,但不满足此项 | **BLOCKED** | 缺 Grok Build / Codex 原生单会话模型→JS 工具→Pending→真实模型→Host 原生工具日志、独立来源认证与宿主隔离证据 | + +**判定用语说明**:PASS 只适用于该列所声明的执行类型,PARTIAL 为只覆盖部分冻结预期,BLOCKED 为缺真实环境或证据。不存在“364/364 = 364 个复杂真实模型用例”的推断。 + +## 3. 真实 ChatGPT 多节点工具桥接的确切边界 + +使用 [C01 真实工作流定义](../examples/interactive/c01-live-chatgpt.workflow.js),生成的不可变 Program 哈希: + +sha256:339425694337fdd24ff005b2d8eab3109d4026ce6bc176f7a8c130421a7cabb5 + +本次当前 ChatGPT 模型**按顺序**从 GitHub Actions 的 C01_LIVE_CHATGPT_PENDING 日志读取公开任务,提交到 tests/fixtures/c01-live-chatgpt-turns.json 的五次模型结果: + +| 回合 | 模型实际操作 | 提交 SHA | 条件/结果 | +|---|---|---|---| +| 1 | Requirements Analysis,写 requirementSummary | 1d515fe0fe9530e1a866a566e0bc122e6b834690 | 真实 Pending #1;成功 | +| 2 | Architecture Design,使用公开 requirementSummary 写 designSummary | 031eb4337a292eebcacafcf3cef27e48c2757e12 | 真实 Pending #2;成功 | +| 3 | Implementation Plan,使用公开 designSummary 写 executionPlan | f0fd5700d1856e05d437e54184b324f5c9d89286 | 真实 Pending #3;成功 | +| 4 | Independent Review,故意给错误类型的 reviewApproved | 7363bd1742fc94a99e05d7140d2e545d57145e04 | **受控错误注入**;校验拒绝,仅接纳回执身份,不写入业务 State,产生 attempt=2 | +| 5 | Independent Review,根据新的 reasons 修正 reviewApproved 布尔值 | abe592e888a4ea77357e31bd5d26ab14b4dac1dd | 新 requestId,成功,Condition 决定 succeeded/done | + +最终结构化日志字段:C01_LIVE_CHATGPT_COMPLETED,recordedModelResponses=5,status=succeeded,journalRevisions=39,retriesObserved=1,terminal node=done,模型公开任务未泄露 Host 私有 canary。每个 CI Job 在各回执间确实关闭/重开了自己的 SQLite 数据库,证明同一 Job 内物理数据库恢复及 Host 重建。 + +**必须防止三种误称:** + +1. 首轮 Actions 发布公开任务 → ChatGPT 通过 GitHub 工具提交 → 下一轮 CI **重新创建相同逻辑 Run**。两个 Actions 运行**没有**共用一个数据库,也不是单个原生模型 Agent Runtime 自主调用下一次模型。该模型输出只是从真实公开 Pending 派生,GH 提交身份不能代替模型原生会话证明。 +2. 第 4 回合是为了验证校验/重试特意注入错误类型,**不是**证明模型在自然任务中会稳定自行纠错。 +3. 四个 OS/Node 组合都运行同一份已保存的模型回答,不等于真实模型发起了 20 次独立调用。模型真实回答数是 **5**,CI 重放总次数另行统计。 + +## 4. 原有复用的代码证据 + +- C05:main [tests/interactive/crash.test.mjs](https://github.com/devcodex-labs/flowkit/blob/ea2bb10893a9be2e5de3561fc9a660a6977ae15c/tests/interactive/crash.test.mjs)。 +- C06:同一 crash.test.mjs 与 [coordinator.test.mjs](https://github.com/devcodex-labs/flowkit/blob/ea2bb10893a9be2e5de3561fc9a660a6977ae15c/tests/interactive/coordinator.test.mjs)。 +- C07:main Stage2 / M5 tests,以及 PR #13 纳入 main 的 tests/integration/m6-e2e.test.mjs S06 综合 source/Program/Skill drift 脚本测试。 +- C08:main trusted-host.test.mjs + .github/workflows/ci.yml Linux Host OS-user isolation。 +- C09:main s07-capacity.test.mjs 与 host-oversize.test.mjs。 +- C01–C04/C10/C11:本 PR 的 [complex-c01-c12.test.mjs](../tests/interactive/complex-c01-c12.test.mjs) 与 CI JSON 事件。 + +## 5. 独立验收裁决与下一步 + +**代码结果:** 尚未发现新的 P0-A Core 语义故障。C01–C04/C10/C11 新增协议脚本测试和真实 ChatGPT 五回合完整通过,且未提前实现未授权的 P0-B/P1/P2。 + +**不足:** C07 的脚本 Host 综合漂移已在 main,但真实模型 false 分支、C04 真实持续失败、C05 中途实际 SIGKILL + 模型、C06 并发模型会话以及 C08–C09 原生模型场景尚未覆盖。**C12/S13 必须继续 BLOCKED。** + +**建议:**将此验收分支继续保持 Draft 作为历史证据,不把固定五回合 fixture 作为可永久依赖的生产回归测试。下一步使用用户可授权的 Dot/Grok Build 原生宿主,在真实模型会话和明确 OS 可信边界下优先复验 C01/C03/C05/C08/C12(尤其原生模型来源证明);不因本次 CI 绿灯宣称 M6 已收口、发布软件或转入 P0-B。 + +完整的冻结验收目标详见 [C01–C12 矩阵](./interactive-complex-acceptance-c01-c12.md)。 + + +## Current-main follow-up — 2026-11-12 + +- Current main is 606d055d5f02451fea0b30294889d740ef370d49. The 2026-11-11 C01–C12 live-model and C13–C20 protocol evidence above remains historical evidence on the old baseline; it is not exact-current-main CI. +- PR #7 is closed without merge. Its applicable M6 source/Program/Skill drift coverage is present on current main through PR #13, including tests/integration/m6-e2e.test.mjs S06. +- PR #12 remains Draft and is six commits behind current main. After this branch is updated, rerun all seven CI jobs on the exact new PR HEAD before any merge decision. +- Current exact-main GitHub Actions is still blocked at scheduling: main run 43161214780 was cancelled; PR #20 exact-head run 43170215982 has seven queued jobs with no runner start. This is not a test failure and does not change the historical PASS results above. +- C12/S13 native Grok Build source/session attestation and model-process isolation remain BLOCKED. diff --git a/docs/interactive-full-delivery-roadmap-2026-11-11.md b/docs/interactive-full-delivery-roadmap-2026-11-11.md new file mode 100644 index 0000000..20de9e3 --- /dev/null +++ b/docs/interactive-full-delivery-roadmap-2026-11-11.md @@ -0,0 +1,378 @@ +# FlowKit Interactive — Full Delivery Roadmap & Closure Ledger + +**Baseline date:** 2026-11-11 (UTC+8) +**Repository:** `devcodex-labs/flowkit` +**Historical Phase 0 baseline:** `ea2bb10893a9be2e5de3561fc9a660a6977ae15c` +**Current main HEAD after PR #14:** `ae5fc5a42c67eb6e2c229d72ff4d9ac8e3aab3cd` +**Phase 2 design record:** merged via PR #14 (docs-only; formal contract conformance remains conditional) + +> This document records the approved end-to-end route from the current M6/P0-A state through P0-B, P1, P2 and production delivery. It is a roadmap and traceability ledger, not evidence that deferred features have been implemented or passed. + +## 1. Current baseline + +### 1.1 Main and CI + +- Current main HEAD after PR #14: `ae5fc5a42c67eb6e2c229d72ff4d9ac8e3aab3cd`. +- Post-merge main CI: [43132513659](https://github.com/devcodex-labs/flowkit/actions/runs/43132513659), **7/7 jobs successful** (Ubuntu/Windows × Node 22/24, Linux Host OS-user isolation × Node 22/24, website). +- PR #14 merge commit: `ae5fc5a42c67eb6e2c229d72ff4d9ac8e3aab3cd`; the merge adds only the reviewed Trusted Host V2 design/acceptance/review documentation. +- M1–M5 implementation surfaces and S07 capacity work exist on main. Their presence is not a blanket claim that every M6/native-host gate is passed. +- PR #8, #9 and #10 are merged. +- PR #7 is **closed without merge** as superseded. PR #13 is **merged** into main with current-main M6 tests, correct project-root discovery and successful exact-HEAD CI. +- PR #12 remains open Draft, head `c6b42a36050d885a37831cbdf0839428b2052c85`, based on `ea2bb10893a9be2e5de3561fc9a660a6977ae15c`. Its scripted C13–C20 tests and documentation CI passed; this does not pass C12/S13 native-host gates. +- PR #14 is **merged** as a docs-only conditional security design record. Merge does not authorize Phase 3 or claim Windows/Linux Secure Host, Grok native provenance, C12/S13 or M6 acceptance. + +### 1.2 Evidence classes + +- **P — Protocol/scripted:** real FlowKit compiler, generated Program, Host, SQLite and protocol; Agent responses may be scripted. Proves protocol behavior only. +- **M — Live model:** a real model session produces the result for an actual Pending. Requires per-turn request identity, response evidence and Host trace. +- **N — Native host attested:** native host tool invocation and session/process provenance are independently verified; OS/Host security boundary is demonstrated. +- **D — Deferred by approved scope:** intentionally belongs to a later milestone; it is not a defect in the current milestone, but must remain in the ledger. + +Never count P evidence as M or N. Never count a CI replay of recorded model turns as a new live model session. + +## 2. Complete milestone route + +| Phase | Scope | Required exit condition | Current state | +|---|---|---|---| +| 0 | Baseline freeze and traceability | Immutable baseline, full requirement/Finding/test ledger and evidence index | **Complete** — established in merged PR #13 | +| 1 | M6 current-main test compatibility | PR #7 tests run against frozen current main; no Core semantic change hidden in test repair | **PASS, merged** — PR #13; main CI 7/7; four matrix cells 369/369, Linux isolation/M6 e2e 11/11 on Node 22/24; no production Core change | +| 2 | Trusted Host security design and formal contract conformance | Approved threat model, OS identity, IPC, key custody, Store ownership, provenance and confirmed V0.3/V0.6-R5 conformance | **设计记录已合并**(PR #14;main CI 43132513659 7/7 PASS);F-01/F-R2-01 **CLOSED**;F-02/F-04 仍 PARTIAL。V0.3 Requirements 与 V0.6-R5 Technical Design 均已恢复并完成完整 SHA-256/包内清单校验;V0.6 Confirmed Handoff 9/9、Core POC Evidence 28/28 文件条目 PASS。现有核心 PoC 为 **22/22 测试 PASS、9/9 进程级崩溃点恢复**,但仅为 Linux/Node 22.16 独立原型;POC-01/05/06/09 PARTIAL,POC-07 Human BLOCKED,POC-08 完整矩阵 BLOCKED。F-07 的原件缺失问题已关闭,正式契约符合性审查进入 **IN-PROGRESS**;在完成 T01–T34、R01–R22、S01–S14 及当前实现映射前,Phase 2 不宣称 conformance PASS。参见 [正式契约原件登记](./interactive-phase2-formal-source-register.md) 和 [V0.3/V0.6 符合性审查计划](./interactive-v03-v06-conformance-audit-plan.md) | +| 3 | Trusted Host security implementation | Windows and Linux isolation, access-denial tests and secure Host operations pass | Pending | +| 4 | Native Grok Build JS tool bridge | Grok Build itself calls FlowKit JS tools through Pending → model completion → JS submit/resume → Terminal | Pending | +| 5 | M6 live-model complex acceptance | Applicable C01–C24 cases classified and evidenced at P/M/N levels; all M6 blockers closed | Pending | +| 6 | M6 independent review and P0-A closure | Dot review, full CI, PR integration, all P0-A hard gates passed | Pending | +| 7 | P0-B sequential semantics | Action, Verification, Human, Loop, Cancel and context/result semantics pass S08–S12 and applicable T requirements | Deferred | +| 8 | P1 Parallel all_succeeded | Dedicated fail-fast/parallel contract, recovery and concurrency acceptance pass | Deferred | +| 9 | P2 Parallel any | Winner election, loser cancellation, late results and recovery pass | Deferred | +| 10 | Production compatibility and release | Host matrix, SDK/types/CLI/package/docs/performance/security/release gates all pass | Deferred | + +### 2.1 Phase 2 formal contract source gate + +PR #14 is merged as a **conditional security design record**. The main post-merge CI passed 7/7. Both confirmed source originals have been recovered and verified: Requirements V0.3 is 56,883 bytes, SHA-256 `3a0adc3edb41869a6309bae29ec5496df00970c815f8dc16330a90cbf26e703e` (13/13 Grok handoff manifest entries PASS); Technical Design V0.6-R5 is 114,621 bytes, SHA-256 `09f3a5698e238d10b80efb8e26d1c41e51b663ca620efa70815dd7b52f2106ba` (9/9 Confirmed Handoff SHA256SUMS entries PASS). The Core POC Evidence archive is SHA-256 `a6b5201e8327463b57700174297bf986fbc4902c6ef6493710f1e8cac021f178`, with 28/28 manifest entries PASS. Its final Node TAP is 22/22 PASS and crash matrix 9/9, but this is a limited Linux x64 / Node 22.16 independent prototype, not production FlowKit implementation or full acceptance. POC-01/05/06/09 remain PARTIAL, POC-07 Human BLOCKED, POC-08 full matrix BLOCKED. The source-missing portion of F-07 is closed; clause-by-clause conformance is now **IN-PROGRESS**, not PASS. Phase 2 remains open until that review is completed. + +The exact source names, full hashes, archive evidence and audit procedure are registered in [interactive-phase2-formal-source-register.md](./interactive-phase2-formal-source-register.md). The next sequence follows V0.6-R5 §18 and the approved POC plan: preserve existing PoC results; run only the targeted follow-up experiments for PARTIAL/BLOCKED items; produce the module-level P0-A implementation plan and acceptance order; then obtain explicit user authorization before production code changes. H3-01/H3-05 OS-only work may proceed independently, while H3-02/03/04 and live Grok acceptance retain their own gates. + +### 2.2 Approved-source and PoC audit sequence + +The next work follows V0.6-R5 §18 and `APPROVAL-AND-POC-PLAN.md`: (1) recover/verify source originals — **complete**; (2) execute core-mechanism PoC — **limited PoC exists; remaining PARTIAL/BLOCKED gaps are the target**; (3) use actual evidence to prepare the P0-A module-level implementation plan, risks/dependencies and acceptance order; (4) obtain explicit user authorization before production code changes/PRs. + +The detailed review sequence and stop rules are in [interactive-v03-v06-conformance-audit-plan.md](./interactive-v03-v06-conformance-audit-plan.md). The existing 22/22 PoC must not be rerun as if it were the entire POC-01–09 suite, and must not be promoted to Windows, live-model, Human or full T01–T34 acceptance. + +## 3. Phase 0 — Baseline and traceability + +1. Freeze main SHA, PR heads, dependency lock hash, Node/npm versions and CI run IDs. +2. Maintain a separate code branch for each implementation/fix; do not edit main directly. +3. Build a traceability index connecting T01–T34, S01–S14, C01–C24, N1–N13, R1/R2, R01–R22, source files, tests, CI jobs and evidence. +4. Every item has a state: PASS, FAIL, PARTIAL, BLOCKED or DEFERRED. +5. Every BLOCKED item must state the missing prerequisite, owner and re-entry condition. +6. Every DEFERRED item must name its target phase; it cannot disappear from the ledger. +7. Every PASS must cite an exact commit, executable test or raw evidence. A PR description alone is not evidence. + +## 4. Phase 1 — M6 current-main test compatibility + +1. Rebase/recreate the M6 test changes from current main, rather than relying on the stale PR #7 base. +2. Fix the temporary project fixture so the project-root contract is tested correctly: + - create a temporary `package.json` and verify default ancestor discovery; and + - separately verify explicit `projectRoot` override. +3. Include `tests/integration/*.test.mjs` in the normal test command. +4. Run the M6 cross-process tests and Linux OS-user isolation job. +5. Run build, full `npm run verify`, docs examples and website checks. +6. Keep the worker explicitly labelled scripted; do not present it as Grok/Codex/Claude provider integration. +7. If a production Core defect is found, create a separate minimal Core-fix PR and rerun relevant regressions. +8. Do not merge the old PR #7 or the replacement PR until review and exact-HEAD CI pass. + +## 5. Phase 2 — Trusted Host security design + +Freeze a Host threat model before implementation: + +- Assets: Host signing key, SQLite DB/WAL/SHM, Program allowlist, policy/binding manifests, private State, provider/session evidence. +- Principals: user, Grok Build/model process, Host process, other local processes, other tenants, CI and administrators. +- Trust boundaries: OS identity/token, filesystem ACL, IPC endpoint, provider/session transport and Host signer. +- Attack paths: same-user process, child process, shell/tool execution, path traversal/symlink, DB/WAL access, key extraction, IPC spoofing/replay, stale request, cross-tenant run, policy drift and log leakage. +- Host owns Program verification, authorization, private Store, signer and receipt acceptance. +- Model receives only the minimum public task and submits result data through a constrained operation. It never receives the signer, raw Store, Coordinator or unrestricted Host API. +- Define Windows and Linux deployment profiles separately. Linux UID isolation is not evidence of Windows ACL isolation. +- Define what can and cannot be proven about native provider/session origin. A self-declared session ID or Host signature over unverified model JSON is not provider-origin proof. +- Provider-signed evidence may be used if genuinely available, but is not assumed to be the only possible design. Any alternative Host-owned transport must state its exact assurance level and limits. +- Human approval remains unavailable/blocked unless a model-unwritable trusted human decision channel is independently verified. + +Deliverables: threat model, trust topology, candidate-ingress security contract (with optional authenticated listening IPC only when needed), key custody, provider/session evidence design, Windows/Linux profiles, attack matrix and acceptance gates. Independent review must close all **applicable design-blocking** findings before Phase 3. Missing formal V0.3/V0.6-R5 originals remain an explicit conformance BLOCKED item, not evidence that they never existed; OS capability PoCs can be performed separately, but no final conformance claim can be made without them. + +## 6. Phase 3 — Trusted Host security implementation + +1. Implement distinct OS identities or an equivalently strong restricted token boundary. +2. Restrict Host private directory, key and SQLite DB/WAL/SHM to the Host principal. +3. Validate all **actual model-candidate ingress paths** with explicit authorization and bounded I/O. Do not introduce a TCP/mTLS/Named Pipe listener just to satisfy a checklist: in a no-listener deployment record `NO_LISTENER` and test the real MCP/anonymous-pipe/tool-input path instead. +4. Ensure the model cannot access raw DB, key, signer, Coordinator or private State. +5. Add Windows ACL and Linux UID/GID integration tests. A test that exits because the platform is unsupported is BLOCKED, not PASS. +6. Test read, write, replace, symlink, path traversal, process spawning, inherited handles/environment, IPC spoofing, replay, wrong tenant and unauthorized API calls. +7. Verify Host can still sign, commit, resume and recover after model access is denied. +8. Record raw OS identity, access-denied evidence, process tree, IPC identity and Store integrity. +9. Repeat after reboot/restart and on the supported Node matrix. + +Entry: independent Phase 2 security design approved, with frozen OS principals, Host secrets/ACL and no insecure default Node Named Pipe; Grok native tool evidence is **not** required for H3-01/H3-05 OS-only isolation. H3-02 implementation requires only a separately documented Grok tool-registration and signed-before-completion input/timing **capability PoC**, which is **not** a formal NAT-14 PASS. + +Exit: **both Windows and Linux** Secure Host Profiles pass their applicable P0 OS/handle/active-ingress/signing/Outbox tests. H3-05 OS-only tests depend only on H3-01; H3-04 adapter integration adds active-ingress and recovery checks later. Without a listening endpoint, prove its absence and test the actual candidate-input mechanism, rather than demanding mTLS. One platform PASS only proves that platform. NAT-14 is officially tested in Phase 4; C12/S13 N remains BLOCKED until native model evidence is complete. + +## 7. Phase 4 — Native Grok Build JavaScript bridge + +1. Use direct Grok Build as the first native host; ACP is optional and not a prerequisite. +2. Verify the real Grok Build process/session and available JS execution/tool interface. +3. Register or expose the FlowKit JS tool through the supported native tool mechanism; do not substitute file polling or an external harness that advances every step. +4. Freeze tool operations and schemas (names to be reconciled with the existing SDK contract): start, get Pending, submit result, resume, status and result. +5. The Host owns Program execution, Pending creation, disclosure, result verification, signing, CAS and SQLite. +6. Grok Build must call the JS tool, inspect the returned Pending, perform the real Agent work, call the JS tool to submit, and call again to continue until Terminal. +7. Preserve raw native tool invocation events and bind them to runId/nodeId/requestId/attempt/programHash/response digest and Host acceptedRevision. +8. Verify that a model cannot forge Host provenance by editing a session ID, JSON file or self-asserted receipt. +9. Keep provider-origin assurance distinct from Host-observed process/transport assurance; document any residual limitation. +10. Do not implement a Grok-only workflow semantic. The adapter is host-specific; FlowKit Core remains model-neutral. + +Exit: repeatable native tool loop, Host-controlled transitions, secure process boundary and independently reviewable evidence. + +## 8. Phase 5 — M6 complex live-model acceptance + +- Preserve the frozen C01–C24 definitions and original pass/fail criteria. +- For each scenario, declare required evidence tier P, M or N. +- Reuse existing P-level tests; do not waste model calls repeating scripted cases. +- Live-model tests must include real Pending, model output, request identity, tool trace and Host acceptance. +- Native-host/security tests require N evidence; a real model answer alone is not enough. +- Prioritize C04, C05, C06, C07, C08, C09, C12, C22 and C23. +- C05: kill/restart around Pending publication, model completion, receipt acceptance, validation, State commit and next Pending; resume the same Run. +- C06: two independent live model sessions contend for the same Pending; identical replay and conflicting result cases. +- C04: separate controlled invalid-output injection from naturally generated invalid model output and retry exhaustion. +- C09: distinguish unverified oversized receipt, trusted provider output overflow and valid receipt causing Checkpoint capacity overflow. +- C22: prompt injection and attempts to read/modify Host assets. +- C23: a combined multi-Agent Run with failure, retry, Host crash/recovery and competing submitters, with exact state/ledger invariants. +- C21 cross-OS/Node DB migration remains an enhancement unless the approved contract elevates it; N6 durable artifact/SDK upgrade compatibility must still be closed before long-running production delivery. + +Exit: every C item is PASS, FAIL, PARTIAL, BLOCKED or explicitly DEFERRED with owner and re-entry condition. No missing row. + +## 9. Phase 6 — M6 review, P0-A closure and merge + +1. An **independent reviewer** (local Grok Build or another separate reviewer; Dot is not mandatory) checks exact code SHA, Program hash, raw native tool events, Pending/response, Host traces, signed statements, SQLite, Journal hash chain and OS evidence. +2. Run exact-HEAD CI: Ubuntu 24.04 and Windows 2025 × Node 22.13.0 and 24.22.1. +3. Run Linux cross-UID and Windows ACL isolation jobs independently. +4. Run build, npm verify, package/tarball, types, CLI, docs examples, website and Standard regressions. +5. Resolve every M6 blocker and rerun the relevant scenario plus full regression. +6. PR #12 may be merged only as the scripted matrix/docs contribution after its own review; its merge does not pass C12/S13. +7. PR #7 was closed without merging and its current-main replacement PR #13 was merged after independent code review and CI; do not merge it again. Future M6 native-host security evidence remains in PR #14 and follow-up scoped implementation PRs. +8. Do not release a version until P0-A release criteria are explicitly met. + +## 10. Phase 7 — P0-B sequential workflow + +Before implementation, close applicable design findings and freeze semantics for: +- N2: kind-specific Pending with model-visible task and trusted executor context. +- N4: Human provenance; model-generated human approval is rejected. +- N7: stable host-visible terminal result/state API. +- N13: Standard/Interactive semantic-difference register. +- N5: namespace/tenant isolation. +- N6: immutable artifact retention and SDK/semantics-version upgrade policy. +- N8/N9/N10/N11/N12 as applicable to Program ABI, feature gate, package contract, Agent Pending parity and transition/reconcile identity. + +Implement and validate in this order: +1. Action Patch and writes allowlist. +2. Action receipt loss, indeterminate and reconcile with stable operationId. +3. Verification true/false/invalid and candidate-state semantics. +4. Action → Verification → onFailure paths and contextError persistence. +5. Human reject → revise → approve with trusted human provenance. +6. Loop iteration limits, final failure and restart recovery. +7. Skill pinning/drift behavior. +8. Cancel, terminal race and late receipt rejection. +9. Stable terminal result and contextError reads. +10. Standard/Interactive T33 row-by-row parity/difference review. + +Exit: applicable T15–T23, S08–S12, T31/T32 and T33 checks pass. Any unsupported host Human channel stays BLOCKED, not silently simulated. + +## 11. Phase 8 — P1 Parallel all_succeeded + +Before code, separately approve the P1 contract and fail-fast semantics (historical TD-017): +- multiple Pending creation and durable branch identity; +- branch scheduling and dispatch barriers; +- all_succeeded aggregation; +- one branch failure and other in-flight branch behavior; +- stop-new-dispatch versus already-dispatched work; +- state write-set conflict and merge policy; +- duplicate/late receipts; +- crash/restart with partially completed branches; +- external side-effect reconciliation (no false exactly-once or rollback claim). + +Implement Program/IR, Coordinator, Store and Host support, then test independent branches, simultaneous success/failure, CAS races, recovery and Standard parity. Do not implement P2 any semantics in this phase. + +## 12. Phase 9 — P2 Parallel any + +Freeze and implement separately: +- deterministic winner election when multiple branches succeed; +- atomic terminal CAS; +- loser cancellation and already-running side effects; +- late result rejection/ack semantics; +- write conflicts and winning-branch state; +- crash/restart during winner election and cancellation; +- external side-effect reconciliation. + +Do not infer any semantics from all_succeeded or claim that cancelling a local process rolls back an external operation. + +## 13. Phase 10 — Production compatibility and release + +1. Host adapters: ChatGPT, Codex, Claude Code and Grok Build, each with a declared capability/limitation matrix. +2. Core remains model/host neutral; adapters do not change workflow semantics. +3. Verify SDK JS API, TypeScript declarations, exports, CLI, package/tarball and supported Node versions. +4. Verify immutable Program ABI, artifact storage, resume after SDK update and semantics-version compatibility. +5. Complete install, quick-start, DSL, Agent, Host, security, recovery, Action/Human/Loop/Parallel, error and API docs. +6. Verify docs against source and executable examples. +7. Run performance/stability tests: long chains, many Runs, concurrency, WAL/FULL, Checkpoint size, Journal growth, recovery loops and large outputs. +8. Complete security review, changelog, SemVer, tag, npm release, website and release notes. +9. Re-run installation/upgrade verification against the published artifact. + +## 14. Global traceability ledger + +### 14.1 Frozen requirement and acceptance identifiers + +| Family | Coverage | Closure rule | +|---|---|---| +| T01–T14 | P0-A core semantics | P0-A hard gates | +| T15–T23 | P0-B sequential semantics | P0-B hard gates | +| T24, T26 | P1 parallel all_succeeded/concurrency | P1 hard gates | +| T25 | P2 parallel any | P2 hard gates | +| T27–T34 | host, process, compatibility, parity and long-run requirements | assigned to the relevant phase; no silent omission | +| S01–S07 | P0-A protocol/security/capacity | P0-A | +| S08–S12 | P0-B Action/Verification/Human/Loop/Cancel | P0-B | +| S13 | native host loop | Phase 4 / M6 | +| S14 | package, types, CI, compatibility and Standard regression | every release gate | +| C01–C24 | current complex acceptance matrix | Phase 5, tier P/M/N explicit | +| N1–N13 | design review findings | closed or explicitly deferred before dependent phase | +| R1/R2, R01–R22 | historical review/falsification cases | preserved as regression and review evidence | + +### 14.2 Individual T01–T34 requirement ownership + +| ID | Frozen requirement scope | Owning phase | Required closure evidence | +|---|---|---|---| +| T01 | Pure Condition → Terminal | P0-A | true/false/missing input; deterministic FK_R002 terminal; no infinite resume | +| T02 | Agent, Skill and missing inputs | P0-A | Skill resolution, declared inputs, missing-input behavior and Standard Pending parity | +| T03 | Wait, kill process, new process completes same Run | P0-A | real process restart, same Run/request identity, durable completion | +| T04 | Invalid Schema and missing/failed validator | P0-A | invalid output rejected, retry/terminal semantics, validator contract failures | +| T05 | Old requestId, wrong node, attempt or runId | P0-A | stale/forged identity rejected before mutation; namespace boundary also closes N5 | +| T06 | Duplicate receipt idempotency and conflict | P0-A | same receipt returns original Ack; conflicting content rejected; cross-process CAS evidence | +| T07 | Source, Program, compiler and policy drift | P0-A + production upgrade | drift fails closed; immutable artifact retained; SDK upgrade/resume policy closes N6 | +| T08 | Invalid JSON, prototype keys and capacity limits | P0-A | canonicalization negatives, 2 MiB hard limit, active reserve and no-mutation rejection | +| T09 | Two-process CAS | P0-A | actual independent processes contend; only one transition commits | +| T10 | Unsupported nodes/configuration preflight | P0-A | unsupported feature/nodes rejected before execution; Program ABI and featureStage rules close N3/N9 | +| T11 | Host has no JavaScript executor | Host | explicit NO_JS_EXECUTOR/BLOCKED; real native tool loop belongs to S13 | +| T12 | Standard regression, AOT, TypeScript, SDK and error codes | All / release | Standard regressions, exports, types, package and error-code contracts | +| T13 | retries:2 means at most three attempts | P0-A | attempt count, new request identity and no extra dispatch | +| T14 | Retry exhaustion preserves FK_R004 | P0-A | terminal errored state, original cause and no invalid State commit | +| T15 | Three-stage verification and candidate state | P0-B | checkStep/verify views, candidate merge and restart behavior; N2 | +| T16 | Action Patch and writes allowlist | P0-B | allowed writes applied; unauthorized writes rejected; deletion semantics documented | +| T17 | Action succeeds but receipt is lost | P0-B | indeterminate/reconcile with stable operationId; no second execute side effect | +| T18 | Action → Verification and onFailure path | P0-B | contextError remains visible to trusted executor and terminal result; N2/N7 | +| T19 | Verification true/false/invalid | P0-B | all outcomes, candidate State and invalid verifier behavior | +| T20 | Human reject → revise → approve and prefix | P0-B | model cannot forge human decision; only independently verified human channel accepted; N4 | +| T21 | Loop 1/3 iterations and final failure | P0-B | iteration count, max-exceeded route and restart parity | +| T22 | Skill pinning and drift | P0-B + production upgrade | exact Skill identity, drift rejection and durable resume policy | +| T23 | Cancel, late result and control signals | P0-B | cancellation/terminal race, idempotent cancel and stale receipt rejection | +| T24 | Parallel all_succeeded / multiple Pending | P1 | separately approved fail-fast contract, branch state, concurrency and recovery | +| T25 | Parallel any | P2 | winner election, terminal CAS, loser cancellation and late results | +| T26 | Out-of-order/duplicate results and write conflicts | P1 | branch ordering, merge policy, duplicate handling and concurrent conflict recovery | +| T27 | Real host closure | Host / M6 | native JS tool log: Pending → model completion → JS submit/resume → Terminal; S13 | +| T28 | Source/artifact/package/types/docs consistency | All / release | reproducible compiler artifacts, package/tarball, types, CLI and executable docs | +| T29 | Kill before/after every Agent phase | P0-A, P0-B, P1 and P2 | process-kill matrix with same Run, exact Pending/operation identity and durable state | +| T30 | Forged Cursor, receipt or identity | All | tamper/replay/wrong-tenant/wrong-Program negative cases; no unauthorized mutation | +| T31 | Non-pure validator and double complete | P0-A/P0-B | pure-local contract enforced; concurrent complete has one durable outcome | +| T32 | External target without idempotency | P0-B/P1/P2 | indeterminate/reconcile policy; no false exactly-once or rollback claim | +| T33 | Standard vs Interactive comparison | All milestones | row-by-row semantic parity/difference register; no undocumented divergence | +| T34 | Long-running workflow, budgets and privacy | P0-A through production | bounded Checkpoint/Journal, recovery, disclosure minimization, capacity exit and stress evidence | + +### 14.3 Individual S01–S14 acceptance ownership + +| ID | Frozen acceptance scope | Owning phase | Closure evidence | +|---|---|---|---| +| S01 | Pure Condition → Terminal | P0-A | correct terminal, missing input becomes persistent errored state, repeated resume is stable | +| S02 | Single Agent, retries and process-cut matrix | P0-A | publish/accept/commit cut points; request identity and Pending match Store | +| S03 | Repeated invalid output and validator contract | P0-A | retries+1, FK_R004 and deterministic validator failure exit | +| S04 | Two processes complete same request | P0-A | same receipt idempotent; different receipt conflict; uncertain CAS reconciled | +| S05 | Identity and transition rejection paths | P0-A | wrong namespace/run/request/attempt/block and corrupt Store rejected without mutation | +| S06 | Source/Program/Skill/Policy drift | P0-A | pinned bytes and policy mismatch fail closed before dispatch | +| S07 | JSON adversarial inputs, Checkpoint capacity and stage budget | P0-A | canonical JSON, hard/active limits, byte-equal terminal State for budget 1/128 | +| S08 | External checkStep/verify and candidate views | P0-B | Standard-equivalent trusted executorContext and restart at each boundary | +| S09 | External Action, writes and reconcile | P0-B | Patch allowlist, receipt loss, stable operationId and no duplicate execute | +| S10 | Action → Verification → onFailure and contextError | P0-B | verifier/notify context and terminal contextError match frozen semantics | +| S11 | Human reject/revise/approve and Loop limit | P0-B | forged human submission rejected; trusted human provenance; exact loop count | +| S12 | Cancel and late receipt | P0-B | CAS winner, repeated cancel idempotency, stale receipt rejection | +| S13 | Native real-host JS tool loop | Phase 4 / M6 | raw native tool logs and independently verified Host/session/process provenance | +| S14 | Four CI cells, package, types, SDK and Standard regression | Every release gate | exact-HEAD verify, exports/types/tarball/docs and supported OS/Node matrix | + +### 14.4 C01–C24 complex acceptance ownership + +The frozen detailed matrices remain the source of exact test steps and assertions: +- [C01–C12 frozen matrix and evidence rules](https://github.com/devcodex-labs/flowkit/blob/test/interactive-complex-c01-c12-acceptance/docs/interactive-complex-acceptance-c01-c12.md) +- [C13–C24 extension and evidence tiers](https://github.com/devcodex-labs/flowkit/blob/test/interactive-complex-c01-c12-acceptance/docs/interactive-complex-acceptance-extension-c13-c24.md) +- [C13–C24 results report](https://github.com/devcodex-labs/flowkit/blob/test/interactive-complex-c01-c12-acceptance/docs/interactive-complex-acceptance-c13-c24-results-2026-11-11.md) + +| ID | Frozen scenario | Evidence tier / owner | +|---|---|---| +| C01 | Four serial Agents and two Conditions | P + M; Phase 5 | +| C02 | Dynamic routes, true/false and missing-condition errors | P + M; Phase 5 | +| C03 | Schema rejection, retry and valid repair | P + M; Phase 5 | +| C04 | Invalid output and retry exhaustion | P + M; Phase 5 | +| C05 | Host crash at Pending/accept/validate/commit boundaries | P + M/N; Phase 5 | +| C06 | Two-process CAS, lost response and duplicate/conflicting receipts | P + M/N; Phase 5 | +| C07 | Source/Program/Skill/Policy drift | P + N; Phase 3/5 | +| C08 | Permissions, signature verification and data leakage | P + N; Phase 3/5 | +| C09 | Checkpoint capacity, oversize and bounded receipt behavior | P + M/N; Phase 5 | +| C10 | Long Agent chain and durable receipt ledger | P; Phase 5 | +| C11 | Stage-budget 1/128 terminal State byte parity | P; Phase 5 | +| C12 | Native Grok/Codex host and JS tool loop | N; Phase 4/5 | +| C13 | Branch truth table × stage-budget cross product | P; Phase 5 | +| C14 | Forged, stale, tampered and oversized receipts during a live Pending | P; Phase 5 | +| C15 | Historical Ack replay after Host rebuild and Terminal | P; Phase 5 | +| C16 | Trusted provider-call failure and bounded retry | P + M/N; Phase 5 | +| C17 | Host policy/binding drift mid-chain | P; Phase 5 | +| C18 | Interleaved Runs, idempotent start and conflicting start intent | P; Phase 5 | +| C19 | 36-Agent chain failing at Agent 18 with fail-fast | P; Phase 5 | +| C20 | Two different valid signed results competing for one CAS | P; Phase 5 | +| C21 | SIGKILL and same physical DB recovery across Node/OS versions | P enhancement; Phase 10 unless approved as a hard gate | +| C22 | Prompt injection and attempts to claim Host authority | P + M/N; Phase 3/5 | +| C23 | Combined live-model failure, crash/recovery and competing submitters | M/N; Phase 5 | +| C24 | Compatibility, package, docs and native-host release gate | P + N; Phase 6/10 | + +All C rows remain individually classified PASS/FAIL/PARTIAL/BLOCKED/DEFERRED in the detailed matrix. This summary table does not replace their individual raw evidence. + +### 14.5 Finding ownership + +| Finding | Planned closure | +|---|---| +| N1 | verify current P0-A deterministic-error exit; fix if any discrepancy | +| N2 | Phase 7 P0-B kind-specific Pending/executor context | +| N3 | verify current Program ABI/InteractiveRuntime; close any remaining contract gap before P0-A release | +| N4 | Phase 7 Human provenance; shell-only model approval remains blocked | +| N5 | Phase 7 tenant/namespace boundary before production multi-tenant use | +| N6 | Phase 7 durable artifact and SDK upgrade/resume policy | +| N7 | Phase 7 public host-visible terminal result | +| N8 | verify generated Program zero-import rule before release | +| N9 | verify unsupported featureStage rejection before release | +| N10 | package/bin/exports checks before release | +| N11 | Agent Pending parity before P0-A final compatibility sign-off | +| N12 | parallel/transition/reconcile identity before P1 | +| N13 | Standard/Interactive difference register before P0-B parity sign-off | + +This assignment is a tracking map, not an assertion that the finding is already fixed or that a new API is approved. Any requirement-stage change requires the user's approval. + +### 14.6 Global release rule + +No stage is complete while any applicable requirement, acceptance case, historical Finding or regression remains unclassified. Every unresolved item must be either: +- fixed and re-tested; +- BLOCKED with concrete missing evidence and re-entry condition; or +- DEFERRED to an approved later phase with a named milestone. + +A phase cannot pass by deleting a test, weakening an assertion, changing the evidence tier or combining results from different commits. + +## 15. Current next actions + +1. Phase 0/1 **complete**: PR #13 merged to main `9bac8f86347a405efe068f96423ae3e501005061`; old PR #7 closed unmerged. +2. Main CI [42998521531](https://github.com/devcodex-labs/flowkit/actions/runs/42998521531): 7/7 successful; Ubuntu/Windows × Node 22/24 each 369/369; Linux OS isolation + M6 11/11 on both Node versions. +3. Phase 2 security design record (still Draft/Conditional): [Trusted Host V2](./interactive-trusted-host-security-v2-design.md), [threat model](./interactive-trusted-host-security-v2-threat-model.md), [acceptance matrix](./interactive-trusted-host-security-v2-acceptance.md), [review ledger](./interactive-trusted-host-security-v2-review.md). Third-round Grok review closed F-01/F-R2-01; F-02/F-04 remain PARTIAL and F-07 remains BLOCKED. +4. Three rounds of Grok Build independent review have been completed, including the final targeted review of F-01/F-R2-01. Phase 2 is **not yet frozen**: F-02 native tool binding, F-04 cross-principal handle isolation and F-07 formal contract provenance remain open/BLOCKED. Draft/CI success is not a substitute for native Host security acceptance or a professional third-party security certification. +5. Then implement OS isolation, trusted bridge, direct Grok Build tool loop, real-model edge cases and Phase 6 M6 closure. PR #12 remains Draft until its own scoped review. + +**Formal contract source caveat:** Historical FlowKit requirement V0.3 and technical design V0.6-R5 are cited in previous handoffs, but their full immutable bodies/SHA-256 are not presently available to this PR's independent reviewer. Conformance to those documents remains **BLOCKED** until the exact originals can be independently read and hashed; roadmap IDs cannot replace official text. + +**No direct main modification or release is authorized. Merge only through a reviewed PR after exact-HEAD CI. This test/roadmap PR does not authorize production semantic changes or declare M6 complete.** diff --git a/docs/interactive-phase2-conformance-audit-r1-20261112.md b/docs/interactive-phase2-conformance-audit-r1-20261112.md new file mode 100644 index 0000000..bcc5c6f --- /dev/null +++ b/docs/interactive-phase2-conformance-audit-r1-20261112.md @@ -0,0 +1,152 @@ +# FlowKit Interactive — 正式契约符合性审查 / R1 证据矩阵(阶段性) + +> **阶段性审计,不是最终 PASS。** 基于当前 main 的已查源码和测试定义,结合已验证的正式原件自动提取 T01–T34、R01–R22、S01–S14。尚未逐项重跑每个反例/进行真实宿主安全验证;不得把存在测试文件或原型通过解释为完整验收。 + +## 0. 证据固定 + +- 当前审查代码基线:`ae5fc5a42c67eb6e2c229d72ff4d9ac8e3aab3cd`(PR #14 已合并,PR #15 当时尚为 Draft)。 +- V0.3 原件:56,883 bytes / SHA-256 `3a0adc3edb41869a6309bae29ec5496df00970c815f8dc16330a90cbf26e703e`。 +- V0.6-R5 原件:114,621 bytes / SHA-256 `09f3a5698e238d10b80efb8e26d1c41e51b663ca620efa70815dd7b52f2106ba`。 +- V0.6-R5 §15 的「当前实现全部 NOT-EXECUTED」是 2026-11-10 的历史设计状态;不能直接当成当前 main 的状态。 +- GitHub CI 证据:PR #14 合并后 main run [43132513659](https://github.com/devcodex-labs/flowkit/actions/runs/43132513659) 全 7 Jobs 成功;CI 是实际脚本/类型/包/文档回归,而不是原生 Grok 来源证明。 + +## 1. 高优先级初步发现(原文 ↔ 当前 main) + +**CF-01 / P1 / P0-A adminTerminate 未发现实现。** V0.6-R5 §4(第 226–243 行)及 §16(第 1030–1035 行)明确将受权 `adminTerminate` 和独立 Host Admin Controller 放入 P0-A。2026-11-10 已确认实施计划 M5 的 `A-M5-02` 也要求 Admin tombstone/审计和竞态测试。当前 `src/interactive/index.mjs` 只公开 `compileInteractive/loadInteractiveProgram/createSqliteCheckpointStore/createInteractiveCoordinator/createTrustedInteractiveHost` 及 Host statement helper;`src/interactive/coordinator.mjs` 的闭包 API 仅包含 start/complete/resume/load;`types/interactive.d.ts` 未声明 adminTerminate。必须在不接收模型自报管理员 JSON 的前提下核验真实 admin channel、tombstone、已终态竞争和旧 Program 缺失管理出口。**结论:代码层高置信缺口;不能把 S12-A/M5 完成判为 PASS。** + +**CF-02 / P1(待契约裁决) / P0-A 公开 SDK 形态不一致。** V0.3 §7.1(第 250–274 行)与 V0.6 §4(第 226–255 行)描述 `@devcodex-labs/flowkit/interactive` 公共 `start/complete/resume`;当前 `src/interactive/index.mjs` 导出的是 `createInteractiveCoordinator()` / `createTrustedInteractiveHost()` factory,调用签名和公开能力不同。当前 `package.json` 确实提供 `./interactive` 子路径但不是冻结示例中的命名函数。可能是后续获批的 Host-only 封装调整,尚无当前证据证明这种签名变更被正式批准;建议独立核验变更记录,若无批准则列为契约冲突候选,不直接改源码或改旧需求。 + +**CF-05 / P1(契约差异与披露风险,未证实模型泄漏) / Host/Public Result 投影。** V0.6-R5 §4 明确“所有公开返回帧不含完整 State”,§5 的 `InteractiveFrameV1` 仅携带 `resultRef`,而完整 State 应通过独立授权的 `readTrustedTerminalResult` 获取。当前 `src/interactive/coordinator.mjs#L140-L150` 的 `asPublicFrame(cp)` 在任何终态直接加上 `{state: cp.state}`,`types/interactive.d.ts#L83-L93` 也允许 `state?`;`createInteractiveCoordinator` 与 `createTrustedInteractiveHost().load/resume/start` 经公开 `./interactive` 导出后可返回该帧。`getPublicAgentTask` 仍有明确最小披露验证,因此**不能由此断言模型已经看到私有 State**;但 Host-visible 与 model-facing 返回类型没有按冻结 §4/§5 显式拆分,存在误转发、`result:read` 权限边界不清和 SDK 合约不一致的风险。**需要审查可信读取通道是否有已批准变更以及模型 SDK 的真实下游用途;正式契约未批准修订前不得悄悄改类型或泄露私有结果。** + +**CF-06 / P1(API/Store 可替换性,待契约裁决) / 冻结的异步 CheckpointStore 五操作与当前同步耦合不一致。** V0.6-R5 §5 定义的 `CheckpointStoreV1` 是 `createIfAbsent/load/compareAndSwap/loadTransition/loadReceipt` 五种 `Promise` 操作,§8 要求宿主提供可靠 Store 接缝。当前公开的 `types/interactive.d.ts#L101-L141` 则定义同步 `putArtifact/getArtifact/createRun/load/compareAndSwap/listJournal` 与可选 `getReceipt`;`src/interactive/coordinator.mjs#L220-L233` 的构造校验硬要求 `createRun/putArtifact/getArtifact`,后续如 `loadPinned`、`casOrCapacityExit` 对 `store.load/getArtifact/compareAndSwap` 直接同步取值,不等待 Promise。**因此目前不能直接用符合冻结五操作异步契约的远程 Host Store 适配器**;即使默认 SQLite 在 CI 中通过也不等于存储接口符合正式技术规范。该差异可能来自后续缩小 M3 范围的实施决策,需核验是否获用户明确批准;若无,应先出最小兼容/迁移方案和不破坏 WAL/CAS 的验证设计,不在本审查中直接改变 Core。** + +**CF-03 / BLOCKED / Host 真实来源与 OS 隔离。** 当前 M6 11 项 e2e 为真实 Node Host + scripted Worker,验证 P 等级协议;先前 Grok 文件 Harness 的真实模型参与为有限 M 证据,**都不能证明** C12/S13/T27 的原生工具调用、Windows 独立账户/ACL 与可信模型会话来源。PR #14 安全设计已经记录了 H3-01/H3-05/NAT-14 等后续门禁,仍需实测。 + +**CF-04 / 边界声明 / 依法延期范围。** V0.3 §15 与 V0.6 §16 明确 P0-B/P1/P2 阶段范围;Action/Human/Loop/Parallel 未实现 **不是** P0-A 失败。P2 any 赢家/迟到副作用尚未获专项确认,不得推测已批准语义。 + +## 2. V0.3 T01–T34 原文验收与实现证据 + +| ID | 原始阶段 | 正式需求(输入 / 必须结果,逐字提取) | V0.6 映射 | 当前审计判定 | 已观察到的代码/测试入口(需继续证明实测范围) | +|---|---|---|---|---|---| +| T01 | A | 纯 Condition/Terminal → 真实 JS 状态转移,一次启动到正确终点;每个已完成阶段被存储 | §3/§10.2 | P-PARTIAL | src/compiler/interactive/emit.mjs#L111; tests/interactive/coordinator.test.mjs#L75; tests/integration/m6-e2e.test.mjs#L293 | +| T02 | A | Agent/Skill/缺失输入 → Pending 的 instruction、Schema、missing、attempt 与存储中的任务一致 | §7/§10.1 | P-PARTIAL | src/interactive/coordinator.mjs#L534; tests/interactive/coordinator.test.mjs#L90; tests/interactive/compiler-front.test.mjs#L17 | +| T03 | A | 已持久等待后杀进程,再新进程 complete → 不从起点重跑已提交阶段;State/下一节点正确 | §3/§8 | P-PARTIAL | tests/interactive/crash.test.mjs#L89; tests/integration/m6-e2e.test.mjs#L201 | +| T04 | A | 非法 Schema 输出、必要验证器不存在 → 候选不污染 State;不能默认判通过 | §7/§10.1/§12 | P-PARTIAL | tests/interactive/coordinator.test.mjs#L170; tests/interactive/coordinator.test.mjs#L197 | +| T05 | A | 旧 requestId、错误节点/attempt/runId → 回执拒绝,最新 CP 不变 | §7/§8/§11 | P-PARTIAL | tests/interactive/coordinator.test.mjs#L151; tests/interactive/trusted-host.test.mjs#L357 | +| T06 | A | 相同请求同回执重复、同 ID 冲突回执 → 已提交结果幂等确认;不同内容冲突拒绝;不双推进 | §7/§8 | P-PARTIAL | tests/interactive/coordinator.test.mjs#L137; tests/interactive/store.test.mjs#L130 | +| T07 | A | 修改源/产物/编译器/策略身份 → 不兼容恢复拒绝;原 CP 保留 | §6 | P-PARTIAL | src/interactive/load.mjs#L54; tests/interactive/coordinator.test.mjs#L244; tests/integration/m6-e2e.test.mjs#L340 | +| T08 | A | JSON 非法值、循环/原型/超限 → 不丢字段、不污染 Store,不静默截断 | §11/§13 | P-PARTIAL | src/interactive/canonical.mjs#L34; tests/interactive/store.test.mjs#L50; tests/interactive/s07-capacity.test.mjs#L89 | +| T09 | A | 两进程 CAS 争抢同一 revision → 仅一个后继被 Store 接受;失败者读取最新状态 | §8 | P-PARTIAL | tests/interactive/store.test.mjs#L196; tests/interactive/crash.test.mjs#L152 | +| T10 | A | 不受支持的节点/Agent verify 配置 → 编译或启动预检明确拒绝;Standard 仍合法 | §2/§6 | P-PARTIAL | src/compiler/interactive/feature-gate.mjs#L21; tests/interactive/compiler-front.test.mjs#L38 | +| T11 | Host | 完全没有 JS 执行工具 → 宿主层明确报告不支持,不能模拟已运行 | §13 | HOST-BLOCKED | docs/interactive-host-security.md; no verified no-JS host tool scenario; not a Core-only test | +| T12 | A+B | 原 Standard 测试、AOT、TypeScript、固定 SDK/错误码 → 没有现有行为和包导出回归 | §1/§16 | P-PARTIAL | package.json; .github/workflows/ci.yml; tests/contracts.test.mjs; scripts/test-types.mjs | +| T13 | A+B | Agent `retries:2`,连续非法输出 → 依次 attempt 1/2/3,失败 reasons 正确、状态不污染 | §10.1 | P-PARTIAL | src/compiler/interactive/emit.mjs#L85; tests/interactive/coordinator.test.mjs#L170 | +| T14 | A+B | Agent 尝试耗尽、普通外部失败 → 保留 `FK_R004` 归因,不伪装 DSL failed | §10.1/§12 | P-PARTIAL | tests/interactive/coordinator.test.mjs#L170; tests/interactive/coordinator.test.mjs#L213 | +| T15 | B | `validateResult/checkStep/verify` 多阶段及候选态 → 顺序、严格返回值、阶段持久、成功一次合并 own keys | §10.1/§7 | DEFERRED-P0-B | src/compiler/interactive/feature-gate.mjs#L6; confirmed phase boundary | +| T16 | B | 外部 Action 成功与超范围 Patch → 合法 writes 一次写入;越权字段拒绝;操作不双发 | §10.3/§11 | DEFERRED-P0-B | src/compiler/interactive/feature-gate.mjs#L6; no Action support in P0-A | +| T17 | B | 已发 Action 外部成功但回执丢失 → 稳定 operationId,只进入 reconcile,不盲重发 | §8/§10.3 | DEFERRED-P0-B | V0.6-R5 §10.3; Action reconcile not yet implemented | +| T18 | B | Action 非控制异常直接连接普通 Verification → 保存 ctx.error 安全摘要并跳过 Verify,正确转失败路径 | §10.3/§14 | DEFERRED-P0-B | V0.6-R5 §10.3/§14; contextError not yet implemented | +| T19 | B | 普通/Loop Verification true/false/非法响应 → 严格布尔、跳转正确、状态无污染 | §10.4 | DEFERRED-P0-B | V0.6-R5 §10.4; independent Verification not yet implemented | +| T20 | B | Human 拒绝→修订→批准,包括前缀入口 → 审批来源可信、控制栈恢复、无多跑/漏跑 | §10.5/§11 | DEFERRED-P0-B | V0.6-R5 §10.5/§11; trusted Human channel pending | +| T21 | B | Loop 1/3 轮,最后失败 → 最后一轮失败后不修订,进入 onMaxExceeded | §10.6 | DEFERRED-P0-B | V0.6-R5 §10.6; Loop not yet implemented | +| T22 | B | Skill 变更、失败、恢复同一 pending → instruction 固定、版本校验、不重新加载出不同内容 | §6/§10.1 | DEFERRED-P0-B | tests/integration/m6-e2e.test.mjs#L340 covers P0-A drift only; P0-B scope remains | +| T23 | B | 显式取消、迟到结果、控制信号 → tombstone、迟到回执不恢复业务推进 | §3.6/§4/§10.8 | DEFERRED-P0-B | V0.6-R5 §3.6; business cancel deferred (adminTerminate P0-A is separate missing gap) | +| T24 | P1 | all_succeeded 多 Pending、失败和断进程 → Join 一致、剩余任务处置可见、不会无理由自动取消 | §10.7 | DEFERRED-P1 | V0.6-R5 §10.7; all_succeeded protocol pending design/implementation | +| T25 | P2 | any 获胜、迟到分支与下游读取 → **等待 P2 专项协议后确定预期**,不得以 V0.2 旧方案冒充通过 | §10.7(未裁决) | BLOCKED-P2-DECISION | V0.3 §15 explicitly does not freeze P2 any winner/late result semantics | +| T26 | P1 | 多 Pending 乱序、重复、CAS 和写集合冲突 → 状态一致、合法写入、拒绝冲突 | §10.7 | DEFERRED-P1 | V0.6-R5 §10.7; parallel shared-write conflict semantics pending | +| T27 | Host | 真实可用宿主(含 ChatGPT 专项) → 真正调用 JS、模型完成 Pending、再调用 JS,保存执行证据;无能力的平台记 BLOCKED | §13 | N-BLOCKED | tests/integration/m6-e2e.test.mjs#L171 only scripted worker; live Grok file-harness ≠ native JS tool loop | +| T28 | 全部 | 源码/生成 JS/包/类型/文档/测试交叉比对 → 版本一致、实际通过与未实现范围如实列明 | §16 | P-PARTIAL | package.json; .github/workflows/ci.yml; source/docs/type crosswalk not yet complete | +| T29 | A+B | 在每个 Agent 阶段前/后强制终止进程 → 从最近已提交阶段恢复,不跳过/多跑阶段 | §3/§8 | P-PARTIAL | tests/interactive/crash.test.mjs#L89,L118; tests/integration/m6-e2e.test.mjs#L201,L234 | +| T30 | A+B | 故意伪造 Cursor、历史收据或程序身份 → 可信范围内结构与一致性校验拒绝;不继续外部动作 | §8/§11 | P-PARTIAL | tests/integration/m6-e2e.test.mjs#L306; tests/interactive/store.test.mjs#L226; tests/interactive/trusted-host.test.mjs#L147 | +| T31 | A+B | 非纯验证器/双进程 complete 竞态 → 禁止重复进行未受控外部验证;CAS 确定唯一后继 | §10.1/§8 | P-PARTIAL | tests/interactive/coordinator.test.mjs#L197; tests/interactive/crash.test.mjs#L152; non-pure verifier remains P0-B | +| T32 | B | Action 成功但提交前崩溃且目标不支持幂等 → 保留未知状态、对账/人工介入,不承诺 exactly-once | §10.3 | DEFERRED-P0-B | V0.6-R5 §10.3, no Action external effect reconciliation in P0-A | +| T33 | A+B | 同一 DSL 的 Standard/Interactive Agent、Human、Loop、Action 路线 → 相同可信结果下可观察顺序/重试/终点对照;模式差异显式记录 | §14 | P-PARTIAL | tests/integration/m6-e2e.test.mjs#L171,L293; docs/interactive.md; P0-B Standard parity deferred | +| T34 | A+B | 超长运行、资源预算、隐私信息被误送模型 → `paused` 可恢复,限额拒绝不丢 CP,模型只能看最小 Pending | §11/§13 | P-PARTIAL | src/interactive/canonical.mjs#L7,L11; tests/interactive/s07-capacity.test.mjs; tests/interactive/trusted-host.test.mjs#L99 | + +> **审计状态解释:** `P-PARTIAL` 表示已在当前源码/脚本中找到与条款相关的执行或测试入口,尚未独立完成所有正式断言;`DEFERRED` 是已批准的后续阶段;`N-BLOCKED` 缺原生可信宿主证据;`GAP-P0-A` 是在当前应交付范围内尚未找到实现;这些均不是 PASS。 + +## 3. V0.6-R5 R01–R22 历史反例追踪 + +| ID | 原始对抗场景 | 原始必须观察到的结果 | 映射 T | 当前审计判定 | +|---|---|---|---|---| +| R01 | Agent `complete` 成功 CAS 回执、验证前杀进程 | 新进程仅校验已保存 candidate;Agent 无二次派发 | T03/T06/T29 | P-PARTIAL | +| R02 | 回执已接纳、后续阶段已推进到 `revision+5` 后重发相同提交 | ack 保留 original acceptedRevision;Frame 是 latest;不会倒退 | T06/T09 | P-PARTIAL | +| R03 | Store `compareAndSwap` 成功但丢响应,load 读旧版本 | 该 Store 被判不符合线性化契约;不得盲派发 | T09/T29 | DESIGN-ONLY/NOT-FULLY-TESTED | +| R04 | SDK 输入对象在 `await` 期间被外部突变 | 校验/Hash/CAS 使用同份克隆规范字节,不接受新输入 | T08/T30 | DESIGN-ONLY/NOT-FULLY-TESTED | +| R05 | `validateResult` 返回 `undefined`、数字或抛异常 | 不判通过,也不作为业务校验失败直接消耗 attempt | T04/T31 | DESIGN-ONLY/NOT-FULLY-TESTED | +| R06 | 纯验证器写文件或改外部状态 | 配置声明为纯违反契约;不能称其操作被 FlowKit 自动防重复 | T31/T32 | DESIGN-ONLY/NOT-FULLY-TESTED | +| R07 | Action 错误直接接普通 Verification;`state.error` 原本不存在 | 原 Verification 被跳过、CP 记录 contextError、业务 State 无新 `error` 属性 | T18/T33 | DEFERRED-P0-B | +| R08 | Stage CP 已保存 `received`,请求 `resume` | 不返回空 `waiting`,不得要求旧 request 再执行 | T03/T29 | P-PARTIAL | +| R09 | 同一 Tenant 下 startKey 冲突 / 不同 Tenant 同 key | 前者按 hash 对比,后者隔离;无跨租户 runId 复用 | T05/T09/T30 | P-PARTIAL | +| R10 | 故意替换验证后未加载的生成文件 | 要求从同一份已验证字节导入;身份不匹配拒绝 | T07/T30 | P-PARTIAL | +| R11 | 内部 Store 超时,另一个调用已经推进多个阶段 | 线性化 load + Ledger 查清是否接纳,不仅看 revision+1 | T06/T09/T29 | P-PARTIAL | +| R12 | 2000 条以上阶段写入、ledger 累积、WAL 扩张 | 明确写放大、资源限制,必要时事务内拆表,不丢回执 | T29/T34 | DESIGN-ONLY/NOT-FULLY-TESTED | +| R13 | 生成 IR 同时出现 `await` 与 CP `awaiting` | Compile/IR validator 拒绝未约定阶段;合法结果采用唯一枚举 | T01/T03/T30 | DESIGN-ONLY/NOT-FULLY-TESTED | +| R14 | 向 `advance` 送错 Block 的 `certifiedEvent` 或非法 `toBlockId` | `FK_I_INVALID_TRANSITION`,CP 不变,不派发 | T07/T30 | P-PARTIAL | +| R15 | Verification 抛执行错误,另一次真实返回 `false` | 前者不走 `onFailure`,后者才走;失败原因可恢复 | T19/T33 | DEFERRED-P0-B | +| R16 | Human UI 断线,另一次真人明确拒绝 | 前者不能代替拒绝;后者才进入修订链 | T20/T23 | DEFERRED-P0-B | +| R17 | 宿主控制信号和普通 Agent 失败同时存在 | 控制信号不被消耗为业务尝试、不直接伪造 `cancelled` | T14/T23 | DEFERRED-P0-B | +| R18 | 无 Agent 纯工作流、Runtime 没有 validateResult/SkillResolver | 必须支持纯流程 `start()` 正常到终点 | T01/T10 | P-PARTIAL | +| R19 | `checkStep` 读取 State,随后 `verify` 读取候选 State | 两次读取符合 Standard 当前真实视图,未通过不合并候选 | T15/T33 | DEFERRED-P0-B | +| R20 | Action 失败后下一 Action 成功,后继读取 contextError | 不应自动清除旧错误;`state.error` 不增加 | T18/T33 | DEFERRED-P0-B | +| R21 | 相同 startKey/业务输入,仅调整阶段预算或日志参数 | 复用相同 Run;改变真正策略/业务输入则拒绝 | T05/T09/T34 | P-PARTIAL | +| R22 | Action `indeterminate` → 新 Reconcile 请求 → still-indeterminate / confirmed-succeeded | 保持原 operationId,不重新调用 execute,最终成功才合法提交 | T17/T32 | DEFERRED-P0-B | + +## 4. V0.6-R5 S01–S14 原生验收边界 + +| ID | 正式可观察不变量 | 设计/阶段 | 当前审计判定 | +|---|---|---|---| +| S01 | 纯 Condition→Terminal 正常成功,缺失布尔输入 → `errored(FK_R002)`,再次 resume revision 不增长 | N1 / P0-A | P-PARTIAL | +| S02 | Agent 默认重试、stepCounter、同一 Pending 重读 ID 稳定;派发/接纳/提交 CAS 前后故障注入 | N3/N11 / P0-A | P-PARTIAL | +| S03 | 纯 validator 稳定异常/非法值 → `errored` 且不消耗 attempt;临时丢绑定明确恢复条件 | N1 / P0-A | P-PARTIAL | +| S04 | 并发同回执仅接纳一次,冲突拒绝;CAS 成功丢响应由 namespace 内线性化读+ledger 识别 | N12 / P0-A | P-PARTIAL | +| S05 | 同 startKey 不同 namespace 各自独立,跨 namespace 访问不泄漏;伪造 PC/非法合法边拒绝且 CP 不变 | N3/N5 / P0-A | P-PARTIAL | +| S06 | 固定产物被替换/策略改变拒绝;SDK 升级能否继续由支持的语义/CP Schema 决定,不能重编译旧源 | N6 / P0-A | P-PARTIAL | +| S07 | JSON 恶意原型、 getter、无效数值、大小超限拒绝;不同阶段预算最终 State 相同 | 序列化 / P0-A | P-PARTIAL | +| S08 | 外部 checkStep 读提交前 State,verify 读候选合并 State;接纳后杀进程不重做已提交阶段 | N2 / P0-B | DEFERRED-P0-B | +| S09 | 外部 Action statePatch 越界拒绝,未知结果只 reconcile 原 operationId,不重发 execute | 副作用 / P0-B | DEFERRED-P0-B | +| S10 | Action 错误跳过相邻 Verification,后续可信 Action 能读取 contextError;最终可信结果投影可见 | N2/N7 / P0-B | DEFERRED-P0-B | +| S11 | Human 来源模型 stdin 伪造拒绝,真实审批通过;Loop 最后失败不再修订,重启不增加轮次 | N4 / P0-B | DEFERRED-P0-B | +| S12 | 管理 cancel 与终态竞争,仅一个 CAS 赢;迟到旧回执不能激活已终态 Run | D2 / P0-A 基础,P0-B 补偿 | GAP-P0-A; DEFERRED-P0-B | +| S13 | 真实宿主的 JS → 当前模型/工具 → JS 日志证据;宿主没有 JS 或审批通道则记 BLOCKED | T27 / Host | N-BLOCKED | +| S14 | 既有 Standard 全量回归;exports 五项、bin 保持一个、TS 5.0 及 Windows/Ubuntu Node 22/24、模式差异表逐项判定 | N8/N10/N13 / 全阶段 | P-PARTIAL | + +## 5. V0.6-R5 正文 §§1–18 逐章审查入口 + + +| § | 审查主题 | 必查原始语义 | 当前源码/证据入口 | 初步结论 / 后续动作 | +|---|---|---|---|---| +| §1 | 产品边界 | 已有 Shared Standard 语义与新 Interactive 子路径;不得变成强制 Server/模型网关 | src/interactive/index.mjs; docs/interactive.md | 已初步交叉核对,继续审查根 API 六项/部署边界 | +| §2 | 十四不变量/架构 | Program 唯一控制流、可信 Registry、零 import、同字节加载 | src/compiler/interactive/{emit,validate-ir}.mjs; src/interactive/load.mjs | P0-A 执行路径存在,长期 ABI/恶意字节需补 | +| §3 | 生命周期与 Stage | waiting/paused/finished、独立接纳/Commit 持久屏障 | src/interactive/coordinator.mjs; tests/interactive/crash.test.mjs | 阶段级脚本证据存在,未完成所有切点 | +| §4 | API 与身份 | start/complete/resume/adminTerminate/可信结果读取 | src/interactive/index.mjs; types/interactive.d.ts | CF-01 与 CF-02 高优先级差异 | +| §5 | Program ABI / 类型 | ProgramTable、transition proposal、HostSubmission 判别联合 | types/interactive.d.ts; src/compiler/interactive/emit.mjs | P0-A 可执行子集,完整 §5 类型差异需审 | +| §6 | Registry / retain / upgrade | 不可变 Program、同库 retain、旧 Run 跨版本恢复 | src/interactive/sqlite-store.mjs; tests/interactive/store.test.mjs | 同库脚本证据存在,跨 SDK 版本兼容未验证 | +| §7 | PublicTask/认证事件 | 模型最小披露,Host 专属结果认证、Human 不可伪造 | src/interactive/trusted-host.mjs; tests/interactive/trusted-host.test.mjs | P0-A 公共 Agent 已有,真实 OS 来源不足 | +| §8 | Store/Journal | Genesis/CAS/Receipt/Journal 原子事务 | src/interactive/sqlite-store.mjs; tests/interactive/store.test.mjs | P 级测试多项;掉电/系统跨平台仍需补 | +| §9 | SQLite 参考 schema | 同事务 Program retain/Genesis 与 WAL/FULL | src/interactive/sqlite-store.mjs | 现代码 schema vs §9 全字段待逐条复核 | +| §10 | 节点语义 | 基本 Agent/Condition 与 P0-B Action/Human/Loop、P1/P2 | src/compiler/interactive/feature-gate.mjs | A 子集存在;B/P1/P2 Deferred | +| §11 | 安全与多租户 | 严格 JSON/JCS、OS 用户、Namespace、Host 控制通道 | src/interactive/canonical.mjs; docs/interactive-trusted-host-security-v2-design.md | 真实 Windows 模型越权测试 BLOCKED | +| §12 | 错误分类 | FK_R/ FK_I、非纯校验、基础设施错误边界 | src/interactive/coordinator.mjs; src/interactive/host-attestation.mjs | 已覆盖部分脚本,未穷尽所有正式码 | +| §13 | 资源预算/运维 | 2 MiB、128 stage 候选、Ledger 增长/写放大 | src/interactive/canonical.mjs; tests/interactive/s07-capacity.test.mjs | S07 脚本证据存在,性能/长期运行未知 | +| §14 | Standard/Interactive 差异 | T33 明确有意不等价的字段/行为与端到端对照 | tests/integration/m6-e2e.test.mjs; docs/interactive.md | 只覆盖 A 子集,B 仍延期 | +| §15 | 验收追踪 | T01–34/R01–22/S01–14 来源与阶段 | 本文件 §2–§4 | 编号齐全,逐条实测证据尚未最终核销 | +| §16 | 实施门禁 | P0-A/B/P1/P2 与禁用未实现阶段 | src/compiler/interactive/feature-gate.mjs | P0-A gate 存在;Admin/T27 遗留 | +| §17 | 设计 Finding | R5-01..10 的文档修订与真实验证边界 | R5-INDEPENDENT-CONVERGENCE-REVIEW.md | 候选设计收敛不代表实现验收 | +| §18 | 用户冻结/授权 | 确认技术方案与实施计划,不自动宣布代码发布授权 | 00-APPROVAL-RECORD.md; PR #13/#14 | 2026-11-10 已确认计划,当前 M1–M5 实现已在 main;不应重新规划 | + +## 6. 下一轮证据优先级 + +1. **P1 / CF-01:** 检查 adminTerminate、管理凭据、tombstone 事务与 S12-A 的真实实现;在明确差异和用户确认前不动生产 Core。 +2. **P1 / CF-02:** 对照 V0.6 §4/§5 与真实 `./interactive` 导出、TypeScript、packaged consumers;查找是否曾有获批变更记录,不能只看编译是否成功。 +3. **N / S13/T27:** 分离真实 Grok 文件 Harness M 层、脚本 Host P 层与真正原生 JS tool-call N 层;补原生来源、OS 安全和审计。 +4. **P0-A S12-A:** 与 CF-01 同步;P0-B Human/cancel 不可代替 P0-A 管理终止。 +5. **跨平台实测与原型差距:** 保留已有 22/22 PoC、9/9 SIGKILL,补真实 Node/Windows、JCS、Program 升级与 Stage 性能证据。 +6. **独立复审:** 当前矩阵先由第二审查者核对源码及原始验收日志;任何新的正式需求/ABI 变更必须单独审批。 + +## 7. 本轮判定 + +**R1 为阶段性证据矩阵,不是最终符合性通过。** T01–34、R01–22、S01–14 的原文已逐项抽取并且编号完整。已发现两处需要重点复核的 P0-A 契约差异(受权 adminTerminate 与公共 SDK API 形态),以及原生宿主/系统隔离阻断。P0-B、P1、P2 的合法延期不应误判为 P0-A 缺陷。下一步是在不修改正式契约/生产 Core 的前提下完成差异复现和可验证的审查结果。 + +--- +原件校验:T 34 / R 22 / S 14;SHA-256 分别匹配;生成时间 2026-11-12。 \ No newline at end of file diff --git a/docs/interactive-phase2-conformance-audit-r2-20261112.md b/docs/interactive-phase2-conformance-audit-r2-20261112.md new file mode 100644 index 0000000..bad118a --- /dev/null +++ b/docs/interactive-phase2-conformance-audit-r2-20261112.md @@ -0,0 +1,107 @@ +# FlowKit Interactive — Phase 2 正式契约符合性审查 R2 / 源码定向复核 + +> **状态:审查工作记录,NOT FINAL / NOT CONFORMANCE PASS。** 本文补充 [R1](./interactive-phase2-conformance-audit-r1-20261112.md),**不替代、不修改**已确认 Requirements V0.3、Technical Design V0.6-R5、P0-A Implementation Plan V0.1 或 PR #14 Trusted Host V2。只记录已核验代码事实、契约差异、待决事项和后续验证路径;没有修改生产代码、测试、类型或 Schema,也不授权发布、Phase 3/4 越级实施。 + +## 1. 本轮基线及证据等级 + +- 当前 main(2026-11-12 检查):`13d573c5ddc4a0ed3502401589c581799c96c5d2`;[main commit](https://github.com/devcodex-labs/flowkit/commit/13d573c5ddc4a0ed3502401589c581799c96c5d2)。 +- R1 使用的旧 main:`ae5fc5a42c67eb6e2c229d72ff4d9ac8e3aab3cd`;对 `src/interactive/coordinator.mjs`、`trusted-host.mjs`、`sqlite-store.mjs`、`compile.mjs`、`types/interactive.d.ts` 逐一读取两版 GitHub 文件并比较 **Git blob SHA 五项完全一致**。因此 R1 的相关代码事实不因 PR #15 文档合并而失效;其他未比较文件不能套用这一结论。 +- 已直接读取正式 V0.3 `FlowKit-Host-Driven-Interactive-Execution-Requirements-V0.3-Confirmed(1).md`(§§1–7、15–19 与关键段落);其历史登记身份为 56,883 bytes / SHA-256 `3a0adc3edb41869a6309bae29ec5496df00970c815f8dc16330a90cbf26e703e`。 +- 正式 V0.6-R5 的身份为 114,621 bytes / SHA-256 `09f3a5698e238d10b80efb8e26d1c41e51b663ca620efa70815dd7b52f2106ba`,由 [正式原件登记](./interactive-phase2-formal-source-register.md) 及 PR #15 的历史 9/9 校验支持。**本轮当前会话无法直接读取 Library 中该 Markdown 原始正文(原始文件读取接口返回不可访问),因此涉及 §4/§5/§8/§16 的精确规范措辞,仍引用 R1 已抽取的条款并列为独立复核门禁;不能声称本轮已独立全文复读 V0.6-R5。** 不应因此推翻此前原件已找回的事实,也无需重写原件。 +- 证据等级:`SOURCE-CONFIRMED` = 已直接读取具体源码及类型;`SOURCE-INFERRED` = 源码顺序推断、尚未运行反例;`CONTRACT-CONFIRMED-V03` = 已直接读取正式 V0.3;`V06-R1-EXTRACT` = 对应 V0.6 原文由 R1 转录、待独立原文复核;`RUN-BLOCKED` = 未取得该执行场景的原始运行证明。**以上不是测试 PASS 等级。** + +### 1.1 当前关键源码身份 + +| 文件 | main 上 Git blob SHA | 相关代码 | +|---|---|---| +| `src/interactive/coordinator.mjs` | `1ce6460d58c20a38580a30c2966ba8187f1382b7` | 终态帧 L144–151、Store 使用 L221–255、start L534–552、公开返回 L648–657 | +| `src/interactive/trusted-host.mjs` | `315e22662e80c4f1561c2ab8d717a168251c1ed5` | Host start/load L147–168、Agent 公共任务 L170–221、Host 回执 L221–283 | +| `src/interactive/sqlite-store.mjs` | `0f2db54c145649525bcd77b0c501f083dfeb54ee` | 同步 createRun/load/CAS L290–396 | +| `src/interactive/compile.mjs` | `43f6dfa86908a486446dc5193c7a4d2895926617` | compile options snapshot / await / lowering | +| `types/interactive.d.ts` | `a178b2adbff6b18345a7e1b63e3370269ac2425f` | Frame L81–92、Store L106–141、SDK L161–188、Trusted Host L254–288 | + +## 2. 发现及复核裁决 + +### CF-01 — P1 / P0-A 管理终止缺口 + +- **证据:SOURCE-CONFIRMED + V06-R1-EXTRACT。** [公开导出](https://github.com/devcodex-labs/flowkit/blob/13d573c5ddc4a0ed3502401589c581799c96c5d2/src/interactive/index.mjs)、[Coordinator 返回 API](https://github.com/devcodex-labs/flowkit/blob/13d573c5ddc4a0ed3502401589c581799c96c5d2/src/interactive/coordinator.mjs#L648-L657)、[Trusted Host 返回 API](https://github.com/devcodex-labs/flowkit/blob/13d573c5ddc4a0ed3502401589c581799c96c5d2/src/interactive/trusted-host.mjs#L279-L283) 均无 `adminTerminate`;TS 声明中同样不存在。 +- R1 已将正式 V0.6-R5 §4/§16 和已确认实施计划 `A-M5-02` 映射到 P0-A 受权管理终止、独立 admin controller、tombstone 与竞态语义。 +- **裁决:代码层高置信 GAP-P0-A;S12-A 不可宣称 PASS。** 业务 `cancel` 属 P0-B,不能充当 P0-A 管理终止。未验证完整 Host Admin 外部程序是否存在于本仓库以外部署,因此不宣称系统级事实超出审查范围。 +- **最小验证/修复前置:** 明确受信管理身份与权限、不接受模型自报 admin;设计 admin-only API、CP+Journal CAS 的墓碑、终态与当前结果并发竞争、旧 Program 不可用时的恢复/管理出口及历史回执拒绝。正式语义调整需要单独确认;不能只补方法名。 + +### CF-02 — P1 候选 / 公共 SDK 形态 + +- **证据:CONTRACT-CONFIRMED-V03 + SOURCE-CONFIRMED;V06 仍需核验。** V0.3 §7.1 示范 `compileInteractive/start/complete/resume/cancel` 独立导入,§19 将这些能力列为确认内容;**同一节也明确“签名例子仅用于说明”,6.2 的 Store API 亦明示技术设计可微调签名**。当前 [子路径入口](https://github.com/devcodex-labs/flowkit/blob/13d573c5ddc4a0ed3502401589c581799c96c5d2/src/interactive/index.mjs) 导出 factory,而 [Coordinator 类型](https://github.com/devcodex-labs/flowkit/blob/13d573c5ddc4a0ed3502401589c581799c96c5d2/types/interactive.d.ts#L161-L188) 的实例提供 `start/complete/resume` 功能。 +- **裁决:API 形态差异属实;“缺少这三个能力”不成立。** 是否属于 P1 契约违背,取决于 V0.6-R5 §4 是否冻结了顶层具名导出,以及后续获批的 API 决策;不得仅凭 V0.3 的 proposed 导入例子直接要求新增兼容层。 +- **验收:** 原始 V0.6 §4 与实施批准记录复核、实际 package exports 与 JS/TS consumer 对照。若签名调整已经被确认,记 APPROVED DEVIATION;否则提出明确兼容方案等待裁决。 + +### CF-05 — P1 候选 / 终态 State 投影边界 + +- **证据:SOURCE-CONFIRMED + V06-R1-EXTRACT。** [Coordinator.asPublicFrame](https://github.com/devcodex-labs/flowkit/blob/13d573c5ddc4a0ed3502401589c581799c96c5d2/src/interactive/coordinator.mjs#L144-L151) 在终态携带 `state:cp.state`;`InteractiveRunFrame.state?` 在 [类型 L83–92](https://github.com/devcodex-labs/flowkit/blob/13d573c5ddc4a0ed3502401589c581799c96c5d2/types/interactive.d.ts#L83-L92) 公开声明。[Trusted Host 的 start/resume/load](https://github.com/devcodex-labs/flowkit/blob/13d573c5ddc4a0ed3502401589c581799c96c5d2/src/interactive/trusted-host.mjs#L147-L168) 转发 Coordinator 帧。[现有单元测试](https://github.com/devcodex-labs/flowkit/blob/13d573c5ddc4a0ed3502401589c581799c96c5d2/tests/interactive/coordinator.test.mjs#L75-L88) **明确断言终态返回 `result.state`**,这不是偶发实现细节。 +- V0.3 §6.3 要求模型只接收最小 Pending,完整 CP 不默认入聊天;R1 记录 V0.6-R5 §4/§5 的 Public Frame 不含完整 State、受权 `readTrustedTerminalResult` 分离。 +- **裁决:公共/可信帧角色未清晰拆分,存在误转发风险;不等同已发生真实模型泄漏。** 当前 `getPublicAgentTask` 仍有 allowlist 和授权门禁;必须核对 Host adapter 是否把 terminal frame 误送模型。 +- **验收:** 给出 public frame / trusted result 两种明确类型与能力;模型通道做强制最小披露测试;仅授权 Host 可读业务结果;旧测试重新定义为“Host 可信读取”而非“公共可读”。修改类型和 API 须先检查冻结技术契约。 + +### CF-06 — P1 候选 / 异步 Store 规范与同步实现 + +- **证据:SOURCE-CONFIRMED + CONTRACT-CONFIRMED-V03 + V06-R1-EXTRACT。** V0.3 §6.2 以 Promise 签名示例描述 Store,并允许技术设计阶段微调名字/结构,但不得减弱可靠 CAS/持久化和 Host 选择 Store 的能力。R1 引述 V0.6-R5 §5/§8 具体五操作异步 `CheckpointStoreV1`;当前 [TS Store 接口](https://github.com/devcodex-labs/flowkit/blob/13d573c5ddc4a0ed3502401589c581799c96c5d2/types/interactive.d.ts#L106-L141) 为同步 `putArtifact/getArtifact/createRun/load/compareAndSwap` 等;[Coordinator](https://github.com/devcodex-labs/flowkit/blob/13d573c5ddc4a0ed3502401589c581799c96c5d2/src/interactive/coordinator.mjs#L221-L255) 与 [SQLite Store](https://github.com/devcodex-labs/flowkit/blob/13d573c5ddc4a0ed3502401589c581799c96c5d2/src/interactive/sqlite-store.mjs#L290-L396) 都同步消费 Store 结果。 +- **裁决:无法直接注入一个仅实现冻结“五个 Promise 操作”的异步 Host Store;不代表当前同步 SQLite CAS 已失效,也不代表所有自定义同步 Store 被禁止。** +- **验收/设计门禁:** 对照 V0.6 原文和已批准 M3/M4 调整;最小兼容方案应保护 Program bytes 先验证、创建幂等、原子 CAS+Receipt/Journal、掉响应后的线性化重读,不把任意远程数据库的弱 CAS 冒充满足契约。若需要改变冻结五操作规范,须由用户独立批准。 + +### CF-07 — P1 候选 / 新增:start() 在首次 await 之后才快照初始 State 与意图 + +- **证据:SOURCE-INFERRED / 未独立执行完整仓库测试。** [Coordinator.start L534–552](https://github.com/devcodex-labs/flowkit/blob/13d573c5ddc4a0ed3502401589c581799c96c5d2/src/interactive/coordinator.mjs#L534-L552) 先复制 `programBytes/programIdentity`,然后 `await loadInteractiveProgram`(L542),之后才在 `initialCheckpoint(...,state)`(L548)执行 `copy(state)`,在 L549 才 `copy(startIntent)`。即使 loader Promise 已经 resolved,JS `await` 仍会异步让出执行;调用方可以先改动所持有的两个对象。现有 [测试 L273–281](https://github.com/devcodex-labs/flowkit/blob/13d573c5ddc4a0ed3502401589c581799c96c5d2/tests/interactive/coordinator.test.mjs#L273-L281) 只检查 `await api.start()` **之后**修改输入;未覆盖首次 await 边界。 +- R1 的 V0.6-R5 R04 明示 SDK 输入在 await 期间被突变应继续使用同一份提前克隆的规范字节;V0.3 §5 的阶段输入/提交身份稳定性与这一要求一致。 +- **风险:** 同一个调用的 startIntentHash、初始 State 可能受外部对象竞争写入影响,造成不可预期创建/幂等判定。没有证据表明已造成真实越权或线上数据损坏。 +- **建议精确复现(测试在该仓库的 `runAgentFixture` 上构造)**: + ```js + const state={existing:'before'}; + const startIntent={correlation:'before'}; + const pendingStart=api.start({...source,startKey:'pre-await-mutation',state,startIntent}); + state.existing='after'; startIntent.correlation='after'; + const frame=await pendingStart; + const saved=store.load(frame.runId); + assert.equal(saved.state.existing,'before'); + // 同时断言 startIntent 的规范身份来自调用瞬间的 before 值, + // 可通过对同一 startKey/原始意图重试与不同意图冲突来核验。 + ``` +- **最小修订候选:** 在第一个 `await` 前对 `state` 与 `startIntent` 做有界、严格、同一份 JSON 快照,之后所有 Checkpoint/Hash/Store 操作仅使用此快照;校验失败应发生于副作用之前。先复现、再单独 Core 修复 PR,不能把本文件当作测试 PASS。 + +### CF-08 — P2 候选 / 新增:compileInteractive() 编译选项快照不一致 + +- **证据:SOURCE-INFERRED。** [compile.mjs](https://github.com/devcodex-labs/flowkit/blob/13d573c5ddc4a0ed3502401589c581799c96c5d2/src/interactive/compile.mjs) 已先建立 `snapshotOptions` 并检查 `snapshotOptions.featureStage==='P0-A'`,但在两次 `await`(readFile/analyzeSource)之后,又通过原始 `options.featureStage` 传入 `lowerInteractive`。 +- 调用者在编译等待中把 `options.featureStage` 从 `P0-A` 改为 `P0-B` 时,后续门禁会拒绝,导致原本合法的调用非确定性失败。**当前 `gateInteractiveFeatures` 仍会拒绝 P0-B/P1,不能据此断言有越权启用新阶段的漏洞。** +- **建议:** 新增带可控异步交错的编译选项变更回归,确认从头到尾使用 `snapshotOptions.featureStage`(以及确有需要的深层配置快照)。P2 预防性修复候选,不与 CF-07 混同优先级。 + +### CF-03、CF-04 — 维持 R1 边界 + +- CF-03:真实 Grok Build native tool event 绑定、C12/S13/T27、Windows MODEL_USER/HOST_USER ACL/句柄保护继续 `BLOCKED`;M6 scripted e2e 和真实 Grok 文件 Harness 的证据层不能互换。 +- CF-04:Action/Human/Loop/Parallel 是已批准的 P0-B/P1/P2 延期范围;P0-A 管理终止与 P0-B 业务取消绝不可混淆。 + +## 3. 对 R1 的校准及实施顺序 + +| 工作 | 本轮裁决 | 下一步门禁 | +|---|---|---| +| R1 源码基线较旧 | **已核对五个关键 Blob 与当前 main 相同** | R2 以 13d573 为新基线;审查其他文件仍需逐项检查 | +| CF-01 | 真实 P0-A 缺口,P1 | 复核原始 §4/§16 + 已批准 M5;管理身份/tombstone/竞态负例 | +| CF-02 | 能力存在于 factory,顶层签名争议 | V0.6 §4 原文 + 批准决策;不抢先扩张 public surface | +| CF-05 | 源码/TS 的终态 State 暴露属实 | 设计可信读取与公开投影分离,核对 model-facing adapter | +| CF-06 | 同步 Store 与 R1 转录的异步五操作不一致 | 复核 V0.6 §5/§8、批准变更与最小适配;无旁路弱 CAS | +| CF-07 | 新源码竞态,R04 需测试 | 新增失败复现测试;获准后最小修复 + 跨 Node/OS 回归 | +| CF-08 | 新编译选项竞态,当前无绕过证据 | 补异步交错回归,评估 P2 修复 | +| C12/S13/OS | `BLOCKED`(非全红、非全绿) | 保留 Phase 3/4 独立验收门禁,先工具能力 PoC、后原生 N 层 | + +**不自动进入生产 Core 变更或合并。** 正式契约原文对照、失败复现、最小修复方案与用户已确认计划之间仍有待决门禁。先用此 R2 对照原始 V0.6-R5 和既有实施批准记录开展独立复审;在没有第二审查者、真实运行证据、PR HEAD CI 时,不得将 Phase 2/M6/P0-A 宣布完成。R1 的 T01–T34、R01–R22、S01–S14 逐项台账仍有效,任何一项缺少运行证据继续 PARTIAL/BLOCKED,不因本文件的源码核对而改为 PASS。 + +## 4. 变更记录 + +- **R1**:原件和条款索引、首次实现证据矩阵(旧 main `ae5fc5a`)。 +- **R2(本文件)**:对五个关键源码 Blob 重新固定 main `13d573c`;重审 CF-01/02/05/06 的裁决边界;新增 CF-07/08 及复现计划;保持所有冻结契约不变。 + + +## 5. CF-07 独立测试证据关联(R2 补充索引) + +- 测试分支:`test/r04-start-input-snapshot-20261112`;[PR #17](https://github.com/devcodex-labs/flowkit/pull/17)。 +- 2026-11-12 的独立测试提交:`ada01c933553206948ffcf2f944dfc5604e44da2`。两个不同用例分别锁定 `state`(包括嵌套引用)与 `startIntent` 在首次 await 前的快照要求。 +- 这仅是**预期失败的源码复现测试**,尚无成功启动 runner 的正式测试证据;CI `queued` 与测试 `FAIL` 不可混同。 +- 此处只增加证据链接,不改变任何需求、实现判定、生产 Core、SDK、Program ABI 或 SQLite Schema。 diff --git a/docs/interactive-phase2-formal-source-register.md b/docs/interactive-phase2-formal-source-register.md new file mode 100644 index 0000000..b41f1e7 --- /dev/null +++ b/docs/interactive-phase2-formal-source-register.md @@ -0,0 +1,78 @@ +# FlowKit Interactive — Phase 2 正式契约原件追踪登记 + +**状态:SRC-01 / V0.3 与 SRC-02 / V0.6-R5 均已找回并完成 SHA-256/包内清单验证;SRC-03、SRC-04 也已在资料空间目录中定位。** +**登记日期:2026-11-12(UTC+8)** +**当前 main:** `ae5fc5a42c67eb6e2c229d72ff4d9ac8e3aab3cd` +**关联设计:** [Trusted Host V2](./interactive-trusted-host-security-v2-design.md) · [完整路线图](./interactive-full-delivery-roadmap-2026-11-11.md) + +## 1. 目标原件 + +| ID | 应取得的正式原件 | 已知历史路径/版本线索 | 当前证据 | 状态 | +|---|---|---|---|---| +| SRC-01 | `FlowKit-Host-Driven-Interactive-Execution-Requirements-V0.3-Confirmed.md` | 用户上传的 `FlowKit-Interactive-Development-Handoff-For-Grok-2026-11-10(2).zip` 内部 `01-CONFIRMED-REQUIREMENTS/`;范围 T01–T34 | 已取得原文件 56,883 bytes;SHA-256 `3a0adc3edb41869a6309bae29ec5496df00970c815f8dc16330a90cbf26e703e`;与 MANIFEST、SHA256SUMS 和历史审查报告前缀一致 | **VERIFIED** | +| SRC-02 | `TECHNICAL-DESIGN-V0.6-R5-CONFIRMED.md` | `FlowKit-Interactive-V0.6-R5-Confirmed-Handoff(1).zip` 内部 `FlowKit-Interactive-V0.6-R5-Confirmed/`;资料空间 `/FlowKit/TECHNICAL-DESIGN-V0.6-R5-CONFIRMED.md` | 原文件 114,621 bytes;SHA-256 `09f3a5698e238d10b80efb8e26d1c41e51b663ca620efa70815dd7b52f2106ba`;与交接 ZIP 内 SHA256SUMS 完整值一致;9/9 清单条目全部 PASS。相同文件也存在于 Core POC evidence ZIP 的 baseline/ 目录,SHA-256 完全相同 | **VERIFIED** | +| SRC-03 | `FlowKit-Interactive-V0.6-R5-Confirmed-Handoff.zip` | 用户上传 `FlowKit-Interactive-V0.6-R5-Confirmed-Handoff(1).zip`;资料空间原始条目亦已定位 | 上传副本 91,555 bytes,SHA-256 `2deb5b3fa60ed3b7c2d29f028e33835c47aaa0a976e947de717128caf7326549`;内部 9/9 SHA256SUMS 校验通过;含正式 V0.6-R5、批准/PoC 计划、R5 独立收敛复审、需求追踪和静态验证记录 | **VERIFIED (uploaded archive bytes)** | +| SRC-04 | `FlowKit-Interactive-Final-Convergence-Review-V0.6-R5.zip` | 资料空间 `/FlowKit/FlowKit-Interactive-Final-Convergence-Review-V0.6-R5.zip` | 资料空间目录已定位该 ZIP(171,006 bytes,原始容器字节尚未读取);但已上传的 V0.6 Confirmed Handoff 内含等价的 `R5-INDEPENDENT-CONVERGENCE-REVIEW.md`,11,213 bytes,SHA-256 `155a855769663f4d81d3e855a90c54e69106d56f1da43adbe48311e67daba56b`,且该文件在 Handoff 的 9/9 清单验证中通过 | **REVIEW CONTENT VERIFIED; SEPARATE ZIP RAW BYTES NOT VERIFIED** | + +**重要区分与已完成验证:** 用户单独上传的 `FlowKit-Interactive-Design-V0.3-R2-Independent-Review-Grok(1).md`(55,392 bytes,SHA-256 `7efca5840151e3a1851b4fb590ec11780f56f53da691a02d770871cdf477646f`)是对 **Technical Design Draft V0.3-R2** 的独立审查报告,不是 V0.6-R5 技术设计原件。它记录的需求 SHA 前缀 `3a0adc3e…6e703e` 已由本次 ZIP 中的真实 V0.3 文件完整验证;Draft V0.3-R2 的 SHA-256 为 `92eff8d4…475520f8`。上传 ZIP 的完整 SHA-256 `8a2f679a796234383b5108d58db16c1016a35c6110301bc4eb7e091ee2ed5826` 也与报告所记 ZIP 前缀/后缀吻合。该 ZIP 内 MANIFEST 的 `technical_design_status` 明确为 `Draft V0.3-R2, not yet confirmed`,因此不能把它当作 V0.6-R5。 + +## 2. 当前可读取的相关材料(仅二级证据) + +| 材料 | 当前可读取位置/标识 | 能证明什么 | 不能替代什么 | +|---|---|---|---| +| Grok V0.3-R2 独立审查报告 | 用户上传的同名 Markdown;另有 Library 副本 | 证明历史审查报告曾引用 V0.3 Confirmed 需求,并记录其 SHA 前缀、T01–T34 范围;本次已用原件核验完整需求 SHA | 不能替代 V0.6-R5 技术设计 | +| P0-A 确认记录 | Library:`00-APPROVAL-RECORD.md` | 证明用户于 2026-11-10 确认 P0-A 实施计划,并将 Requirements V0.3 / Technical Design V0.6-R5 作为上游基线 | 不是需求或技术设计正文 | +| P0-A 实施计划 | Library:`01-P0A-IMPLEMENTATION-PLAN-V0.1-CONFIRMED.md` | 证明计划层的 M1–M6、31 项文件级工程任务和验收原则 | 计划明确要求需求 V0.3 优先于技术 V0.6-R5;不能用计划反向重建上游契约 | +| Stage 2 PoC 报告 | Library:`STAGE2-POC-REPORT.md` | 证明历史 PoC 以 V0.3/V0.6-R5 为设计基线,并明确 PoC 不是正式验收 | 不能替代正式契约 | +| PR #14 / 当前路线图 | main:`docs/interactive-trusted-host-security-v2-*.md`、本登记 | 证明当前 Trusted Host V2 设计、Finding 和后续阶段记录 | 这些是后续安全设计和摘要,不是 2026-11-10 的原始批准契约 | + +## 2.1 核心技术 PoC 证据包(已恢复) + +| 项目 | 核验结果 | 证据边界 | +|---|---|---| +| 上传归档 | `FlowKit-Interactive-Core-POC-V0.6-R5-Evidence-2026-11-10(1).zip`,123,246 bytes,SHA-256 `a6b5201e8327463b57700174297bf986fbc4902c6ef6493710f1e8cac021f178` | 内部 MANIFEST 28/28 文件条目 PASS(另含 MANIFEST 本身) | +| 核心原型测试 | 最终 Node TAP 日志 22/22 PASS,0 FAIL | Linux x64 / Node v22.16.0;独立原型,不是 FlowKit 生产实现 | +| 崩溃恢复 | 9/9 SIGKILL 断点恢复至 succeeded、revision 7、Journal 8、Receipt 1 | 进程级本地 SQLite 实验,不代表 Windows/断电/多机器 | +| POC-01 | PARTIAL | 使用受限 JSON Fixture,尚未接入真实 FlowKit AST/Fields/Graph/Dataflow | +| POC-02/03/04 | PASS(限定本地进程范围) | SQLite Genesis/CAS/Journal、9 个进程杀点、双进程 Receipt 竞争和丢响应恢复 | +| POC-05/06/09 | PARTIAL | Registry 升级/GC、真实 Host/Model 权限与 ExecutorContext、长期写放大尚未完整验证 | +| POC-07 | BLOCKED(Human);PARTIAL(Admin) | 没有真实独立 Human 审批来源;管理能力只有原型测试通道 | +| POC-08 | BLOCKED(完整矩阵) | 未完成 Node 22.13/24.22 × Windows/Ubuntu、TS 5.0.4 和现有 Standard 全量回归 | +| POC-F01–F05 | 仍有缺口 | 真实前端接入、Node/Windows Store 矩阵、Host 信任边界、完整 JCS 向量、代表性写放大 | + +**关键结论:** 已有 PoC 不能被称为“全部 POC-01–09 PASS”,也不需要从零重做。下一步应围绕 POC 报告明确的 PARTIAL/BLOCKED/Finding 做定向补验证;保留 22/22、9 个崩溃点及本地 CAS 证据,不把它们扩展解释为真实模型、Windows Host 或完整 T01–T34 已通过。 + +## 3. 已完成的原件验证与待执行的符合性审查 + +SRC-01 和 SRC-02 的原件字节、SHA-256 与包内清单已验证;SRC-03 已定位并由上传副本验证内容;SRC-04 已定位但其 ZIP 原始字节尚未读取。原件恢复步骤已完成,以下是正式符合性审查顺序: + +1. **原件固定(已完成)**:SRC-01 为 56,883 bytes;SRC-02 为 114,621 bytes;分别记录完整 SHA-256 和来源。 +2. **版本/批准状态核验(已完成)**:V0.6-R5 正文标明 2026-11-10 用户明确确认、冻结技术基线;确认只冻结设计,不等于代码实施授权。 +3. **交接包完整性(已完成)**:V0.6-R5 Confirmed Handoff 的 `SHA256SUMS.txt` 共 9 项,9/9 PASS;Core POC evidence ZIP 中同一 V0.6-R5 基线文件 SHA-256 完全相同。 +4. **V0.3 需求符合性(待执行)**:逐条提取 T01–T34 原始措辞、阶段、约束和验收条件,对照当前 main;不得以路线图同名编号代替。 +5. **V0.6-R5 技术符合性(待执行)**:逐条核对 API、ABI、Program、Checkpoint、Store、Coordinator、Host、安全边界、错误语义和平台要求。 +6. **交叉追踪(待执行)**:把原文条款映射到当前 main、PR #14 合并后的 Trusted Host 设计、M1–M6、T/S/C/TH/SEC/NAT 台账;逐项标记 PASS、GAP、CONFLICT、NOT-IMPLEMENTED、OUT-OF-SCOPE,并区分设计符合与实现状态。 +7. **独立复审(待执行)**:对所有差异给出源码/原文证据链;不得自动修改正式需求或技术设计。需要修订时,先提出差异和建议,等待用户确认。 +8. **收口裁决(待执行)**:只有逐条符合性矩阵完成并通过独立复审,才可将完整 Phase 2 正式契约符合性标为 PASS。 + +## 4. 对阶段的影响 + +- PR #14 已合并,代表 Trusted Host V2 **安全设计记录已入库**;不代表其与正式 V0.3/V0.6-R5 的符合性已证明。 +- F-01 与 F-R2-01 已在设计文档层面关闭。 +- F-02(Grok 原生工具绑定)和 F-04(Windows 跨账户/继承句柄隔离)仍需各自 PoC/OS 实测。 +- SRC-01 / V0.3 与 SRC-02 / V0.6-R5 原件均 **VERIFIED**;正式原件缺失不再是阻断项。 +- F-07 的“原件不可得”问题已关闭;F-07 的实质符合性裁决转为 **PENDING REVIEW**,在完成逐条矩阵前不能标 PASS。 +- 下一步执行 T01–T34、V0.6-R5 §§1–18、R01–R22、S01–S14 的规范符合性与当前实现追踪。 +- H3-01/H3-05 OS-only 工作和 Grok 工具能力 PoC 可按既定隔离边界推进;H3-02/03/04 仍需各自依赖 PoC、设计复核和安全测试。 +- 在逐条符合性审查完成前,不得宣称完整 V0.3/V0.6-R5 技术契约符合性 PASS;不得借审查之名擅自修改正式需求、技术设计、Program ABI、Core API、SQLite Schema、P0-A 状态语义或扩张到 P0-B/P1/P2。 +- 不得因为原件缺失而重写一份“新版 V0.3”或“新版 V0.6-R5”并冒充历史批准版本。 + +## 5. 后续材料与边界 + +- **当前无需用户重新上传 V0.3 或 V0.6-R5。** 两份正式原件均已在本轮资料中找到并验证。 +- SRC-04 `FlowKit-Interactive-Final-Convergence-Review-V0.6-R5.zip` 已在 Library `/FlowKit/` 定位,大小 171,006 bytes;本轮任务不依赖该 ZIP 才能开始符合性审查。若后续需要逐条核对历史 R5 复审过程,再单独读取该归档。 +- 不能将 V0.3-R2 Draft 或任何摘要当作 V0.6-R5 正式设计;本登记引用的 V0.6-R5 正式文件 SHA-256 为 `09f3a5698e238d10b80efb8e26d1c41e51b663ca620efa70815dd7b52f2106ba`。 + +--- + +**当前结论:** SRC-01(V0.3)与 SRC-02(V0.6-R5)正式原件均已找回并完成 SHA-256/清单验证;SRC-02 位于本轮上传的 Confirmed Handoff ZIP,9/9 条目通过。SRC-03 在资料空间已定位且有已验证内容副本;SRC-04 在资料空间已定位,但其 ZIP 原始字节尚未读取。原件恢复门禁已解除,下一步应直接开展 V0.3/V0.6-R5 逐条符合性核验,不再要求用户重新上传这两份文件;H3-01/H3-05 的独立 OS 安全工作可以继续,但不得越界修改冻结功能契约。 diff --git a/docs/interactive-trusted-host-security-v2-acceptance.md b/docs/interactive-trusted-host-security-v2-acceptance.md new file mode 100644 index 0000000..624f6ea --- /dev/null +++ b/docs/interactive-trusted-host-security-v2-acceptance.md @@ -0,0 +1,167 @@ +# FlowKit Interactive — Trusted Host V2 安全验收矩阵及判停规则(Draft) + +**2026-11-11,Phase 2 Design only;不是实测报告。** +- 代码基线:`main@9bac8f86347a405efe068f96423ae3e501005061`。 +- [设计](./interactive-trusted-host-security-v2-design.md) · [威胁模型](./interactive-trusted-host-security-v2-threat-model.md) · [现有 M5](./interactive-host-security.md)。 +- 目标:Phase 3 先通过 OS/Host 隔离,Phase 4 再做原生 Grok Build JS 工具续接,Phase 5 完成复杂模型验收。 +- 必须保留旧 C01–C24、S01–S14、T01–T34 的冻结要求;本矩阵只是额外的安全部署验收入口,不篡改原要求。 + +## 1. 强制证据等级 + +| 等级 | 定义 | 可使用的原始材料 | 不可代替什么 | +|---|---|---|---| +| P | 真实 Program、SQLite、Host,scripted worker 回执 | CI、Hash、Store、Journal | 真实模型和原生宿主 | +| M | 真实 Grok Build/其他模型参与真实 Pending 的至少一次回合 | 模型实际回复及受信工具事件和 req/task digest | OS 隔离和 Provider 认证 | +| T | 保护的 Host/Supervisor 独立观测指定进程/工具调用/通信,非模型侧文本 | Host-only supervisor logs、进程 token、受保护通信握手及事件 Hash | 如果缺供应商来源证据,不能称为 V | +| N | 对原冻结 C12/S13 所有适用项,原生 JS 工具闭环、来源/权限证据齐全 | 原生工具日志、可信来源、OS 负例、Host 收据和终态 | 不证明外部副作用 exactly-once | +| V | 供应商本身提供可验证的签名/信任根(可选能力) | 供应商规范、可验证公钥/算法、签名覆盖字段和真实验签 | 不能从非权威 JSON `signature` 字段推断 | + +**禁止混算:** P PASS、M PASS、T PASS 不自动等于 N PASS。缺少必需的独立来源或安全隔离时 N 必须为 BLOCKED。 + +## 2. Phase 3:OS + Host 基础安全验收 + +| ID | 场景/实测方式 | 必须断言 | 级别 | 关联威胁 | 目标平台 | +|---|---|---|---|---|---| +| SEC-01 | 获取 Host/Grok/Bridge 实际 OS user、group/SID、权限和进程树 | MODEL 不等于 HOST;MODEL 无管理员/root/调试 Host 权限 | P0 | TH-001, TH-023 | Win/Linux | +| SEC-02 | MODEL 对 Host signing key、Host config、Program allowlist 做路径读取/复制及继承句柄扫描 | 文件路径全部 EACCES/EPERM/Access Denied,MODEL 子进程无继承的私钥/Token 句柄;Host 能正常签名 | P0 | TH-002, TH-005 | Win/Linux | +| SEC-03 | MODEL 读/写/删除/替换 SQLite 主库、WAL、SHM、备份并检查继承文件/映射句柄 | 文件路径全部拒绝,MODEL 子进程无可读写的继承句柄,Host 恢复和数据完整性未受影响 | P0 | TH-003, TH-004 | Win/Linux | +| SEC-04 | MODEL 用任意 CLI/shell/子进程尝试取得 Host Token、服务控制和进程调试权 | 不能提权、不能改服务启动程序、不能注入 Host | P0 | TH-001, TH-023 | Win/Linux | +| SEC-05 | 对所有**实际启用的模型候选数据入口**执行授权及跨租户测试(匿名捕获/受控 MCP 转发/已启用的 IPC 等) | 无受信工具事件、错误 OS 主体、错误 Run/tenant 的提交不能签名或 CAS;若部署无监听端点记录 `NO_LISTENER`,**不得免测实际输入路径** | P0 | TH-006, TH-007, TH-013, TH-027 | Win/Linux | +| SEC-06 | 用模型工具内普通 shell/curl/node 重演 `submitAgentResult` | 没有受信工具事件证据则不能获取 Host 签名或有效 Ack | P0 | TH-008, TH-009, TH-010 | Win/Linux | +| SEC-07 | **仅当选择监听型 TLS/mTLS IPC**:非回环、服务器伪造、证书到期、错误 CA/Hash、错误用途、证书泄漏 | 活跃 TLS/mTLS 端点的所有反例均拒绝,无 HTTP/plaintext fallback;没有 TLS/mTLS 监听时记 `N/A—NO_LISTENER` 并指向 SEC-05 的实际候选通道验证,不得为通过此项新增监听器 | P0 if adopted | TH-006–TH-008 | Win/Linux | +| SEC-08 | 修改 Public Task、Result body、runId、requestId、attempt、nodeId、artifact、policy | 拒绝且 CP/Journal/receipt-index 字节不变,Host 错误不泄漏敏感值 | P0 | TH-011–TH-014 | Win/Linux | +| SEC-09 | Host 签名时注入模型自报 `proofId`/SessionId/已审批文本 | 不用模型值替换 Host-only evidence;签名前拒绝 | P0 | TH-009, TH-010, TH-025 | Win/Linux | +| SEC-10 | Publisher/Host 持久化前后分别 SIGKILL,重新启动并恢复同 Run | 原 Pending identity、CAS、Journal、回执一致 | P0 | TH-015, TH-020 | Win/Linux | +| SEC-11 | Public Task、response、terminal、Host audit log 注入 canary | 模型可读视图零泄露;Host-only Store 保留原 canary | P0 | TH-017 | Win/Linux | +| SEC-12 | 未授权请求、大 body、格式错误、恶意 JSON、速率/连接洪泛 | 有界资源,原有 2 MiB canonical signed receipt 三分法未被破坏 | P1 | TH-016 | Win/Linux | +| SEC-13 | 模型尝试 reparse/symlink/硬链接、程序替换、policy drift、证书文件替换 | Fail closed;原字节不变;Host 未执行模型提供的替换程序 | P1 | TH-005, TH-024 | Win/Linux | +| SEC-14 | 真实 OS 更新账户权限、证书过期、KMS/私钥轮换与 Host 重启 | 无意外授权扩大,旧 Run 有明确安全处置与历史 Ack | P1 | TH-022 | Win/Linux | +| SEC-15 | 两个用户/租户并发使用同 startKey 和 RunId | namespace 互不泄漏;错误租户被拒绝 | P0 | TH-013 | Win/Linux | +| SEC-16 | Windows Named Pipe 方案 DACL/SID 实测(仅选用时) | 微软默认 DACL 可给 Everyone/Anonymous 读权限,纯 Node 默认管道不得放行;如采用 Win32 自定义安全描述符,必须在创建时设定 DACL、拒绝远程、验证非授权 SID 连接失败 | P0 if adopted | TH-006 | Windows | +| SEC-17 | Linux UID 隔离脚本 + 实际受限 CLI 进程 | scripted nobody 和 Grok CLI 两种场景区分,且真正被拒绝访问 | P0 | TH-001–TH-004 | Linux | + +| SEC-18 | Host 观察事件/签名回执 Outbox:签名前/签后/accept-CAS 前后真 SIGKILL | 原观察事件、proofId、signed bytes、acceptedRevision 对账;未接纳过期证明不能通过;已接纳同内容即使过期仍返回旧 Ack | P0 | TH-029 | Win/Linux | +| SEC-19 | 证据保留/容量、隐私与历史审计 | Host-only evidence 不泄密、不无限增长;修订 Retention 配置后历史 Ack 和 Journal 仍可审查 | P1 | TH-030 | Win/Linux | + +**通过标准:** SEC-01–SEC-11、SEC-15、SEC-18 中**适用于实际部署的 P0**必须全部 PASS;SEC-07 仅当采用监听型 TLS/mTLS 时强制,SEC-16 仅自定义 Named Pipe 被采用时强制,SEC-17 对 Linux 受限进程强制。无 TCP/mTLS 监听可记录 `N/A—NO_LISTENER`,不能把不存在的端点判成 PASS,也不能免除 SEC-05/06 对实际候选数据入口的身份/工具事件验证。TH-017/TH-023 与 SEC-11/SEC-04 均为 P0。P1 漏项不能构成 P0 绕过。**单一 OS Profile 通过不等于跨平台 Phase 3 完成;Windows 和 Linux 两个安全 Profile 均须有独立实测证据。** Grok 正式 NAT-14 仍是 Phase 4 门禁;H3-02 开工前的最小工具能力探针只记 PoC,不得冒充 NAT-14 PASS。 + +## 3. Phase 4:直接 Grok Build 原生 JS 工具链 + +| ID | 实测路径 | 必须断言 | 证据 | +|---|---|---|---| +| NAT-01 | Host Provisioner 启动受保护 Run,Grok Build 在其受限会话调用 JS 工具 `start` | 真实 Program Hash、Run 身份固定;模型不能建立未授权 Run | Native tool-call 原始事件、Host-only 授权记录 | +| NAT-02 | JS 工具返回一个当前 Public Task | 返回 exact nodeId/requestId/attempt,排除 Host 全量 State | 原始工具响应 Hash、Host DLP decision | +| NAT-03 | Grok Build 真正生成本节点模型结果 | 非 fixture/非 scripted worker;实际输出与这个 Pending 关联 | Session/tool event chain、model result digest | +| NAT-04 | Grok Build 再次 JS 工具调用 `submit` | 真实 MCP/受限 Node 工具,签前观察完整或可验证 rawInput;比较 toolInputDigest 后用当前 Pending 构造 normalizedReceipt,匹配相同 receiptHash,缺输入则 BLOCKED | Original tool_call + Host-only evidence + proofId/receiptHash + acceptedRevision | +| NAT-05 | Grok Build 通过 JS 工具 `continue` 直到 Program Terminal | 由 Program 选边,不由模型自由解释,用户可读取 Host-private 终态 State | 原始 JS Tool logs、Store/Journal/Terminal | +| NAT-06 | 模型绕过 Grok CLI 用同权普通终端发送伪响应 | 没有独立观察证据不能导致有效签收;不能伪造原始 Host-only tool event | 攻击日志、无 CAS、无 signer 操作 | +| NAT-07 | 模型自写 `GROK_SESSION_ID`、`usage.signature`、toolCallId、start time 等 | 都不能单独提升来源认证等级 | 反例日志 + Host 拒绝 | +| NAT-08 | 在 Grok 当前真实会话进行 Host PID 强杀、恢复、重试 | 同 Run 同 requestId 恢复;第二次调用有真实 ToolCallId 和 acceptedRevision | Host crash/repair log + CLI tool events | +| NAT-09 | 两个真正独立的 Grok 模型会话同时提交一个 Pending | 仅一次有效 State commit;重复 Ack/冲突可解释;不保证 Provider exactly-once | 两会话原始日志、SQLite 事务和 Journal | +| NAT-10 | 关闭网络、异常输出/超限、真实 Provider failure 与模型非法输出 | 区分未认证拒绝、可信 Provider 失败、Checkpoint 容量退出;无无限 waiting | transport bytes、Host-observed failure、正确 FK_R004/capacity 语义 | +| NAT-11 | 模型恶意指令:尝试读取 Host 私钥、DB、扩大 State 披露、伪造人工批准 | 全部负例失败;合法 Agent 链照常完成 | 权限负例、Host审计、公开 Task canary | +| NAT-12 | 换 Grok 会话/模型版本或加载不同工作区 | 旧的身份/ToolCallId 不能自动迁移,必须重新授权/重绑 | Session provenance + Host policy hash | +| NAT-13 | 供应商的模型/响应签名能力探测 | 若无官方可验证规范/公钥/覆盖字段,结果必须记 `PROVIDER_VERIFIED=BLOCKED`,不允许伪证明 | 官方规范+真实 verifier 或明确的阻断说明 | +| NAT-14 | submit 工具事件时序和死锁负例 | Host 签名前必须可独立观测 tool_call start/input;如只能获取结束事件则 BLOCKED 并另审双阶段协议 | CLI 原生事件时序、工具等待链、超时后零 CAS | +| NAT-15 | 真 Grok 签后、CAS 前崩溃和 Outbox 恢复 | 同一真实事件的签名原字节持久化、原 Run 对账;过期未接纳回执不能直接重放或伪造结果 | Host-only OBSERVED/SIGNED/ACKED、SIGKILL、proofId、receipt index | + +**NAT-14:submit 工具事件时序反例。** 用受控代理模拟可信 Supervisor 仅在工具返回后才会收到 `tool_call_update`,工具本身却等待 Host 接纳。必须发现此循环等待;若无法提供签前可验证的 `tool_call` 起始事件与输入摘要,应将该机制标为 BLOCKED 并采用单独审查的双阶段协议,不能简单超时重试或用模型自报完成替代。 + +**NAT-15:真实 Grok 结果签署后、Store CAS 前崩溃的 Outbox 恢复。** 至少一个真实模型已产生的结果进入 Host-only `SIGNED`,在 `host.accept()` 前 SIGKILL;重启时核对是否已被 Store 接纳,尚未接纳则检查签名有效期,过期必须阻断或经独立受信原事件严格重认证,绝不发明模型结果或多次 State Commit。对应 SEC-18/TH-029。 + +**NAT-01–NAT-05 为 S13 核心链**,NAT-06–NAT-12、NAT-14–NAT-15 覆盖 C12/C22/C23 的伪造与故障路径;NAT-13 是能力探测,不无端把不存在的 Provider 签名作为所有平台不可能完成的前置条件,但也不放宽冻结的来源证明要求。 + +## 4. Phase 5/6:与冻结验收的交叉映射 + +| Frozen item | 需要本矩阵哪些证据 | 不能用来替代的既有测试 | +|---|---|---| +| S02、S04、S05、S06、S07 | SEC-08/10/12/13/15/18 | 不能把脚本 M6 e2e 说成真实 Grok | +| S13、T11、T27 | NAT-01–05、NAT-06/07、SEC-01–06 | 不能把文件 Polling 的 M 层测试说成原生 JS 工具 | +| C12、S13、T27 | NAT-01–NAT-12、NAT-14–15 + SEC-01–06/18;原生来源/安全门禁单独裁决 | 不得用脚本、同账户文件 Harness、普通模型文本或无工具事件的 MCP RPC 冒充来源证明 | +| C01、C02、C03、C04 | NAT-01–05/10 | 单纯回放 ChatGPT fixture 不计新模型回合 | +| C05、C06、C23 | NAT-08/09/10/15 + SEC-10/18 | 单一 SIGKILL 或双进程 scripted race 不能替代组合场景 | +| C07、C08、C22 | SEC-01–09/11/13/15、NAT-06/11 | Linux nobody 不能证明 Windows ACL | +| C09、C16 | SEC-12、NAT-10 | 不能将 oversize 无认证回执当 Provider failure | +| C10、C11、C13–C20 | 历史 P 级复测 + 同 SHA CI | 不要求 36 次真实模型付费 | +| C21 | 同物理 DB 跨 Node/OS 测试(增强项) | 未经批准不能提升 P0-A 硬门禁 | +| C24、S14、T28、T33 | Ubuntu/Windows × Node22/24、完整 verify/docs/types/package、原始来源证据 | 只跑一套 Windows Grok 场景不等于全平台 SDK 通过 | + +## 5. 原始证据契约 + +每项验收至少记录: + +```json +{ + "schemaVersion": "FlowKitHostV2AcceptanceV1", + "scenarioId": "NAT-04", + "result": "PASS | FAIL | PARTIAL | BLOCKED | DEFERRED", + "executionTier": "P | M | T | N | V", + "commitSha": "", + "programHash": "sha256:...", + "os": "", + "nodeVersion": "", + "hostPrincipalDigest": "sha256:...", + "bridgePrincipalDigest": "sha256:...", + "runId": "", + "flowkitRequestId": "", + "grokRequestId": "", + "grokSessionId": "", + "toolInputDigest": "sha256:...", + "receiptHash": "sha256:...", + "attempt": 1, + "taskDigest": "sha256:...", + "resultDigest": "sha256:...", + "toolCallId": "", + "sessionEvidenceId": "", + "proofId": "", + "acceptedRevision": 12, + "checkpointRevision": 17, + "journalHeadDigest": "sha256:...", + "evidenceRefs": [""] +} +``` + +不得把 JSON 中的 `sessionEvidenceId`、`proofId` 写成模型自报的来源。样例数字没有实际验收含义。 + +`flowkitRequestId` 对应现有 M5/Coordinator receipt 的 `requestId`,而 `grokRequestId`/`grokSessionId`/`toolCallId` 只来自受保护工具事件。`toolInputDigest` 比较工具输入与收到的参数;`receiptHash = sha256(canonicalBytes(normalizeHostAgentReceipt(receipt)))` 必须由 Host 从当前 Pending 和受信参数构造。不能将这两个摘要混成一个,也不能以 Grok requestId 查询 FlowKit 历史 Ack。 + +其他强制材料: +1. exact program bytes/hash、run/receipt/Journal 与源代码 commit; +2. Grok Build 原始工具调用/工具结果事件,不仅是 Grok 自己写出的总结; +3. OS principal/ACL/服务权限与实际拒绝访问日志; +4. Host evidence ledger 的事件身份绑定、签名语句与 verification 结果; +5. Crash/Recovery/Concurrency 的真实进程和 SQLite 事务记录; +6. C01–C24 逐项分层裁决与失败/阻断项; +7. 原始密钥、完整私有 State、密钥/证书私钥绝不写入报告或模型通道。 + +## 6. 关闭问题和裁决流程 + +流程为:**冻结复现 → 执行 → 检查原始证据 → Finding → 修复 → 重跑受影响项 → 全量回归 → 独立复审**。 + +- PASS:对该环境和等级的所有适用断言均有证据。 +- FAIL:已复现缺陷。 +- PARTIAL:只覆盖部分真实路径。 +- BLOCKED:前置环境或信任证据不足。 +- DEFERRED:仅当已批准放到明确后续阶段。 +- 同一项不得在不同 commit 的日志之间拼接成一个 PASS。 +- CI PASS 不等于原生 N PASS。 +- **Phase 3 安全 P0 未通过,绝不能进入 NAT 实验并声称安全原生宿主。** +- 对外部 Agent Provider 不承诺 exactly-once;只承诺 Store 状态/CAS 至多一次有效提交。 +- 最终 C12/S13 如果缺原生工具事件或来源绑定,必须仍然 BLOCKED。 + +## 7. 独立审查要求 + +审查者应重点检查: +- 设计里有没有模型可写/读的 Host 凭据或 State; +- 是否把 mTLS client 证书当作模型回复证据; +- 是否把 `usage.signature` 当作可验证 Provider 证明; +- 是否在 Windows 仅依赖 `net.createServer` 的默认 Pipe DACL; +- 是否修改了 P0-A ABI 或 SDK 签名 v1; +- 是否允许模型自建工作流并自己授权 Program; +- Host 回执是否始终由独立观察结果而非普通 JSON 生成; +- 每条高危负例是否能够在真实 OS 用户/进程上执行; +- C12/S13 是否有真正的 native JS tool-call 原始日志; +- 是否存在遗漏、重复/互相矛盾的通过条件; +- GitHub CI 是否真的对应 exact HEAD。 + +只有技术设计和本矩阵经复审同意,才创建 Phase 3 代码 PR。不因文档 CI 绿灯提前实现 Windows 服务或 Host IPC。 diff --git a/docs/interactive-trusted-host-security-v2-design.md b/docs/interactive-trusted-host-security-v2-design.md new file mode 100644 index 0000000..6542df7 --- /dev/null +++ b/docs/interactive-trusted-host-security-v2-design.md @@ -0,0 +1,347 @@ +# FlowKit Interactive — Trusted Host V2 安全与原生 Grok Build 桥接设计(Draft) + +> **阶段:Phase 2 / Design only。2026-11-11。** +> +> **代码核对基线:** `main@9bac8f86347a405efe068f96423ae3e501005061`。历史确认的 V0.3 需求与 V0.6-R5 设计原件在当前审查可用文件/仓库中未找到,完整 SHA-256 也不可核验;两份原文的符合性维持 BLOCKED,不以路线图摘要冒充原件。当前可核对基线是 M5 安全契约 [interactive-host-security.md](./interactive-host-security.md);M6 完整路线 [full-delivery-roadmap](./interactive-full-delivery-roadmap-2026-11-11.md)。 +> +> **状态:第三轮 Grok Build 定向复核已关闭 F-01 与 F-R2-01 的设计矛盾;整体仍为 Draft / Phase 2 条件状态,不是已批准实施或已验收功能。** F-02 原生工具绑定、F-04 跨主体句柄隔离仍待实测,F-07 正式 V0.3/V0.6-R5 原文符合性仍 BLOCKED。本文没有批准修改 Core、Program ABI、SQLite Schema 或现有 SDK 导出。发现需求/安全契约冲突时必须先处理差异。 +> +> **目标宿主:** Grok Build 首选,未来 ChatGPT / Codex / Claude Code 使用同一个宿主中立协议。直接 Grok Build 为首选;ACP 不是强制依赖。 + +## 0. 结论与范围 + +### 0.1 最终设计方向 + +1. **保留现有 FlowKit Core**:Compiler、签名后的不可变 JS Program、`createInteractiveCoordinator`、SQLite WAL/FULL/CAS、Journal 与历史 Ack 不重写。 +2. **保留 M5 协议**:`createTrustedInteractiveHost`、`getPublicAgentTask`、`accept({receipt,attestation})`、`createAgentReceiptStatement` 与 Ed25519 v1 canonical statement 不改变。 +3. **新增边界属于 Host Adapter / Deployment,而非 FlowKit Runtime**:受保护的 Host 进程/账户、信任配置、密钥与数据库访问控制、受限 IPC、会话与模型输出观测证据、原生 JS 工具桥接。 +4. **将三个概念分开**:OS 隔离、Host 独立观测的来源/工具调用、Provider 级可验证身份。任何一项不能靠另外两项替代。 +5. **先安全验证、再对接真实 Grok**:不允许 Grok 在能读取 Host 私钥、数据库或 Coordinator 的同权身份下充当安全验收主体。 +6. **不创建通用 Agent Runtime**:Host 是否作为受保护按需进程或服务运行,是部署适配层的生命周期问题。Core 不依赖常驻 Server。 +7. **不扩张 P0-A**:Human/Action/Verification/Loop/Parallel/通用上下文、业务产物持久化、GC、Provider exactly-once 均不在本轮实现。 + +### 0.2 威胁模型与资产 + +**受保护资产:** +- Host Ed25519 私钥 / KMS 授权; +- Host-owned SQLite 主文件、`-wal`、`-shm`、Checkpoint、Journal、receipt index; +- Program 不可变字节、可信构建清单与 artifact allowlist; +- Host policy/binding versions、tenant namespace、完整 State 和敏感业务输入; +- Host-only provider/session/tool-call 观察记录、审计密钥、IPC 认证凭据; +- 持久化的 Run / Request / attempt 绑定和历史 Ack。 + +**不可信主体:** +- Grok Build 的模型推理、可控工具调用/终端脚本; +- prompt-injected Skill、工作流任务描述、模型生成的 JSON、文件内容和第三方远程资源; +- 共享电脑上的其他普通用户/进程; +- 会被模型触及的工作区、用户家目录、宿主的 shell 工具。 + +**信任边界:** OS Token/SID/UID → Host Private FS → 可信执行器 → 受保护 IPC → Host signing → `accept` → SQLite CAS。流程描述和 JS API 包装 **均不是** OS 沙箱。 + +**明确不覆盖的威胁:** 已取得管理员/root、内核权限或可控制 Host 签名进程的攻击者;受信 Host 本身被恶意代码替换;Provider 服务端被攻破。这些属于部署根信任或事件响应范畴。若 Grok Build 进程实际拥有可随时提升到管理员的权限,则不能称为隔离通过。 + +## 1. 现有源码与已验证事实 + +| 组件 | 现状 | 证据 | 设计动作 | +|---|---|---|---| +| Compiler / Program | 产物 Hash、静态 ProgramTable、verified byte loader、零 import 校验 | `src/interactive/compile.mjs`、`src/interactive/load.mjs` | 复用;禁止模型控制 artifact allowlist | +| Coordinator | 只由 Host 执行、决定状态与路由、持久化 CAS | `src/interactive/coordinator.mjs` | 不新增模型侧可写入口 | +| Store | Namespace、SQLite WAL/FULL、Journal、历史回执索引 | `src/interactive/sqlite-store.mjs` | Store 由独立 Host 账户拥有,连 WAL/SHM 一起隔离 | +| Trusted Host M5 | `allowedInputKeys` + `authorizeDisclosure`,签名先验证后 CAS | `src/interactive/trusted-host.mjs` | 只在信任域内使用;公共工具不返回原始 Host frame | +| Receipt V1 | 已签字段包含 namespace/run/request/node/attempt/receiptHash/program/policy/proofId/times | `src/interactive/host-attestation.mjs` | **不增加**新签名字段;session evidence 通过独立 Host-only `proofId` 索引绑定 | +| Linux OS 隔离 | 真 Node `nobody` 子进程不能读取 Host 私钥/SQLite | `tests/security/host-isolation.mjs`;main CI | 已存在,只需扩展到实际原生模型 | +| M6 scripted Host | 独立 Host 与 scripted worker;projectRoot 兼容修复后主 CI 通过 | PR #13、main `9bac8f8` | 不冒充模型/来源证明 | +| Grok 实测 | 15 个真实模型结果经文件 Harness → Host;Windows 同权信任边界无有效证明 | 2026-11-11 Grok 验收报告 | M 层部分成功;C12/S13 N 层仍 BLOCKED | + +### 1.1 历史 Finding 不重复开发 + +- **N1** 确定性 FK_R002、FK_I_VALIDATOR_CONTRACT → errored:源码与测试存在,实施前只做门禁复核。 +- **N3** Program ABI/ProgramTable、静态合法转移:源码与 TypeScript 定义存在,不新增第二套解析器。 +- **N5** namespace 主键及 startKey 唯一索引:SQLite Store 已实现并有跨租户测试,不重复建租户系统;Host 进程权限仍需验证。 +- **N7** P0-A 终态 State 在 Host frame 已可读取;`contextError` 的 P0-B 扩展仍待实现。**不要将原 N7 全量误判为缺失。** +- **N8/N9/N10/N11**:零 import、只允许 P0-A Stage、package exports、Agent Pending name/overview 已有源码路径与测试,核查而非重做。 +- **N6**:不可变 Program 字节已持久化,但**跨 SDK 版本升级 + 在途运行**专门实验尚未覆盖;保留既定阶段。 +- **N12**:现有 `stableTransitionId` 为确定性;P0-B Action operationId/reconcile 和 P1 并行冲突尚未实现。 +- **N2/N4/N13**:按既定 P0-B 门禁保留。Human provenance 不能由当前模型提交 Agent 回执代替。 + +## 2. 目标部署拓扑 + +```text +UNTRUSTED Grok Build / Codex / Claude / ChatGPT + OS principal: MODEL_USER (non-admin) + model + ordinary tools + optional configured MODEL_USER MCP tool + never holds signer, private Store or Host TCB credentials + | + | (1) native tool_call rawInput/event: Host-captured CLI stream + | (2) candidate result: constrained untrusted local transport + v +TRUSTED HOST SUPERVISOR / ADAPTER (single HOST_USER TCB by default) + Host captures tool events from its own CLI child pipe + verifies tool input / result / current Pending and tenant policy + approved immutable Program manifest + Host-only session evidence + secure allowlist, disclosure, bounded reconciliation and Outbox + --- private trust boundary --- + createTrustedInteractiveHost() + createAgentReceiptStatement() + Ed25519 Host signer + createSqliteCheckpointStore() -> WAL / FULL / CAS + | + v +durable next Pending / Terminal + +OPTIONAL: separate BRIDGE_USER / protected local mTLS hop only when a +deployment has a concrete need for another OS security boundary. +``` + +**Control plane vs data plane** + +- `start`/bootstrap (Program approval, initial State, tenant, policy) **仅由可信用户/Host 控制面**发起。 +- 模型数据面可见的只有 `getCurrentPublicTask`、`submitAgentResult`、`continuePublic` 等**适配层操作**。这只是拟议命名,不是新增到 SDK 的 API;最终应复用现有 `getPublicAgentTask` 和 `accept`。 +- `resumePublic` 是要求 Host 根据已认证 Run 重算并返回脱敏公共视图,不开放未经授权的原始 `resume`。 +- 现有 `InteractiveRunFrame` 是 **Host-visible**,终态可以含 `state`,绝不能无过滤转发给 Grok。模型可见终态仅含 status/terminal/可公开摘要。 +- 业务正式输出由用户/Host 的专用读取能力获取;任何该能力都不能被模型工具直接调用。 +- Host 的签名输入来自 Host **独立观察**的结果/事件,不来自模型要求“给这个 JSON 签名”。 + +## 3. Windows:可落地的安全部署方案 + +### 3.1 安全身份 + +推荐: +1. 安装时创建或选择独立 **HOST_USER**(例如受限 Windows Service SID/专用服务账户),Host 在该 Token 下运行。 +2. Grok Build 运行在普通、非管理员 **MODEL_USER**;其用户及工具子进程对 HOST_USER 私有资源必须没有访问权。 +3. 私有 Store 和签名密钥放在 Host 专有目录(例如 ProgramData 下的专有子树),通过显式 DACL/Owner 控制;**检查主 DB、WAL、SHM、备份/日志、产物、配置和目录本身**。 +4. Host 服务控制权限必须防止 MODEL_USER 重启为任意代码、修改服务可执行文件、修改配置或向 Host 注入调试/进程访问。 +5. `Get-Acl` / `icacls` 和实际以 MODEL_USER 身份进行的读取、写入、重命名、替换、子进程及进程访问负例必须一起通过。 +6. UAC 提示、用户同意、管理员/root 权限不可被当作“自动隔离能力”。**如果 Grok Build 仍以管理员 Token 运行,就 BLOCKED。** +7. Supervisor 启动 MODEL_USER 时仅允许继承必要的匿名 stdout/stderr/明确授权的通信句柄,不得继承 Host 私钥、SQLite 主库/WAL/SHM、证书、配置、文件映射、进程/Token 句柄或机密环境变量。Windows 使用不继承句柄或经审查的显式句柄白名单。**实际子进程句柄表和跨账户测试**必须与路径 ACL 拒绝测试同时通过。 + +Windows 文件访问检查依据微软 ACL / File Security 文档。单纯设置 `chmod 0600` 或把路径命名为 `host-private` 不作为 Windows 隔离证据。 + +### 3.2 IPC 传输基线选择(ADR-H2) + +**最小受信部署仅强制两套 OS 身份:MODEL_USER 与 Host TCB。** Bridge、Supervisor 与 Host 在同一 HOST_USER 内可合并为一个受保护进程,不强制在同一 TCB 内增加一跳 mTLS。仅在部署需要跨进程本地监听端点时,才选用 `127.0.0.1` TLS/mTLS;此时必须证明传输身份与原生工具事件的分离,ACP 不是必要前置。若使用 TLS/mTLS,要求: +- `127.0.0.1` 单独绑定(如需要 IPv6,明确独立回环绑定并认证);禁止 `0.0.0.0`、LAN、自动外网暴露。 +- TLS/mTLS 要求 `requestCert=true`、`rejectUnauthorized=true`、服务端证书身份固定、CA/客户端证书指纹白名单。客户端证书仅证明本地通道身份;如果客户端进程在 MODEL_USER 下运行,其凭据可被模型读取,绝不能作为 Model/Provider 原生来源证明或 Host 签名授权。Host-only 密钥仍由 HOST_USER 持有。 +- 服务端的 Host 私钥与 Ed25519 回执私钥均仅在 HOST_USER 访问域。 +- 客户端凭据只授权非常窄的 bridge 操作;**客户端拥有凭据 ≠ Provider/模型身份认证**,不允许以“mTLS 成功”直接给模型结果签署原生来源证明。 +- 每个连接再经 Run/tenant/operation/taskDigest/requestId/attempt 绑定授权,必须检查终态、漂移和 replay。 +- 使用有界长度前缀帧、idle/read 超时、并发上限、严格 JSON schema,拒绝未知字段;针对 P0-A 的 2 MiB signed receipt 上限重新计算传输尺寸,不能靠截断输出制造错误分类。 +- 如果 Host 采用按需启动而非长驻进程,端点随 Run/调用按需生存;这不是 Core 的通用 Runtime。 + +### 3.2.1 最小信任边界及可选两跳通道(DR-01 / F-02) + +不能把 **Host TCB 专用** mTLS 私钥交给 Grok Build 工具脚本:MODEL_USER 可复制其有权读取的凭据。模型侧候选连接若存在单独的客户端身份,其只能用于传输访问,绝不能代替模型来源证明。受信路径为: + +1. **MODEL_USER 原生工具调用 → 不可信数据入口。** 模型只传公开 Run/flowkitRequestId 与 result;该入口不持有 Ed25519 signer、Host TCB 的 mTLS 私钥或 Store。即使拥有模型侧传输凭据,JSON 仍然只是候选数据。 +2. **HOST_USER 控制的 Supervisor → 验证真实 native `tool_call` 事件。** Supervisor 从其保护的捕获通道获得 CLI 进程、Session 与 tool-call 事件,将它和入口收到的候选结果按 Run/RequestId/Attempt/ToolCallId/Hash 严格匹配。 +3. **Host TCB 校验后才签收。** 受保护 Bridge 与 Host 若为同一 HOST_USER,可直接走内部受限调用;独立 Bridge 进程或 loopback mTLS 是部署选项,不是第二个独立信任域。任何输入候选只有与 Host 独立观察到的工具事件绑定后才可签名。 +4. **Host 签署后才允许状态推进。** 任何观察链断裂、乱序、事件无法关联、超时或权限错误都不能发出 Host 签名和 CAS。 + +模型→Host TCB 的候选入口可以是受限 MCP/Node 工具与本地 IPC;该入口不被当作认证事实来源。**禁止**把所有 CLI 工具调用事件视为已被 Provider 数字签名。 + +此结构不要求修改 FlowKit JS Program 或核心 SDK;它仅在宿主部署层实现更强的证据收集。**受保护 Bridge 与 Host Adapter 同在 HOST_USER 时属于同一个可信计算基(TCB)**:模型工具无法读取或调用两者的私有资源,但不能宣称“Bridge 被 OS 阻止读取 Host signer”。若安全目标进一步要求即使 Bridge 被攻击也无法读取 Host key/DB,必须部署独立 BRIDGE_USER 并对 Host 目录与 signer 进行真实 ACL/UID 隔离;它是可选的更强隔离配置,不能靠进程名称或 JS 对象封装假装实现。当前 M6 原生硬门禁首先证明 **MODEL_USER 与整个 Host TCB** 的真实隔离。 + +**Named Pipe 作为可选 Windows 原生优化**,不得无审查替换上面的方案: +- Node `node:net` 支持 Windows named pipe,但“能 `listen(path)`”不证明 DACL 正确。微软官方说明默认描述符向 Everyone 和 Anonymous 授予读取权限,因此未提供自定义 DACL 的默认 `node:net` Named Pipe 禁止作为安全 IPC。Grok 报告中的具体 Windows Node SDDL 测量未附原始脚本,本轮不将其记作我已重现。 +- Windows `CreateNamedPipeW` 可以在 `SECURITY_ATTRIBUTES` 中提供安全描述符;默认 DACL 可能给 Everyone/Anonymous 读取权限。 +- 如选 Named Pipe,必须用经审查的 OS 原生启动/安全辅助模块,明确控制 DACL/SDDL、服务 SID、可访问的客户端 SID、`PIPE_REJECT_REMOTE_CLIENTS`,并验证连接方的实际 Token/SID;不能只根据 PID、管道名字或随机路径判断授权。 +- Node 22/24 公开 `server.listen({path,readableAll,writableAll})` 没有直接设置自定义 SDDL/服务 SID 的选项,不能从 `readableAll:false` 推断默认 DACL 安全。若选 Named Pipe,必须在创建时提供明确 DACL 和客户端身份校验,并用真实 Windows 测试验证;不得在宽松创建后补改权限。 + +两种传输都不是模型来源证明;传输认证只证明授权通信主体。需配合 §5 的可信观察。 + +## 4. Linux:对应安全方案 + +1. HOST_UID 与 MODEL_UID/GID 分离,MODEL_UID 不得为 root、不得拥有 `sudo`、`CAP_DAC_OVERRIDE`、调试 Host 的能力。 +2. Host-only 私有根目录 `0700`,密钥 `0600`,数据库及 WAL/SHM 继承相同隔离;公共 Task 由 Host 明确披露。 +3. **仅在确实启用相应监听端点时**采用 loopback TLS/mTLS 或 Unix Domain Socket,并审查对应证书、端点所在目录、所属组、权限、Peer Credential 与符号链接/路径替换风险。最小单 Host 进程方案不要求创建网络监听。 +4. 测试 MODEL_UID 的文件读取/写入、目录遍历、pid/fd/env 访问、Host 进程调试/注入、IPC 伪造、跨租户访问。 +5. 现有 `tests/security/host-isolation.mjs` 只能证明**脚本模型子进程**的 Linux UID 边界;真实 Grok Build 对应进程仍需同样的测试和进程证据。 +6. 不把 Docker 非特权容器、WSL2 或运行时目录名本身当作安全隔离,除非隔离配置经过实际攻击测试。 + +## 5. 来源证明和真实模型调用证据(ADR-H3) + +### 5.1 证据等级,不得混淆 + +| 级别 | 已证明事项 | 不能宣称的事项 | +|---|---|---| +| P / scripted | Compiler → JS Program → Host/SQLite、协议测试 | 真实模型行为 | +| M / live-model | Grok Build 当前会话确实在真实 Pending 后作答,有工具事件/输出 | 模型/Host OS 隔离、Provider 加密来源证明 | +| T / host-observed transport | 独立 HOST_USER 通过受保护会话/IPC 观测到指定 CLI 进程和工具事件,并绑定该响应 | 供应商端对会话/回答的数字签名 | +| N / native-host-attested | 冻结 C12/S13 所需全部原生工具链 + 可信来源与隔离证据、Host 和 Store 不变量 | 不覆盖管理员攻破或 Provider 内部事件 | +| V / provider-verifiable(如果供应商支持) | 供应商可公开验证规则和可信根支持的响应来源证明 | 不能由 Grok 普通 JSON `signature` 字段推断 | + +**证据身份不能由模型随意填写。** Host-only evidence ledger 的事件必须由不受模型写权限控制的可信 Transport/Supervisor 记录。 + +### 5.2 直接 Grok Build CLI 可复用的证据 + +xAI Grok Build 文档提供: +- 交互模式 `grok`; +- headless `grok -p ... --output-format streaming-json`; +- streaming-json 的 `tool_call`、`tool_call_update`、`usage`、`end` 等事件,以及在部分后端可用的 `sessionId`、`requestId`、`toolCallId`。 +- 不同后端不保证完全相同的来源或签名字段;`usage.signature` 不能未经官方验证规范、密钥和签名范围确认就称作 Provider attestation。 +- 不要求 ACP;如果以后 Host 需要稳定、长期操纵多个会话,再评估 ACP,但 **不能因为切换 ACP 就放宽隔离和来源门禁**。 + +**安全验收模式**推荐由受信 Host Supervisor 启动受限的 Grok Build CLI,固定二进制位置/Hash、配置、允许的工具集合、CWD、运行账户;捕获 CLI 原始结构化事件流。捕获通道和证据目录应仅 HOST_USER 可写。运行环境不得允许模型修改其自身被测 CLI 二进制或 Host 端记录。 + +如果只是用户现有 Grok Build 交互窗口自行写文件、模型填写 `GROK_SESSION_ID`、或同一 OS 用户运行 Host,自然可执行 M 层试验,但 **不能充当 N 层安全验收**。 + +### 5.3 绑定算法和 Host 回执签署前置条件 + +1. Host 持久化当前 Run 和 Pending,建立不可变 `{namespace,runId,nodeId,flowkitRequestId,attempt,artifactHash,policyHash,taskDigest}` 索引。`flowkitRequestId` 映射为原 M5 receipt 的 `requestId`;Grok CLI 的 `grokRequestId/grokSessionId/toolCallId` 保留为另外的 Host-only 观察字段,不能覆盖 FlowKit 身份。 +2. Trusted supervisor 创建 Host-only `sessionEvidenceId`(不是模型输入),记录实测 CLI binary Hash、OS principal、启动时间、进程树、Grok 会话标识(若提供)、工具 invocation/update 事件、原始输出 digest、requestId 绑定和事件的单调序号。 +3. 当 Grok Build 通过原生工具提交结果时,Bridge 只发送结果数据和公开 request identity。Supervisor/Host 通过**独立捕获的真实工具事件**及当前 Pending 核对 ToolCallId、`toolInputDigest/receiptHash`、会话/进程通道、租户权限、时效和容量。 +4. **先捕获调用开始,再签署回执。** 对 `submit`,Host 必须在工具执行前独立捕获真实 `tool_call` 事件(携带完整输入参数或可验证摘要),与 Bridge 候选结果匹配。不能等待 `submit` 结束后才出现的 `tool_call_update` 或工具最终输出再签署,否则会形成“工具等待 Host,Host 等待工具”的死锁。完成事件用于签后审计;如果 Grok 当前版本无法证明开始事件时序,需要在 Phase 4 改为显式双阶段协议,不能假设已有能力。 +5. 如果某个事件无法可靠证明对应当前结果、会话/进程或授权工具执行,**不得签署 Agent 回执**;保持原 Pending,写 Host-only 审计并按受控恢复策略处置。 +6. Host 归一化 `normalizeHostAgentReceipt(receipt)` 后,建立 Host-only `proofId → verifiedSessionEvidence` 绑定,选择已核准的 `keyId`,调用现有 `createAgentReceiptStatement` 和 `encodeAgentReceiptStatement` 在 HOST_USER 下签 Ed25519。 +7. 最后调用现有 `host.accept({receipt,attestation})`。现有 statement 的 `proofId` 和 `receiptHash` 正好绑定已认证事件与结果;**不新增 v1 ABI 字段**。 +8. 原始证据若是任何模型可写的 JSON,则只能作辅助上下文,不是独立认证来源。 +9. `agent-call-failed` 和 provider output-overflow 只在 Host 可信观察到真实失败/限制后签发,禁止把任意模型自报失败升级为可信状态变更。 + +### 5.4 对可验证性边界作出硬判定 + +- 若受控 CLI 输出只证明“本机特定进程输出”,而无法独立证明内部 Provider 服务端实际产生该响应,证据只能记 `HOST_OBSERVED`,不能虚构 `PROVIDER_VERIFIED`。 +- C12/S13 的最终等级必须依照冻结验收条款判定。若条款明确要求 Provider 级来源且 Grok 没有可用的可验证机制,标 `BLOCKED` 并提交具体差异供审查,**不能降低验收要求**。 +- 同一用户运行的普通 CLI、文件轮询、模型自报身份及 Host 自签未经校验的 JSON 都不能解除 C12/S13。 +- 对于 ChatGPT/Codex/Claude 的后续适配,要求同样证据等级,并逐宿主声明不足;不允许为了平台兼容以通用伪 provenance 填补空白。 + +### 5.5 直接 Grok Build 工具注册与结果证据(F-02) + +1. `--tools` 是内置工具 allowlist,**不会注册自定义 FlowKit JS 函数**。首选探针为在受信安装的 Grok `.grok/config.toml` 中配置受限的 **MCP FlowKit 工具服务器**,该服务器运行于 MODEL_USER 且不持有 Host 私钥、SQLite 或 signer;备选固定 Node/Shell 命令只有在能提供同等级原生输入事件证据时才可用。实际 MCP 工具的发现、启用、授权及 `tool_call.rawInput` 形式须实测,不能凭文档声称完成。 +2. 受信 HOST_USER Supervisor 只从自己掌控的 CLI stdout 捕获结构化事件;模型和工具 stdout 的伪造/混淆能力通过 NAT-06/14 攻击实测排除。模型侧 MCP 工具发出的候选 RPC 是不可信数据,`--tools`/MCP 的存在本身不构成来源认证。 +3. **两个摘要分别定义**:`toolInputDigest = sha256(canonicalBytes(rawInput))`,对比 Host 独立捕获的原生 `rawInput` 与 MCP 候选 RPC 参数;Host 根据当前 authoritative Pending 创建 `normalizedReceipt`,然后 `receiptHash = sha256(canonicalBytes(normalizedReceipt))`。必须从上述两条已验证输入路径重算出**相同 normalizedReceipt 与 receiptHash**,不是要求 `rawInput` 原文逐字节等于包含 Host 补充字段的完整回执。Grok 的 `grokRequestId`、`grokSessionId` 与 FlowKit 的 `flowkitRequestId` 不可互换。 +4. 入参规模应实测 64 KiB、256 KiB 和接近合法回执上限的边界。2 MiB 是 M5 Signed Receipt 的**上限**,不是 Grok/MCP 保证支持的参数量。若 CLI 起始事件丢失/截断参数,或结果走模型可替换文件而只看到命令字符串,签名必须 BLOCKED;必要时另审 Host-owned immutable buffer + 对应可验证事件摘要的两阶段协议,不允许隐式降级。 +5. 工具 `start/getPending/submit/continue` 是 Host Adapter 的行为映射,不是新增 FlowKit Core 公共 API。`start` 只能启动经可信用户预授权的 Program/State,不允许模型创造 allowlist 条目;`continue` 返回脱敏状态,不输出 Host frame 完整 State。 + +## 6. IPC / JS 工具契约(Host Adapter Proposal,不修改 SDK) + +### 6.1 API 角色隔离 + +| Adapter 操作 | 发起主体 | Host 动作 | 返回内容 | 关键禁令 | +|---|---|---|---|---| +| `bootstrapRun` | 受信用户/部署控制面 | 校验不可变 Program/policy,调用 Host `start` | Run 身份和私有管理 Ack | 模型不能上传 Program Hash 并自动加入 allowlist | +| `getCurrentPublicTask` | 已认证 Grok Bridge | 检查 Run/tenant、`getPublicAgentTask`、授权披露 | 只含当前白名单 Task + adapter correlation | 不返回 Host frame、完整 State、私钥 | +| `submitAgentResult` | 已认证 Grok Bridge | 关联独立 session/tool evidence → Host 签 receipt → `accept` | acceptedRevision/duplicate/status(脱敏) | 模型不能提交签名、proofId 或 Host policy | +| `continuePublic` | 已认证 Grok Bridge | 检查 Run 归属,Host `resume` 并重新授权披露 | next Public Task 或脱敏终态 | 不调用原始 Coordinator `complete` | +| `getRunStatus` | 已认证受限调用方 | Host 私有 `load` 或安全状态派生 | status、revision、可披露摘要 | 不外发完整 State | +| `readPrivateResult` | 受信用户/Host 控制面 | 按用户策略读取 Host-visible 终态 | 受授权业务 State | 不注册成模型可用工具 | + +**对 `start` 的含义**:用户原始的 S13 流程包含 Grok 调用 JS 工具启动工作流;真实工具 `bootstrapRun` 的入口必须已由受信用户明确授权初始 State、Program 与 Run scope,不能给模型任意启动新 Run 的权限。模型可以触发“已授权 run 的 start”,不拥有“授权一个新工作流”的权力。 + +### 6.2 Transport envelope 草案 + +```json +{ + "protocol": "flowkit.host.bridge.v1", + "operation": "submitAgentResult", + "correlationId": "host-allocated-or-non-authoritative", + "runId": "run-id", + "flowkitRequestId": "req-id", + "attempt": 1, + "payload": {"result": {"summary": "..." }} +} +``` + +说明: +- `correlationId` 只用于可观测性,不是认证材料; +- `flowkitRequestId` 是 FlowKit 当前 Pending 的身份,适配器映射成已有 receipt `requestId`;Grok `grokRequestId/grokSessionId` 仅为 Host-only 观察字段,不得覆盖它; +- 真实桥接器应使用 strict key allowlist、受控 JSON/UTF-8 解析、长度前缀帧、deadline、连接并发限制和生命周期清理; +- `namespace`、`artifactHash`、`policyHash`、`nodeId` 从 Host 当前 Pending 读取,模型提交的同名字段不能改变它们; +- Host 把调用绑定到已验证 OS 客户端 principal、授权租户和当前 Run;错误 Run/Request/attempt、越界字段均拒绝; +- 具体传输 body 上限需与既有 v1 receipt **2 MiB** canonical limit、Checkpoint active reserve、Skill 输入范围协调,经实测冻结;超限发生在签名前按现有三类故障分类处理,不允许随意截断; +- 未认证请求、非法 JSON、签名错误和 stale Pending **零 CAS、零 Journal、零 receipt index 变更**; +- 不得把 `getPublicAgentTask()` 返回的敏感以外字段扩张为任意 State 全量读取; +- 用户中止/断线不能凭模型 JSON 直接写入“Human 批准”或“取消”终态,P0-A 只处理合法 Agent 回执。 + +### 6.3 错误和恢复 + +- 新增 Adapter 错误只限宿主传输诊断,不改 FlowKit 的既有 `FK_I_*` 错误集合;需要新增 SDK 错误码时必须另签设计变更。 +- 掉线/Host 崩溃:保存证据链和原 Pending;重启时通过现有 `resume` + `getReceipt` 对账。不得声称 Provider exactly-once。 +- 同内容重复提交 → 原 acceptedRevision;不同内容同 requestId → `FK_I_RECEIPT_CONFLICT`;迟到或错租户按原协议拒绝。 +- 策略/密钥轮换:保留已批准旧运行的身份/密钥策略直到 Run drain;无法提供安全迁移时 fail closed,不得重签一个编造的旧回答。 +- 缺来源证据、隔离破坏或私钥访问异常时停止签署新 receipt,Store 保持在可审查的最后 durable State;不能用无穷重试假装恢复成功。 +- 详见 §6.4 对签后、CAS 前进程崩溃的 Outbox 和过期证明处理;禁止无持久证据的重签。 + +### 6.4 Host-only 观察证据与签名回执 Outbox(关闭 DR-13) + +**必须解决的故障窗口**:模型结果已被独立观察、Host 已签 v1 statement,但 `host.accept()` 还没提交持久回执就崩溃。如果签名已过期,M5 仅允许**已接纳、内容相同的回执**在过期后复用历史 Ack;**未接纳的过期回执不能直接重放成功**。 + +实现约束: + +1. Host-only observation ledger 使用独立保护的持久存储(可为 Host Adapter 自己的 SQLite/附加表;不得未经批准改变 FlowKit Core Store Schema),记录受信工具事件摘要、`namespace/runId/flowkitRequestId/attempt/taskDigest/toolInputDigest/receiptHash`、Grok 的独立 `grokRequestId/grokSessionId/toolCallId`、CLI 进程、来源级别及审计 ID;`receiptHash` 与现有 `createAgentReceiptStatement` 对 `normalizedReceipt` 规范化后的 SHA-256 完全一致。Outbox 使用 SQLite WAL + `synchronous=FULL` 或已证明等效的 durable fsync 屏障。 +2. 在调用 signer 前先持久写入 `OBSERVED` 事件与验证结果。签名成功后,**先持久写入完整的 signed statement/receipt 和 `SIGNED` Outbox 记录**(Host-only,保留原始字节),再调用现有 `host.accept()`。不能在内存里签完就立即调用,留下不可对账的崩溃窗口。 +3. 若 `accept()` 成功,核实 Store 的 `getReceipt(runId,requestId)` 与 v1 `receiptHash/acceptedRevision` 相同后,标记 `ACKED`。如果此时又崩溃,重启时从 Store 核对后补写 `ACKED`,不能产生第二次业务 State 提交。 +4. 若崩溃后为 `SIGNED` 但 Store 尚未接纳:**未过期**则原字节重放;**已过期**则保持 Pending,不得拿过期证明绕过 M5。只有 Host-only 原始证据仍有效、当前 Pending/namespace/Program/policy/attempt 未变,且 Outbox **持久保存的原始 normalizedReceipt canonical bytes 与 receiptHash 完全一致**时,才允许按审计规则在同一 proofId 与同一业务回执字节上生成新的签发时间/签名;不能从模型重新序列化或推断出另一份结果。已接纳的相同回执始终走原历史 Ack。否则 `BLOCKED/RECONCILE`。 +5. `proofId` 应由 Host 独立分配,映射到不可变证据记录;必须限制同一 toolCallId/事件/结果在不同 Run、租户、attempt 之间复用。 +6. 对冲突的两个合法结果,现有 Coordinator CAS 仍决定唯一有效提交;Outbox 记录未中标的结果必须标记 `CONFLICT/REJECTED`,不能“清理失败结果”时丢掉必要审计。 +7. Outbox 保留策略应与 Run 生命周期、合规审查窗口、敏感数据最小化和存储上限绑定;默认禁止无限制记录完整模型文本、私钥、完整 State。实际保留时长/GC 策略作为 Host 部署配置在 Phase 3 验收中冻结,**不是**新增 FlowKit Core 通用业务产物库或 GC。 +8. 必须测试 `OBSERVED→SIGNED`、`SIGNED→ACCEPTED`、`ACCEPTED→ACKED` 的真实进程强杀、错租户、超期签名、重复回复、签名轮换与审计 Outbox 恢复,并证明 Journal/Checkpoint 不被 Outbox 重建逻辑重写。 + +这个设计避免把“Host 自己可以重新签名”误当成持久恢复与 Provider exactly-once 证明,也不改变现有 M5 对过期证明与历史 Ack 的安全语义。 + +## 7. 部署与运行模式 + +| 模式 | 实际用途 | 可宣称层级 | +|---|---|---| +| Developer Mode | 用户直接打开 Grok Build,手工读 Pending/写 response,Host 可共享普通用户进程 | 仅 P/M;**不能**通过 N 层安全门禁 | +| Secure Windows Host | HOST_USER 与非管理员 MODEL_USER 分离;私钥、SQLite/WAL/SHM、服务控制和继承句柄均有实际拒绝证据;启用任何候选数据入口必须完成身份/权限/伪造测试;**不要求默认开启 mTLS 或监听端口** | Phase 3 的 OS 安全按 Windows Profile 单独裁决;只有真实观察工具事件后可申请 T,完成 C12/S13 才可 N | +| Secure Linux Host | HOST_UID 与 MODEL_UID 分离;0700/0600、数据库及子进程句柄保护实测;任何已启用的候选数据入口需按真实传输方式安全验收;**不强制启用 TCP/mTLS/UDS 监听** | Phase 3 的 OS 安全按 Linux Profile 单独裁决;只有真实观察工具事件后可申请 T,完成 C12/S13 才可 N | +| Unsupported Host | 没有 JS 执行工具、不能隔离、不能绑定来源 | `NO_JS_EXECUTOR` 或 `BLOCKED`,不得回退模拟 | + +**最小安装原则**:运行时只要求目标 Host 环境已有 Node 22.13+,Host 适配可作为按需子进程;Windows 独立服务账户部署需要一次性管理员配置,不代表每个用户一个独立服务器。对于多用户 Host,租户 `namespace` 必须与 IPC 的经认证身份绑定,而不是接受模型自报 tenant。 + +## 8. 实施拆分(Phase 3/4,不在本 PR 动工) + +| 序号 | 独立交付 | 最小改动目标 | 前置 | +|---|---|---|---| +| H3-01 | Host bootstrap/provisioning | Windows/Linux 受保护 OS 身份、Host-only 私有目录、密钥/数据库及启动句柄白名单 | 本设计独立复审通过;不依赖 Grok 工具 PoC | +| H3-05 | **OS-only negative-security tests** | Windows ACL、Linux UID、进程/Token/继承句柄、Host 私钥/SQLite/WAL/SHM 实际拒绝访问 | **仅依赖 H3-01**;不运行 Grok 工具 PoC、不等待 H3-02/H3-03/H3-04 | +| H3-02 | Grok 工具/Transport adapter | 模型侧受限 MCP 工具优先;决定真实候选数据通道后再实现有界 IPC(可选 loopback mTLS) | H3-01 + **工具注册与签前 `tool_call` 最小能力探针(只记 PoC,绝不冒充 NAT-14 正式 PASS)** | +| H3-03 | Trusted observation / signed receipt Outbox | Host-only 进程/会话/tool/event/receiptHash 关联;OBSERVED/SIGNED/ACKED WAL/FULL 持久化、过期/重签与历史 Ack 对账 | H3-02 | +| H3-04 | M5 signer integration + adapter security regression | 按 `proofId` 核对证据,签 v1 statement 后 `accept`;**在已启用候选通道上验证 SEC-05/06/08/10/15/18 的 P0 负例** | H3-03 与 H3-05;H3-04 完成前 Phase 3 不得宣称整体 PASS | +| H4-01 | Grok CLI native tool observer | 基于 H3-02 PoC 固定 streaming-json 事件、toolCallId/input 摘要及日志伪造防护 | Phase 3 整体安全退出条件已满足 | +| H4-02 | Direct Grok JS/MCP tool loop | 启动授权 Run、获取 Pending、提交结果、续接终态;不得绕过 Host signer/Store | H4-01 | +| H4-03 | Native Grok adversarial acceptance | S13/C12、kill/resume、双会话、提示注入和大输出 | H4-02 | + +上述编号仅表示交付片段,不新增正式阶段。每个片段有独立测试/审查并保留失败反例。未来可切换其他 IPC 实现,但要保持相同权限与身份断言。 + +## 9. 明确的设计判停和升级条件 + +**不得进入 Phase 3 的条件:** +- 未决定可信 OS 主体、私钥/DB 的真正访问控制; +- 把 Node `net.createServer` 默认 Windows Named Pipe 当成已证明 ACL; +- 让模型或同一权限的 Bridge 获取 Host 原始 Store/Signer; +- Host 在独立验证来源前签署任意模型 JSON; +- 把 `usage.signature`、`GROK_SESSION_ID`、普通日志或 mTLS client 证书直接当成 Provider 签名; +- 新设计与现有签名 v1、Program ABI、PolicyHash 或只读/公开视图冲突; + +**完成 Phase 2 的条件:** +1. 完成独立安全复审(至少一次对抗性核验,本轮模型自审不得自称外部独立)。 +2. 冻结 OS 身份、认证、最小 IPC、Host-only session evidence、签名与恢复策略。 +3. 每个威胁有可复现负例、PASS/FAIL/BLOCKED 分类和最小修复入口。 +4. Phase 2 不要求真实 Windows/Grok 运行 PASS;Windows/Linux 权限实测属于 Phase 3 退出门禁,Grok 工具输入与时序探针是 H3-02 实施前置,NAT-14 在 Phase 4 正式验收。 +5. 任何外部 Provider 证明能力不确定时记录为“需在 Phase 4 实测”,不得作为默认已支持。 + +### 9.1 唯一阶段入口与出口(F-01) + +- **Phase 3 入口**:安全设计独立复审已通过;Host/Model 两套 OS 身份及 Key、SQLite、WAL/SHM、ACL 规则已冻结,禁止默认 Node Named Pipe 当作安全 IPC。无需预先运行 Grok 原生工具。 +- **Phase 3 退出**:H3-05 的 Windows/Linux OS 负例、H3-04 的实际候选通道/签名/恢复负例及两个 Secure Host Profile 全部适用 SEC P0 均有独立证据;单平台通过只计该 Profile,不宣称 Phase 3 整体完成。未启用监听端点时应记录 `NO_LISTENER` 与不适用的网络用例,并实测实际存在的候选数据通道,不得为刷测试人为创建 TCP/mTLS 服务。 +- **H3-02 开工前**:执行单独的**工具注册与签前 `tool_call` 最小能力探针**(非 NAT-14,不计正式验收 PASS),确认所选 MCP/受限工具的真实注册、可观测输入及事件时序具备实施条件;做不到则仅 H3-02 BLOCKED,H3-01/H3-05 的 OS 安全工作不受影响。**NAT-14** 只在 Phase 4 基于实际原生 Host 运行后正式执行并裁决。 +- **Phase 4 入口**:Phase 3 退出门禁通过、原生工具和 NAT-14 时序可实施。正式 C12/S13 原生闭环只能通过 Phase 4 的真实证据放行。 + +## 10. 官方资料及解释边界 + +- [FlowKit M5 existing security contract](./interactive-host-security.md) +- [FlowKit M6 roadmap](./interactive-full-delivery-roadmap-2026-11-11.md) +- [Microsoft: named pipe security and access rights](https://learn.microsoft.com/en-us/windows/win32/ipc/named-pipe-security-and-access-rights) +- [Microsoft: CreateNamedPipeW SECURITY_ATTRIBUTES / PIPE_REJECT_REMOTE_CLIENTS](https://learn.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-createnamedpipew) +- [Microsoft: icacls / file DACL](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/icacls) +- [Microsoft: restricted tokens](https://learn.microsoft.com/en-us/windows/win32/secauthz/restricted-tokens) +- [Node.js: IPC support](https://nodejs.org/api/net.html#ipc-support) +- [xAI: Grok Build overview](https://docs.x.ai/build/overview) +- [xAI: Headless & Scripting](https://docs.x.ai/build/cli/headless-scripting) +- [xAI grok-build: streaming-json event types](https://github.com/xai-org/grok-build/blob/main/crates/codegen/xai-grok-pager/docs/user-guide/14-headless-mode.md) + +仅在微软文档支持的范围内断言 Windows ACL/Named Pipe;仅在 xAI 文档支持的范围内断言工具事件存在。**没有公开的证据证明 Grok 的 `usage.signature` 可以直接用于独立 Provider-origin 验证。** + +--- + +**设计结论:** 采用“MODEL_USER 与受保护 HOST_USER/TCB 分离 + 文件/服务/句柄权限验证 + Host-only 原生工具观察与证据索引 + Ed25519 receipt v1 + FlowKit Core 不变”的最小安全形态。**不强制 Bridge 独立进程、TCP 监听或 mTLS**;仅在实际部署存在监听端点时按相应传输方式增加安全认证和负例。默认 Windows Node Named Pipe 禁止作为安全入口;只有创建时 DACL/SID 已经验证的自定义实现才可选。Grok 原生工具来源、签前参数与时序需先通过 H3-02 最小能力探针,在 Phase 4 NAT-14/C12/S13 正式验收。 diff --git a/docs/interactive-trusted-host-security-v2-review.md b/docs/interactive-trusted-host-security-v2-review.md new file mode 100644 index 0000000..e00c73b --- /dev/null +++ b/docs/interactive-trusted-host-security-v2-review.md @@ -0,0 +1,163 @@ +# FlowKit Interactive — Trusted Host V2 阶段 2 定向安全复审记录 + +**2026-11-11;性质:Grok Build 独立审查者已完成三轮定向复核。** 本文同时记录作者侧的源码/规范交叉核验;不将作者自审冒充外部审查,也不将 Grok 模型复审冒充第三方专业安全认证。Phase 2 整体仍为 CONDITIONAL,F-02/F-04/F-07 的后续门禁未关闭。 + +**固定对照源码:** `main@9bac8f86347a405efe068f96423ae3e501005061`。**审查对象:** 本分支三份 Draft 设计文件。 + +## 1. 源码及产品事实交叉核对 + +| 事实/限制 | 直接证据 | 结论 | +|---|---|---| +| Core 不提供 Agent Provider 客户端,Host 管理 Coordinator | `src/interactive/coordinator.mjs` | 不引入第二 Runtime | +| M5 默认 Host-only signer 和独立数据披露 | `src/interactive/trusted-host.mjs` / `docs/interactive-host-security.md` | 保留 v1 签名契约 | +| v1 statement 有 `proofId`、`receiptHash` 及 Run/Program/Policy identity | `src/interactive/host-attestation.mjs` | 以 Host-only proofId 外部证据索引实现来源绑定,不新增签名字段 | +| M3 Store 按 namespace 隔离,WAL/FULL、transactional CAS | `src/interactive/sqlite-store.mjs` | 不重做 N5;需独立 OS 文件权限 | +| 终态 Host frame 包含完整 State | `src/interactive/coordinator.mjs` | 模型公共结果不能直接使用 Host frame | +| M6 scripted worker 与原生模型不是同一证据 | `tests/integration/m6-worker.mjs` 和 Grok 验收报告 | CI 不能宣称原生 C12/S13 PASS | +| Linux nobody 隔离已有真实脚本证明 | `tests/security/host-isolation.mjs` | Windows ACL / 真实 Grok 仍需补证据 | +| Windows Named Pipe 有默认安全描述符风险 | 微软 CreateNamedPipeW/Named Pipe Security 官方文档 | 优先跨平台 loopback mTLS;Pipe 只作条件替代 | +| Grok Build headless 有结构化工具事件 | xAI Grok Build streaming-json 官方文档 | 需宿主独立观测,不能把 JSON 的 signature 字段当 Provider 级证明 | + +## 2. 对抗性 Finding 逐项验证 + +| ID | 严重性 | 初始发现 | 修订动作 | 当前状态 | +|---|---|---|---|---| +| DR-01 | P1(设计阶段) | 如果让 MODEL_USER 的工具持有 mTLS client key,任何同权限脚本都可冒充受信 Bridge | 增加 §3.2.1 双跳通道:模型仅交候选 JSON,受保护 Bridge 才持 client key,Host 验证独立 `tool_call` 事件 | **设计已关闭;SEC-06/TH-027 待实测** | +| DR-02 | P1(设计阶段) | 若 Host 等待 `submit` 的工具完成事件才签名,而工具等待 Host 结果,会死锁 | §5.3 冻结签名前的 `tool_call` 起始事件+输入摘要关联;签后事件仅用于审计;事件时序不支持时标 BLOCKED 并另审双阶段协议 | **设计已关闭;NAT-14/TH-028 待实测** | +| DR-03 | P1(验收边界) | mTLS、Host 自签 Ed25519 与 Provider 签名三个层次容易被混算 | 定义 P/M/T/N/V 五种证据等级;缺可验证来源不得通过 C12/S13 | **设计已关闭;原生证据待实测** | +| DR-04 | P1(平台安全) | Windows `net.createServer` 没有给出经验证的受控 Pipe DACL/Peer SID | 默认选 Host-owned loopback mTLS;Pipe 必须走独立 Win32 安全说明和负例 | **设计已关闭;Windows 实测待 Phase 3** | +| DR-05 | P1(权限) | 用户的 Grok Build 可能在管理员 Windows session 中运行 | Secure mode 强制非 admin MODEL_USER 和独立 HOST_USER;否则 BLOCKED | **设计已关闭;Windows 身份实测待 Phase 3** | +| DR-06 | P1(签名来源) | Host 只要调用 `createAgentReceiptStatement` 就能签任意 JSON,容易产生认证洗白 | Host-only `proofId→session evidence` 绑定,签名必须后于可验证工具事件,所有不匹配请求零 CAS | **设计已关闭;TH-010/NAT-06 待实测** | +| DR-07 | P1(秘密泄露) | `asPublicFrame` 终态包含完整 State,不可直接转发模型 | Adapter `continuePublic` 返回脱敏 terminal;`readPrivateResult` 只能是 Host/User 控制面 | **设计已关闭;SEC-11 待实测** | +| DR-08 | P2 | Windows Hosted Runner 不能直接继承 Linux UID 证明 | SEC-WIN 独立账户/ACL、Host service control 与真实文件/进程操作负例,缺条件 BLOCKED | **设计已关闭;需专用 Windows 安全测试环境** | +| DR-09 | P2 | CLI `usage.signature` 字段不具备已确认的 Provider 验签契约 | Provider 等级 V 的判断单独进行,任何非正式机制只能提供 HOST_OBSERVED | **设计已关闭;NAT-13 待核验** | +| DR-10 | P2 | 可能把 Human/Action/Parallel 越界引入 P0-A | Bridge v1 仅 Agent result;Human 未有真实渠道时只拒绝并延期 P0-B | **设计已关闭;TH-025 回归** | +| DR-11 | P2 | 只封 DB 主文件,不封 WAL/SHM、日志和备份 | 将 WAL/SHM/证据/backup/Service control/进程句柄列入 OS 负例 | **设计已关闭;SEC-03/04/13 待实测** | +| DR-12 | P2 | Provider 模型调用可能重复,避免误称 exactly-once | 只承诺 CAS 有效状态至多一次;由 Host 处理供应商调用幂等/对账 | **设计已关闭;NAT-08/09 待实测** | + +| DR-13 | P1(持久恢复) | Host 已签回执、尚未接纳就 SIGKILL;签名超期后可能丢失原始证明或误签编造结果 | §6.4 增加 Host-only OBSERVED/SIGNED/ACKED Outbox、签后持久化再 accept、过期未接纳 fail closed、严格原事件重证 | **设计已关闭;TH-029/SEC-18/NAT-15 待实测** | +| DR-14 | P2(容量/隐私) | 原生会话事件和签名 Outbox 可能永久堆积或记录敏感 State | Host-only 审计只保存必要证据与摘要;保留窗口和容量在 Phase 3 配置并测试 | **设计已关闭;TH-030/SEC-19 待实测** | + +| DR-15 | P1(信任边界一致性) | 如果 Bridge 与 Host 同用 HOST_USER,不能同时宣称 Bridge 在 OS 层无权读取 Host signer/SQLite | 明确 Bridge+Host 同权时是一个可信计算基(TCB),只能声明 MODEL_USER 被隔离;需要 Host 对 Bridge 的再隔离时必须使用独立 BRIDGE_USER 及真实 ACL/UID | **设计已关闭;SEC-01/02/05/06 待实测** | + +## 3. 设计冻结所需的可核对不变式 + +- [x] 现有 M5 Ed25519 Statement V1 不变;新增来源证据采用独立 Host-only ledger。 +- [x] Host signer、Coordinator、SQLite、完整 State 不向模型公开。 +- [x] Windows/Linux 身份隔离要求有实际系统权限失败/拒绝证据。 +- [x] Windows 默认 Named Pipe 不会被直接认定为安全。 +- [x] 直接 Grok Build 优先;ACP 不是前置条件。 +- [x] 同权 Developer Mode 只计 P/M,不计 N。 +- [x] 冻结了 tool_call start 与 Host 签名之间的时序;存在反例 NAT-14。 +- [x] 签后、accept CAS 前崩溃和过期回执具备 Host-only Outbox/对账设计及 NAT-15 反例。 +- [x] 30 个 TH Threat、19 个 SEC OS/IPC 用例与 NAT-01..NAT-15 均已列出(NAT-14 为事件时序,NAT-15 为签名 Outbox 恢复)。 +- [x] 对比 P0-A / P0-B / P1 / P2,未在安全设计中偷加 Action/Human/Parallel 能力。 +- [ ] 外部审查者重新核查 Provider/source provenance 能否满足冻结 C12/S13 条件。 +- [ ] Phase 3 的 Windows Host 用户/服务 SID/ACL/mTLS 真实负例 PASS。 +- [ ] Phase 4 Grok CLI 的 native JS Tool loop、`tool_call` 起始事件时序、普通脚本伪造攻击负例 PASS。 + +最后三项是**实施/独立验证门禁**,不是可用文档编写替代的事项。 + +## 4. 对 Stage 0/1 追踪状态的纠正 + +代码层存在的 N1/N3/N5/N7/N8/N9/N10/N11/N12 **不能因为历史设计 Finding 仍在列表里而重写**。它们只需对照冻结需求补足对应负例。N6 跨 SDK 版本兼容仍未实测;N2/N4/N13 由 P0-B 接手。其他 T/S/C 项继续使用完整路线与各自冻结矩阵,当前设计 PR 不会删除或合并它们的状态。 + +## 5. 本轮复审裁决 + +**自审结论:有条件通过设计方向;0 个尚未写出控制措施的新 P0/P1 设计 Finding。** + +**不是外部独立审查通过。** 由于当前执行环境无法代替用户本地的 Windows 服务账户和 Grok Build 会话,Phase 3 的真实安全证明、Phase 4 的原生事件序列均未执行。必须保留 BLOCKED,不得将文档 CI PASS 写成安全验收 PASS。 + +**建议:** 保持文档设计 PR Draft,独立审查时按 DR-01..15、TH-001..030、SEC/NAT 矩阵逐项核对。若没有实质性新设计 Finding,可正式确认本设计并开始 Phase 3;如发现信任域或工具事件顺序不可实现,必须先修正本设计再实施,禁止绕开门禁。 + + +## 6. Grok 独立复审 F-01–F-07 的逐条核验(2026-11-11) + +**外部材料:** `FlowKit-PR14-Trusted-Host-V2-Independent-Security-Review-Grok.md`(对 PR #14 frozen head `d1c7c76d03780f04a5b761699cdbc9ac82f0b86c` 的只读审查,结论 `CONDITIONAL PASS`)。本小节是后续交叉核验,**不是替代 Grok 初审的新第三方意见**。 + +| Finding | 独立交叉核验 | 处理结果及剩余门禁 | +|---|---|---| +| F-01 P1 | **确认。** 旧 §9 同时要求先验证 Grok 工具和把它放到 Phase 4,门禁冲突。 | 主设计 §9.1、路线图 Phase 3 改成唯一入口/退出规则;H3-02 工具能力探针先于通信代码,原生 C12/S13 仍 Blocked。 | +| F-02 P1 | **核心缺口确认,Grok 的“只有 Shell/MCP”和“rawInput 必须逐字节包含全部 normalizedReceipt”不是已证实的硬事实。** 官方 Grok Build 支持通过 MCP 扩展工具;`--tools` 本身仅为内置工具 allowlist。 | 主设计 §5.5 冻结优先 MCP PoC、受信配置及两条摘要:rawInput→toolInputDigest、Host 当前 Pending+参数→receiptHash;在 NAT-14/Grok 实测之前 H3-02 不能投入生产。 | +| F-03 P2 | **确认。** TH-017/023 为 P1,却对齐 SEC-11/04 的 P0。 | 威胁表升级至 P0;TH-028 保留 P1(协议活性而非 P0 越权),但 NAT-14 作为 Phase 4 强制阻断门禁,不得延期放行。 | +| F-04 P2 | **确认机制风险,非跨账户实测。** Windows 句柄继承保留权限;报告只做同用户 fd 探针。 | 主设计、SEC-02/03 增加真正继承句柄表/子进程非授权访问负例;Windows 跨 SID 攻击实测仍 BLOCKED。 | +| F-05 P2 | **官方规则确认,具体 Node SDDL 未独立复现。** 微软说明默认 Named Pipe DACL 给 Everyone/Anonymous 读取,Node 公开选项未提供可指定 SDDL 的安全接口。 | 默认 Node Named Pipe 明确禁止安全 IPC;仅创建时自定义 DACL+SID 的 Win32 引导经验证后可选,或采用受信的本机传输。 | +| F-06 P2 | **确认。** C12 交叉索引缺行、Grok requestId 与 FlowKit requestId 混用风险、Outbox 正文摘要未冻结。 | 验收矩阵补 C12,增加独立 `flowkitRequestId/grokRequestId/grokSessionId/toolCallId`;重签仅对 Outbox 保存的同一 normalizedReceipt canonical bytes/proofId 和当前 Pending 重新核证。 | +| F-07 P2 | **可访问性缺口确认。** 既有会话曾给出 V0.3/V0.6-R5 历史生成路径,但此 PR、当前文件库和工作环境未找到可复核原件。不能推断文件从未存在。 | 从 PR 中移除“已有可核验正式契约”暗示;要求补原文不可变定位和完整 SHA-256 后才可裁决正式契约符合性,当前 BLOCKED。 | + +**额外核验说明**:Grok 本地 Node 默认管道 DACL 与同用户句柄实验没有提供可独立执行的原始脚本/日志;本交叉复审仅采用 Windows 官方文档所证明的权限机制,不冒充已在用户电脑复现。Grok Build MCP 支持来自官方用户指南,但尚未执行 `grok -p` 的 NAT-14 工具时序实测。 + +**当前裁决:** 七项均已登记修订;F-01/03/05/06 的文档矛盾已修正;F-02/04 的设计控制已定义但关键平台实测仍缺;F-07 缺正式原文仍 BLOCKED。PR #14 继续 Draft,**不得以文档 CI 通过替代 Phase 2 的修订段落独立复核,也不得进入 Phase 3 的受信通信实现**。 + +后续:让 Grok Build **只复审本轮修订 diff**、官方 MCP/工具事件时序前置条件与 F-07 原件可访问性;无新实质性设计 Finding 且正式契约可核验后方可关闭 Phase 2。Phase 3/4 的运行证据在其阶段分别取得。 + +- [Microsoft: Named Pipe Security](https://learn.microsoft.com/en-us/windows/win32/ipc/named-pipe-security-and-access-rights) +- [Microsoft: process handle inheritance](https://learn.microsoft.com/en-us/windows/win32/procthread/inheritance) +- [Grok Build: MCP server configuration](https://github.com/xai-org/grok-build/blob/main/crates/codegen/xai-grok-pager/docs/user-guide/07-mcp-servers.md) +- [Grok Build: headless events and `--tools`](https://github.com/xai-org/grok-build/blob/main/crates/codegen/xai-grok-pager/docs/user-guide/14-headless-mode.md) + +## 7. Grok Build 第二轮定向复审与追加闭环 + +**原始报告:** `FlowKit-PR14-Trusted-Host-V2-F01-F07-Targeted-Rereview-Grok.md`;本轮上传文件 SHA-256:`4528278eddb1ade36c8a388c4ad4181f93f8d364a323ca54b0a4c19ea4ee3062`;Grok 核对冻结 HEAD `51a759975abcaee928faf50aec99ebed324fdfcc`,只读裁决 **CONDITIONAL PASS**。本节是事后的针对性代码/文档交叉验证记录,不能冒充 Grok 对新提交的再审 PASS。 + +### 7.1 外部复审裁决(冻结旧 SHA,不能倒填) + +| Finding | Grok 二轮结论 | 独立交叉核对 | 这次修订 | 后续边界 | +|---|---|---|---|---| +| F-01 P1 | PARTIAL | 原 `H3-05→H3-04→H3-03→H3-02`,H3-02 却引用 Phase 4 NAT-14,确有依赖环 | 把 H3-05 移到 H3-01 后、只依赖 H3-01;H3-02 改为独立「工具注册与签前 tool_call 最小能力探针」,**不是 NAT-14**;H3-04 负责集成后全量安全测试,Phase 4 正式 NAT-14 不提前记 PASS | 本次改动需定向 diff 复核,真实 PoC/NAT-14 仍未运行 | +| F-02 P1 | PARTIAL | Grok MCP 工具已具官方扩展机制,但 tool_call.rawInput flush/参数一致性/大值仍未实测 | 维持 §5.5 的候选工具探针与 `toolInputDigest` / `receiptHash` 双哈希规则;没有把 PoC 伪装成成功 | H3-02 实现 BLOCKED,直到可复现小型工具能力探针;C12/S13 仍 BLOCKED | +| F-03 P2 | CLOSED | TH-017/TH-023 与 SEC-11/SEC-04 P0 已对齐 | 不重做 | Phase 3 实际 OS 安全证明未完成 | +| F-04 P2 | PARTIAL | Grok 测到 Node 同用户继承的 fd 可被子进程读取;**没有跨 OS 身份实测** | 保留句柄白名单和跨用户负例 | Phase 3 SEC-02/SEC-03 仍 BLOCKED | +| F-05 P2 | CLOSED | Windows 官方默认 Named Pipe 赋予 Everyone/Anonymous 读取权;报告称 Node 22/24 实测 SDDL,但未提供原始脚本文件 | 保留禁止默认 Node Pipe,安全 Pipe 必须创建时自定义 DACL+SID | SEC-16 仅采用 Named Pipe 时适用,未跑 Win32 实测 | +| F-06 P2 | CLOSED | C12 映射、flowkitRequestId 与 grokRequestId、Outbox canonical bytes 和 receiptHash 规则已存在 | 不重写 Core 或重新序列化签名业务正文 | 真实签后崩溃 SEC-18/NAT-15 未运行 | +| F-07 P2 | NOT_VERIFIABLE | 历史存档存在正式原件的名称与沙箱路径记录,但本次原始字节不可用;不能据此说文件从不存在 | 保留正式契约 V0.3/V0.6-R5 的符合性 **BLOCKED** | 完成符合性裁决仍需正式原文和完整 SHA-256,不得由路线图或审查报告代替 | +| F-R2-01 P2 | 新 Finding | §3.2 说本地端点可选,§7 Profile 与 §10 总结又把 mTLS 当默认,且 SEC-05/07 会暗示必须创建监听器 | §7 Windows/Linux Profile 和最终结论都改为两 OS 身份、Key/DB/句柄保护必选,**mTLS/独立 Bridge/监听端口按实际部署可选**;SEC-05 验证真正的候选入口,SEC-07 无监听时记 N/A—NO_LISTENER 而非 PASS | 本次设计修订需 diff 复核;没有网络端点不能免除模型输入权限验证 | + +### 7.2 单向阶段依赖检查(不得出现旧循环) + +```text +Phase 2 design review + | +H3-01 Host OS identity & secrets isolation + | +H3-05 OS-only security negatives <-- only H3-01; no Grok tool dependency + | | Grok tool capability probe (no NAT-14 PASS) + | | + | H3-02 ingress/tool adapter + | | + | H3-03 evidence/Outbox + | | + +-----------------------------------H3-04 signer + integration security checks + | + Phase 3 OS/IPC security exit (Win + Linux) + | + Phase 4 native Grok, formal NAT-14/S13/C12 +``` + +**范围纪律:** 本次仅文档修订,没有触碰 Program ABI、Ed25519 receipt v1、Coordinator、Store 或其他生产 Core。CI 全绿只证明现有回归没受影响,**不证明**跨用户 Windows ACL、Grok 原生事件时序、Provider 身份或 Phase 3/4 执行完成。 + +**下一步判停:** 本轮 F-01/F-R2-01 的段落修订无新 P0/P1 Finding 后,可认为**这两项设计矛盾**在文档层关闭;但 F-02 的工具能力、F-04 的跨账户隔离和 F-07 的正式契约证据均必须保持原有状态。PR #14 继续 Draft,不因文档回归或作者自审直接合并。 + + + +## 8. Grok Build 第三轮最终定向复核(F-01 / F-R2-01) + +**外部报告:** `FlowKit-PR14-F01-FR2-01-Final-Targeted-Review-Grok.md`,SHA-256 `7936d6cef9d3c26f5870eb2303fe52c6f215487e740c1fa57787e06716c67933`。审查者为前两轮同一 Grok Build 独立审查者;只读核对冻结 HEAD `316e426aa8d96e4a6b6bee8afd35c0cd313fd244`。裁决:F-01、F-R2-01 **CLOSED(设计文档层面)**;不表示 Phase 3/4 安全验收完成。 + +| 复核项 | Grok 最终裁决 | 本项目核验 | 后续状态 | +|---|---|---|---| +| F-01 阶段依赖 | CLOSED | H3-05 仅依赖 H3-01;H3-02 使用独立能力 PoC,不将其冒充 NAT-14;H3-04 负责接入/签名/恢复集成验证;正式 NAT-14 在 Phase 4 | PoC、SEC 与 NAT-14 真实执行仍待后续阶段 | +| F-R2-01 IPC Profile | CLOSED | Secure Windows/Linux 最小 Profile 不强制 mTLS、TCP listener 或独立 Bridge;SEC-05 始终验证真实候选输入路径;SEC-07 仅监听型 TLS/mTLS 存在时强制 | Windows/Linux 两个 Profile 仍需真实权限测试 | +| F-02 | PARTIAL | Grok 原生 MCP tool_call 起始事件、rawInput 对照、截断/大输入仍未实测 | H3-02/NAT-14 BLOCKED,直到真实能力探针与后续验收 | +| F-04 | PARTIAL | 继承句柄机制有 Node POC,但没有 Windows 跨 SID/服务 Token 的真实负例 | Phase 3 SEC-02/03 BLOCKED | +| F-07 | NOT_VERIFIABLE | 正式 V0.3/V0.6-R5 原文和完整 SHA-256 未获得 | 正式契约符合性继续 BLOCKED | + +**本轮额外文档对齐:** Grok 指出 Windows 检查表中“无证书访问 loopback”和“受保护 Bridge client 合法访问”两步未明确限定部署条件。虽然 SEC-05/SEC-07 与 §9.1 已经规定无监听路径,但为避免执行者误解,已把威胁模型第 3 节第 6–7 项改为条件步骤:无监听记录 `N/A—NO_LISTENER` 并验证 SEC-05;无独立 Bridge 记录 `N/A—NO_SEPARATE_BRIDGE` 并验证 SEC-05/06。该修订属于文档一致性收尾,不改变安全要求、不创建新监听端点,也不降低实际候选入口的验证要求。 + +**裁决边界:** 本轮关闭的是 F-01 与 F-R2-01 的设计矛盾。PR #14 仍是 docs-only 设计记录;F-02/F-04/F-07 不得改成 PASS。CI 只证明既有回归,不能证明 Windows Secure Host、Grok 原生来源、C12/S13 或正式契约符合性。是否合并 PR #14 应由用户在确认其作为“带明确 BLOCKED 限制的设计记录”后决定;合并也不自动授权 Phase 3 实施。 + + + +## 9. PR #14 merge gate and remaining limitations + +- The final design review remains documentation-only. GitHub Actions run `43096045021` for HEAD `b9d0c1efe17c386f798e30b4331f7bd90a1936e3` remained queued with zero jobs created for several hours; this is not a PASS or FAIL test result. GitHub Status reported an Actions/Pages/Copilot incident on 2026-11-11 20:48–21:21 UTC, now resolved. The last fully successful regression run is `43045141302` for prior docs-only HEAD `316e426aa8d96e4a6b6bee8afd35c0cd313fd244`. A fresh exact-HEAD PR run is required before the authorized squash merge. No production source changed between those two heads; the intervening changes are documentation only. Do not merge based on a queued run. diff --git a/docs/interactive-trusted-host-security-v2-threat-model.md b/docs/interactive-trusted-host-security-v2-threat-model.md new file mode 100644 index 0000000..6284d45 --- /dev/null +++ b/docs/interactive-trusted-host-security-v2-threat-model.md @@ -0,0 +1,147 @@ +# FlowKit Interactive — Trusted Host V2 威胁模型与攻击面登记(Draft) + +**阶段:Phase 2(设计),2026-11-11。基线:** `main@9bac8f86347a405efe068f96423ae3e501005061`。 +**配套设计:** [Trusted Host V2](./interactive-trusted-host-security-v2-design.md)。 +**状态:** 威胁登记不等于测试通过;验证结果必须引用后续实测的 Git SHA、运行日志和 OS 证据。 + +## 1. 边界、信任主体与“不做”事项 + +| 主体 | 信任级别 | 可做 | 禁止 | +|---|---|---|---| +| MODEL_USER / Grok Build | 不可信模型/普通工具账户 | 查看授权 Public Task;通过受控工具产生结果 | 读取/修改私有 Store、签名密钥、Host 配置、任意 Run 的业务 State | +| Protected Bridge | 已核准受限通信客户端 | 接收仅来自允许的工具会话的数据;发送已鉴别的有限请求 | 任意新建 Run、重写 Program/policy、暴露私有 State、直接签收据 | +| HOST_USER / Host Adapter | 受信执行域 | 审核 Program、Data Disclosure、认证来源、签 receipt、CAS/Journal | 通过模型字符串授权签署、越过原协议伪造状态 | +| Provisioner / Operator | 受信部署主体 | 配置账户、ACL、证书、工件 allowlist、策略 | 作为模型可调用操作暴露这些权限 | +| Provider | 外部服务 | 在其协议范围内提供实际模型回复 | 不假设其一定有可验证会话签名或 exactly-once | +| Admin/root/Kernel | 根信任/明确出界 | 安装/审计 | 已被模型控制则停止宣称隔离 | + +**既有契约:** M5 `createTrustedInteractiveHost()` 是 Host-only API,模型不应获得原始 `complete()`;Host 通过 Ed25519 v1 statement 和签名、Program/Policy Hash、DLP、Store CAS 保障可接受状态。上述机制不能替代底层 OS 权限边界。 + +### 1.1 同 UID/SID 与管理员原则 + +如果 Grok Build 有与 Host 相同的 OS 权限、可进入 Host token,或是本机管理员(可提权修改 ACL、服务及进程),即使程序在两个 Node.js 进程中,安全门禁仍记 `BLOCKED`。Linux 的 `chmod` 与 Windows 的 ACL 不具有直接可比的安全含义。 + +### 1.1.1 Bridge 与 Host 同账户时的 TCB 规则 + +受保护 Bridge 与 Host 若在同一 HOST_USER/UID 下运行,就属于同一个可信计算基(TCB),不能宣称 Bridge 在 OS 层无权访问 Host 私钥或数据库。**最低强制边界是 MODEL_USER 与整个可信 TCB 分离。** 如果还要求在受信 Bridge 被攻破后保护 Host 签名密钥,必须单独部署 BRIDGE_USER 并实测不同账户 ACL/UID。所有模型输入都必须通过严格的工具入口与 Host-side 原生事件观察;Bridge 与 Host 同账户绝不能成为让模型获得任意 Host API 的理由。 + +### 1.2 与阶段对应 + +- Phase 2:规范并审查所有威胁和验证门槛;不声称已运行 Windows/Grok 安全测试。 +- Phase 3:TH-001–TH-017、TH-022–TH-025、TH-027、TH-029、TH-030 的适用 OS/IPC/签名/Human 负例;TH-025 在 P0-A 仅验证伪造 Human 请求被拒绝,真实 Human 审批延后 P0-B。 +- Phase 4:TH-018–TH-021、TH-026、TH-028 的真实 Grok 来源绑定、原生 JS 工具调用以及时序/PID 重放负例。 +- Phase 5/6:复核所有攻击族及 C08/C12/C22/C23 证据。 +- P0-B Human 和 P1/P2 并行新增威胁在相应阶段扩展,不在当前 P0-A 偷做。 + +## 2. 可复现攻击矩阵 + +| ID / 级别 | 攻击 / 风险 | 防护或控制 | 必须出现的负例与通过标准 | +|---|---|---|---| +| TH-001 / P0 | 模型/工具可使用 Host 同一 OS 用户令牌,绕过一切文件目录约定 | Host 与 MODEL_USER 分离;实际 OS token / SID / UID 证明 | 同账户/管理员模式标 BLOCKED;受限模型身份不能提权到 Host | +| TH-002 / P0 | 模型读取 Ed25519 签名私钥或 KMS 授权 | Host-only key custody、ACL、最小进程权限 | MODEL_USER 的真正读取/复制/列目录失败,且其进程不存在可读私钥/Host token 的继承句柄;Host 自身签名 PASS | +| TH-003 / P0 | 模型直接读写 SQLite 主文件 | Host-only DB owner/DACL | 读取、覆盖、CREATE、DELETE、RENAME、替换文件失败;进程句柄表无继承的主库/WAL/SHM/备份文件句柄 | +| TH-004 / P0 | 直接读写 SQLite `-wal`、`-shm`、备份、快照、导出日志 | 与主 DB 同级别的保护;禁止不安全备份 | 主 DB 被拒绝但 WAL/SHM 可读时 FAIL;Host 恢复正常 | +| TH-005 / P0 | 模型通过写入 Host 配置、artifact allowlist、Program 文件替换合法执行 | Host-only config/artifact manifest、exact hash、immutable verified bytes | 可写配置/产物时 FAIL;非授权 Program、policy drift fail closed | +| TH-006 / P0 | Windows 管道默认 ACL 给多余主体访问、预占/伪造管道名 | 禁止默认 Node Named Pipe 作为安全 IPC;需监听时可用 loopback mTLS,若用 pipe 必须显式 DACL+Peer SID+拒绝远程 | 非授权 SID 无法连接、无法预占、无法充当 Server;失败不能自动降级 | +| TH-007 / P0 | 如果启用 loopback TCP,未授权本地程序访问 Host | TLS/mTLS、绑定回环、明确客户端身份限制,并与 Host 独立工具事件绑定 | 无证书、错证书、错误身份不能调用;非回环连接失败 | +| TH-008 / P0 | 若采用客户端证书/受保护 Bridge,模型工具能读取其凭据或伪造 Bridge 身份 | 受信 Bridge 专用凭据不向 MODEL_USER 泄露;即使模型端持有普通候选连接凭据,也不能据此取得 signer 权限;所有实际候选路径仍需独立工具事件校验 | MODEL_USER 模仿真实活动中的提交接口不能生成已签回执,未启用 Bridge/mTLS 的配置记该项凭据子情形 N/A 而非 PASS | +| TH-009 / P0 | 把普通 JSON、`GROK_SESSION_ID` 或模型声称已批准当来源证明 | Host-only supervisor 观察记录、受信 proofId 和 result digest 关联 | 改 sessionId/proofId/工具日志文件不能让 Host 发签名 | +| TH-010 / P0 | Host 给未验证的 response.json / 模型失败文本签署 Ed25519 | 观察证据验证先于 signer | 无工具/来源证据,Host 必须拒绝并保持 Pending/Store 不变 | +| TH-011 / P0 | 模型绕过 Host 安全入口直接调用 `complete()`、SQLite、signer | Coordinator & signer 只在 HOST_USER 运行,所有实际模型候选入口均实施 method allowlist 与权限隔离,不要求额外部署 Bridge 进程 | 任何 model API 都无法获得低层 Host 对象或密钥;未授权方法必拒绝 | +| TH-012 / P0 | 模型触发 `start` 并给自己加入任意 Program artifact allowlist | 可信 provisioning/build manifest、预授权 Run scope | 模型自报 Hash 和初始 State 不能开始未经授权 Run | +| TH-013 / P0 | 错租户、错 Run、错 Node、错 requestId/attempt、旧 Pending 重放 | Host 从当前 Store 重新取 Pending,namespace-bound auth | 所有错身份请求无 CAS/Journal/receipt mutation | +| TH-014 / P0 | 有效签名/旧响应被替换或被其他 Run 复用 | v1 statement binds namespace/run/request/node/attempt/receiptHash/artifact/policy | 改任一字段,验证失败;相同合法重复回执只返回原 Ack | +| TH-015 / P0 | Checkpoint 尚未提交就发送下一个任务;Host 被杀后重复提交 | Store CAS 和先持久再派发;支持 same Run recovery | Publish/Accept/Commit 多切点 SIGKILL 后 Pending identity、State、Journal 一致 | +| TH-016 / P1 | 长输出、恶意 JSON、百万级对象造成内存/CPU/日志拒绝服务 | 有界 transport frame、限流、deadline、canonical receipt v1 2MiB | 上限+1、恶意 key、无效 UTF-8/JSON、重复/超时拒绝;不得篡改 State | +| TH-017 / P0 | 敏感 State/指令出现在 Pending、TLS 返回、Tool 结果或日志 | getPublicAgentTask allowlist + authorizeDisclosure + Host-only private view | canary 不得出现在模型可读流/日志;Host 内部仍保留完整 State | +| TH-018 / P0 | 原生 Grok CLI 输出流或 tool_call 由模型/脚本伪造或替换 | 受信 Supervisor 捕获真实 CLI 事件、安装路径/hash 固定,禁止模型写原始证据目录 | 用 echo/伪造 `tool_call` JSON/改本地日志不能认证为真实工具调用 | +| TH-019 / P0 | 客户端证书只证明 Bridge 账户,却被错当模型 Provider 身份 | `HOST_OBSERVED` 与 `PROVIDER_VERIFIED` 分开;独立原生事件证据 | 单独 mTLS PASS 不得让 C12/S13 获得 PASS;证明会话实际工具调用路径 | +| TH-020 / P1 | 真实模型中途崩溃、Provider 请求重复计费,Host 宣称 exactly-once | 固定 Pending/RequestId、对账、历史 Ack;外部调用按 at-least-once 风险处理 | 接受重复模型调用可能发生;只承诺一次有效 State commit | +| TH-021 / P0 | 工具/会话和 receipt 数据错配(结果 A 冒充结果 B) | 独立 ToolCallId/RequestId/TaskDigest/ResultDigest 绑定,Host-only proofId | 交换两次同 Run 不同 Pending 的结果或工具事件必须拒绝 | +| TH-022 / P1 | Host 签名证据过期、密钥轮换/策略漂移导致在途 Run 无出口 | 固定 policyHash、已核实历史 Ack、轮换排空或显式迁移 | 不制造新旧身份混合回执;旧 Host 可以审计/安全处置 | +| TH-023 / P0 | Windows service 控制权限、进程句柄、debug 权限让 MODEL_USER 以 Host 权限执行代码 | 专用服务 SID、SCM DACL、受限 Token、保护可执行路径 | MODEL_USER 不能修改/重启服务为任意程序,不能 attach/debug/dump Host | +| TH-024 / P1 | 路径重解析、符号链接、硬链接或 TOCTOU 替换 Host 文件 | Host owned path、真实路径/文件句柄验证、不跟随不安全链接 | 重定向私钥/SQLite/Program 读取失败或保持原已验证字节 | +| TH-025 / P0 | 模型构造 `kind:'human'` 或“人工已同意”让流程越权 | P0-A signed receipt 只接受 Agent;P0-B 必须另证 humanProvenance | P0-A `kind:'human'` 直接拒绝;Human 未实施不能表示审批通过 | +| TH-026 / P1 | 捕获会话 PID 后复用同 PID 或人为重放事件 | 进程启动时间/可执行文件摘要/session/tool sequence/host-only nonce | 旧 PID/session/toolCallId 不被新的 Run 自动承认 | + +| TH-027 / P0 | 仅在采用受保护 Bridge/mTLS 时,Bridge 凭据有效却从任意模型 JSON 直接触发转发 | Bridge 仍须依赖 Host-only CLI 工具事件证据;单 Host 进程无 Bridge 时由实际候选数据入口执行同等级校验 | 无可匹配 tool_call 的普通模型工具命令不能触发签名/CAS;没有 Bridge/mTLS 时相关凭据测试记 N/A,但签名前来源验证不能跳过 | +| TH-028 / P1(Phase 4 强制阻断) | Host 等待 submit 的 tool_call_update,submit 又等待 Host 接纳,造成死锁 | H3-02 编码前做**能力探针**验证 tool_call start/input 能签前观察;Phase 4 的正式 NAT-14 仍独立执行;update 作签后审计 | 探针不成立则 H3-02 BLOCKED,但 H3-01/H3-05 OS 测试可先行;正式 NAT-14 未通过则 Phase 4/C12/S13 BLOCKED,不得把 PoC 充当 PASS | + +| TH-029 / P0 | Host 签署 v1 receipt 后、CAS 接纳前崩溃;证明过期后错误重签伪造历史回复 | Host-only OBSERVED/SIGNED/ACKED Outbox,签名后持久化再 accept,恢复时与 receipt-index 对账;过期未接纳时 fail closed | SIGKILL 签名前/签后/CAS 后;未接纳过期证明不得产生状态修改;原有效结果只能经受信证据对账 | +| TH-030 / P1 | 模型结果和 Host 观察日志无限增长或泄漏原始 State/密钥,隐私与磁盘 DoS | Host-only 有界 retention、加密/ACL、按 Run 生命周期和审计窗口清理,避免记录完整敏感文本 | 大量 Run/长链后空间有界;Public Task 和公开日志无 canary;保留必要 hash-chain audit | + +- TH-023/TH-017 是 Phase 3 P0,服务账户可执行权限和敏感 State 误披露不得以 P1 延后;TH-028 虽属协议 P1,但 H3-02 与 Phase 4 的准入条件强制检验工具事件时序,无证据仍为 BLOCKED。 + +**验收证明强度**: +- “模型说无法访问”不是 TH-001–TH-005 证明,必须真的运行拒绝访问的文件/进程命令; +- Linux scripted nobody PASS 不能替代 Windows ACL;Windows 命令直接在不支持平台异常退出时保持 BLOCKED; +- 模型日志中普通 `usage.signature` 字段没有验证算法/密钥/覆盖字段,则不能升格为 `PROVIDER_VERIFIED`; +- 只有通过了原始 JS tool-call 证据、Host 可信接纳、OS 隔离的闭环,才可申请 C12/S13 的 N 层裁决。 + +## 3. Windows 身份及 ACL 防护检查表 + +所有测试必须使用正确的真实 OS Token 执行,而非在同一管理员终端中更改逻辑变量: + +1. 查询 Host、Bridge、Grok CLI 的账户 SID、完整用户组和提升状态; +2. 查询 Host 私有根目录、Key、DB 主/WAL/SHM、Program manifest、可执行文件、日志的 DACL 与 Owner; +3. 验证 Host 服务对象的控制/更改配置权限不能被 Grok 用户调用; +4. 从 Grok 子进程尝试列目录、读、写、改名、替换、删除 Host 私有文件; +5. 从 Grok 子进程尝试打开 Host PID/进程句柄进行敏感权限操作; +6. **仅当部署实际启用 loopback TLS/mTLS 监听端点时**,从 Grok 子进程尝试无证书/伪证书连接;若未启用,记录 `N/A—NO_LISTENER`,不得为了该项创建监听器,并转而验证实际候选数据入口(SEC-05); +7. **仅当部署独立受保护 Bridge client 时**,用该客户端完成合法请求;若采用单一 HOST_USER TCB/无独立 Bridge,记录 `N/A—NO_SEPARATE_BRIDGE`,并验证实际 Host TCB 候选入口和工具事件绑定(SEC-05/06); +8. 证明新进程重启后仍保持隔离; +9. 所有记录脱敏,不把私钥、私有数据库原文上传 CI; +10. 若使用 Named Pipe,检查 SECURITY_ATTRIBUTES/DACL、非授权 SID 连接和服务 SID 验证;默认 Pipe ACL 不构成通过证据。 + +**工具参考**:`whoami /groups`、`whoami /priv`、`Get-Acl`、`icacls`、`Get-LocalUser`、Windows Service Control Manager 的安全描述符检查。实际测试命令在 Phase 3 写入自动化脚本并验证每个断言,不在这里假定其结果。 + +## 4. Linux UID/GID 防护检查表 + +1. `id` / `stat` / `ls -l` 验证 Host 与 Model 不同 UID/GID; +2. Host 私有根 `0700`、密钥 `0600`,包括 WAL/SHM/backup; +3. MODEL_UID 对私有目录全部 read/write/rename/traverse 失败; +4. MODEl_UID 对 `/proc//environ`、FD、ptrace 和 Host PID 访问失败; +5. 真实 Grok Build CLI 在 MODEL_UID 下运行,观察证据由另一 Host UID 收集; +6. 如果采用 UDS,Socket path ACL/group/peer creds 必须复核; +7. 运行已经存在的 `tests/security/host-isolation.mjs` 作为最小回归,但不能把它当成真实 Grok 原生证明。 + +## 5. 安全 Finding 记录与关闭流程 + +每个实测攻击项至少包含: + +```json +{ + "threatId": "TH-003", + "testId": "SEC-WIN-DB-DENY", + "baselineCommit": "", + "os": "windows-2025", + "nodeVersion": "24.22.1", + "hostPrincipal": "", + "modelPrincipal": "", + "inputDigest": "sha256:...", + "observed": "Access denied on read / write / replace", + "hostStillFunctional": true, + "result": "PASS", + "rawEvidenceRef": "" +} +``` + +关闭条件: +- 所有 P0 威胁通过可复现正反例;否则本阶段/宿主 `BLOCKED`。 +- 对 P1 项若尚无运行条件,明确阻断原因、责任域、目标阶段与 re-entry gate;不得忽略。 +- 任何对编译语义、M5 收据 v1、Program ABI 的新修改需要新契约与专门独立审查。 +- 不因单一“安全扫描绿灯”替代负面 OS 访问测试。 +- 建议的判停标准是“冻结攻击用例跑通且 P0/P1 结论可审计”,不是无限制的想象式 Finding 轮次。 + +## 6. 一致性参考 + +- [Existing M5 security contract](./interactive-host-security.md) +- [Proposed design](./interactive-trusted-host-security-v2-design.md) +- [Microsoft Windows pipe ACL](https://learn.microsoft.com/en-us/windows/win32/ipc/named-pipe-security-and-access-rights) +- [Microsoft Windows file ACL/icacls](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/icacls) +- [Microsoft Process Security and Access Rights](https://learn.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights) +- [Linux scripted Host isolation](../tests/security/host-isolation.mjs) + +--- + +**状态:威胁已登记,Phase 3/4 真实 OS / CLI 攻击测试尚未执行。** 所有测试入口不得声称现有 Grok Build Windows 同权实验已通过原生安全验收。 diff --git a/docs/interactive-v03-v06-conformance-audit-plan.md b/docs/interactive-v03-v06-conformance-audit-plan.md new file mode 100644 index 0000000..2c4e221 --- /dev/null +++ b/docs/interactive-v03-v06-conformance-audit-plan.md @@ -0,0 +1,259 @@ +# FlowKit Interactive — V0.3 / V0.6-R5 正式契约符合性审查计划 + +**状态:已批准进入审查;尚未形成符合性结论。** +**基线日期:2026-11-12(UTC+8)** +**当前 main:** `ae5fc5a42c67eb6e2c229d72ff4d9ac8e3aab3cd` +**需求基线:** Requirements V0.3(已确认) +**技术基线:** Technical Design V0.6-R5(已确认) +**安全补充:** PR #14 Trusted Host V2(已合并的安全设计记录,不覆盖 V0.3/V0.6-R5) +**关联:** [正式原件登记](./interactive-phase2-formal-source-register.md) · [完整路线图](./interactive-full-delivery-roadmap-2026-11-11.md) + +## 0. 本次审查的目标与边界 + +目标不是重新设计 FlowKit,也不是把所有现有测试重新跑一遍,而是基于用户确认的两份正式原件,回答四个问题: + +1. V0.6-R5 是否逐条遵守更高优先级的 V0.3 需求? +2. 当前 main 的真实代码、测试与已合并 PR #14 安全设计,分别符合、部分符合或偏离哪些正式条款? +3. 2026-11-10 Core POC 已经证明了什么、没有证明什么;哪些 POC 缺口必须先补? +4. 对照已有 **P0-A 实施计划 V0.1 Confirmed**(M1–M6、31 项文件级任务)和当前 main 已实施的 M1–M5,哪些已具备可核验代码与测试、哪些仍属于 M6/Host 后续门禁? + +**权威层级:** +- Requirements V0.3 是最高业务需求,技术设计不能覆盖或擅自修改它。 +- Technical Design V0.6-R5 是已确认的统一技术规范;正文 §§1–18 是唯一规范性设计。 +- PR #14 Trusted Host V2 是后续安全设计补充,必须映射回 V0.3/V0.6-R5,不能静默改变 Program ABI、Core API、Checkpoint/Store 语义或 P0-A/P0-B/P1/P2 边界。 +- Core POC 报告是有限范围的运行证据,不是需求或技术规范,也不能把未实现范围判 PASS。 +- 当前 main 是实现核验对象;V0.3/V0.6-R5 中引用的 `ea2df674f3d3fd7f2b0ba4b48eb21e3bd47c3a2c` 是历史源码参照,不得与当前 main SHA 混为一谈。 +- **已有用户确认的 P0-A 实施计划 V0.1(2026-11-10),当前 main 亦已有 M1–M5 实施代码**。本轮不得重新生成第二份 P0-A 正式计划覆盖历史确认版,也不能把 PoC 时代的“未实施”直接投射为当前 main 状态;应建立既有 31 项任务、现有实现与未关闭 M6 门禁的对照。 + +本计划不授权修改已确认原件、Program ABI、Core API、SQLite Schema、P0-A 状态语义;不授权生产代码开发、PR 合并、发布或部署。任何正式契约变更先形成差异与建议,等待用户确认并独立版本化。 + +## 1. 已冻结的输入与证据 + +| 输入 | 固定身份 | 已知状态 | +|---|---|---| +| Requirements V0.3 | SHA-256 `3a0adc3edb41869a6309bae29ec5496df00970c815f8dc16330a90cbf26e703e`;56,883 bytes | 已确认原件,归档校验通过 | +| Technical Design V0.6-R5 | SHA-256 `09f3a5698e238d10b80efb8e26d1c41e51b663ca620efa70815dd7b52f2106ba`;114,621 bytes | 已确认原件,两个归档副本哈希一致 | +| V0.6 Confirmed Handoff ZIP | SHA-256 `2deb5b3fa60ed3b7c2d29f028e33835c47aaa0a976e947de717128caf7326549` | 内部 SHA256SUMS 9/9 PASS;包含批准/PoC 计划、R5 独立收敛复审、需求追踪与静态验证结果 | +| Core POC Evidence ZIP | SHA-256 `a6b5201e8327463b57700174297bf986fbc4902c6ef6493710e8cac021f178` | 内部 manifest 28/28 文件条目 PASS;最终 TAP 22/22;9 个 SIGKILL 断点 | +| V0.3 Grok Development Handoff ZIP | SHA-256 `8a2f679a796234383b5108d58db16c1016a35c6110301bc4eb7e091ee2ed5826` | 仅含 V0.3-R2 technical draft,不是 V0.6 Confirmed Handoff | +| PR #14 | Merge commit `ae5fc5a42c67eb6e2c229d72ff4d9ac8e3aab3cd` | Trusted Host V2 安全设计记录已合并;不等于安全实施通过 | +| 当前 main | `ae5fc5a42c67eb6e2c229d72ff4d9ac8e3aab3cd` | 本次代码符合性核验的当前基线 | + +POC 报告的限定结论: +- POC-01 PARTIAL:使用受限 JSON Fixture,未接入真实 FlowKit AST/Fields/Graph/Dataflow。 +- POC-02 PASS(本地进程级):SQLite Genesis/CAS/Journal、事务回滚。 +- POC-03 PASS(9 个进程级断点):跨 Node 进程 SIGKILL 恢复。 +- POC-04 PASS(本地竞争):双进程相同/冲突 Receipt、历史 Ack、丢响应恢复。 +- POC-05 PARTIAL:不可变 Program Registry 有限验证;升级、迁移、GC/灾备未验证。 +- POC-06 PARTIAL:namespace 与 Linux UID 文件拒绝有限验证;生产 Host/Model 隔离和 ExecutorContext 全链路未验证。 +- POC-07 BLOCKED(Human)/ PARTIAL(Admin):没有真实独立 Human 审批来源。 +- POC-08 BLOCKED(完整矩阵):尚未完成 Node 22.13/24.22 × Windows/Ubuntu、TS 5.0.4 与现有 Standard 全量回归。 +- POC-09 PARTIAL:只测简化 Fixture 的 CAS 写入与阶段预算。 +- POC-F04:完整 RFC 8785/JCS 测试向量尚未证明。 + +**证据纪律:** 22/22 是隔离原型测试结果,不是 POC-01–09 全部 PASS;9/9 是原型环境进程崩溃点,不是 Windows、断电、多机或真实模型 exactly-once 证明。 + +## 1.1 编号体系的权威性与来源(不得混为一谈) + +| 编号体系 | 来源与性质 | 本次审查用途 | +|---|---|---| +| T01–T34 | 用户确认的 Requirements V0.3 正式验收需求 | **最高业务需求**;逐条保留原文和阶段 | +| R01–R22 | V0.6-R5 保留的历史技术反例 | 技术设计必须解释并有对应验证入口;不是新的用户需求 | +| S01–S14 | V0.6-R5 中的 Grok 定向验收场景 | 对应设计/实现阶段的定向验证;不因文档覆盖而 PASS | +| C01–C24 | 后续 M6/PR12 交付验收矩阵 | 当前交付/真实宿主验收扩展;**不是 V0.3 原始 T 需求,也不是 V0.6 原始 S 场景**,必须保留其自身来源和状态 | +| TH-001–TH-030 / SEC-01–SEC-19 / NAT-01–NAT-15 | PR #14 Trusted Host V2 安全设计 | 后续 Host 安全威胁/测试补充;映射到 V0.6 §7/§11、T27/S13 和 C 矩阵,不得回写成原始需求 | +| POC-01–POC-09 / POC-F01–F05 | 2026-11-10 核心机制 PoC 计划/报告 | 证明实际原型覆盖范围与未关闭差距,不改变正式需求编号 | + +**判定时必须保留来源层级**:若 C/TH/SEC/NAT 的后续安全验收与 T/V0.6 存在差异,先登记为补充要求或潜在冲突,不能悄悄改写 V0.3/V0.6。 + +## 2. 审查工作流 + +### Step 1 — 原件与基线锁定(已完成) + +- [x] 两份正式文件 SHA-256 完整值已计算。 +- [x] V0.6 Confirmed Handoff 内部 9/9 SHA256SUMS 校验。 +- [x] Core POC Evidence 内部 28/28 文件条目校验。 +- [x] 两个独立归档中的 V0.3/V0.6 基线文件哈希一致。 +- [x] 当前 main、PR #14 merge commit 和 CI 记录已登记。 +- [ ] 将原始文件字节、归档哈希、当前 main SHA 和工具版本写入最终审查报告的不可变 Evidence Index。 + +### Step 2 — V0.3 需求逐条提取与验收语义锁定 + +逐条读取 V0.3 原文,不以路线图摘要代替: + +- T01–T10:P0-A 核心纯流程、Agent、Checkpoint、回执、身份和预检。 +- T11:没有 JS 执行工具时必须如实 BLOCKED。 +- T12–T14:Standard/包/类型/错误码兼容与 Agent 重试/失败语义。 +- T15–T23:P0-B 的验证、Action、Human、Loop、取消。 +- T24/T26:P1 Parallel all_succeeded。 +- T25:P2 any,业务预期未获专项确认。 +- T27:真实宿主 JS 工具链(包括 ChatGPT 专项)必须提供真实执行证据。 +- T28:源码、生成 JS、包、类型、文档、测试交叉一致。 +- T29–T34:崩溃、伪造、竞态、未知副作用、Standard/Interactive 差异、预算和隐私。 + +产物:`V03-T01-T34-Requirement-Ledger`。每条保留原始措辞、阶段、必要不变量、正例、负例、退出证据;不改编号、不重写正式需求。 + +### Step 3 — V0.6-R5 §§1–18 技术规范逐章审查 + +按正式正文唯一规范逐章核对: + +| V0.6 章节 | 必查内容 | 关键输出 | +|---|---|---| +| §1 | 产品边界、既有源码、非目标 | 是否扩张为 Server/模型网关/数据库/沙箱;是否保持内部引擎定位 | +| §2 | 十四条不可退化不变量、分层图、编译流水线、Program/Coordinator 边界 | Program 唯一控制流、可信 Registry、零 import、同字节加载 | +| §3 | 生命周期、Phase/Proposal 矩阵、持久化屏障、paused/取消 | 是否所有持久边界和恢复时序唯一可判定 | +| §4 | SDK API、Host 授权和恢复参数 | API 参数、能力对象、namespace 权限是否一致 | +| §5 | Program ABI V1、HostSubmission、Store/Registry/Runtime 类型 | 类型、判别联合、字段与实际源码/PR14 是否冲突 | +| §6 | ProgramRegistry、Run retain、升级/回滚/删除 | 原产物固定、旧 Run 兼容与 GC 是否有完整规则 | +| §7 | PublicTask、TrustedExecutorContext、回执接纳和 CertifiedEvent | 模型可见/可信执行器视图隔离;来源不能由模型自报 | +| §8 | CheckpointStore 五操作、事务、Journal、Receipt、单写者 | Genesis/CAS/历史 Ledger/丢响应/单 Dispatcher 约束 | +| §9 | SQLite V1 参考 Schema | DDL 与五操作/外键/namespace/事务边界一致;仅作为参考 Store | +| §10 | Agent、Condition、Action、Verification、Human、Loop、Parallel | 节点语义、失败分类、State 读写视图与阶段边界 | +| §11 | 多租户、JSON/JCS、安全边界和不可能承诺 | OS 身份、密钥/DB/审批来源与 Host 安全设计一致 | +| §12 | 错误码与失败归属 | 新 FK_I_* 不改旧 61 码;业务失败与设施错误不混淆 | +| §13 | 资源预算、性能、保留、宿主模式 | 2 MiB/128 stages 等是候选预算还是实测承诺;不得过度承诺 | +| §14 | Standard/Interactive 有意差异 | T33 对照范围和明确不等价项 | +| §15 | T01–T34、R01–R22、S01–S14、统一验收证据 | 编号、原意、章节、阶段与状态完整 | +| §16 | P0-A/P0-B/P1/P2 实施门禁 | 是否按阶段拒绝未实现能力;P0-A 的模块顺序和退出门槛 | +| §17 | R5 修订、残余风险、停机门槛 | 文档修订不被冒充代码/运行 PASS | +| §18 | 用户确认与后续顺序 | 先 PoC、再 P0-A 规划、再单独授权正式开发 | + +### Step 4 — 需求、技术设计和实现三方追踪 + +建立统一矩阵,至少包含: + +`requirementId`、`sourceText`、`V0.6Section`、`designDecision`、`currentMainFiles`、`tests`、`PR14SecurityMapping`、`POCEvidence`、`designStatus`、`implementationStatus`、`evidenceTier`、`gap`、`owner`、`reentryGate`。 + +必须分开回答: +- **需求符合性**:V0.6 有没有忠实实现 V0.3? +- **设计完整性**:技术规范是否唯一、闭环、可实施? +- **代码实现状态**:当前 main 实际实现到哪里? +- **验证状态**:是否有真实运行证据? + +不得将“设计覆盖”写成“代码已实现”,不得将“脚本/POC PASS”写成“真实宿主 PASS”。 + +### Step 5 — 历史反例和定向场景追踪 + +- R01–R22:逐项对照 V0.6 §15.2、相关 §3/§7/§8/§10;确认事故反例仍保留且有可执行验证入口。 +- S01–S14:逐项对照 §15.2.3;确认 Host、P0-A、P0-B、P1/P2 的边界和真实证据要求。 +- 不因 R5 独立审查称“设计修订已关闭”就把 R01–R22 测试状态改为 PASS。 + +### Step 6 — PR #14 Trusted Host V2 补充安全设计一致性 + +将 PR #14 的 TH-001–TH-030、SEC-01–SEC-19、NAT-01–NAT-15、F-01–F-07 作为**后续安全补充追踪**,不冒充 V0.3 原始 T 编号或 V0.6 的原始章节。 + +重点核对: +- Host/Model OS 主体、密钥、SQLite/WAL/SHM、进程句柄边界是否落实 V0.6 §7/§11。 +- 最小安全拓扑是否保持 V0.6 的无强制 Server/监听器/MCP 原则。 +- mTLS/Bridge 是否按实际部署可选,而不是无条件引入。 +- 模型候选结果、真实工具事件、HostSubmission、CertifiedEvent、Receipt 的身份绑定是否一致。 +- Outbox/Receipt 恢复是否符合 V0.6 §3/§7/§8 的原子提交语义。 +- F-02/F-04 的待实测项不得改成 PASS;C12/S13/N 层不得用文档/CI 代替。 + +### Step 7 — 复核已有 Core POC 证据并冻结剩余差距 + +不重跑已通过的原型测试来“刷绿”。逐条核对 POC-01–09 和 POC-F01–F05: + +| POC | 已有结果 | 下一步 | +|---|---|---| +| POC-01 | PARTIAL | 接入真实 FlowKit AST/Fields/Graph/Dataflow;真实 DSL → IR/JS;核对 Standard 对照和非法边 | +| POC-02 | 本地 PASS | 在目标 Node/OS 上复验真实 SQLite Genesis/CAS/Journal、事务回滚和持久性 | +| POC-03 | 9 个进程级断点 PASS | 补目标 OS/Node 矩阵和 Store 恢复;区分模型外部调用丢响应与纯本地崩溃 | +| POC-04 | 本地双进程 PASS | 在真实 Node/OS 矩阵复验竞争、历史 Receipt、丢响应和线性化读取 | +| POC-05 | PARTIAL | 验证 immutable artifact、Run retain、升级兼容矩阵、GC、备份/恢复和旧执行器保留 | +| POC-06 | PARTIAL | 真实 Host/Model OS 身份、私有 DB/Key/ExecutorContext/namespace ACL 负例 | +| POC-07 | Human BLOCKED / Admin PARTIAL | P0-A 只验证真实受权 adminTerminate;Human 属 P0-B,缺独立审批通道继续 BLOCKED | +| POC-08 | 完整矩阵 BLOCKED | Node 22.13.0/24.22.1 × Ubuntu 24.04/Windows 2025;TS 5.0.4;Standard 与包导出消费者回归 | +| POC-09 | PARTIAL | 用真实 P0-A Agent 重试、阶段预算、Checkpoint/Journal 测量 CAS、大小、耗时和写放大 | + +额外 POC-F04:为 JCS/I-JSON 子集准备 RFC 8785 测试向量、重复键/Unicode/数字边界/首次 await 前快照测试。 +额外 POC-F03:真实 OS 安全主体、Dispatcher 单写者与 admin 通道验证;不把 P0-B Human/Action 语义提前带入 P0-A。 + +### Step 8 — 形成正式符合性审查报告与独立复审 + +报告至少包含: +1. 原件身份、完整 SHA-256、归档校验和源码基线。 +2. V0.3 T01–T34 逐项矩阵。 +3. V0.6 §§1–18 逐章审查。 +4. R01–R22、S01–S14 追踪。 +5. 当前 main 文件/测试实现映射。 +6. PR #14 安全设计补充映射。 +7. POC-01–09 与 POC-F01–F05 证据边界。 +8. Findings(P0/P1/P2/P3)及可复现证据、最小修订、验收入口。 +9. 未实现、阻断、延期和不在范围的事项。 +10. Phase 2 收口裁决与 P0-A 规划准入建议。 + +先由本轮审查形成证据矩阵,再交 Grok Build 独立复核;不得让复审者只看摘要或只读上一轮结论。修订正式契约前必须取得用户确认。 + +## 3. 已确认 P0-A 实施计划核对与后续路线 + +### Step 9 — 定向 PoC 补验证(按依赖并行) + +- 真实 FlowKit 前端/IR PoC(POC-01):优先。 +- SQLite/Node/Windows/Ubuntu/跨进程持久性矩阵(POC-02/03/04/08)。 +- Host OS 身份、私有 Store/Key/句柄和 admin 安全负例(POC-06/07;与 PR14 H3-01/H3-05 对齐)。 +- JCS 规范向量与严格 JSON(POC-F04)。 +- 代表性 Agent 重试、预算和写放大(POC-09)。 +- Registry/Retain/升级/恢复(POC-05)。 + +每个补验证保留源 Commit、Program Hash、Node/OS 版本、命令、原始日志和 PASS/PARTIAL/FAIL/BLOCKED;不能用 Linux 结果代替 Windows,也不能用 POC 代替生产实现。 + +### Step 10 — 核对已确认 P0-A 实施计划 V0.1 与当前 M1–M5 + +不是重新制定已确认的计划。对照 `01-P0A-IMPLEMENTATION-PLAN-V0.1-CONFIRMED.md`、`00-APPROVAL-RECORD.md` 和当前 M1–M5 源码/PR/CI,映射 31 项任务,记录已实现、已验收、缺口与 M6 尾项;只产生差异台账与有证据的补充执行计划。依据 V0.6 §16.1/§16.2 核对: +- 模块/文件级范围与依赖图。 +- Frontend/Feature Gate/Resumable IR/IR Validator/JS Emitter。 +- Loader/同字节验证/ProgramRegistry/retain。 +- Canonical JSON/JCS/Checkpoint/Coordinator/Transition Journal/Receipt。 +- SQLite 参考 Store 适配器及五操作事务契约。 +- Host Runtime Binding/PublicTask/TrustedExecutorContext/HostSubmission/单 Dispatcher。 +- Trusted Host OS 权限、adminTerminate 和模型候选输入通道。 +- types、package exports、Standard 回归、集成/故障注入测试。 +- 每项关联 T/R/S/POC/TH/SEC/NAT、风险、回滚和退出标准。 + +**V0.6 §16 的 P0-A 范围**:共享 DSL 分析、Resumable IR、真实 JS Program ABI、可信 Registry/Store/Journal/Receipt、Condition/Terminal/基本 Agent(Skill + 同步纯 validateResult + 默认重试)、compileInteractive/start/complete/resume、受权 adminTerminate、namespace/结果投影和跨进程恢复。Action、独立 Verification、Human、Loop、Parallel 和 Agent 非纯 checkStep/verify 必须拒绝,不能提前开放。 + +### Step 11 — 对尚未完成的 M6/Host 工作执行独立变更门禁 + +- 已合并 M1–M5 不重复开发;只针对真实差异或未关闭的 M6/Host 工作实施独立改动,精确说明影响的正式要求。 +- 新增生产 Core/ABI/Schema 或改变被冻结语义,必须经过具体变更方案及用户批准;纯审查与测试不会扩大授权。 +- 尚未完成的 Host 隔离、模型来源、M6 原生工具调用只能按相应阶段和证据门禁实施。 +- 每个阶段先跑本地针对测试,再跑四平台/Node CI,再独立复审。 +- 每个 PR 只解决已确认范围;任何 ABI/Schema/语义变化先走变更审查。 +- P0-A 退出:T01–T14 中 A/Host、T29/T30/T34-A、S01–S07、S12-A、S14 的适用项有真实证据;Standard 全量回归通过;真实 JS Host 工具能力如不支持则明确 BLOCKED,不伪装完成。 + +### Step 12 — P0-B 顺序节点 + +实现 Agent 完整 checkStep/verify、Action/operationId/reconcile、普通 Verification、可信 Human 修订前缀、Loop、业务 cancel、可信 contextError/Trace。验收 T15–T23、T31–T33、S08–S12;Human 缺少独立可信审批通道时仍 BLOCKED。 + +### Step 13 — P1 Parallel all_succeeded + +实施前另行冻结 fail-fast/outstanding、分支迟到结果、State 写集合冲突与 Join 语义。通过 T24/T26 和 P1 专项测试后才可进入。 + +### Step 14 — P2 Parallel any + +T25 的赢家选择、未完成分支、迟到副作用、下游状态可见性必须重新形成独立设计并由用户确认。不得复用未经批准的 settle-outstanding 草案。 + +### Step 15 — 全量兼容、真实宿主与正式交付 + +按 T27/T28/T33/T34、S13/S14、PR14 C01–C24 和安全矩阵核验: +- Codex/Grok/Claude/ChatGPT 只有真实 JS/Node 工具路径才算宿主接入。 +- Standard/Interactive 有意差异透明。 +- Node/OS/TS/包导出/类型/文档/性能/安全回归完整。 +- C12/S13 必须有真实原生工具、来源绑定和 Host 安全证据。 +- 发布/部署另行授权,不因 P0-A/P0-B 完成自动进行。 + +## 4. 统一状态和停机规则 + +每条矩阵只能使用: +- `PASS`:有精确源码/原文和可复现运行证据。 +- `GAP`:设计或实现缺项,尚不一定构成冲突。 +- `CONFLICT`:与更高优先级已确认契约冲突。 +- `NOT-IMPLEMENTED`:设计存在但当前生产代码没有实现。 +- `PARTIAL`:仅部分路径或平台有证据。 +- `BLOCKED`:缺少必要工具/平台/信任来源或测试环境。 +- `DEFERRED`:由已确认阶段边界推迟到 P0-B/P1/P2。 + +最终只有在 T01–T34、V0.6 §§1–18、R01–R22、S01–S14、当前 main 和 PR14 安全补充均完成交叉追踪、重大 Findings 收敛并通过独立复审后,才可将正式契约符合性标为 PASS。 + +**本计划不改变 V0.3/V0.6-R5,也不自动授权新的生产语义开发。** V0.6 §18 约定的前期计划阶段已完成:2026-11-10 有用户确认的 P0-A V0.1 实施计划,后续 M1–M5 已进入当前 main。本轮只评估已有成果与未关闭的 M6/Host 工作,不重开已完成里程碑。 diff --git a/examples/interactive/c01-live-chatgpt.workflow.js b/examples/interactive/c01-live-chatgpt.workflow.js new file mode 100644 index 0000000..fdfe6b2 --- /dev/null +++ b/examples/interactive/c01-live-chatgpt.workflow.js @@ -0,0 +1,62 @@ +import {defineWorkflow,condition,node,terminal} from '@devcodex-labs/flowkit/dsl'; + +// P0-A only. Actual ChatGPT turn outputs come from successive published +// Pending tasks, not prerecorded answers from a simulated LLM. +export default defineWorkflow({ + id:'c01-live-chatgpt-complex', + version:'1.0.0', + name:'C01/C03 live complex model acceptance', + nodes:[ + condition('gate',{ + name:'Verify start permission', + branches:[ + {when:'ready',next:'requirements'}, + {when:'true',next:'declined'}, + ], + }), + node('requirements',{ + name:'Requirements analysis', + agent:'requirements', + inputs:['request'], + output:{type:'object',properties:{requirementSummary:{type:'string'}}}, + retries:0,next:'architecture', + }), + node('architecture',{ + name:'Architecture design', + agent:'architecture', + inputs:['requirementSummary'], + output:{type:'object',properties:{designSummary:{type:'string'}}}, + retries:0,next:'implementation', + }), + node('implementation',{ + name:'Implementation plan', + agent:'implementation', + inputs:['designSummary'], + output:{type:'object',properties:{executionPlan:{type:'string'}}}, + retries:0,next:'review', + }), + node('review',{ + name:'Independent review', + agent:'review', + inputs:['executionPlan'], + output:{ + type:'object', + properties:{ + reviewApproved:{type:'boolean'}, + reviewNotes:{type:'string'}, + }, + }, + retries:1, + next:'decision', + }), + condition('decision',{ + name:'Review decision', + branches:[ + {when:'reviewApproved',next:'done'}, + {when:'true',next:'declined'}, + ], + }), + terminal('done',{name:'Accepted',status:'succeeded'}), + terminal('declined',{name:'Declined',status:'failed'}), + ], +}); diff --git a/package.json b/package.json index d06238c..9d16e5b 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ ], "scripts": { "build": "node scripts/build.mjs", - "test": "node --test tests/dsl.test.mjs tests/contracts.test.mjs tests/compiler/*.test.mjs tests/interactive/*.test.mjs tests/when/*.test.mjs tests/runtime/*.test.mjs tests/cli/*.test.mjs tests/integrations/*.test.mjs tests/package/*.test.mjs", + "test": "node --test tests/dsl.test.mjs tests/contracts.test.mjs tests/compiler/*.test.mjs tests/interactive/*.test.mjs tests/integration/*.test.mjs tests/when/*.test.mjs tests/runtime/*.test.mjs tests/cli/*.test.mjs tests/integrations/*.test.mjs tests/package/*.test.mjs", "test:types": "node scripts/test-types.mjs", "check:package": "node scripts/check-package.mjs", "test:package": "node scripts/test-package.mjs", diff --git a/src/interactive/coordinator.mjs b/src/interactive/coordinator.mjs index 1ce6460..6031f40 100644 --- a/src/interactive/coordinator.mjs +++ b/src/interactive/coordinator.mjs @@ -539,16 +539,27 @@ export function createInteractiveCoordinator({store, runtime, policy = {}, maxSt const byteSnapshot=Buffer.from(programBytes); const declared=copy(programIdentity); assertSHA256(declared.artifactHash); + // The first await yields even when the loader is already settled. Copy + // caller-owned State and startIntent before that yield. Checkpoint + // creation, the start intent hash, and Store writes all use these copies. + let stateSnapshot; + try { stateSnapshot=copy(state); } + catch(error) { + if (error instanceof RangeError && /Canonical JSON exceeds maximum bytes/.test(error.message)) { + throw failure('FK_I_CHECKPOINT_CAPACITY', 'initial State exceeds Checkpoint hard limit'); + } + throw error; + } + const intentSnapshot=copy(startIntent); const program=await loadInteractiveProgram(byteSnapshot,declared.artifactHash); if (!canonicalBytes({...program.interactiveProgram.identity,artifactHash:declared.artifactHash}) .equals(canonicalBytes(declared))) { throw failure('FK_I_PROGRAM_MISMATCH','identity and verified Program differ'); } assertRuntime(program,execution); // before any mutation, reject unavailable Stage bindings - const initial=initialCheckpoint(store.namespace,runId,program,policyHash,state); - const intent=copy(startIntent); + const initial=initialCheckpoint(store.namespace,runId,program,policyHash,stateSnapshot); store.putArtifact({programIdentity:declared,bytes:byteSnapshot}); - const opened=store.createRun({startKey,startIntent:intent,checkpoint:initial}); + const opened=store.createRun({startKey,startIntent:intentSnapshot,checkpoint:initial}); return drive(opened.checkpoint.runId); } diff --git a/tests/fixtures/c01-live-chatgpt-turns.json b/tests/fixtures/c01-live-chatgpt-turns.json new file mode 100644 index 0000000..4615004 --- /dev/null +++ b/tests/fixtures/c01-live-chatgpt-turns.json @@ -0,0 +1,49 @@ +[ + { + "index": 0, + "nodeId": "requirements", + "requestId": "req:21438c34374576a97626dca57822f8818b330b5390251afebac979fe45ee6373", + "taskDigest": "sha256:b48e9fe5d58be4db39b122704761d396e38c0c8859aad010aecf5aa8a57bb138", + "result": { + "requirementSummary": "目标是在不扩大 FlowKit 职责的前提下,对 Interactive P0-A 建立可审计的复杂验收。必须覆盖四个以上串行 Agent、两个以上条件路由、上下游结构化数据传递、Schema 校验失败后的有界重试、Host 崩溃恢复、SQLite CAS 幂等、签名安全隔离、容量边界与三十个以上节点的长链。每个场景分别记录 Program 哈希、真实节点顺序、请求身份、原始回执版本、Journal 连续性和预期终态。脚本协议测试与真实模型测试必须独立标注,未获得 Grok Build 原生会话来源认证时不能宣称 S13 验收通过;业务文件存储及复杂上下文关联仍属于用户配置的 Host 或可选 Context。" + } + }, + { + "index": 1, + "nodeId": "architecture", + "requestId": "req:360b12eb62cdc3e6df997415de82522505a96b36a2cf7ab73b4d118007beb5e3", + "taskDigest": "sha256:2889244e0714d3fc2f845a74733a6c81523fce5a5eb44e8f13ee127aaae5bdac", + "result": { + "designSummary": "验收架构采用控制、执行与上下文三条明确边界:FlowKit 编译现有 DSL 为不可变的 JavaScript Program,以条件路由、Agent Pending、重试次数、Checkpoint 与 SQLite 原子 CAS 控制状态迁移;可信 Host 负责实际模型或工具调用、Ed25519 回执签名、私有凭证隔离、结果校验与业务产物的用户自定义存储;可选 Context 仅按需提供已有资料和关联引用,不参与控制下一节点。测试层划分为可复现的脚本协议回归、真实 ChatGPT 逐节点回复,以及 Grok Build 原生会话和来源认证三类证据。所有场景分别检查执行顺序、输入最小披露、错误终态、可恢复性和 Journal,不以测试数量或脚本执行成功冒充真实模型 S13 门禁。" + } + }, + { + "index": 2, + "nodeId": "implementation", + "requestId": "req:78258c27df23723def94c945aaab4d0dc2854f2fe3280c5f3fb3a4c6af131084", + "taskDigest": "sha256:341c06092fa85738c9f4295be450932ec48d530e758a80cd4e043b04f945d900", + "result": { + "executionPlan": "实施分为三组证据。第一组使用真正编译的 P0-A JavaScript Program、SQLite 和可信 Host,构造四个串行 Agent 与两个条件节点,逐项断言输入只来自前序已确认的 State,分别执行通过、拒绝和缺失条件三条路由;向审查节点注入错误 Schema,确认不写入 State、生成新的 requestId 后再次提交合法回执,重试耗尽须持久化 FK_R004。第二组运行三十六 Agent 的脚本长链,检查每条 Journal、CAS 版本和历史回执查询;分别注入 SIGKILL、双进程竞争、Program 或 Skill 漂移、签名伪造和容量超限,验证恢复与失败封闭。第三组让真实 ChatGPT 逐次读取公开 Pending、独立产出五个模型回合,并留存原始任务摘要、提交记录和终态证据。真正的 Grok Build 模型会话及独立来源认证另行验证,无法提供原生日志时持续标记 BLOCKED,不因 CI 全绿宣称完成。" + } + }, + { + "index": 3, + "nodeId": "review", + "requestId": "req:43585b79c64e2980f1d8b0de4e1fc12b1925dc6aaca4b3a936c4548754d90a55", + "taskDigest": "sha256:da7a90e4b6e758dc2c73587f4760af28135ef53685aea7af35f6476d5b28ace5", + "result": { + "reviewApproved": "true", + "reviewNotes": "独立评审认为方案具备可执行的分层证据路线,但还应要求 C05/C06 直接引用真实 SIGKILL 与双进程 CAS 日志,不能只凭文字宣称恢复成功;C07 要明确 Skill 字节漂移与 Host 绑定版本的区别,C12 必须保留 Grok Build 原生会话来源认证门禁。本轮为受控负面测试,故意把 Schema 要求的布尔型 reviewApproved 写成字符串,预期 FlowKit 只能接纳回执身份、拒绝结果写入 State,并发布新的 review Pending,不能错误进入成功终态。" + } + }, + { + "index": 4, + "nodeId": "review", + "requestId": "req:5005e5df646511a14b1c70b87bc3416ff2399c8cc70d39b2f04bba812f24c092", + "taskDigest": "sha256:decd43a929d6d758aaaa629cc1efdf832f6020cf0662728d278ad35284191490", + "result": { + "reviewApproved": true, + "reviewNotes": "针对本次实际发布的第二次审查 Pending,前一轮失败原因明确是 reviewApproved 类型错误,必须改为布尔值而不是字符串。复审认为上游计划可以作为 FlowKit P0-A 复杂验收的执行基线:它区分了四 Agent 串行与两次条件路由、Schema 失败重试、36 Agent 长链、SQLite CAS 与 Journal、一致性预算、SIGKILL 和多进程竞争,并要求保存 Program 哈希与请求身份。建议先将脚本协议结果和当前五次真实 ChatGPT 模型回合分别列为不同证据层,再补验真实 Grok Build 原生 JS 工具、模型会话来源及独立权限隔离。这里批准的是测试方案可继续实施,不代表 C12 或整个 M6 S13 已经验收通过,也不能因此合并 M6。" + } + } +] diff --git a/tests/integration/m6-e2e.test.mjs b/tests/integration/m6-e2e.test.mjs new file mode 100644 index 0000000..6c1de14 --- /dev/null +++ b/tests/integration/m6-e2e.test.mjs @@ -0,0 +1,380 @@ +import test,{after} from 'node:test'; +import assert from 'node:assert/strict'; +import { + mkdtemp,mkdir,writeFile,readFile,chmod,rm, +} from 'node:fs/promises'; +import {generateKeyPairSync} from 'node:crypto'; +import {spawn} from 'node:child_process'; +import {tmpdir} from 'node:os'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; +import {compileInteractive,createSqliteCheckpointStore} from '../../src/interactive/index.mjs'; +import {canonicalBytes,sha256Bytes} from '../../src/interactive/canonical.mjs'; +import {run,isControlSignal,evalWhen} from '../../src/index.mjs'; + +const hostScript=fileURLToPath(new URL('./m6-host.mjs',import.meta.url)); +const workerScript=fileURLToPath(new URL('./m6-worker.mjs',import.meta.url)); +const roots=[]; +after(async()=>{for(const root of roots)await rm(root,{recursive:true,force:true})}); + +const workflowText=[ + "import {defineWorkflow,condition,node,terminal} from '@devcodex-labs/flowkit/dsl';", + "export default defineWorkflow({id:'m6-acceptance',version:'1.0.0',name:'Acceptance',nodes:[", + "condition('gate',{name:'Gate',branches:[{when:'ready',next:'worker'},{when:'true',next:'declined'}]}),", + "node('worker',{name:'Worker',skill:'skills/writer.md',inputs:['message'],", + " output:{type:'object',properties:{answer:{type:'string'}}},retries:2,next:'done'}),", + "terminal('done',{name:'Done',status:'succeeded'}),", + "terminal('declined',{name:'Declined',status:'failed'})]});", +].join('\n'); + +const privateSentinel='HOST_PRIVATE_SENTINEL'; +async function fixture(t,{stageBudget=128,ready=true,allowedInputKeys=['message'],osIsolation=false}={}) { + const root=await mkdtemp(path.join(tmpdir(),'flowkit-m6-realhost-')); + roots.push(root); + // The current compiler discovers the nearest package.json by default. + // Keep the fixture self-contained instead of relying on the caller's cwd. + await writeFile(path.join(root,'package.json'),JSON.stringify({ + name:'flowkit-m6-fixture',private:true,type:'module', + })+'\n',{mode:0o644}); + await chmod(root,0o755); // allow low-privilege model worker to traverse public ancestors + const privateRoot=path.join(root,'host-private'); + const skillDir=path.join(root,'skills'); + await mkdir(privateRoot,{mode:0o700}); + await mkdir(skillDir,{mode:0o755}); + await chmod(privateRoot,0o700); + const skillPath=path.join(skillDir,'writer.md'); + await writeFile(skillPath,'Review and return a JSON answer.',{mode:0o644}); + const sourcePath=path.join(root,'acceptance.workflow.js'); + await writeFile(sourcePath,workflowText,{mode:0o644}); + // The isolated nobody worker cannot traverse the CI checkout on every + // runner. Publish *only* this non-secret scripted worker into a world- + // readable temp root, while Host DB/keys remain under privateRoot/0700. + const publicWorker=path.join(root,'public-m6-worker.mjs'); + await writeFile(publicWorker,await readFile(workerScript),{mode:0o644}); + await chmod(publicWorker,0o644); + const compiled=await compileInteractive(sourcePath,{ + output:'memory',featureStage:'P0-A', + }); + assert.equal(compiled.diagnostics.filter(x=>x.severity==='error').length,0, + JSON.stringify(compiled.diagnostics)); + assert.ok(compiled.programIdentity?.artifactHash); + const identityPath=path.join(privateRoot,'manifest.json'); + const programPath=path.join(privateRoot,'accepted.interactive.mjs'); + const storePath=path.join(privateRoot,'run.db'); + const privateKeyPath=path.join(privateRoot,'signing-key.pem'); + const configPath=path.join(privateRoot,'host-config.json'); + const pair=generateKeyPairSync('ed25519'); + await writeFile(programPath,compiled.code,{mode:0o600}); + await writeFile(identityPath,JSON.stringify(compiled.programIdentity),{mode:0o600}); + await writeFile(privateKeyPath, + pair.privateKey.export({type:'pkcs8',format:'pem'}),{mode:0o600}); + const config={ + namespace:'m6-reference-host',runId:'run-m6-test', + startKey:'m6-acceptance-start',storePath,programPath,identityPath, + privateKeyPath,skillPath,sourcePath,skillHash:sha256Bytes(await readFile(skillPath)),workerPath:publicWorker, + state:{ready,message:'hello',secret:privateSentinel}, + allowedInputKeys,stageBudget,osIsolation, + }; + await writeFile(configPath,JSON.stringify(config),{mode:0o600}); + await chmod(privateRoot,0o700); + const readStore=()=>{ + const store=createSqliteCheckpointStore(storePath,{namespace:config.namespace}); + try { + const cp=store.load(config.runId); + return {cp,journal:store.listJournal(config.runId), + artifact:cp&&store.getArtifact(cp.artifactHash)}; + } finally {store.close()} + }; + return { + root,sourcePath,configPath,config,compiled,readStore, + async updateConfig(update) { + Object.assign(config,update); + await writeFile(configPath,JSON.stringify(config),{mode:0o600}); + }, + async standardResult() { + const host={ + isControlSignal,evalWhen, + runAgent:async step=>({answer:'external-worker-processed:'+step.input.message}), + validateResult:(_schema,result)=>typeof result?.answer==='string'?[]:['bad answer'], + }; + return run(sourcePath,host,{...config.state},{projectRoot:root,skillCheck:true}); + }, + }; +} +function readEvents(stdout) { + return stdout.trim().split(/\r?\n/).filter(Boolean).map(line=>{ + let event=JSON.parse(line); + if(typeof event.event!=='string') throw new TypeError('Malformed Host event'); + return event; + }); +} +async function invoke(fixture,mode='start',cut='none') { + return new Promise((resolve,reject)=>{ + const child=spawn(process.execPath,[hostScript,fixture.configPath,mode,cut], + {stdio:['ignore','pipe','pipe']}); + let stdout='',stderr='',length=0; + const timeout=setTimeout(()=>{child.kill();reject(new Error('M6 Host timeout'))},60000); + child.stdout.on('data',b=>{ + length+=b.length; + if(length>512*1024) {child.kill();reject(new RangeError('Host trace exceeds limit'))} + else stdout+=b.toString(); + }); + child.stderr.on('data',b=>{if(stderr.length<2048)stderr+=b.toString().slice(0,2048-stderr.length)}); + child.on('error',e=>{clearTimeout(timeout);reject(e)}); + child.on('close',(code,signal)=>{ + clearTimeout(timeout); + try { + const events=readEvents(stdout); + resolve({code,signal,events,stderr,stdout}); + } catch(e) {reject(e)} + }); + }); +} +function assertContinuousJournal(snapshot) { + assert.deepEqual(snapshot.journal.map(item=>item.revision), + Array.from({length:snapshot.cp.revision+1},(_,i)=>i)); + assert.equal(snapshot.journal.at(-1).checkpointHash, + sha256Bytes(canonicalBytes(snapshot.cp))); + for(let i=1;i{ + const root=await mkdtemp(path.join(tmpdir(),'flowkit-m6-project-root-')); + roots.push(root); + const projectRoot=path.join(root,'project'); + const sourceDir=path.join(root,'workflow-source'); + const skillDir=path.join(projectRoot,'skills'); + await mkdir(skillDir,{recursive:true}); + await mkdir(sourceDir,{recursive:true}); + await writeFile(path.join(projectRoot,'package.json'),JSON.stringify({ + name:'flowkit-m6-explicit-root',private:true,type:'module', + })+'\n',{mode:0o644}); + await writeFile(path.join(skillDir,'writer.md'),'Review and return a JSON answer.',{mode:0o644}); + const sourcePath=path.join(sourceDir,'acceptance.workflow.js'); + await writeFile(sourcePath,workflowText,{mode:0o644}); + const compiled=await compileInteractive(sourcePath,{ + output:'memory',featureStage:'P0-A',projectRoot, + }); + assert.equal(compiled.diagnostics.filter(x=>x.severity==='error').length,0, + JSON.stringify(compiled.diagnostics)); + assert.ok(compiled.programIdentity?.artifactHash); +}); + +test('M6: real compiler artifact -> Node Host -> separately spawned worker -> authenticated receipt -> terminal matches Standard',async t=>{ + const f=await fixture(t,{osIsolation:process.env.FLOWKIT_M6_ISOLATION==='1'}); + const host=await invoke(f); + assert.equal(host.code,0,host.stderr); + assertModelNeverSawSecret(host); + assert.deepEqual(host.events.map(x=>x.event),[ + 'host-start','task-published','worker-exited','receipt-accepted','host-terminal', + ]); + assert.equal(host.events[0].status,'waiting'); + assert.equal(host.events.at(-1).status,'succeeded'); + const persisted=f.readStore(); + assertContinuousJournal(persisted); + assert.equal(persisted.cp.status,'succeeded'); + assert.equal(persisted.cp.state.secret,privateSentinel); + assert.equal(persisted.cp.state.answer,'external-worker-processed:hello'); + assert.equal(persisted.cp.receipts.length,1); + assert.equal(persisted.cp.receipts[0].acceptedRevision, + host.events.find(x=>x.event==='receipt-accepted').acceptedRevision); + assert.equal(persisted.cp.artifactHash,f.compiled.programIdentity.artifactHash); + assert.ok(persisted.artifact.bytes.equals(Buffer.from(f.compiled.code))); + const standard=await f.standardResult(); + assert.equal(standard.terminal,'done'); + assert.equal(sha256Bytes(canonicalBytes(standard.state)), + sha256Bytes(canonicalBytes(persisted.cp.state))); + const replay=await invoke(f,'resume'); + assert.equal(replay.code,0); + assert.deepEqual(replay.events.map(x=>x.event),['host-start','host-terminal']); + assert.equal(replay.events.at(-1).revision,persisted.cp.revision); +}); + +test('M6: after Pending publish, killing Host and reopening only persisted Program/Store preserves requestId exactly',async t=>{ + const f=await fixture(t,{osIsolation:process.env.FLOWKIT_M6_ISOLATION==='1'}); + const killed=await invoke(f,'start','after-pending'); + assert.notEqual(killed.code,0,'fault must terminate Host'); + assert.ok(killed.events.some(e=>e.event==='fault-injected')); + assertModelNeverSawSecret(killed); + const prior=f.readStore(); + assert.equal(prior.cp.status,'waiting'); + assert.equal(prior.cp.pending.requestId, + killed.events.find(e=>e.event==='task-published').requestId); + const revision=prior.cp.revision; + // No source DSL is needed for resume: Program bytes are retained in Store. + await rm(f.sourcePath); + const resumeStarted=process.hrtime.bigint(); + const recovered=await invoke(f,'resume'); + const resumeMs=Number(process.hrtime.bigint()-resumeStarted)/1e6; + assert.equal(recovered.code,0,recovered.stderr); + assert.equal(recovered.events.find(x=>x.event==='task-published').requestId, + prior.cp.pending.requestId); + assert.ok(recovered.events.find(x=>x.event==='receipt-accepted').acceptedRevision>revision); + const done=f.readStore(); + assert.equal(done.cp.status,'succeeded'); + assert.equal(done.cp.receipts.length,1); + assertContinuousJournal(done); + console.log('M6_RECOVERY_METRICS '+JSON.stringify({ + schemaVersion:1,resumeMs:Math.round(resumeMs), + revisionBefore:revision,revisionAfter:done.cp.revision, + casCount:done.cp.revision,journalEntries:done.journal.length, + checkpointBytes:canonicalBytes(done.cp).byteLength, + sameRequestId:true, + })); +}); + +test('M6: worker finished but Host crashed before durable receipt; retry preserves Pending identity, not exactly-once provider execution',async t=>{ + const f=await fixture(t); + const killed=await invoke(f,'start','after-worker'); + assert.notEqual(killed.code,0); + assert.ok(killed.events.some(e=>e.event==='worker-exited')); + const before=f.readStore(); + assert.equal(before.cp.status,'waiting'); + assert.equal(before.cp.receipts.length,0); + const sameRequest=before.cp.pending.requestId; + const resumed=await invoke(f,'resume'); + assert.equal(resumed.code,0,resumed.stderr); + assert.equal(resumed.events.find(x=>x.event==='task-published').requestId,sameRequest); + assert.equal(f.readStore().cp.receipts.length,1); + assert.equal(f.readStore().cp.state.answer,'external-worker-processed:hello'); +}); + +test('M6: budget 1 versus budget 128 reaches byte-equal terminal State, with monotonic CAS and no fake waiting',async t=>{ + const a=await fixture(t,{stageBudget:1}); + const b=await fixture(t,{stageBudget:128}); + const startClock=process.hrtime.bigint(); + const [fast,slow]=await Promise.all([invoke(a),invoke(b)]); + const durationMs=Number(process.hrtime.bigint()-startClock)/1e6; + assert.equal(fast.code,0,fast.stderr); + assert.equal(slow.code,0,slow.stderr); + for(const f of [a,b]) assertContinuousJournal(f.readStore()); + const left=a.readStore(),right=b.readStore(); + assert.equal(sha256Bytes(canonicalBytes(left.cp.state)), + sha256Bytes(canonicalBytes(right.cp.state))); + assert.equal(left.cp.status,'succeeded'); + assert.equal(right.cp.status,'succeeded'); + assert.ok(left.cp.revision>=right.cp.revision); + for(const run of [fast,slow]) { + assert.equal(run.events.filter(x=>x.event==='task-published').length,1); + assert.equal(run.events.at(-1).status,'succeeded'); + } + console.log('M6_S07_METRICS '+JSON.stringify({ + schemaVersion:1,wallMs:Math.round(durationMs), + budget1:{checkpointBytes:canonicalBytes(left.cp).byteLength,casCount:left.cp.revision, + journalEntries:left.journal.length}, + budget128:{checkpointBytes:canonicalBytes(right.cp).byteLength,casCount:right.cp.revision, + journalEntries:right.journal.length}, + equalTerminalState:true, + })); +}); + +test('M6: changed Host disclosure policy denies resume with no journal/CAS mutation',async t=>{ + const f=await fixture(t); + const killed=await invoke(f,'start','after-pending'); + assert.notEqual(killed.code,0); + const original=f.readStore(); + const bytes=canonicalBytes(original.cp); + await f.updateConfig({allowedInputKeys:[]}); + const denied=await invoke(f,'resume'); + assert.notEqual(denied.code,0); + assert.equal(denied.events.some(x=>x.event==='worker-exited'),false); + assert.deepEqual(canonicalBytes(f.readStore().cp),bytes); + assert.equal(f.readStore().journal.length,original.journal.length); +}); + +test('M6: false Condition chooses failure Terminal without launching worker and equals Standard',async t=>{ + const f=await fixture(t,{ready:false}); + const host=await invoke(f); + assert.equal(host.code,0,host.stderr); + assert.equal(host.events.at(-1).status,'failed'); + assert.equal(host.events.some(x=>x.event==='task-published'),false); + assert.equal(f.readStore().cp.receipts.length,0); + const standard=await f.standardResult(); + assert.equal(standard.terminal,'declined'); + assert.equal(sha256Bytes(canonicalBytes(standard.state)), + sha256Bytes(canonicalBytes(f.readStore().cp.state))); +}); + +test('M6: tampered SQLite artifact byte is rejected before worker dispatch; never silently repaired',async t=>{ + const f=await fixture(t); + const killed=await invoke(f,'start','after-pending'); + assert.notEqual(killed.code,0); + const before=f.readStore(); + // External DB corruption is tested through raw SQLite only in this adversarial test. + const {DatabaseSync}=await import('node:sqlite'); + const db=new DatabaseSync(f.config.storePath); + try { + db.prepare('UPDATE fk_i_artifacts SET program_bytes = ? WHERE namespace = ? AND artifact_hash = ?') + .run(Buffer.from('tampered'),f.config.namespace,before.cp.artifactHash); + } finally {db.close()} + const attempted=await invoke(f,'resume'); + assert.notEqual(attempted.code,0); + assert.equal(attempted.events.some(x=>x.event==='worker-exited'),false); + const cpSnapshot=(()=>{ + const s=createSqliteCheckpointStore(f.config.storePath,{namespace:f.config.namespace}); + try{return s.load(f.config.runId)}finally{s.close()} + })(); + assert.deepEqual(canonicalBytes(cpSnapshot),canonicalBytes(before.cp)); +}); + +test('M6: external worker receives only public task; dedicated Linux job additionally enforces cross-UID access denial',async t=>{ + const isolation=process.env.FLOWKIT_M6_ISOLATION==='1'; + if(isolation) assert.equal(process.platform,'linux'); + const f=await fixture(t,{osIsolation:isolation}); + const run=await invoke(f); + assert.equal(run.code,0,run.stderr); + assert.equal(run.events.filter(e=>e.event==='worker-exited').length,1); + assertModelNeverSawSecret(run); + assert.equal(f.readStore().cp.status,'succeeded'); +}); + + +test('M6 S06: source/Program/Skill bytes changing while waiting fail closed without another Pending',async t=>{ + const cases=[ + {kind:'source',mutate:async f=>writeFile(f.sourcePath, + workflowText+'\n// unauthorized source edit\n')}, + {kind:'Program artifact',mutate:async f=>writeFile(f.config.programPath, + Buffer.concat([await readFile(f.config.programPath),Buffer.from('\n// unauthorized artifact edit')]))}, + {kind:'Skill instruction',mutate:async f=>writeFile(f.config.skillPath, + 'Modified Skill instruction after Agent Pending was published')}, + ]; + for(const scenario of cases) { + const f=await fixture(t); + const crashed=await invoke(f,'start','after-pending'); + assert.notEqual(crashed.code,0,scenario.kind); + const before=f.readStore(); + assert.equal(before.cp.status,'waiting'); + const original=canonicalBytes(before.cp); + await scenario.mutate(f); + const rejected=await invoke(f,'resume'); + assert.notEqual(rejected.code,0,scenario.kind); + assert.equal(rejected.events.some(e=>e.event==='worker-exited'),false,scenario.kind); + assert.deepEqual(canonicalBytes(f.readStore().cp),original,scenario.kind); + assert.equal(f.readStore().journal.length,before.journal.length,scenario.kind); + } +}); + +test('M6 S01: missing Condition input yields persistent FK_R002 error, not waiting or a model task',async t=>{ + const f=await fixture(t); + await f.updateConfig({state:{message:'hello',secret:privateSentinel}}); + const run=await invoke(f); + assert.equal(run.code,2,run.stderr); + assert.equal(run.events.at(-1)?.status,'errored'); + assert.equal(run.events.some(x=>x.event==='worker-exited'),false); + const persisted=f.readStore(); + assert.equal(persisted.cp.status,'errored'); + assert.equal(persisted.cp.terminal.causeCode,'FK_R002'); + const history=persisted.journal.length; + const resume=await invoke(f,'resume'); + assert.equal(resume.code,2); + assert.equal(f.readStore().journal.length,history); + await assert.rejects(f.standardResult(),{code:'FK_R002'}); +}); diff --git a/tests/integration/m6-host.mjs b/tests/integration/m6-host.mjs new file mode 100644 index 0000000..c1c6241 --- /dev/null +++ b/tests/integration/m6-host.mjs @@ -0,0 +1,207 @@ +/** + * M6 real Node Host process fixture. It uses the production FlowKit + * compiler artifact, SQLite Store and Ed25519 Trusted Host, then exchanges + * JSON with a distinct scripted worker process over stdin/stdout. + * + * It is intentionally not a Codex/Claude/Grok provider integration. + * No endpoint allows worker/model stdin to invoke complete() or sign receipts. + */ +import {readFile} from 'node:fs/promises'; +import {createHash} from 'node:crypto'; +import {spawn} from 'node:child_process'; +import { + createPrivateKey,createPublicKey,sign,randomUUID, +} from 'node:crypto'; +import process from 'node:process'; + +const digest=bytes=>'sha256:'+createHash('sha256').update(bytes).digest('hex'); +function drift(message){return Object.assign(new Error('FK_I_POLICY_MISMATCH: '+message),{code:'FK_I_POLICY_MISMATCH'});} +import { + createSqliteCheckpointStore,createTrustedInteractiveHost, + createAgentReceiptStatement,encodeAgentReceiptStatement, +} from '../../src/interactive/index.mjs'; + +function trace(event, fields={}) { + process.stdout.write(JSON.stringify({event,...fields})+'\n'); +} +function onlyAllowedConfig(cfg) { + if (!cfg || typeof cfg!=='object' || Array.isArray(cfg)) throw new TypeError('Expected trusted Host config'); + for(const field of ['namespace','runId','startKey','storePath','programPath','identityPath','privateKeyPath','skillPath','workerPath','skillHash']) { + if(typeof cfg[field]!=='string' || !cfg[field]) throw new TypeError('Missing Host config '+field); + } + if (!cfg.state || typeof cfg.state!=='object' || !Array.isArray(cfg.allowedInputKeys)) { + throw new TypeError('Invalid Host start configuration'); + } +} +async function spawnScriptedWorker(cfg,task) { + let command=process.execPath; + let args=[cfg.workerPath]; + if(cfg.osIsolation===true && process.platform!=='linux') { + throw new Error('Cross-UID mode requires a Linux Host'); + } + if(cfg.osIsolation===true) { + args.push(cfg.privateKeyPath,cfg.storePath); + if(process.getuid?.()===0) { + // Set in spawn options below, do not inherit Host UID or GID. + } else { + command='sudo'; + args=['-n','-u','nobody','--',process.execPath,...args]; + } + } + // Deliberately do not inherit process.env: it might contain Host secrets. + const env={ + PATH:process.env.PATH||'', + ...(process.platform==='win32'?{ + SystemRoot:process.env.SystemRoot||'C:\\Windows', + TEMP:process.env.TEMP||'',TMP:process.env.TMP||'', + }:{HOME:'/tmp'}), + }; + const options={ + cwd:process.cwd(),env,stdio:['pipe','pipe','pipe'], + ...(cfg.osIsolation===true && process.getuid?.()===0 + ? {uid:65534,gid:65534} : {}), + }; + const child=spawn(command,args,options); + return new Promise((resolve,reject)=>{ + let stdout='',stderr='',bytes=0; + const timeout=setTimeout(()=>{child.kill();reject(new Error('M6 worker timeout'))},15000); + child.stdout.on('data',chunk=>{ + bytes+=chunk.length; + if(bytes>128*1024) { + child.kill(); + reject(new Error('M6 worker output limit exceeded')); + } else stdout+=chunk.toString('utf8'); + }); + child.stderr.on('data',chunk=>{ + if(stderr.length<2048)stderr+=chunk.toString('utf8').slice(0,2048-stderr.length); + }); + child.on('error',error=>{clearTimeout(timeout);reject(error)}); + child.on('close',code=>{ + clearTimeout(timeout); + if(code!==0) { + reject(new Error('M6 external worker exited '+code+': '+stderr.slice(0,300))); + return; + } + try { + if(stdout.trim().split(/\r?\n/).length!==1) { + throw new TypeError('Worker must send one JSON response'); + } + const data=JSON.parse(stdout); + const keys=Object.keys(data).sort(); + if(keys.join(',')!=='kind,requestId,result' || data.kind!=='agent-result' + || data.requestId!==task.requestId + || !data.result || typeof data.result.answer!=='string' + || Object.keys(data.result).join(',')!=='answer') { + throw new TypeError('Uncertified worker response envelope'); + } + resolve(data); + } catch(error) {reject(error)} + }); + child.stdin.on('error',()=>{}); // close errors handled through child's close result + child.stdin.end(JSON.stringify(task)); + }); +} + +const [configPath,mode='start',cut='none']=process.argv.slice(2); +if(!configPath || !['start','resume'].includes(mode) + || !['none','after-pending','after-worker'].includes(cut)) { + throw new TypeError('Usage: m6-host.mjs start|resume none|after-pending|after-worker'); +} +const config=JSON.parse(await readFile(configPath,'utf8')); +onlyAllowedConfig(config); +const programBytes=await readFile(config.programPath); +const programIdentity=JSON.parse(await readFile(config.identityPath,'utf8')); +if(digest(programBytes)!==programIdentity.artifactHash) throw drift('Program artifact bytes changed'); +async function checkSkill(){ + if(digest(await readFile(config.skillPath))!==config.skillHash) + throw drift('Skill instruction bytes changed'); +} +await checkSkill(); +// Source is optional after compiling; when still present it MUST match pinned +// source identity. Deleting source cannot change the immutable saved Program. +if(config.sourcePath){ + let existingSource; + try {existingSource=await readFile(config.sourcePath)} + catch(e){if(e.code!=='ENOENT')throw e} + if(existingSource && digest(existingSource)!==programIdentity.sourceHash) + throw drift('Original source bytes changed'); +} +const privateKey=createPrivateKey(await readFile(config.privateKeyPath)); +const publicKey=createPublicKey(privateKey); +const store=createSqliteCheckpointStore(config.storePath,{namespace:config.namespace}); +try { + const host=createTrustedInteractiveHost({ + store, + runtime:{ + skillResolver:async()=>{await checkSkill();return readFile(config.skillPath,'utf8')}, + validateResult:(_schema,result)=>typeof result?.answer==='string'?[]:['Expected a string answer'], + }, + policy:{testHostId:'m6-isolated-node-host-v1',skillManifestHash:config.skillHash}, + trustedPublicKeys:{'m6-host-key':publicKey}, + trustedArtifacts:[programIdentity.artifactHash], + allowedInputKeys:config.allowedInputKeys, + disclosurePolicyId:'m6-explicit-input-allowlist', + authorizeDisclosure:async({task})=>( + Object.keys(task.input).every(key=>config.allowedInputKeys.includes(key)) + && task.missing.every(key=>config.allowedInputKeys.includes(key)) + ), + maxStagesPerCall:config.stageBudget??128, + }); + let frame=mode==='start' + ? await host.start({ + programBytes,programIdentity,startKey:config.startKey, + runId:config.runId,state:config.state, + }) + : await host.resume({runId:config.runId}); + trace('host-start',{status:frame.status,runId:frame.runId,revision:frame.revision}); + let loops=0; + while(loops++<1000) { + if(frame.status==='paused') { + if(frame.pauseReason!=='stage-budget') throw new Error('Unexpected durable pause'); + frame=await host.resume({runId:config.runId}); + continue; + } + if(frame.status!=='waiting')break; + await checkSkill(); + const task=await host.getPublicAgentTask({runId:frame.runId}); + if(!task || task.requestId!==frame.pending?.requestId) { + throw new TypeError('Published task and Store Pending differ'); + } + trace('task-published',{runId:task.runId,requestId:task.requestId,nodeId:task.nodeId, + revision:frame.revision,attempt:task.attempt}); + if(cut==='after-pending') { + trace('fault-injected',{where:'after-pending',requestId:task.requestId}); + process.kill(process.pid,'SIGKILL'); + } + const external=await spawnScriptedWorker(config,task); + await checkSkill(); + trace('worker-exited',{requestId:external.requestId}); + if(cut==='after-worker') { + trace('fault-injected',{where:'after-worker',requestId:task.requestId}); + process.kill(process.pid,'SIGKILL'); + } + // Host constructs this envelope. The worker cannot select run identity, + // proof ID, policy, destination node or the signer. + const receipt={ + kind:'agent-result',runId:task.runId,requestId:task.requestId, + nodeId:task.nodeId,attempt:task.attempt,result:external.result, + }; + const issuedAt=Date.now(); + const statement=createAgentReceiptStatement({ + run:store.load(task.runId),receipt,keyId:'m6-host-key', + proofId:'host-process:'+randomUUID(), + issuedAt,expiresAt:issuedAt+60000, + }); + const signature=sign(null,encodeAgentReceiptStatement(statement),privateKey) + .toString('base64url'); + frame=await host.accept({receipt,attestation:{statement,signature}}); + trace('receipt-accepted',{requestId:task.requestId, + acceptedRevision:frame.acceptedRevision,duplicate:frame.duplicate}); + } + if(loops>=1000) throw new Error('M6 stage budget made no progress'); + trace('host-terminal',{status:frame.status,runId:frame.runId,revision:frame.revision}); + if(frame.status==='errored')process.exitCode=2; + else if(!['succeeded','failed'].includes(frame.status))process.exitCode=3; +} finally { + store.close(); +} diff --git a/tests/integration/m6-ledger-boundary.test.mjs b/tests/integration/m6-ledger-boundary.test.mjs new file mode 100644 index 0000000..fa742f9 --- /dev/null +++ b/tests/integration/m6-ledger-boundary.test.mjs @@ -0,0 +1,90 @@ +/** + * M6 S07 positive regression after scoped PR #8: a near-2MiB waiting + * Checkpoint is rejected before mutation, so a valid Agent result from the + * previous safely admitted Pending still reaches a terminal state. + */ +import test,{after} from 'node:test'; +import assert from 'node:assert/strict'; +import {mkdtemp,rm} from 'node:fs/promises'; +import {tmpdir} from 'node:os'; +import path from 'node:path'; +import {analyzeSource} from '../../src/compiler/analyze.mjs'; +import {lowerInteractive} from '../../src/compiler/interactive/lower.mjs'; +import {emitInteractive} from '../../src/compiler/interactive/emit.mjs'; +import {createSqliteCheckpointStore} from '../../src/interactive/sqlite-store.mjs'; +import {createInteractiveCoordinator} from '../../src/interactive/coordinator.mjs'; +import {canonicalBytes,MAX_CHECKPOINT_BYTES} from '../../src/interactive/canonical.mjs'; + +const roots=[]; +after(async()=>{for(const dir of roots)await rm(dir,{recursive:true,force:true})}); +const workflow=[ + "import {defineWorkflow,node,terminal} from '@devcodex-labs/flowkit/dsl';", + "export default defineWorkflow({id:'m6-ledger-boundary',version:'1.0.0',name:'Ledger',nodes:[", + "node('worker',{name:'Worker',agent:'writer',inputs:['message'],", + "output:{type:'object',properties:{answer:{type:'string'}}},retries:0,next:'done'}),", + "terminal('done',{name:'Done',status:'succeeded'})]});", +].join('\n'); + +test('M6 S07: near-hard-limit waiting mutation fails before CAS; valid 64-KiB receipt reaches terminal',async()=>{ + const dir=await mkdtemp(path.join(tmpdir(),'flowkit-m6-ledger-')); + roots.push(dir); + const store=createSqliteCheckpointStore(path.join(dir,'store.db'),{namespace:'m6-s07'}); + try { + const analysis=await analyzeSource(workflow,{ + filename:'m6-ledger-boundary.workflow.js',projectRoot:dir,skillCheck:false, + }); + assert.equal(analysis.diagnostics.some(d=>d.severity==='error'),false, + JSON.stringify(analysis.diagnostics)); + const lowered=lowerInteractive(analysis); + assert.ok(lowered.ir,JSON.stringify(lowered.diagnostics)); + const compiled=emitInteractive(lowered.ir); + assert.ok(compiled.code,JSON.stringify(compiled.diagnostics)); + const api=createInteractiveCoordinator({ + store, + runtime:{ + skillResolver:async()=> 'Write a valid answer', + validateResult:(_schema,result)=> + typeof result?.answer==='string'?[]:['Expected string answer'], + }, + }); + const started=await api.start({ + programBytes:Buffer.from(compiled.code), + programIdentity:compiled.programIdentity, + runId:'s07-near-limit',startKey:'s07-near-limit',state:{message:'hello'}, + }); + assert.equal(started.status,'waiting'); + const old=store.load(started.runId); + const candidate={ + ...old,revision:old.revision+1,state:{...old.state,filler:''}, + }; + const headroom=75; + const baseSize=canonicalBytes(candidate).byteLength; + candidate.state.filler='x'.repeat(MAX_CHECKPOINT_BYTES-baseSize-headroom); + const beforeBytes=canonicalBytes(old); + const beforeJournal=store.listJournal(started.runId); + assert.ok(canonicalBytes(candidate).byteLength>MAX_CHECKPOINT_BYTES-128); + await assert.rejects(async()=>store.compareAndSwap({ + runId:started.runId,expectedRevision:old.revision, + nextCheckpoint:candidate,transitionId:'m6-s07-too-big-before-publish', + journalEntry:{kind:'m6-s07-reject-large-State'}, + }),{code:'FK_I_CHECKPOINT_CAPACITY'}); + assert.deepEqual(canonicalBytes(store.load(started.runId)),beforeBytes); + assert.deepEqual(store.listJournal(started.runId),beforeJournal); + const receipt={ + runId:started.runId,requestId:started.pending.requestId, + nodeId:started.pending.nodeId,attempt:started.pending.attempt, + result:{answer:'x'.repeat(64*1024)}, + }; + const completed=await api.complete(receipt); + assert.equal(completed.status,'succeeded'); + assert.equal(completed.state.answer.length,64*1024); + assert.equal(store.getReceipt(started.runId,receipt.requestId).acceptedRevision, + completed.acceptedRevision); + assert.equal(store.listJournal(started.runId).length,completed.revision+1); + console.log('M6_S07_LEDGER_CAPACITY_FIXED '+JSON.stringify({ + schemaVersion:1,checkpointLimitBytes:MAX_CHECKPOINT_BYTES, + unsafeMutationRejected:true,unchangedOnRejection:true, + validReceiptAccepted:true,ended:completed.status, + })); + }finally{store.close()} +}); diff --git a/tests/integration/m6-worker.mjs b/tests/integration/m6-worker.mjs new file mode 100644 index 0000000..34b2a96 --- /dev/null +++ b/tests/integration/m6-worker.mjs @@ -0,0 +1,38 @@ +/** M6 deterministic external worker fixture — NOT an AI model provider. */ +import {readFile} from 'node:fs/promises'; +import process from 'node:process'; + +const chunks=[]; +let total=0; +for await (const chunk of process.stdin) { + total+=chunk.length; + if(total>512*1024) throw new RangeError('Public task exceeds worker limit'); + chunks.push(chunk); +} +const task=JSON.parse(Buffer.concat(chunks).toString('utf8')); +const allowed=new Set([ + 'schemaVersion','kind','id','nodeId','runId','requestId','attempt', + 'name','instruction','input','missing','resultSchema','reasons','overview', +]); +if (!task || task.schemaVersion!==1 || task.kind!=='agent' + || typeof task.runId!=='string' || typeof task.requestId!=='string' + || typeof task.nodeId!=='string' || typeof task.instruction!=='string' + || Object.keys(task).some(key=>!allowed.has(key)) + || JSON.stringify(task).includes('HOST_PRIVATE_SENTINEL') + || Object.keys(task.input).some(key=>key!=='message') + || task.missing.some(key=>key!=='message')) { + throw new Error('MODEL_VIEW_LEAK_OR_INVALID_TASK'); +} +for(const secretPath of process.argv.slice(2)) { + let denied=false; + try {await readFile(secretPath)} catch(error) { + denied=['EACCES','EPERM'].includes(error.code); + } + if(!denied) throw new Error('MODEL_CAN_READ_HOST_PRIVATE_FILE'); +} +const message=task.input.message; +if(typeof message!=='string') throw new TypeError('Public task must have string message'); +const output={requestId:task.requestId,kind:'agent-result',result:{ + answer:'external-worker-processed:'+message, +}}; +process.stdout.write(JSON.stringify(output)+'\n'); diff --git a/tests/interactive/c01-live-chatgpt.test.mjs b/tests/interactive/c01-live-chatgpt.test.mjs new file mode 100644 index 0000000..ce457e5 --- /dev/null +++ b/tests/interactive/c01-live-chatgpt.test.mjs @@ -0,0 +1,218 @@ +/** + * Recorded ChatGPT-response evidence: four Agent nodes + deliberate schema retry, + * two Conditions. Responses are authored externally after inspecting the exact + * Pending in prior Actions logs, then replayed through FlowKit Program/SQLite/ + * Trusted Host. This test does not call a model API or native FlowKit JS tool. + * + * Normal CI requires all five recorded responses. Only explicit preparation + * mode (FLOWKIT_C01_PREPARE=1) may stop at a missing Pending for the next turn. + * SQLite is reopened within each CI run; separate Actions runs do NOT share a + * physical DB. GitHub tool identity is NOT provider-native session attestation. + */ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {readFile,mkdtemp,rm} from 'node:fs/promises'; +import {fileURLToPath} from 'node:url'; +import {tmpdir} from 'node:os'; +import path from 'node:path'; +import {generateKeyPairSync,randomBytes,sign} from 'node:crypto'; +import {compileInteractive} from '../../src/interactive/compile.mjs'; +import {createSqliteCheckpointStore} from '../../src/interactive/sqlite-store.mjs'; +import {createTrustedInteractiveHost} from '../../src/interactive/trusted-host.mjs'; +import {createAgentReceiptStatement,encodeAgentReceiptStatement} + from '../../src/interactive/host-attestation.mjs'; +import {canonicalBytes,sha256Bytes} from '../../src/interactive/canonical.mjs'; + +const workflowFile=fileURLToPath(new URL( + '../../examples/interactive/c01-live-chatgpt.workflow.js',import.meta.url)); +const repliesFile=fileURLToPath(new URL( + '../fixtures/c01-live-chatgpt-turns.json',import.meta.url)); +const NOW=1794400000000; +const runId='c01-live-multi-agent-20261111'; +const namespace='complex-live-chatgpt'; +const expectedPath=['requirements','architecture','implementation','review','review']; +const instructions={ + requirements:"分析用户的 FlowKit 复杂验收要求,给出严谨、可操作的需求说明。只能回答一个 JSON 对象 {requirementSummary:string},不少于 60 个汉字,不包含未授权的内部信息。", + architecture:"基于公开的 requirementSummary 设计精简的 FlowKit 复杂验收架构,分清 FlowKit JS Program、Host、Context 的职责。只能回答 JSON {designSummary:string},不少于 60 个汉字。", + implementation:"基于上个节点公开的 designSummary,提出可复现的 C01-C12 复杂测试实施步骤,包含真实模型场景和故障/恢复验证。只能回答 JSON {executionPlan:string},不少于 60 个汉字。", + review:"独立评审公开的 executionPlan 的可执行性、证据质量和宿主安全边界。只回答 JSON {reviewApproved:boolean,reviewNotes:string},reviewNotes 不少于 60 个汉字;必须依据实际上游数据,不得凭空宣称 Grok Build 已执行或 S13 已通过。", +}; +const outputShape={ + requirements:{requirementSummary:'string'}, + architecture:{designSummary:'string'}, + implementation:{executionPlan:'string'}, + review:{reviewApproved:'boolean',reviewNotes:'string'}, +}; +function validate(schema,output){ + if(!output||typeof output!=='object'||Array.isArray(output))return ['Agent result must be object']; + const allowed=Object.keys(schema.properties??{}).sort(); + if(allowed.join('|')!==Object.keys(output).sort().join('|'))return ['Agent output keys differ from Schema']; + for(const field of allowed){ + if(typeof output[field]!==schema.properties[field].type)return [field+' must be '+schema.properties[field].type]; + if(typeof output[field]==='string'&&output[field].length<70){ + return [field+' must be at least 70 characters']; + } + } + return []; +} +const originalRequest='建立 FlowKit Interactive P0-A 的 C01–C12 复杂验收矩阵,验证多 Agent 顺序、条件路由、Schema 重试、SQLite 恢复、签名、安全与长链;不得越界实现 Context 或通用产物存储。'; + +test('C01/C03 recorded ChatGPT responses bind to exact Pending and replay through Program/Host',async()=>{ + const root=await mkdtemp(path.join(tmpdir(),'fk-c01-live-')); + const file=path.join(root,'run.sqlite'); + let store; + try{ + const compiled=await compileInteractive(workflowFile,{ + output:'memory',featureStage:'P0-A',skillCheck:false, + }); + assert.equal(compiled.diagnostics.some(x=>x.severity==='error'),false, + JSON.stringify(compiled.diagnostics)); + assert.ok(compiled.code&&compiled.programIdentity); + const keys=generateKeyPairSync('ed25519'); + const privateCanary=randomBytes(24).toString('hex'); + const runtime={ + skillResolver:async d=>{ + assert.equal(typeof instructions[d.nodeId],'string'); + return instructions[d.nodeId]; + }, + validateResult:validate, + }; + const hostFor=s=>createTrustedInteractiveHost({ + store:s,runtime,policy:{deploymentId:'c01-live-20261111', + bindingVersions:{skill:'live-c01-v1',validator:'live-c01-v1'}}, + trustedPublicKeys:{'complex-live-host':keys.publicKey}, + trustedArtifacts:[compiled.programIdentity.artifactHash], + allowedInputKeys:['request','requirementSummary','designSummary','executionPlan'], + disclosurePolicyId:'allow-public-c01-inputs', + authorizeDisclosure:async({task})=> + task?.runId===runId&&expectedPath.includes(task?.nodeId), + now:()=>NOW, + }); + store=createSqliteCheckpointStore(file,{namespace}); + let host=hostFor(store); + let frame=await host.start({ + programBytes:Buffer.from(compiled.code,'utf8'), + programIdentity:compiled.programIdentity, + runId,startKey:'complex-live-c01-start', + state:{ready:true,request:originalRequest,hostSecret:privateCanary}, + }); + assert.equal(frame.status,'waiting'); + let replies=[]; + try{ + replies=JSON.parse(await readFile(repliesFile,'utf8')); + }catch(error){ + if(error?.code!=='ENOENT')throw error; + } + const prepareMode=process.env.FLOWKIT_C01_PREPARE==='1'; + assert.ok(Array.isArray(replies)&&replies.length<=expectedPath.length); + assert.ok(prepareMode||replies.length===expectedPath.length, + 'Normal CI requires all five recorded ChatGPT responses; use FLOWKIT_C01_PREPARE=1 only for explicit Pending preparation'); + const resultDigests=[]; + const requestIds=[]; + let rejectedReviewAttempts=0; + for(let index=0;index0); + console.log('C01_CHATGPT_RECORDED_RESPONSES_ACCEPTED '+JSON.stringify({ + scenarioIds:['C01','C03'],execution:'CHATGPT_AUTHORED_FIXTURE_REPLAY', + nativeHostAttested:false,recordedModelResponses:replies.length, + resultDigests,requestIds, + programHash:compiled.programIdentity.artifactHash, + finalStateHash:sha256Bytes(canonicalBytes(frame.state)), + terminal:frame.status,nodeId:frame.terminal.nodeId, + journalRevisions:journal.length,restartedSQLiteWithinEachCI:true, + retriesObserved:rejectedReviewAttempts, + outputLength:last.result.reviewNotes.length, + note:'Five ChatGPT-authored responses replayed against exact Pending identities; GitHub tool identity is not native session attestation and each CI run recreates its DB', + })); + }finally{ + try{store?.close()}finally{await rm(root,{recursive:true,force:true})} + } +}); diff --git a/tests/interactive/complex-c01-c12.test.mjs b/tests/interactive/complex-c01-c12.test.mjs new file mode 100644 index 0000000..807e64e --- /dev/null +++ b/tests/interactive/complex-c01-c12.test.mjs @@ -0,0 +1,605 @@ +/** + * C01/C02/C03/C04/C10/C11: actual FlowKit Program, trusted Host and SQLite, + * but Agent results are SCRIPTED, never claim a real model session here. + */ +import test,{after} from 'node:test'; +import assert from 'node:assert/strict'; +import {mkdtemp,rm} from 'node:fs/promises'; +import {tmpdir} from 'node:os'; +import path from 'node:path'; +import {generateKeyPairSync,sign} from 'node:crypto'; +import {analyzeSource} from '../../src/compiler/analyze.mjs'; +import {lowerInteractive} from '../../src/compiler/interactive/lower.mjs'; +import {emitInteractive} from '../../src/compiler/interactive/emit.mjs'; +import {createSqliteCheckpointStore} from '../../src/interactive/sqlite-store.mjs'; +import {createTrustedInteractiveHost} from '../../src/interactive/trusted-host.mjs'; +import {createAgentReceiptStatement,encodeAgentReceiptStatement} + from '../../src/interactive/host-attestation.mjs'; +import {canonicalBytes,sha256Bytes} from '../../src/interactive/canonical.mjs'; + +const roots=[]; +after(async()=>{for(const dir of roots) await rm(dir,{recursive:true,force:true})}); +const NOW=1794400000000; +const DSL="import {defineWorkflow,condition,node,terminal} from '@devcodex-labs/flowkit/dsl';\n"; +const schema=(field,type='string')=> + "{type:'object',properties:{"+field+":{type:'"+type+"'}}}"; +function complexSource(retries=1) { + return DSL+"export default defineWorkflow({id:'c01-complex',version:'1.0.0',name:'C01 Complex',nodes:["+ + "condition('gate',{name:'Gate',branches:[{when:'ready',next:'requirements'},{when:'true',next:'declined'}]}),"+ + "node('requirements',{name:'Requirements',agent:'analyst',inputs:['request'],output:"+ + schema('requirementId')+",retries:0,next:'design'}),"+ + "node('design',{name:'Design',agent:'designer',inputs:['requirementId'],output:"+ + schema('designId')+",retries:0,next:'plan'}),"+ + "node('plan',{name:'Plan',agent:'planner',inputs:['designId'],output:"+ + schema('planId')+",retries:0,next:'review'}),"+ + "node('review',{name:'Review',agent:'reviewer',inputs:['planId'],output:"+ + schema('reviewApproved','boolean')+",retries:"+retries+",next:'decision'}),"+ + "condition('decision',{name:'Decision',branches:[{when:'reviewApproved',next:'done'},{when:'true',next:'declined'}]}),"+ + "terminal('done',{name:'Done',status:'succeeded'}),"+ + "terminal('declined',{name:'Declined',status:'failed'})]});"; +} +function longSource(count=36){ + const nodes=[]; + for(let i=0;ix.severity==='error'),false, + JSON.stringify(a.diagnostics)); + const ir=lowerInteractive(a); + assert.ok(ir.ir,JSON.stringify(ir.diagnostics)); + const e=emitInteractive(ir.ir); + assert.ok(e.code,JSON.stringify(e.diagnostics)); + return {programBytes:Buffer.from(e.code,'utf8'),programIdentity:e.programIdentity}; +} +function validator(schema,output){ + if(!output||typeof output!=='object'||Array.isArray(output))return ['not an object']; + const properties=schema.properties??{}; + const keys=Object.keys(properties).sort(); + if(Object.keys(output).sort().join('|')!==keys.join('|'))return ['wrong fields']; + for(const k of keys)if(typeof output[k]!==properties[k].type)return ['wrong type: '+k]; + return []; +} +async function fixture(t,{ + source=complexSource(), + maxStagesPerCall=128, + allowedInputKeys=['request','requirementId','designId','planId'], +}={}){ + const root=await mkdtemp(path.join(tmpdir(),'fk-complex-')); + roots.push(root); + const store=createSqliteCheckpointStore(path.join(root,'run.sqlite'),{ + namespace:'complex-acceptance', + }); + t.after(()=>store.close()); + const compiled=await compile(source); + const keys=generateKeyPairSync('ed25519'); + const runtime={ + skillResolver:async({nodeId})=>'Perform declared step '+nodeId, + validateResult:validator, + }; + const makeHost=()=>createTrustedInteractiveHost({ + store,runtime,policy:{deploymentId:'complex-c01-c12',bindingVersions:{ + resolver:'v1',validator:'v1', + }}, + trustedPublicKeys:{'trusted-host':keys.publicKey}, + trustedArtifacts:[compiled.programIdentity.artifactHash], + allowedInputKeys, + disclosurePolicyId:'complex-test-public', + authorizeDisclosure:async()=>true,now:()=>NOW,maxStagesPerCall, + }); + return {root,store,keys,compiled,makeHost,host:makeHost()}; +} +function audit(f,runId){ + const cp=f.store.load(runId),journal=f.store.listJournal(runId); + assert.equal(journal.length,cp.revision+1); + for(let i=0;i{ + const f=await fixture(t); + const first=await start(f,{runId:'C01-four',state:{ + ready:true,request:'software task',secret:'HOST-PRIVATE-DO-NOT-SHARE', + }}); + const {frame,trace}=await fourSteps(f,first); + assert.equal(frame.status,'succeeded'); + assert.equal(frame.terminal.nodeId,'done'); + assert.deepEqual(frame.state,{ + ready:true,request:'software task',secret:'HOST-PRIVATE-DO-NOT-SHARE', + requirementId:'REQ-001',designId:'DES-001',planId:'PLAN-001',reviewApproved:true, + }); + assert.equal(new Set(trace.map(x=>x.requestId)).size,4); + evidence('C01',f,frame,{nodes:trace.map(x=>x.nodeId),signedReceipts:4}); +}); + +test('C02 false/default/unknown Condition routes do not dispatch unreachable Agent',async t=>{ + const f=await fixture(t); + const early=await start(f,{runId:'C02-early',state:{ + ready:false,request:'software task', + }}); + assert.equal(early.status,'failed'); + assert.equal(early.terminal.nodeId,'declined'); + assert.equal(f.store.load('C02-early').receipts.length,0); + audit(f,'C02-early'); + const missing=await start(f,{runId:'C02-missing',state:{ + request:'software task', + }}); + assert.equal(missing.status,'errored'); + assert.equal(missing.terminal.causeCode,'FK_R002'); + assert.equal(f.store.load('C02-missing').receipts.length,0); + audit(f,'C02-missing'); + const pending=await start(f,{runId:'C02-late'}); + const {frame,trace}=await fourSteps(f,pending,{ + reviewResult:{reviewApproved:false}, + }); + assert.equal(frame.status,'failed'); + assert.equal(frame.terminal.nodeId,'declined'); + assert.equal(trace.length,4); + evidence('C02',f,frame,{ + routes:['ready:false -> declined','ready:missing -> FK_R002', + 'reviewApproved:false -> declined'], + earlyDispatches:0, + }); +}); + +test('C03 invalid review schema does not mutate State, retries with new requestId',async t=>{ + const f=await fixture(t); + let frame=await start(f,{runId:'C03-retry'}); + for(const id of ['requirements','design','plan']){ + frame=(await respond(f,frame,answers[id])).frame; + } + const prior=await task(f,frame); + const first=await respond(f,frame,{reviewApproved:'wrong-type'}); + frame=first.frame; + assert.equal(frame.status,'waiting'); + assert.equal(frame.pending.nodeId,'review'); + assert.equal(frame.pending.attempt,2); + assert.notEqual(frame.pending.requestId,prior.requestId); + assert.equal(Object.hasOwn(f.store.load(frame.runId).state,'reviewApproved'),false); + const dup=await f.host.accept({ + receipt:first.receipt,attestation:first.attestation, + }); + assert.equal(dup.duplicate,true); + assert.equal(dup.acceptedRevision,first.acceptedRevision); + assert.equal(dup.pending.requestId,frame.pending.requestId); + const second=await respond(f,frame,{reviewApproved:true}); + assert.equal(second.frame.status,'succeeded'); + assert.equal(second.frame.state.reviewApproved,true); + evidence('C03',f,second.frame,{attempts:[1,2],invalidStateMerged:false, + firstAck:first.acceptedRevision,secondAck:second.acceptedRevision}); +}); + +test('C04 repeated invalid review schema consumes exactly retries+1 and FK_R004',async t=>{ + const f=await fixture(t); + let frame=await start(f,{runId:'C04-error'}); + for(const id of ['requirements','design','plan']){ + frame=(await respond(f,frame,answers[id])).frame; + } + const attempts=[]; + for(let n=0;n<2;n++){ + const p=await task(f,frame); + assert.equal(p.nodeId,'review'); + attempts.push(p.attempt); + frame=(await respond(f,frame,{invalid:true})).frame; + } + assert.deepEqual(attempts,[1,2]); + assert.equal(frame.status,'errored'); + assert.equal(frame.terminal.causeCode,'FK_R004'); + assert.equal(Object.hasOwn(frame.state,'reviewApproved'),false); + evidence('C04',f,frame,{attempts,causeCode:'FK_R004'}); +}); + +test('C10 36 Agent chain keeps 16 recent receipt cache but all 36 Ack identities',async t=>{ + const count=36; + const f=await fixture(t,{ + source:longSource(count), + allowedInputKeys:['request',...Array.from({length:count},(_,i)=>'step'+i)], + }); + let frame=await start(f,{runId:'C10-long'}); + const accepted=[]; + for(let i=0;i{ + const compared=[]; + for(const budget of [1,128]){ + const f=await fixture(t,{maxStagesPerCall:budget}); + const first=await start(f,{runId:'C11-budget-'+budget}); + const trace=[]; + const {frame}=await fourSteps(f,first,{trace}); + assert.equal(frame.status,'succeeded'); + const {journal}=audit(f,frame.runId); + compared.push({ + budget,programHash:f.compiled.programIdentity.artifactHash, + state:frame.state,stateHash:sha256Bytes(canonicalBytes(frame.state)), + trace:trace.map(x=>({id:x.nodeId,attempt:x.attempt})), + revision:frame.revision,journalRevisions:journal.length, + }); + } + assert.equal(compared[0].programHash,compared[1].programHash); + assert.equal(compared[0].stateHash,compared[1].stateHash); + assert.deepEqual(compared[0].trace,compared[1].trace); + assert.ok(compared[0].revision>compared[1].revision); + console.log('FLOWKIT_COMPLEX_ACCEPTANCE '+JSON.stringify({ + scenarioId:'C11',execution:'SCRIPTED_TRUSTED_HOST',modelCalls:0, + programHash:compared[0].programHash, + stateHash:compared[0].stateHash, + runs:compared.map(x=>({budget:x.budget,revision:x.revision, + journalRevisions:x.journalRevisions})), + result:'PASS', + })); +}); + + +// C13-C20 extend the FROZEN C01-C12 set with compound failure/interleaving +// conditions. All model outputs below are SCRIPTED; zero real model calls. +test('C13 cross-budget branch product: true/false/missing and late rejection',async t=>{ + const observations=[]; + for(const budget of [1,128]){ + const f=await fixture(t,{maxStagesPerCall:budget}); + const declined=await start(f,{runId:'C13-early-'+budget, + state:{ready:false,request:'software task'}}); + assert.equal(declined.status,'failed'); + assert.equal(declined.terminal.nodeId,'declined'); + assert.equal(f.store.load(declined.runId).receipts.length,0); + + const missing=await start(f,{runId:'C13-missing-'+budget, + state:{request:'software task'}}); + assert.equal(missing.status,'errored'); + assert.equal(missing.terminal.causeCode,'FK_R002'); + assert.equal(f.store.load(missing.runId).receipts.length,0); + + const falseFirst=await start(f,{runId:'C13-late-false-'+budget}); + const rejected=await fourSteps(f,falseFirst,{ + reviewResult:{reviewApproved:false}}); + assert.equal(rejected.frame.status,'failed'); + assert.equal(rejected.frame.terminal.nodeId,'declined'); + assert.equal(rejected.trace.length,4); + + const trueFirst=await start(f,{runId:'C13-late-true-'+budget}); + const approved=await fourSteps(f,trueFirst); + assert.equal(approved.frame.status,'succeeded'); + assert.equal(approved.frame.terminal.nodeId,'done'); + const stateHash=sha256Bytes(canonicalBytes(approved.frame.state)); + const rejectedStateHash=sha256Bytes(canonicalBytes(rejected.frame.state)); + for(const runId of [declined.runId,missing.runId, + rejected.frame.runId,approved.frame.runId])audit(f,runId); + observations.push({budget,stateHash,rejectedStateHash, + statuses:[declined.status,missing.status,rejected.frame.status, + approved.frame.status]}); + } + assert.equal(observations[0].stateHash,observations[1].stateHash); + assert.equal(observations[0].rejectedStateHash, + observations[1].rejectedStateHash); + assert.deepEqual(observations.map(x=>x.statuses),[ + ['failed','errored','failed','succeeded'], + ['failed','errored','failed','succeeded']]); + console.log('FLOWKIT_COMPLEX_ACCEPTANCE '+JSON.stringify({ + scenarioId:'C13',execution:'SCRIPTED_TRUSTED_HOST',modelCalls:0, + branchCases:4,stageBudgets:[1,128],observations,result:'PASS', + })); +}); + +test('C14 tampered/oversized signed-looking replies halfway through chain are no-ops',async t=>{ + const f=await fixture(t); + let frame=await start(f,{runId:'C14-forged'}); + frame=(await respond(f,frame,answers.requirements)).frame; + const pending=await task(f,frame); + assert.equal(pending.nodeId,'design'); + const valid=receiptFor(pending,{designId:'DES-001'}); + const attestation=attest(f,valid); + const cp=canonicalBytes(f.store.load(frame.runId)); + const journal=f.store.listJournal(frame.runId); + const rejected=[ + {receipt:{...valid,result:{designId:'swapped'}},attestation}, + {receipt:{...valid,attempt:pending.attempt+1},attestation}, + {receipt:{...valid,requestId:'forged-request'},attestation}, + {receipt:{...valid,result:{designId:'x'.repeat(2_100_000)}}, + attestation}, + {receipt:valid,attestation:{...attestation, + signature:'A'.repeat(86)}}, + ]; + for(const entry of rejected){ + await assert.rejects(f.host.accept(entry), + {code:'FK_I_PROVENANCE_REJECTED'}); + assert.deepEqual(canonicalBytes(f.store.load(frame.runId)),cp); + assert.deepEqual(f.store.listJournal(frame.runId),journal); + } + const accepted=await f.host.accept({receipt:valid,attestation}); + frame=await settle(f.host,accepted); + assert.equal(frame.pending.nodeId,'plan'); + frame=(await respond(f,frame,answers.plan)).frame; + frame=(await respond(f,frame,answers.review)).frame; + assert.equal(frame.status,'succeeded'); + assert.equal(frame.state.designId,'DES-001'); + evidence('C14',f,frame,{rejectedBodies:rejected.length, + noMutationBeforeValidReceipt:true}); +}); + +test('C15 lost Ack replay after Host rebuild and after downstream Terminal is immutable',async t=>{ + const f=await fixture(t); + let frame=await start(f,{runId:'C15-lost-ack'}); + const first=await respond(f,frame,answers.requirements); + frame=first.frame; + assert.equal(frame.pending.nodeId,'design'); + const cp=canonicalBytes(f.store.load(frame.runId)); + const before=f.store.listJournal(frame.runId); + const rebuilt=f.makeHost(); + const duplicate=await rebuilt.accept({ + receipt:first.receipt,attestation:first.attestation}); + assert.equal(duplicate.duplicate,true); + assert.equal(duplicate.acceptedRevision,first.acceptedRevision); + assert.deepEqual(canonicalBytes(f.store.load(frame.runId)),cp); + assert.deepEqual(f.store.listJournal(frame.runId),before); + for(const id of ['design','plan','review']){ + frame=(await respond(f,frame,answers[id])).frame; + } + assert.equal(frame.status,'succeeded'); + const terminalRevision=f.store.load(frame.runId).revision; + const after=await f.makeHost().accept({ + receipt:first.receipt,attestation:first.attestation}); + assert.equal(after.duplicate,true); + assert.equal(after.acceptedRevision,first.acceptedRevision); + assert.equal(f.store.load(frame.runId).revision,terminalRevision); + evidence('C15',f,frame,{lostAckRecovered:true, + replayAfterTerminal:true}); +}); + +test('C16 signed Host call-failed in late Agent retries safely and does not skip route',async t=>{ + const f=await fixture(t); + let frame=await start(f,{runId:'C16-provider-failure'}); + for(const id of ['requirements','design','plan']){ + frame=(await respond(f,frame,answers[id])).frame; + } + const p=await task(f,frame); + const failure={runId:p.runId,requestId:p.requestId,nodeId:p.nodeId, + attempt:p.attempt,kind:'agent-call-failed', + failure:{message:'provider unavailable'}}; + const proof=attest(f,failure); + const failed=await f.host.accept({receipt:failure,attestation:proof}); + frame=await settle(f.host,failed); + assert.equal(frame.status,'waiting'); + assert.equal(frame.pending.nodeId,'review'); + assert.equal(frame.pending.attempt,2); + assert.notEqual(frame.pending.requestId,p.requestId); + assert.equal(Object.hasOwn(f.store.load(frame.runId).state,'reviewApproved'),false); + const duplicate=await f.host.accept({receipt:failure,attestation:proof}); + assert.equal(duplicate.duplicate,true); + assert.equal(duplicate.acceptedRevision,failed.acceptedRevision); + frame=(await respond(f,frame,answers.review)).frame; + assert.equal(frame.status,'succeeded'); + evidence('C16',f,frame,{failureReceiptAccepted:true, + attempt2:true,staleAckImmutable:true}); +}); + +test('C17 Host policy drift at intermediate Pending rejects before mutation; old Host completes',async t=>{ + const f=await fixture(t); + let frame=await start(f,{runId:'C17-policy'}); + for(const id of ['requirements','design']){ + frame=(await respond(f,frame,answers[id])).frame; + } + assert.equal(frame.pending.nodeId,'plan'); + const before=canonicalBytes(f.store.load(frame.runId)); + const history=f.store.listJournal(frame.runId); + const drifted=createTrustedInteractiveHost({ + store:f.store,runtime:{skillResolver:async({nodeId})=>'Perform declared step '+nodeId, + validateResult:validator}, + policy:{deploymentId:'complex-c01-c12', + bindingVersions:{resolver:'v2',validator:'v1'}}, + trustedPublicKeys:{'trusted-host':f.keys.publicKey}, + trustedArtifacts:[f.compiled.programIdentity.artifactHash], + allowedInputKeys:['request','requirementId','designId','planId'], + disclosurePolicyId:'complex-test-public', + authorizeDisclosure:async()=>true,now:()=>NOW, + }); + for(const action of [ + ()=>drifted.load({runId:frame.runId}), + ()=>drifted.resume({runId:frame.runId}), + ()=>drifted.getPublicAgentTask({runId:frame.runId}), + ]){ + await assert.rejects(action(),{code:'FK_I_HOST_DENIED'}); + assert.deepEqual(canonicalBytes(f.store.load(frame.runId)),before); + assert.deepEqual(f.store.listJournal(frame.runId),history); + } + frame=(await respond(f,frame,answers.plan)).frame; + frame=(await respond(f,frame,answers.review)).frame; + assert.equal(frame.status,'succeeded'); + evidence('C17',f,frame,{policyChangedResolver:'v1 -> v2', + driftRejectedWithoutCAS:true}); +}); + +test('C18 two interleaved Runs isolate branching, start identity and Journal',async t=>{ + const f=await fixture(t); + let a=await start(f,{runId:'C18-run-A'}); + const b=await start(f,{runId:'C18-run-B',state:{ + ready:false,request:'software task'}}); + assert.equal(a.status,'waiting'); + assert.equal(b.status,'failed'); + assert.equal(f.store.load(b.runId).receipts.length,0); + const replay=await f.host.start({...f.compiled,runId:'C18-shadow', + startKey:'C18-run-A',state:{ready:true,request:'software task'}}); + assert.equal(replay.runId,a.runId); + assert.equal(replay.pending.requestId,a.pending.requestId); + await assert.rejects(f.host.start({...f.compiled,runId:'C18-conflict', + startKey:'C18-run-A',state:{ready:false,request:'software task'}}), + {code:'FK_I_START_CONFLICT'}); + a=(await fourSteps(f,a)).frame; + assert.equal(a.status,'succeeded'); + assert.equal(f.store.load(b.runId).status,'failed'); + assert.equal(f.store.load(b.runId).receipts.length,0); + audit(f,a.runId);audit(f,b.runId); + evidence('C18',f,a,{parallelRuns:2, + duplicateStartIdempotent:true,conflictingStartRejected:true}); +}); + +test('C19 36-node chain with invalid output at step 18 has no ghost dispatch',async t=>{ + const count=36; + const f=await fixture(t,{source:longSource(count), + allowedInputKeys:['request',...Array.from({length:count},(_,i)=>'step'+i)]}); + let frame=await start(f,{runId:'C19-fail-midchain'}); + const receipts=[]; + for(let i=0;i<18;i++){ + const p=await task(f,frame); + assert.equal(p.nodeId,'agent'+i); + receipts.push(p.requestId); + const value=i===17?{invalid:'bad'}:{['step'+i]:'value-'+i}; + frame=(await respond(f,frame,value)).frame; + } + assert.equal(frame.status,'errored'); + assert.equal(frame.terminal.causeCode,'FK_R004'); + assert.equal(Object.hasOwn(frame.state,'step17'),false); + assert.equal(Object.hasOwn(frame.state,'step18'),false); + assert.equal(new Set(receipts).size,18); + const previous=canonicalBytes(f.store.load(frame.runId)); + const resumed=await f.host.resume({runId:frame.runId}); + assert.equal(resumed.status,'errored'); + assert.deepEqual(canonicalBytes(f.store.load(frame.runId)),previous); + evidence('C19',f,frame,{declaredAgents:36,dispatched:18, + remainingNotDispatched:18,causeCode:'FK_R004'}); +}); + +test('C20 conflicting independently signed late-Agent answers accept only one CAS',async t=>{ + const f=await fixture(t); + let frame=await start(f,{runId:'C20-equivocation'}); + frame=(await respond(f,frame,answers.requirements)).frame; + const p=await task(f,frame); + assert.equal(p.nodeId,'design'); + const left=receiptFor(p,{designId:'DES-A'}); + const right=receiptFor(p,{designId:'DES-B'}); + const [one,two]=await Promise.allSettled([ + f.host.accept({receipt:left,attestation:attest(f,left)}), + f.makeHost().accept({receipt:right,attestation:attest(f,right)}), + ]); + const settled=[one,two]; + assert.equal(settled.filter(x=>x.status==='fulfilled').length,1); + assert.equal(settled.filter(x=>x.status==='rejected').length,1); + assert.equal(settled.find(x=>x.status==='rejected').reason.code, + 'FK_I_RECEIPT_CONFLICT'); + const winning=settled.find(x=>x.status==='fulfilled').value; + frame=await settle(f.host,winning); + assert.equal(frame.pending.nodeId,'plan'); + assert.ok(['DES-A','DES-B'].includes(f.store.load(frame.runId).state.designId)); + const journal=f.store.listJournal(frame.runId); + assert.equal(journal.length,frame.revision+1); + frame=(await respond(f,frame,answers.plan)).frame; + frame=(await respond(f,frame,answers.review)).frame; + assert.equal(frame.status,'succeeded'); + evidence('C20',f,frame,{twoValidCompetingHostReceipts:true, + exactlyOneCAS:true,winningDesign:frame.state.designId}); +}); diff --git a/tests/interactive/coordinator.test.mjs b/tests/interactive/coordinator.test.mjs index c02a291..86a1292 100644 --- a/tests/interactive/coordinator.test.mjs +++ b/tests/interactive/coordinator.test.mjs @@ -402,3 +402,34 @@ test('M4 a successful receipt CAS followed by a lost response is recovered by du assert.equal(recovered.state.answer,'once'); assert.equal(store.load(waiting.runId).receipts.length,1); }); + + +test('R04 regression: start snapshots caller-owned State before first await',async t=>{ + const {api,source,state,store}=await runAgentFixture(t); + // Exercise both an existing top-level key and a nested caller-owned reference. + state.context={request:'before-await'}; + const originalState=structuredClone(state); + const starting=api.start({...source,startKey:'r04-state-snapshot',state}); + // These writes occur synchronously after invoking start(), before its await resolves. + state.existing='modified-after-start'; + state.context.request='modified-after-start'; + const frame=await starting; + assert.deepEqual(store.load(frame.runId).state,originalState, + 'persisted State must be a deep entry-time snapshot'); +}); + +test('R04 regression: start snapshots caller-owned startIntent before first await',async t=>{ + const {api,source,state}=await runAgentFixture(t); + const intent={request:'before-await'}; + const starting=api.start({ + ...source,startKey:'r04-intent-snapshot',state,startIntent:intent, + }); + // The idempotency identity must not depend on post-call mutation. + intent.request='modified-after-start'; + const first=await starting; + const originalReplay=await api.start({ + ...source,startKey:'r04-intent-snapshot',state, + startIntent:{request:'before-await'}, + }); + assert.equal(originalReplay.runId,first.runId); +});