Files
Genarrative/docs/technical/【技术方案】Direct回合行为审计账本-2026-08-31.md
T
k88936 6fe91a169c 移除 AGC Skill 受控引用读取工具
删除 agc_read_skill_resource 的实现、MCP 暴露、审计分支与专用测试

将内置 Skill references 改为 Codex 原生相对路径读取

同步 Skill manifest 指纹、Runtime 提示词与 AGC 项目文档

保留既有 references 安装与隔离 HOME 校验
2026-09-02 19:00:31 +08:00

26 KiB
Raw Blame History

Direct 回合行为审计账本

0. 一句话

Direct GUI 回合已经能看见 Codex item/completed,但只收成 UI 活动词,隔离 CODEX_HOME 随后删除。本方案在项目内留下有界、可共享的回合账本:本轮提供了哪些附件路径、按什么顺序做了读/搜/列表/MCP/写文件,以及第一次定玩法的动作是什么。用来区分「没读附件」和「读了仍走默认收集类」,不灌正文、不强制读取、不拷会话目录。

1. 目标与非目标

目标

一次带 clientTurnId 的 DirectProject GUI 回合结束后,只凭项目目录应能回答:

  1. 本轮 sidecar 是否发出,原名映射到哪些项目相对路径,文件当时的 contentSha256
  2. 模型是否用 native 命令 / 列表 / 搜索 / 看图 / MCP 打开过那些路径(路径 + 当时磁盘 hash,不是 stdout)。
  3. 顺序:读附件是在第一次美术 brief / 第一次写 game/ 之前还是之后。
  4. 第一次「定玩法」动作是什么(优先 taonier_prepare_game_art.brief,否则其它生成类 MCP 或对 game/ 的写入)。

覆盖后续手打回合:只要 GUI Direct 有 clientTurnId 就记账本,附件可以为空。

非目标

  • 不证明「理解并按 GDD 实施」。那是对照 game/index.html、美术产物做的产品判断;账本只提供行为时间线。
  • 不把 issue #212 原文的 NLP / 「关键内容进入上下文」做成自动判决。
  • 不灌附件正文进 prompt,不强制先读再继续,不为 GDD 开协议特例。
  • 不改 sidecar 文案、jsonl 用户原文、工作台气泡。
  • 不改做成游戏固定 prompt / PR #210 注入。
  • 不复用 Supervisor agent.runtime.action_receipt / file.read
  • 不拷隔离 CODEX_HOME、不落 auth.json、不落 aggregated_output / MCP result / patch diff / FunctionCallOutput 正文。
  • 不把原始 item JSON 送进 Tauri 前端事件(现有 DirectCodexTurnObservation 仍只允许安全活动词和流式正文)。
  • 不扫 kind=uploaded 历史附件;只记本轮 sidecar 提供的集合。
  • DirectHome、ToolHost、CLI --direct-codex-chat(无 clientTurnId)本期不写这份账本。
  • 本期不改 UI,不在聊天面板展示审计。
  • 不把 issue #212 标成已修复;sidecar 与本账本是两段工作。

2. 现状

Codex item/completed
  commandExecution / mcpToolCall / fileChange / imageView / …
        │
        ├─ 现用:收成 Activity("validation"|"controlled-tool"|…)
        │         → Tauri 进度,不落盘
        └─ 不用:隔离 CODEX_HOME session(含 stdout)→ tempdir Drop 删除

项目里现有:

产物 记下的 缺的
.agent/conversations/project.jsonl 用户原文 + 助手终稿 sidecar、工具调用
.agent/agent.db init / upload / 美术登记 / 对话指针 native 读、MCP 调用、agc_write_file
.agent/logs/command.log 权限确认 原生命令
asset.register / canvas.asset_generate 路径、切片、部分 source.prompt 与读附件的先后
隔离 CODEX_HOME Codex 自己的 session 回合结束即删

Codex app-server 协议里,commandExecution.commandActions 已分类为 Read | ListFiles | Search | UnknownRead.path 在协议侧会拼成 cwd 绝对路径。Direct cwd 就是项目根(resolve_direct_codex_project_authority 不再强制 game/ 子目录)。抽取时把绝对路径收回项目相对 POSIX,失败则丢路径,不写宿主绝对路径。

agc_write_file 经 tool bridge 落盘,当前不写 agent.db。不给每个 MCP 单独打点;统一在 item/completed 抽一次。

3. 分析用判据(相对 issue 收窄)

落地后,对类似 gameagent-9baa5293 的 run,应能三分:

时间线 结论 下一刀不该打哪
offeredRead.read=falsefirstDesign 已是 taonier_prepare_game_art 且 brief 是收集类 没打开附件就定了玩法 不是「GDD 解析不够」
Read 且 hash 对上,brief 仍是收集类 读了但没用 sidecar 已够;看四切片 / icon-spec「收集物」/ 完成合同
只有 ListFiles / Search 命中 uploads,没有 Read 发现了没读正文 映射可能够,缺的是读
Read 的 path 是 fast_gdd.md 而不是 assets/uploads/… sidecar 没被当成磁盘路径 还是路径合同

不在账本里写「已遵循 GDD」或「未遵循 GDD」布尔。

4. 落点

两层,都在项目 .agent/ 控制面内,模型读不到:

  1. 权威时间线(每回合一个 jsonl,只追加) .agent/runtime/direct-codex/turns/<clientTurnId>.jsonl
  2. 总索引一条摘要(方便继续翻现有 agent.db recordType: "direct.codex.turn"

clientTurnId 沿用现有规则:trim 后 6–160 位 ASCII 字母数字或连字符,首位字母或数字。文件名用规范化后的 id,不再二次编码。

不升级 GAME_CREATOR_AGENT_DB_SCHEMA_VERSION;新 recordType 走 Ordinary 追加。updatedAt / schemaVersion 仍由 serialize_agent_db_record 写入。

jsonl 每条自带 recordedAtMsunix_millis)。同一 clientTurnId 若再次进入(当前 GUI 运行中互斥,结束后理论上可再来):只追加,不截断;后一次 turn_start 视为新 attempt。读摘要时按文件内最后一次 turn_start 到对应 turn_end 计算 offeredReadagent.db 每次 turn_end 再追加一条摘要,分析取该 clientTurnId 最后一条。

5. 记录合同

camelCase JSON。禁止出现附件正文、命令 stdout、patch diff、宿主绝对路径、Token、URL 签名。

5.1 turn_start

在 sidecar 已经渲染之后、Codex turn 启动之前写入。promptSha256 哈希的是 用户原文command 入参 prompt),不是带 sidecar 的全文。

{
  "recordType": "direct.codex.turn_start",
  "clientTurnId": "Abc123-def",
  "sidecarPresent": true,
  "promptSha256": "<sha256 hex of original user text UTF-8>",
  "promptChars": 120,
  "attachments": [
    {
      "name": "fast_gdd.md",
      "localPath": "assets/uploads/upload-1788164530559-fast_gdd.md",
      "mediaType": "text/markdown",
      "size": 8119,
      "status": "imported",
      "contentSha256": "<sha256 hex or omit>",
      "hashSkipped": null
    }
  ]
}
  • attachments 清洗复用 sidecarsanitize_attachment_name / media_type / status / local_path。把这些函数改成 pub(crate),审计模块不要复制一份。
  • 条数上限仍 8;超出只在 sidecar 文案里写「另有 N 个未展开」,账本 attachments 同样只留前 8,另加 attachmentsOmitted: N
  • sidecarPresent:本轮渲染走了 Project 头(任一条有合法 path 或 status)。Home 形态不会出现在本账本(Home 不记账)。
  • contentSha256:对清洗后的 localPath 读项目文件做 SHA-256 小写 hex。文件不存在则省略 hash,hashSkipped: "missing"。超过 DIRECT_CODEX_AUDIT_HASH_MAX_BYTES2 MiB)则 hashSkipped: "too-large".agent / .git / .. 路径本来就不会出现在 sidecar 输出里。

无附件:attachments: []sidecarPresent: false,仍然写 turn_start

5.2 item

item/completeditem/startedoutputDelta 不落盘。

公共字段:

{
  "recordType": "direct.codex.item",
  "clientTurnId": "Abc123-def",
  "seq": 1,
  "itemId": "item-…",
  "itemType": "commandExecution",
  "status": "completed"
}

seq 从 1 起,按成功写入的 item 递增。itemType 取 Codex item.type 原词;未知类型仍记账 itemType,不附带未清洗 payload。

按类型附加字段:

item.type 追加 禁止
commandExecution command 截断 240 字;exitCodedurationMsactions[] aggregatedOutput
mcpToolCall toolserver(可省略默认 agc_tools)、durationMs、§5.4 参数 resulterror 原文(只留 status / errorKind
fileChange changes: [{ path, kind }]kindadd / delete / update diffmovePath 的宿主绝对路径(相对化失败则整条 change 丢 path
imageView path 图像字节
functionCallOutput namenamespace output
webSearch query 截断 400 字 结果页正文
agentMessage / userMessage / plan / reasoning / contextCompaction / hookPrompt 整类跳过(终稿已在 jsonl;推理正文不是本账本)
其它未知 只留公共字段 原始 item 对象

commandExecution.actions[]

{ "type": "read", "path": "assets/uploads/upload-…-fast_gdd.md", "contentSha256": "…", "hashSkipped": null }
{ "type": "listFiles", "path": "assets" }
{ "type": "search", "query": "fast_gdd", "path": null }
{ "type": "unknown" }
  • Read.path 先相对化再清洗;失败则该 action 记 { "type": "read", "pathRejected": true },不写绝对路径。
  • 相对化成功后,对磁盘文件按 §5.1 同一套 hash 规则补 contentSha256
  • command 里若相对化失败,把 command 整段丢掉,改 commandRedacted: true(避免 type C:\Users\…\fast_gdd.md 进账本)。

5.3 MCP 参数白名单

只抄这些键,其它键丢弃。字符串再经 path 清洗或截断。

工具 落盘参数 正文类字段
agc_list_project_files pathquery120)、kindoffsetlimit
agc_write_file pathcontentCharscontent 的字符数,不是正文) 不落 content
taonier_prepare_game_art modebrief(截断 4000)、briefCharsbriefSha256 要 brief 原文(分析定玩法的吸烟枪;上限已是 MCP 合同)
agc_generate_image kindaspectRatioimageSizeassetNameoutputPathprompt 截断 4000、promptCharspromptSha256 不落 32k 全文
agc_edit_image sourceLocalAssetIdassetNameprompt 截断 4000、promptCharspromptSha256 同上
agc_create_or_derive_resource kindmodesourceLocalAssetIdassetNameprompt 截断 4000、promptCharspromptSha256 MCP 上限已是 4000
agc_list_registered_assets kindassetIdincludeSequenceFramesoffsetlimit
agc_list_account_assets folderIdqueryoffsetlimit
agc_import_account_assets assetIds(最多 8 个 id,超出 assetIdsOmitted)、localPaths(清洗后相对路径,最多 8
agc_remove_background sourceLocalAssetIdassetName
agc_browser_playtest attempt
agc_web_search query 截断 400、maxResults
未知 MCP 名 只留 tool + status 不落 arguments

brief / 截断后的 prompt模型自己写的设计文本,不是用户 GDD 转储。这是分析「仍走收集类」的关键,允许进 jsonl。agent.db 摘要只留 briefPreview 240 字。

5.4 turn_end

派生摘要,不是第二真相。字段必须能从本文件已写入的 turn_start + item 重算出来。

{
  "recordType": "direct.codex.turn_end",
  "clientTurnId": "Abc123-def",
  "completed": true,
  "itemCount": 17,
  "itemsTruncated": false,
  "offeredRead": [
    {
      "localPath": "assets/uploads/upload-1788164530559-fast_gdd.md",
      "read": false
    }
  ],
  "firstDesign": {
    "kind": "mcp:taonier_prepare_game_art",
    "seq": 3,
    "tool": "taonier_prepare_game_art",
    "briefPreview": "俯视角收集冒险小游戏…"
  }
}

offeredRead.read=true 当且仅当本 attempt 内存在 actions.type=readimageView 或 MCP 参数里的 path / localPaths,清洗后与 localPath 字符串相等。hash 对不上仍记 read: true,另加 contentSha256Match: false(读了另一份同路径文件或读时文件已变)。没有 hash 可对则省略 contentSha256Match

firstDesign:本 attempt 第一条满足任一条件的 item:

  1. MCPtaonier_prepare_game_art / agc_generate_image / agc_edit_image / agc_create_or_derive_resource
  2. agc_write_file 且 path 以 game/ 开头或文件名是 index.html
  3. fileChange 且任一条 change path 满足 2

列表、搜索、读、Skill 读取、账户素材查询、playtest、web_search 不算 firstDesign。没有则 firstDesign: null

kind 取值:mcp:<tool> / write:<path> / fileChange:<path>

5.5 agent.db 摘要

{
  "recordType": "direct.codex.turn",
  "clientTurnId": "Abc123-def",
  "turnLog": ".agent/runtime/direct-codex/turns/Abc123-def.jsonl",
  "sidecarPresent": true,
  "offeredCount": 1,
  "offeredRead": [
    { "localPath": "assets/uploads/…-fast_gdd.md", "read": false }
  ],
  "firstDesign": {
    "kind": "mcp:taonier_prepare_game_art",
    "seq": 3,
    "briefPreview": "…"
  },
  "itemCount": 17,
  "itemsTruncated": false,
  "completed": true,
  "auditWriteFailed": false
}

turnLog 必须是项目相对 POSIX。不要把 jsonl 全文复制进 agent.db。单条仍受 Ordinary 1 MiB 限制;摘要本身应远小于此。

5.6 上限

每回合 item 条数 256;超出再写一条 recordType: "direct.codex.items_truncated",之后 item 丢弃但仍把 turn_end.itemsTruncated=true
command 240 字
brief / 生成类 prompt 落盘 4000 字
briefPreview 240 字
文件 hash 2 MiB
附件条数 8(与 sidecar 相同)
jsonl 单行 沿用现有 jsonl 追加上限;超长截断正文类字段,不截断结构

6. 调用链

chat_with_game_creator_direct_codex
  规范化 clientTurnId
  DirectCodexTurnAudit::start(root, clientTurnId, originalPrompt, attachments)
      → 写 turn_startfail-open
  render_direct_codex_user_prompt          // 现有 sidecar,不变
  run_direct_game_creator_turn_at_with_creation_type_and_emitter(..., audit)
      → Codex collect 循环在 DirectProject + item/completed 调 audit.observe_item
  Ok/Err 都 audit.finish(completed)
      → 写 turn_end + agent.db 摘要
  • CLI run_direct_game_creator_turn_at 接 audit(无 clientTurnId)。
  • Home command 不接 audit。
  • 不要把 attachments / audit 顺着 CLI inner、pool、ToolHost 往下传。
  • DirectCodexTurnObservation 增加原始 params。审计走独立 DirectCodexTurnAudit,避免 stdout 正文进入 Tauri 事件。

direct_game_creator_codex_chat_at_with_optional_observer 增加可选 audit: Option<&mut DirectCodexTurnAudit>,再传到 run_turn_with_direct_observer。仅 workspace_mode == DirectProjectaudit 为 Some 时抽取。

run_direct_game_creator_turn_inner 的 UI observer 保持只处理 AccumulatedText / Activity

回合失败(生成失败、浏览器试玩失败、回复落盘失败):只要 start 过就 finish(false),保留已观察到的 item。Codex 尚未启动则 itemCount=0

7. 失败语义

审计 不得 把做游戏打失败。所有写盘包在 sink 内:

  • 单次追加失败:记内存 audit_write_failed=true,后续 item 仍尝试写;finish 时摘要带 auditWriteFailed: true
  • 连摘要都写不进去:只在 Direct debug / 现有进度通道能承受的前提下忽略;不新增用户可见报错文案。
  • 不引入新的 Tauri 事件名。

conversation.write 失败不同:助手终稿落盘失败仍按现有逻辑拒绝返回。审计失败不走那条。

8. 安全

  • 路径:与 sidecar 同一套相对 POSIX 清洗;相对化失败不写原绝对路径。
  • 控制面:hash / 读文件只用 resolve_local_project_path;拒绝 .agent / .git / 敏感文件。这些路径若出现在 commandActions 里,只记 pathRejected
  • 不把 aggregated_output、MCP result、function output、diff 暂存在内存再截断——抽取函数根本不读这些键。
  • jsonl 位于 .agent/runtime/**,现有 Direct 控制面边界禁止模型当普通项目文档读。
  • 前端观察者和审计 sink 分叉,禁止图省事 observer(Item { params })

9. 代码落地

新增 apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_audit.rs

  • DirectCodexTurnAudit
  • start / observe_item / finish
  • 相对化、hash、MCP 白名单、firstDesign / offeredRead 派生
  • 单元测试(见 §11

agent.rsmod direct_codex_audit + pub(crate) use

direct_codex_attachments.rs:清洗函数改 pub(crate),行为不变。

codex_app_server.rs

  • run_turn_with_direct_observer / direct_game_creator_codex_chat_at_with_optional_observer 增加 audit: Option<&mut DirectCodexTurnAudit>
  • collect 循环 Item { completed: true, .. } 且 DirectProject 时 audit.observe_item(&params)
  • 不要params 塞进 DirectCodexTurnObservation
  • 现有 Home / ToolHost / 安全活动词测试保持逐字

direct_runtime.rs

  • chat_with_game_creator_direct_codex 创建 audit(原文 + attachments),Ok/Err 都 finish
  • 把 audit 传入 run_direct_game_creator_turn_at_with_creation_type_and_emitter
  • 该函数和 inner 增加可选 audit;CLI 入口签名不变

不要给 agc_write_file / 每个 MCP handler 再写一份平行审计。

前端、DTO、sidecar 文案、home.suite 附件断言:本期不改。不新增 UI。

文档:落地提交时把本文状态改为「现行合同(已按本文落地)」;decision-log.md 记一条;不要把 08-30 技术说明改成已修复。

10. 测试

全部是 Rust 单元测试,用 fixture item JSON,不拉真 Codex。

  1. turn_start:原文 hash 稳定;sidecar 路径清洗后出现;非法 ../ 不进 attachments;无附件 sidecarPresent=false
  2. 附件文件写入临时项目后 contentSha256 与直接 hash 一致;缺文件 hashSkipped=missing;超过 2 MiB too-large
  3. commandExecution + commandActions: [{type:read, path: <abs>}] → 相对路径 + hashaggregatedOutput 即使在 fixture 里也不出现在落盘 JSON。
  4. Read 相对化失败 → pathRejected,落盘 JSON 不含 C:\\ / Users
  5. mcpToolCall taonier_prepare_game_artbrief 保留;result 丢掉。
  6. agc_write_file:有 path 与 contentChars,无 content。
  7. agc_generate_image32k prompt 只留 4000 + promptChars + sha256。
  8. fileChangepath + kind,无 diff。
  9. offeredRead:读路径等于 offered → read=true;只 list/search → read=false
  10. firstDesign:先 read 再 art → kind 是 mcp artseq 是 art 那条;只有 read → null
  11. 第 257 条 item 触发 truncatedturn_end.itemsTruncated=true
  12. agent.db 摘要含 turnLog 相对路径、offeredReadfirstDesign.briefPreview
  13. 写盘注入失败:finish 不 panic、不返回 Err 给调用方(sink 方法是 ())。
  14. 未知 item.type 只留公共字段。
  15. 现有 DirectHome 活动词测试、sidecar 渲染测试不受影响。

不测:真模型是否读 GDD、是否生成弹幕射击、浏览器验收文案。

11. 验收(方案落地后的人工分析)

用一次「上传 md + 做成游戏 / 首页做游戏」的本地项目:

  1. 存在 .agent/runtime/direct-codex/turns/<clientTurnId>.jsonl
  2. agent.db 有对应 direct.codex.turn
  3. turn_start.attachments[].localPath 与 sidecar 项目路径一致。
  4. jsonl 没有 GDD 正文、没有 aggregatedOutput、没有 patch。
  5. 能根据 offeredRead + firstDesign 填上 §3 四行表的其中一行,而不用猜隔离 session。
  6. 工作台气泡仍是用户原文;jsonl 对话仍无 sidecar。
  7. npm run check:encodinggit diff --check、相关 Rust 单测通过。

12. 实现顺序

  1. direct_codex_audit.rs + 清洗函数 pub(crate) + fixture 测试
  2. codex_app_server collect 接 sink;观察者枚举不变
  3. GUI Direct command 创建 / finish sink
  4. encoding 与定向 cargo test
  5. 合入时改本文状态、decision-log、sidecar 文档里「native 审计另排期」那一行改为指向本文

13. 与 sidecar / issue 的边界

  • sidecar:让模型 知道路径。已落地,合同不变。
  • 本账本:让人 看见模型做了什么。不替代 sidecar,也不在本方案里做强制读取。
  • issue #212 主问题仍是消费失败;本账本是为下一刀修复提供证据,不是把 212 关单。