From 64494da8ea4fcdc5a67988ebbc255cf24c8b7d2c Mon Sep 17 00:00:00 2001 From: rocky Date: Wed, 11 Nov 2026 12:58:53 +0900 Subject: [PATCH 01/16] test(interactive): define executable S13 ChatGPT tool-bridge acceptance workflow --- examples/interactive/s13-chatgpt.workflow.js | 33 ++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 examples/interactive/s13-chatgpt.workflow.js diff --git a/examples/interactive/s13-chatgpt.workflow.js b/examples/interactive/s13-chatgpt.workflow.js new file mode 100644 index 0000000..bb56ba5 --- /dev/null +++ b/examples/interactive/s13-chatgpt.workflow.js @@ -0,0 +1,33 @@ +import {defineWorkflow,condition,node,terminal} from '@devcodex-labs/flowkit/dsl'; + +export default defineWorkflow({ + id:'s13-chatgpt-tool-bridge', + version:'0.1.0', + name:'ChatGPT 实际工具链 S13 验证', + nodes:[ + condition('gate',{ + name:'确定进入审核节点', + branches:[ + {when:'ready',next:'review'}, + {when:'true',next:'rejected'}, + ], + }), + node('review',{ + name:'分析 FlowKit 运行边界', + agent:'reviewer', + inputs:['request','constraints'], + output:{ + type:'object', + properties:{ + summary:{type:'string'}, + nextAction:{type:'string'}, + flowkitBoundary:{type:'string'}, + }, + }, + retries:0, + next:'done', + }), + terminal('done',{name:'工作流成功',status:'succeeded'}), + terminal('rejected',{name:'未满足进入条件',status:'failed'}), + ], +}); From 05b1c5019b50a85a36637c6223726877cc271702 Mon Sep 17 00:00:00 2001 From: rocky Date: Wed, 11 Nov 2026 12:58:56 +0900 Subject: [PATCH 02/16] test(interactive): add auditable public Agent instruction for S13 bridge --- examples/interactive/skills/reviewer.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 examples/interactive/skills/reviewer.md diff --git a/examples/interactive/skills/reviewer.md b/examples/interactive/skills/reviewer.md new file mode 100644 index 0000000..a9e01a8 --- /dev/null +++ b/examples/interactive/skills/reviewer.md @@ -0,0 +1,11 @@ +# FlowKit Host 公开 Agent 任务 + +你是本次 FlowKit S13 验证中实际参与执行的 ChatGPT 模型。 + +请根据公开的 `request` 和 `constraints`,返回一个 JSON 对象: +- `summary`:简短说明为什么 FlowKit 的工作流应该由 JavaScript Program 约束执行。 +- `nextAction`:一项可以直接验证的下一步动作。 +- `flowkitBoundary`:说明 FlowKit 与 Host/Context 的职责边界。 + +禁止请求或输出 Host 的私钥、SQLite 数据库、完整 State 或未公开内容。 +这份说明只是公开任务指令,不构成 Host 对模型来源的独立认证。 From 98387df8e6921216a0c4945394ce5fdd75add287 Mon Sep 17 00:00:00 2001 From: rocky Date: Wed, 11 Nov 2026 13:00:47 +0900 Subject: [PATCH 03/16] test(interactive): 2-stage public Pending bridge for real ChatGPT tool-using response and Host resume --- .../s13-chatgpt-tool-bridge.test.mjs | 226 ++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 tests/interactive/s13-chatgpt-tool-bridge.test.mjs diff --git a/tests/interactive/s13-chatgpt-tool-bridge.test.mjs b/tests/interactive/s13-chatgpt-tool-bridge.test.mjs new file mode 100644 index 0000000..b1acda4 --- /dev/null +++ b/tests/interactive/s13-chatgpt-tool-bridge.test.mjs @@ -0,0 +1,226 @@ +/** + * ChatGPT-current-conversation -> real JS tool -> actual FlowKit Host bridge. + * + * Phase 1 (without a reply file): compile & execute the verified Program, + * durably publish a public Agent Pending, and print its EXACT requestId/digest. + * The ChatGPT model then reads that real CI output and writes a reply via its + * authenticated GitHub tool call (a separate conversation action). + * + * Phase 2 (with response fixture): recompile the same workflow, verify that + * the model-authored result is bound to the actual pending request, recreate + * a Host from the persisted SQLite database, sign through a Host-only + * Ed25519 key, accept, CAS, replay the duplicate Ack and check terminal. + * + * This proves actual tool-using ChatGPT participated. It does NOT independently + * attest ChatGPT provider/session origin, Codex-native JS tools or cross-UID + * model isolation. M6 S13 native Host gate remains separately BLOCKED. + */ +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 path from 'node:path'; +import {tmpdir} from 'node:os'; +import {generateKeyPairSync,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/s13-chatgpt.workflow.js',import.meta.url)); +const skillFile=fileURLToPath( + new URL('../../examples/interactive/skills/reviewer.md',import.meta.url)); +const replyFile=fileURLToPath( + new URL('../fixtures/s13-chatgpt-reply.json',import.meta.url)); +const runId='s13-chatgpt-bridge-run-20261111'; +const startKey='s13-chatgpt-bridge-20261111'; +const namespace='s13-chatgpt-bridge-tenant'; +const privateCanary='HOST_PRIVATE_CANARY_DO_NOT_DISCLOSE_20261111'; + +const initialState={ + ready:true, + request:'请审查 FlowKit 如何通过可执行 JavaScript Program 约束 Agent 工作流,并给出一项可验证的下一步。', + constraints:[ + 'State 只保留当前工作流所需的轻量状态和少量值。', + '业务文件/产物由用户配置的 Host 存储处理,Context 可以负责复杂关联。', + '流程推进及路由必须由 FlowKit Program 决定,模型不能绕开可信 Host。', + ], + hostOnlySecret:privateCanary, +}; +const policy={ + deploymentId:'s13-chatgpt-tool-bridge', + bindingVersions:{ + 'runtime.skillResolver':'reviewer-skill-test-v1', + 'runtime.validateResult':'reviewer-schema-test-v1', + }, +}; +const resultFields=['flowkitBoundary','nextAction','summary']; + +test('S13 live ChatGPT turn consumes actual public Pending and returns via Host signature/CAS',async()=>{ + const dir=await mkdtemp(path.join(tmpdir(),'flowkit-s13-chatgpt-')); + const file=path.join(dir,'run.sqlite'); + let store; + try{ + const compiled=await compileInteractive(workflowFile,{ + output:'memory', + featureStage:'P0-A', + skillsRoot:'examples/interactive/skills', + skillCheck:true, + }); + assert.equal(compiled.diagnostics.some(x=>x.severity==='error'),false, + JSON.stringify(compiled.diagnostics)); + assert.ok(compiled.programIdentity?.artifactHash); + const skill=await readFile(skillFile,'utf8'); + const skillHash=sha256Bytes(Buffer.from(skill,'utf8')); + const keys=generateKeyPairSync('ed25519'); + const runtime={ + skillResolver:async({agent,skill:declared})=>{ + assert.equal(agent,'reviewer'); + assert.equal(declared,null); + return skill; + }, + validateResult:(_schema,output)=>{ + if(!output||typeof output!=='object'||Array.isArray(output)) { + return ['result must be object']; + } + const keys=Object.keys(output).sort(); + if(keys.join(',')!==resultFields.join(','))return ['result field set does not match']; + return resultFields.every(k=>typeof output[k]==='string' + && output[k].length>=15 && output[k].length<=1000)?[]:['result field lengths invalid']; + }, + }; + const hostFor=s=>createTrustedInteractiveHost({ + store:s, + runtime,policy, + trustedPublicKeys:{'s13-test-transport':keys.publicKey}, + trustedArtifacts:[compiled.programIdentity.artifactHash], + allowedInputKeys:['request','constraints'], + disclosurePolicyId:'s13-public-only-v1', + authorizeDisclosure:async({task})=> + task?.kind==='agent' && task?.nodeId==='review' + && task?.runId===runId, + }); + store=createSqliteCheckpointStore(file,{namespace}); + const firstHost=hostFor(store); + const frame=await firstHost.start({ + programBytes:Buffer.from(compiled.code,'utf8'), + programIdentity:compiled.programIdentity, + runId,startKey,state:initialState, + }); + assert.equal(frame.status,'waiting'); + const pending=await firstHost.getPublicAgentTask({runId}); + assert.equal(pending.kind,'agent'); + assert.equal(pending.nodeId,'review'); + assert.equal(pending.runId,runId); + assert.deepEqual(pending.input,{ + request:initialState.request,constraints:initialState.constraints, + }); + assert.equal(JSON.stringify(pending).includes(privateCanary),false); + assert.equal(JSON.stringify(pending).includes('hostOnlySecret'),false); + const taskDigest=sha256Bytes(canonicalBytes(pending)); + const firstJournal=store.listJournal(runId); + assert.ok(firstJournal.length>=3); + assert.equal(store.load(runId).pending.requestId,pending.requestId); + + // Actual public Agent task, published only after durable SQLite Pending. + // No Host secrets, Store path, full State or private signing key are printed. + console.log('S13_CHATGPT_PUBLIC_TASK '+JSON.stringify({ + taskDigest,task:pending, + skillSha256:skillHash, + artifactHash:compiled.programIdentity.artifactHash, + })); + let modelReply; + try{ + modelReply=JSON.parse(await readFile(replyFile,'utf8')); + }catch(error){ + if(error?.code!=='ENOENT')throw error; + } + if(!modelReply){ + assert.equal((await firstHost.load({runId})).status,'waiting'); + console.log('S13_CHATGPT_PHASE1_WAITING '+JSON.stringify({ + runId,requestId:pending.requestId,revision:frame.revision, + journalRevisions:firstJournal.length, + result:'Real public task printed for next ChatGPT tool turn', + })); + return; + } + assert.equal(modelReply.runId,runId); + assert.equal(modelReply.requestId,pending.requestId); + assert.equal(modelReply.taskDigest,taskDigest); + assert.ok(modelReply.result); + assert.equal(runtime.validateResult(null,modelReply.result).length,0); + + // Host lifecycle: close actual SQLite Store after durable Pending, then + // reopen and continue without recompiling at the resume boundary. + store.close(); + store=createSqliteCheckpointStore(file,{namespace}); + const resumedHost=hostFor(store); + const recovered=await resumedHost.load({runId}); + assert.equal(recovered.status,'waiting'); + const afterRestartTask=await resumedHost.getPublicAgentTask({runId}); + assert.equal(afterRestartTask.requestId,pending.requestId); + assert.equal(sha256Bytes(canonicalBytes(afterRestartTask)),taskDigest); + const receipt={ + runId, + requestId:pending.requestId, + nodeId:pending.nodeId, + attempt:pending.attempt, + kind:'agent-result', + result:modelReply.result, + }; + // Local Host signer is separate from the public model task, but this + // fixture has NO independent native ChatGPT session attestation. + const now=Date.now(); + const statement=createAgentReceiptStatement({ + run:store.load(runId),receipt,keyId:'s13-test-transport', + proofId:'chatgpt-github-bridge-result-001', + issuedAt:now-1000,expiresAt:now+60000, + }); + const attestation={ + statement, + signature:sign(null,encodeAgentReceiptStatement(statement),keys.privateKey) + .toString('base64url'), + }; + const before=store.load(runId); + assert.equal(before.status,'waiting'); + // Signed, but bogus signature must not mutate Run. + await assert.rejects(resumedHost.accept({ + receipt,attestation:{statement,signature:'forged'}, + }),{code:'FK_I_PROVENANCE_REJECTED'}); + assert.equal(store.load(runId).revision,before.revision); + const completed=await resumedHost.accept({receipt,attestation}); + assert.equal(completed.status,'succeeded'); + assert.equal(completed.terminal.nodeId,'done'); + assert.equal(completed.state.hostOnlySecret,privateCanary); + assert.equal(completed.state.summary,modelReply.result.summary); + assert.equal(completed.state.nextAction,modelReply.result.nextAction); + assert.equal(completed.state.flowkitBoundary,modelReply.result.flowkitBoundary); + const receiptIndex=store.getReceipt(runId,receipt.requestId); + assert.equal(receiptIndex.acceptedRevision,completed.acceptedRevision); + assert.equal(store.listJournal(runId).length,completed.revision+1); + const duplicate=await resumedHost.accept({receipt,attestation}); + assert.equal(duplicate.duplicate,true); + assert.equal(duplicate.acceptedRevision,completed.acceptedRevision); + assert.equal(duplicate.revision,completed.revision); + assert.equal((await resumedHost.resume({runId})).status,'succeeded'); + console.log('S13_CHATGPT_TOOL_BRIDGE_COMPLETED '+JSON.stringify({ + runId,requestId:receipt.requestId, + modelReplyDigest:sha256Bytes(canonicalBytes(modelReply.result)), + artifactHash:compiled.programIdentity.artifactHash, + taskDigest,skillHash, + status:completed.status, + terminal:completed.terminal.nodeId, + acceptedRevision:completed.acceptedRevision, + journalRevisions:completed.revision+1, + duplicateAck:true,restoredFromSQLite:true, + policyPinned:true, + secretNotExposedToPublicTask:true, + warning:'Actual ChatGPT tool-using answer, but NO independent native model-session attestation; S13 remains BLOCKED', + })); + }finally{ + try{store?.close()}finally{await rm(dir,{recursive:true,force:true})} + } +}); From 2595be0b5f781e674b6a2a0a043cd15b37efab4b Mon Sep 17 00:00:00 2001 From: rocky Date: Wed, 11 Nov 2026 13:03:11 +0900 Subject: [PATCH 04/16] test(s13): bind actual ChatGPT model-generated response to published public Pending digest --- tests/fixtures/s13-chatgpt-reply.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/fixtures/s13-chatgpt-reply.json diff --git a/tests/fixtures/s13-chatgpt-reply.json b/tests/fixtures/s13-chatgpt-reply.json new file mode 100644 index 0000000..0800cbe --- /dev/null +++ b/tests/fixtures/s13-chatgpt-reply.json @@ -0,0 +1,10 @@ +{ + "runId": "s13-chatgpt-bridge-run-20261111", + "requestId": "req:6bed7ea39b96e81b99100ffe69bb174ba38533d42f3bbdbefad6239cee0ac829", + "taskDigest": "sha256:96733a8e513b08020acc42ef5dabd93413d52b452d8b982d40afcf3b81ac2876", + "result": { + "summary": "FlowKit 将执行顺序、条件分支、失败路径和终态编译为可执行的 JavaScript Program,由代码而不是模型临时决定下一节点;Agent 只提交当前节点的结果,可信 Host 依据 Program、CAS 与 Journal 验证每次推进。", + "nextAction": "在可信 Host 运行本次 Condition→Agent→Terminal 工作流,核对已持久化 Pending 的 requestId 与任务摘要;接纳模型结构化输出后核验签名回执、最终 SQLite State、Journal 连续性、宿主重启恢复及重复回执的原始 Ack。", + "flowkitBoundary": "FlowKit 负责工作流编译、程序化路由、Checkpoint、回执幂等和恢复;Host 负责模型与 JS 工具调用、来源认证、安全隔离以及用户配置的产物存储;可选 Context 模块负责上下文引用和复杂数据关联,不能替代 Program 决定流程。" + } +} From eac72c66a33e8a6411aa8748240abe9146d0c4c0 Mon Sep 17 00:00:00 2001 From: rocky Date: Wed, 11 Nov 2026 13:06:35 +0900 Subject: [PATCH 05/16] test(s13): generate Host-only private canary inside CI, never commit even a synthetic secret --- tests/interactive/s13-chatgpt-tool-bridge.test.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/interactive/s13-chatgpt-tool-bridge.test.mjs b/tests/interactive/s13-chatgpt-tool-bridge.test.mjs index b1acda4..a99f570 100644 --- a/tests/interactive/s13-chatgpt-tool-bridge.test.mjs +++ b/tests/interactive/s13-chatgpt-tool-bridge.test.mjs @@ -21,7 +21,7 @@ import {readFile,mkdtemp,rm} from 'node:fs/promises'; import {fileURLToPath} from 'node:url'; import path from 'node:path'; import {tmpdir} from 'node:os'; -import {generateKeyPairSync,sign} from 'node:crypto'; +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'; @@ -38,7 +38,7 @@ const replyFile=fileURLToPath( const runId='s13-chatgpt-bridge-run-20261111'; const startKey='s13-chatgpt-bridge-20261111'; const namespace='s13-chatgpt-bridge-tenant'; -const privateCanary='HOST_PRIVATE_CANARY_DO_NOT_DISCLOSE_20261111'; +const privateCanary='HOST_PRIVATE_'+randomBytes(24).toString('hex'); const initialState={ ready:true, From 9bac8f86347a405efe068f96423ae3e501005061 Mon Sep 17 00:00:00 2001 From: rocky Date: Wed, 11 Nov 2026 18:50:14 +0900 Subject: [PATCH 06/16] test(interactive): rebase M6 Host integration on current main Add current-main M6 scripted Host integration coverage, root-discovery fixtures, Linux cross-UID CI, and the full Interactive delivery roadmap. This is test/CI/documentation only and does not complete native-host C12/S13 or M6 final acceptance. --- .github/workflows/ci.yml | 6 + ...active-full-delivery-roadmap-2026-11-11.md | 360 +++++++++++++++++ package.json | 2 +- tests/integration/m6-e2e.test.mjs | 380 ++++++++++++++++++ tests/integration/m6-host.mjs | 207 ++++++++++ tests/integration/m6-ledger-boundary.test.mjs | 90 +++++ tests/integration/m6-worker.mjs | 38 ++ 7 files changed, 1082 insertions(+), 1 deletion(-) create mode 100644 docs/interactive-full-delivery-roadmap-2026-11-11.md create mode 100644 tests/integration/m6-e2e.test.mjs create mode 100644 tests/integration/m6-host.mjs create mode 100644 tests/integration/m6-ledger-boundary.test.mjs create mode 100644 tests/integration/m6-worker.mjs 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/interactive-full-delivery-roadmap-2026-11-11.md b/docs/interactive-full-delivery-roadmap-2026-11-11.md new file mode 100644 index 0000000..e2630de --- /dev/null +++ b/docs/interactive-full-delivery-roadmap-2026-11-11.md @@ -0,0 +1,360 @@ +# FlowKit Interactive — Full Delivery Roadmap & Closure Ledger + +**Baseline date:** 2026-11-11 (UTC+8) +**Repository:** `devcodex-labs/flowkit` +**Frozen main:** `ea2bb10893a9be2e5de3561fc9a660a6977ae15c` +**Planning branch:** `fix/m6-current-main-e2e-20261111` + +> 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 + +- Main HEAD: `ea2bb10893a9be2e5de3561fc9a660a6977ae15c`. +- Main CI run: [42954859078](https://github.com/devcodex-labs/flowkit/actions/runs/42954859078), completed successfully. +- 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 open Draft, head `e1e15a70356e53401288d393aa7527c6723fe797`, based on an older main snapshot `c873e3edf0f1a78b13178966cdeb408391755170`. Its M6 tests need to be revalidated against current main. +- PR #12 is 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. + +### 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** — baseline and ledger committed on this branch | +| 1 | M6 current-main test compatibility | PR #7 tests run against frozen current main; no Core semantic change hidden in test repair | **PASS** — replacement PR #13 exact-HEAD 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 | Approved threat model, OS identity, IPC, key custody, Store ownership and provenance contract | Pending | +| 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 | + +## 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, IPC contract, key custody design, provider/session provenance design, Windows/Linux deployment designs, attack matrix and acceptance criteria. Independent review must close all design findings before Phase 3. + +## 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. Expose only constrained IPC operations with explicit authorization and input/output schemas. +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. + +Exit: security tests pass on both supported OS families, with no same-principal bypass. If the actual Grok Build execution environment cannot be isolated from Host secrets, keep native secure-host acceptance BLOCKED and do not expose Host secrets to that model. + +## 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. Dot independently 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 or its current-main replacement may be merged only after M6 integration CI and security gates pass. +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 baseline/traceability is complete. +2. PR #7's M6 integration tests were ported onto current main in replacement PR #13. +3. The fixture now covers default nearest-package discovery and explicit `projectRoot` override. +4. Exact HEAD `df1eec669d5f46a04f40718abf16fe111db6caf3` passed CI run [42997084061](https://github.com/devcodex-labs/flowkit/actions/runs/42997084061): 7/7 jobs; Ubuntu/Windows × Node 22/24 each passed 369/369 tests; Linux OS-user isolation plus M6 e2e passed 11/11 on Node 22 and Node 24. +5. Independent code review found no production Core changes. PR #13 is ready for review/merge as test infrastructure only; this does not close native C12/S13 or M6 final acceptance. +6. After PR #13 is merged, close/supersede stale PR #7 without merging its old branch, then begin Phase 2 Trusted Host security design. + +**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/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/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'); From ae5fc5a42c67eb6e2c229d72ff4d9ac8e3aab3cd Mon Sep 17 00:00:00 2001 From: rocky Date: Thu, 12 Nov 2026 10:09:27 +0900 Subject: [PATCH 07/16] docs(interactive): add Trusted Host V2 conditional security design Add the reviewed Trusted Host V2 threat model, deployment profiles, security acceptance matrix, and explicit Phase 2 limitations. This is documentation-only: it does not implement Host isolation, Grok native provenance, Windows ACL enforcement, or Phase 3/4 runtime changes. F-02 and F-04 remain pending real validation; F-07 formal V0.3/V0.6-R5 conformance remains blocked. Merge does not authorize Phase 3 or claim C12/S13/M6 acceptance. --- ...active-full-delivery-roadmap-2026-11-11.md | 42 ++- ...ive-trusted-host-security-v2-acceptance.md | 167 +++++++++ ...ractive-trusted-host-security-v2-design.md | 347 ++++++++++++++++++ ...ractive-trusted-host-security-v2-review.md | 163 ++++++++ ...e-trusted-host-security-v2-threat-model.md | 147 ++++++++ 5 files changed, 847 insertions(+), 19 deletions(-) create mode 100644 docs/interactive-trusted-host-security-v2-acceptance.md create mode 100644 docs/interactive-trusted-host-security-v2-design.md create mode 100644 docs/interactive-trusted-host-security-v2-review.md create mode 100644 docs/interactive-trusted-host-security-v2-threat-model.md diff --git a/docs/interactive-full-delivery-roadmap-2026-11-11.md b/docs/interactive-full-delivery-roadmap-2026-11-11.md index e2630de..5986bcb 100644 --- a/docs/interactive-full-delivery-roadmap-2026-11-11.md +++ b/docs/interactive-full-delivery-roadmap-2026-11-11.md @@ -2,8 +2,9 @@ **Baseline date:** 2026-11-11 (UTC+8) **Repository:** `devcodex-labs/flowkit` -**Frozen main:** `ea2bb10893a9be2e5de3561fc9a660a6977ae15c` -**Planning branch:** `fix/m6-current-main-e2e-20261111` +**Historical Phase 0 baseline:** `ea2bb10893a9be2e5de3561fc9a660a6977ae15c` +**Current Phase 2 source baseline:** `9bac8f86347a405efe068f96423ae3e501005061` +**Current design branch:** `design/trusted-host-security-v2-20261111` (historical Phase 0/1 baseline remains immutable) > 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. @@ -11,11 +12,11 @@ ### 1.1 Main and CI -- Main HEAD: `ea2bb10893a9be2e5de3561fc9a660a6977ae15c`. -- Main CI run: [42954859078](https://github.com/devcodex-labs/flowkit/actions/runs/42954859078), completed successfully. +- Current main HEAD after Phase 1: `9bac8f86347a405efe068f96423ae3e501005061`. +- Current main CI: [42998521531](https://github.com/devcodex-labs/flowkit/actions/runs/42998521531), 7/7 jobs successful. - 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 open Draft, head `e1e15a70356e53401288d393aa7527c6723fe797`, based on an older main snapshot `c873e3edf0f1a78b13178966cdeb408391755170`. Its M6 tests need to be revalidated against current main. +- 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 is 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. ### 1.2 Evidence classes @@ -31,9 +32,9 @@ Never count P evidence as M or N. Never count a CI replay of recorded model turn | Phase | Scope | Required exit condition | Current state | |---|---|---|---| -| 0 | Baseline freeze and traceability | Immutable baseline, full requirement/Finding/test ledger and evidence index | **Complete** — baseline and ledger committed on this branch | -| 1 | M6 current-main test compatibility | PR #7 tests run against frozen current main; no Core semantic change hidden in test repair | **PASS** — replacement PR #13 exact-HEAD 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 | Approved threat model, OS identity, IPC, key custody, Store ownership and provenance contract | Pending | +| 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 | Approved threat model, OS identity, IPC, key custody, Store ownership and provenance contract | **CONDITIONAL**:第三轮 Grok 定向复核已确认 F-01、F-R2-01 **CLOSED**;F-02/F-04 仍 PARTIAL(后续 PoC/OS 实测),F-07 正式 V0.3/V0.6-R5 原文符合性仍 **BLOCKED**;PR #14 可作为明确标注限制的设计记录候选,尚未合并 | | 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 | @@ -81,13 +82,13 @@ Freeze a Host threat model before implementation: - 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, IPC contract, key custody design, provider/session provenance design, Windows/Linux deployment designs, attack matrix and acceptance criteria. Independent review must close all design findings before Phase 3. +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. Expose only constrained IPC operations with explicit authorization and input/output schemas. +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. @@ -95,7 +96,9 @@ Deliverables: threat model, trust topology, IPC contract, key custody design, pr 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. -Exit: security tests pass on both supported OS families, with no same-principal bypass. If the actual Grok Build execution environment cannot be isolated from Host secrets, keep native secure-host acceptance BLOCKED and do not expose Host secrets to that model. +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 @@ -132,13 +135,13 @@ Exit: every C item is PASS, FAIL, PARTIAL, BLOCKED or explicitly DEFERRED with o ## 9. Phase 6 — M6 review, P0-A closure and merge -1. Dot independently checks exact code SHA, Program hash, raw native tool events, Pending/response, Host traces, signed statements, SQLite, Journal hash chain and OS evidence. +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 or its current-main replacement may be merged only after M6 integration CI and security gates pass. +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 @@ -350,11 +353,12 @@ A phase cannot pass by deleting a test, weakening an assertion, changing the evi ## 15. Current next actions -1. Phase 0 baseline/traceability is complete. -2. PR #7's M6 integration tests were ported onto current main in replacement PR #13. -3. The fixture now covers default nearest-package discovery and explicit `projectRoot` override. -4. Exact HEAD `df1eec669d5f46a04f40718abf16fe111db6caf3` passed CI run [42997084061](https://github.com/devcodex-labs/flowkit/actions/runs/42997084061): 7/7 jobs; Ubuntu/Windows × Node 22/24 each passed 369/369 tests; Linux OS-user isolation plus M6 e2e passed 11/11 on Node 22 and Node 24. -5. Independent code review found no production Core changes. PR #13 is ready for review/merge as test infrastructure only; this does not close native C12/S13 or M6 final acceptance. -6. After PR #13 is merged, close/supersede stale PR #7 without merging its old branch, then begin Phase 2 Trusted Host security design. +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-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 同权实验已通过原生安全验收。 From 13d573c5ddc4a0ed3502401589c581799c96c5d2 Mon Sep 17 00:00:00 2001 From: rocky Date: Thu, 12 Nov 2026 12:10:09 +0900 Subject: [PATCH 08/16] docs(interactive): recover frozen baselines and establish evidence audit plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Record independently validated Requirements V0.3 and Technical Design V0.6-R5 originals, limited Core PoC results and Phase 2 traceability roadmap. Reconcile the previously confirmed M1–M6 P0-A plan and existing M1–M5 implementation. Documentation only: no Core/API/Program ABI/Store schema changes, no Phase 3 authorization, no C12/S13 or full conformance PASS claim. --- ...active-full-delivery-roadmap-2026-11-11.md | 26 +- ...teractive-phase2-formal-source-register.md | 78 ++++++ ...eractive-v03-v06-conformance-audit-plan.md | 259 ++++++++++++++++++ 3 files changed, 357 insertions(+), 6 deletions(-) create mode 100644 docs/interactive-phase2-formal-source-register.md create mode 100644 docs/interactive-v03-v06-conformance-audit-plan.md diff --git a/docs/interactive-full-delivery-roadmap-2026-11-11.md b/docs/interactive-full-delivery-roadmap-2026-11-11.md index 5986bcb..20de9e3 100644 --- a/docs/interactive-full-delivery-roadmap-2026-11-11.md +++ b/docs/interactive-full-delivery-roadmap-2026-11-11.md @@ -3,8 +3,8 @@ **Baseline date:** 2026-11-11 (UTC+8) **Repository:** `devcodex-labs/flowkit` **Historical Phase 0 baseline:** `ea2bb10893a9be2e5de3561fc9a660a6977ae15c` -**Current Phase 2 source baseline:** `9bac8f86347a405efe068f96423ae3e501005061` -**Current design branch:** `design/trusted-host-security-v2-20261111` (historical Phase 0/1 baseline remains immutable) +**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. @@ -12,12 +12,14 @@ ### 1.1 Main and CI -- Current main HEAD after Phase 1: `9bac8f86347a405efe068f96423ae3e501005061`. -- Current main CI: [42998521531](https://github.com/devcodex-labs/flowkit/actions/runs/42998521531), 7/7 jobs successful. +- 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 is 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 #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 @@ -34,7 +36,7 @@ Never count P evidence as M or N. Never count a CI replay of recorded model turn |---|---|---|---| | 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 | Approved threat model, OS identity, IPC, key custody, Store ownership and provenance contract | **CONDITIONAL**:第三轮 Grok 定向复核已确认 F-01、F-R2-01 **CLOSED**;F-02/F-04 仍 PARTIAL(后续 PoC/OS 实测),F-07 正式 V0.3/V0.6-R5 原文符合性仍 **BLOCKED**;PR #14 可作为明确标注限制的设计记录候选,尚未合并 | +| 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 | @@ -44,6 +46,18 @@ Never count P evidence as M or N. Never count a CI replay of recorded model turn | 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. 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-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 工作,不重开已完成里程碑。 From c938608a5586b1edb6394466f2f2ac85a549f5b2 Mon Sep 17 00:00:00 2001 From: rocky Date: Thu, 12 Nov 2026 14:23:24 +0900 Subject: [PATCH 09/16] docs(interactive): archive Phase 2 formal conformance R1/R2 audit * docs(interactive): add source-backed T/R/S crosswalk and initial P0-A gap findings * docs(interactive): document Host/Public terminal State projection contract difference * docs(interactive): record async Store contract divergence from confirmed V0.6-R5 * docs(interactive): add Phase 2 R2 code-evidence review and R04 mutation findings * docs(interactive): link R04 independent regression PR and retrigger Phase 2 CI --- ...ve-phase2-conformance-audit-r1-20261112.md | 152 ++++++++++++++++++ ...ve-phase2-conformance-audit-r2-20261112.md | 107 ++++++++++++ 2 files changed, 259 insertions(+) create mode 100644 docs/interactive-phase2-conformance-audit-r1-20261112.md create mode 100644 docs/interactive-phase2-conformance-audit-r2-20261112.md 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。 From d5db31201e6f0ae40cb2877756ca36a8f34afb58 Mon Sep 17 00:00:00 2001 From: rocky Date: Thu, 12 Nov 2026 14:24:03 +0900 Subject: [PATCH 10/16] docs(interactive): archive local R04 reproduction and verification evidence * docs(interactive): record local verification of PR 16 and 17 The report keeps the PR heads, commands, R04 failure output, the snapshot fix result, and the merge recommendation. Raw decisive logs are stored beside it. No product code changes. * docs(interactive): record that Actions for PR 16-19 are still queued The 13:15 +08 reconfirm keeps all four draft PRs unmerged. Run status is queued with an empty conclusion. That is not a matrix pass and not a proven Actions outage. --- ...-PR17-Local-Verification-Final-20261112.md | 302 ++++++++++++++++++ .../actions-reconfirm-20261112T1315+0800.txt | 48 +++ .../fix-coordinator-node24-before-full.txt | 31 ++ .../pr17-coordinator-node22.22.2.txt | 196 ++++++++++++ .../pr17-coordinator-node24.txt | 73 +++++ .../20261112-pr16-pr17/probe-cf08-node24.txt | 8 + .../probe-r04-host-node24.txt | 13 + .../probe-r04-side-effects-node24.txt | 15 + 8 files changed, 686 insertions(+) create mode 100644 docs/FlowKit-PR16-PR17-Local-Verification-Final-20261112.md create mode 100644 docs/evidence/20261112-pr16-pr17/actions-reconfirm-20261112T1315+0800.txt create mode 100644 docs/evidence/20261112-pr16-pr17/fix-coordinator-node24-before-full.txt create mode 100644 docs/evidence/20261112-pr16-pr17/pr17-coordinator-node22.22.2.txt create mode 100644 docs/evidence/20261112-pr16-pr17/pr17-coordinator-node24.txt create mode 100644 docs/evidence/20261112-pr16-pr17/probe-cf08-node24.txt create mode 100644 docs/evidence/20261112-pr16-pr17/probe-r04-host-node24.txt create mode 100644 docs/evidence/20261112-pr16-pr17/probe-r04-side-effects-node24.txt 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" +} From 606d055d5f02451fea0b30294889d740ef370d49 Mon Sep 17 00:00:00 2001 From: rocky Date: Thu, 12 Nov 2026 14:24:41 +0900 Subject: [PATCH 11/16] fix(interactive): snapshot start inputs before first await (R04) start() copied caller-owned State and startIntent only after loadInteractiveProgram resolved. That await yields even when the loader is already settled, so a caller could change both objects before the Checkpoint and startIntentHash were created. Copy both with the existing bounded canonical snapshot first, and preserve the initial-State capacity error. Later Store writes use those copies. Includes the R04 regression tests from PR #17. --- src/interactive/coordinator.mjs | 17 +++++++++++--- tests/interactive/coordinator.test.mjs | 31 ++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) 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/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); +}); From d5f69b18bcf09eda17e7aa5713d7938f800843f0 Mon Sep 17 00:00:00 2001 From: rocky Date: Thu, 12 Nov 2026 17:18:06 +0900 Subject: [PATCH 12/16] test(interactive): require explicit preparation mode for ChatGPT response fixture --- .../s13-chatgpt-tool-bridge.test.mjs | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/tests/interactive/s13-chatgpt-tool-bridge.test.mjs b/tests/interactive/s13-chatgpt-tool-bridge.test.mjs index aa49867..912fd24 100644 --- a/tests/interactive/s13-chatgpt-tool-bridge.test.mjs +++ b/tests/interactive/s13-chatgpt-tool-bridge.test.mjs @@ -1,20 +1,19 @@ /** - * ChatGPT-current-conversation -> real JS tool -> actual FlowKit Host bridge. + * ChatGPT-authored response -> exact FlowKit Pending -> trusted Host acceptance. * - * Phase 1 (without a reply file): compile & execute the verified Program, - * durably publish a public Agent Pending, and print its EXACT requestId/digest. - * The ChatGPT model then reads that real CI output and writes a reply via its - * authenticated GitHub tool call (a separate conversation action). + * Explicit preparation mode (FLOWKIT_S13_PREPARE=1) publishes the actual + * Pending and prints its exact requestId/taskDigest for an external ChatGPT + * turn. ChatGPT returns its authored response through an authenticated GitHub + * tool action. That is NOT a FlowKit native JS-tool invocation or S13 Host bridge. * - * Phase 2 runs in a separate Actions invocation with a fresh temporary DB. - * It deterministically recompiles the workflow and recreates the same Pending; - * the fixture is accepted only when runId/requestId/taskDigest match exactly. - * The test then closes/reopens this Phase-2 DB before Host signing/CAS. It does - * NOT continue the physical SQLite database from the Phase-1 Actions run. + * Normal CI mode requires the complete response fixture. It deterministically + * recreates the same Pending, checks runId/requestId/taskDigest, then performs + * Host signing/CAS and SQLite close/reopen within Phase 2. The two phases use + * separate Actions invocations and do NOT share one physical SQLite database. * - * This proves actual tool-using ChatGPT participated. It does NOT independently - * attest ChatGPT provider/session origin, Codex-native JS tools or cross-UID - * model isolation. M6 S13 native Host gate remains separately BLOCKED. + * Evidence is M-level ChatGPT participation only. It does NOT independently + * attest provider/session origin, native Grok/Codex tools or cross-UID isolation. + * Native Host S13 remains BLOCKED. */ import test from 'node:test'; import assert from 'node:assert/strict'; @@ -60,7 +59,7 @@ const policy={ }; const resultFields=['flowkitBoundary','nextAction','summary']; -test('S13 live ChatGPT turn consumes actual public Pending and returns via Host signature/CAS',async()=>{ +test('ChatGPT-authored response is bound to exact Pending and accepted by trusted Host',async()=>{ const dir=await mkdtemp(path.join(tmpdir(),'flowkit-s13-chatgpt-')); const file=path.join(dir,'run.sqlite'); let store; @@ -140,8 +139,10 @@ test('S13 live ChatGPT turn consumes actual public Pending and returns via Host if(error?.code!=='ENOENT')throw error; } if(!modelReply){ + assert.equal(process.env.FLOWKIT_S13_PREPARE,'1', + 'Complete ChatGPT response fixture is required for normal CI; set FLOWKIT_S13_PREPARE=1 only for explicit Pending preparation'); assert.equal((await firstHost.load({runId})).status,'waiting'); - console.log('S13_CHATGPT_PHASE1_WAITING '+JSON.stringify({ + console.log('CHATGPT_PENDING_PREPARED '+JSON.stringify({ runId,requestId:pending.requestId,revision:frame.revision, journalRevisions:firstJournal.length, result:'Real public task printed for next ChatGPT tool turn', @@ -207,7 +208,7 @@ test('S13 live ChatGPT turn consumes actual public Pending and returns via Host assert.equal(duplicate.acceptedRevision,completed.acceptedRevision); assert.equal(duplicate.revision,completed.revision); assert.equal((await resumedHost.resume({runId})).status,'succeeded'); - console.log('S13_CHATGPT_TOOL_BRIDGE_COMPLETED '+JSON.stringify({ + console.log('CHATGPT_RESPONSE_FIXTURE_ACCEPTED '+JSON.stringify({ runId,requestId:receipt.requestId, modelReplyDigest:sha256Bytes(canonicalBytes(modelReply.result)), artifactHash:compiled.programIdentity.artifactHash, @@ -220,7 +221,7 @@ test('S13 live ChatGPT turn consumes actual public Pending and returns via Host phase2DatabaseReconstructed:true,crossRunDatabaseContinuation:false, policyPinned:true, secretNotExposedToPublicTask:true, - warning:'Actual ChatGPT tool-using answer, but NO independent native model-session attestation; S13 remains BLOCKED', + warning:'ChatGPT-authored response delivered through GitHub tool; not a FlowKit native JS-tool call; no independent native session attestation; S13 remains BLOCKED', })); }finally{ try{store?.close()}finally{await rm(dir,{recursive:true,force:true})} From bfdb3f6152de5a748bd184c7094c13a616469f1e Mon Sep 17 00:00:00 2001 From: rocky Date: Thu, 12 Nov 2026 17:18:09 +0900 Subject: [PATCH 13/16] docs(interactive): label ChatGPT response experiment as M-level --- examples/interactive/s13-chatgpt.workflow.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/interactive/s13-chatgpt.workflow.js b/examples/interactive/s13-chatgpt.workflow.js index bb56ba5..e8f25dd 100644 --- a/examples/interactive/s13-chatgpt.workflow.js +++ b/examples/interactive/s13-chatgpt.workflow.js @@ -3,7 +3,7 @@ import {defineWorkflow,condition,node,terminal} from '@devcodex-labs/flowkit/dsl export default defineWorkflow({ id:'s13-chatgpt-tool-bridge', version:'0.1.0', - name:'ChatGPT 实际工具链 S13 验证', + name:'ChatGPT 回复绑定与可信 Host 验收(M-level)', nodes:[ condition('gate',{ name:'确定进入审核节点', From 41980fcab330adaf785eacc0a9751f411ebf4e50 Mon Sep 17 00:00:00 2001 From: rocky Date: Thu, 12 Nov 2026 17:19:35 +0900 Subject: [PATCH 14/16] test(interactive): preserve original workflow identity for recorded Pending fixture --- examples/interactive/s13-chatgpt.workflow.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/interactive/s13-chatgpt.workflow.js b/examples/interactive/s13-chatgpt.workflow.js index e8f25dd..bb56ba5 100644 --- a/examples/interactive/s13-chatgpt.workflow.js +++ b/examples/interactive/s13-chatgpt.workflow.js @@ -3,7 +3,7 @@ import {defineWorkflow,condition,node,terminal} from '@devcodex-labs/flowkit/dsl export default defineWorkflow({ id:'s13-chatgpt-tool-bridge', version:'0.1.0', - name:'ChatGPT 回复绑定与可信 Host 验收(M-level)', + name:'ChatGPT 实际工具链 S13 验证', nodes:[ condition('gate',{ name:'确定进入审核节点', From 08a4a17cd4c7a1e0bd005101aa093e5a16683f2a Mon Sep 17 00:00:00 2001 From: rocky Date: Thu, 12 Nov 2026 17:21:04 +0900 Subject: [PATCH 15/16] docs(interactive): clarify ChatGPT response evidence tier and S13 limits --- docs/interactive-chatgpt-response-evidence.md | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 docs/interactive-chatgpt-response-evidence.md diff --git a/docs/interactive-chatgpt-response-evidence.md b/docs/interactive-chatgpt-response-evidence.md new file mode 100644 index 0000000..e6f884a --- /dev/null +++ b/docs/interactive-chatgpt-response-evidence.md @@ -0,0 +1,41 @@ +# ChatGPT-authored response evidence for FlowKit Interactive + +**Evidence tier: M-level recorded model response. This is not native S13.** + +## What the experiment does + +1. FlowKit compiles the Interactive workflow and durably creates a public Agent Pending in SQLite. +2. The test emits the exact `runId`, `requestId`, `taskDigest`, Program artifact hash and allowlisted task input. +3. A ChatGPT model reads that Pending evidence in the conversation and authors a structured response. +4. The response is returned through an authenticated GitHub tool action and stored as a fixture. +5. A later test run deterministically reconstructs the same Pending and rejects the fixture unless all identity fields match exactly. +6. The trusted Host validates the result, signs its own Host receipt, commits through CAS, verifies duplicate Ack and resumes the Run. + +## What it proves + +- A ChatGPT-authored response was produced after inspection of a real FlowKit Pending. +- The response is bound to the exact deterministic Pending by `runId/requestId/taskDigest`. +- FlowKit's actual Program, SQLite Store and Trusted Host process that recorded response. +- Host signature/CAS, duplicate receipt and same-phase SQLite close/reopen behavior. + +## What it does not prove + +- The ChatGPT model invoked a FlowKit-native JavaScript tool. The response is returned through a GitHub tool action, not a native FlowKit JS-tool call. +- Independent cryptographic attestation of the ChatGPT provider/session identity. +- Grok Build or Codex native tool-call provenance. +- Cross-UID / OS-level model-process isolation. +- One physical SQLite database surviving across the separate Pending-preparation and response-acceptance Actions runs. The second run reconstructs the deterministic Pending and uses its own database. + +Therefore this evidence is useful for M-level real-model participation and response-binding tests, but **must not be counted as native Host S13 / C12 PASS**. + +## Test execution modes + +Normal CI must consume the complete committed response fixture and reach Terminal. If the fixture is absent, the test fails. + +Only an explicit preparation invocation may stop at Pending: + +```bash +FLOWKIT_S13_PREPARE=1 node --test tests/interactive/s13-chatgpt-tool-bridge.test.mjs +``` + +The preparation mode is not used by the normal CI workflow. From 8ccb53055f640539fd86c524f079867fbdb12713 Mon Sep 17 00:00:00 2001 From: rocky Date: Thu, 12 Nov 2026 17:23:28 +0900 Subject: [PATCH 16/16] test(interactive): add C01-C24 complex acceptance evidence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(interactive): freeze C01-C12 scenario matrix and scripted/live/native evidence separation * test(interactive): C01-C04,C10,C11 scripted complex runs with real Program signed Host and SQLite * test(interactive): C01/C03 live multi-Agent two-Condition workflow definition * test(interactive): real ChatGPT multi-Agent 5-turn replay bridge with schema rejection and SQLite reopen * test(c01-live): actual ChatGPT requirements analysis bound to published Pending #1 * test(c01-live): ChatGPT architecture response to real dependent Pending #2 * test(c01-live): actual ChatGPT implementation plan from real Pending #3 * test(c03-live): ChatGPT deliberately submits invalid reviewApproved type to test real retry Pending * test(c01-live): genuine ChatGPT corrected review after real attempt-2 Pending #5 * docs(interactive): record C01-C12 detailed scripted and five-turn real ChatGPT evidence with open native gates * test(interactive): extend complex P0-A acceptance with C13-C20 interleavings * docs(interactive): extend risk-mapped C13-C24 acceptance and native-host gates * test(interactive): inspect durable Store state while Pending frame intentionally redacts State * test(interactive): verify C16 durable Checkpoint while Pending frame hides private State * docs(interactive): record independently checked 7/7 CI and C13-C24 layered verdict * test(interactive): rebase M6 Host integration on current main Add current-main M6 scripted Host integration coverage, root-discovery fixtures, Linux cross-UID CI, and the full Interactive delivery roadmap. This is test/CI/documentation only and does not complete native-host C12/S13 or M6 final acceptance. * docs(interactive): add Trusted Host V2 conditional security design Add the reviewed Trusted Host V2 threat model, deployment profiles, security acceptance matrix, and explicit Phase 2 limitations. This is documentation-only: it does not implement Host isolation, Grok native provenance, Windows ACL enforcement, or Phase 3/4 runtime changes. F-02 and F-04 remain pending real validation; F-07 formal V0.3/V0.6-R5 conformance remains blocked. Merge does not authorize Phase 3 or claim C12/S13/M6 acceptance. * docs(interactive): recover frozen baselines and establish evidence audit plan Record independently validated Requirements V0.3 and Technical Design V0.6-R5 originals, limited Core PoC results and Phase 2 traceability roadmap. Reconcile the previously confirmed M1–M6 P0-A plan and existing M1–M5 implementation. Documentation only: no Core/API/Program ABI/Store schema changes, no Phase 3 authorization, no C12/S13 or full conformance PASS claim. * docs(interactive): archive Phase 2 formal conformance R1/R2 audit * docs(interactive): add source-backed T/R/S crosswalk and initial P0-A gap findings * docs(interactive): document Host/Public terminal State projection contract difference * docs(interactive): record async Store contract divergence from confirmed V0.6-R5 * docs(interactive): add Phase 2 R2 code-evidence review and R04 mutation findings * docs(interactive): link R04 independent regression PR and retrigger Phase 2 CI * docs(interactive): archive local R04 reproduction and verification evidence * docs(interactive): record local verification of PR 16 and 17 The report keeps the PR heads, commands, R04 failure output, the snapshot fix result, and the merge recommendation. Raw decisive logs are stored beside it. No product code changes. * docs(interactive): record that Actions for PR 16-19 are still queued The 13:15 +08 reconfirm keeps all four draft PRs unmerged. Run status is queued with an empty conclusion. That is not a matrix pass and not a proven Actions outage. * fix(interactive): snapshot start inputs before first await (R04) start() copied caller-owned State and startIntent only after loadInteractiveProgram resolved. That await yields even when the loader is already settled, so a caller could change both objects before the Checkpoint and startIntentHash were created. Copy both with the existing bounded canonical snapshot first, and preserve the initial-State capacity error. Later Store writes use those copies. Includes the R04 regression tests from PR #17. * test(interactive): require complete recorded ChatGPT responses in normal CI * docs(interactive): clarify recorded model-response evidence tier --- .../interactive-complex-acceptance-c01-c12.md | 51 ++ ...x-acceptance-c13-c24-results-2026-11-11.md | 58 ++ ...ve-complex-acceptance-extension-c13-c24.md | 51 ++ ...e-complex-acceptance-results-2026-11-11.md | 86 +++ .../interactive/c01-live-chatgpt.workflow.js | 62 ++ tests/fixtures/c01-live-chatgpt-turns.json | 49 ++ tests/interactive/c01-live-chatgpt.test.mjs | 218 +++++++ tests/interactive/complex-c01-c12.test.mjs | 605 ++++++++++++++++++ 8 files changed, 1180 insertions(+) create mode 100644 docs/interactive-complex-acceptance-c01-c12.md create mode 100644 docs/interactive-complex-acceptance-c13-c24-results-2026-11-11.md create mode 100644 docs/interactive-complex-acceptance-extension-c13-c24.md create mode 100644 docs/interactive-complex-acceptance-results-2026-11-11.md create mode 100644 examples/interactive/c01-live-chatgpt.workflow.js create mode 100644 tests/fixtures/c01-live-chatgpt-turns.json create mode 100644 tests/interactive/c01-live-chatgpt.test.mjs create mode 100644 tests/interactive/complex-c01-c12.test.mjs 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/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/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/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}); +});