新增 agc_install_skill_resource:宿主直落 Skill 附件,复用写入合同门
- 新增内置 MCP 工具 agc_install_skill_resource(skillName/relativePath/destinationPath),宿主直接读 AGC_SKILL_PACK_FILES 已审核字节
- 组装成 agc_write_file 的 {path,content} 转发客户端工具桥,复用同一份合同、lease、项目写锁与保护面校验
- read/install 共用 skill_resource_relative_path_is_unsafe;模型自带正文无旁路,正文不过模型上下文,不依赖原生 cp 审批
- direct-tools.json 增补工具描述与 destinationPath,direct.json engineering 指引改用本工具复制 Skill 文件
- 新增两条 install_skill_resource 测试并登记工具目录;同步技术方案与 decision-log
This commit is contained in:
@@ -4,6 +4,8 @@
|
||||
"conversation.list.description": "按序读取当前项目已记录的 Codex 返回摘要。",
|
||||
"conversation.read.description": "读取当前项目的一条已记录 Codex 返回;只能使用 conversation.list 返回的 recordId。",
|
||||
"agc_read_skill_resource.description": "读取审核通过的 AGC Skill 指导文件;仅允许清单内 skillName 和相对文件名。",
|
||||
"agc_install_skill_resource.description": "把审核通过的 AGC Skill 自带文件原样复制到当前项目相对路径;宿主直接读内置字节,不会像读取那样被大文件截断,也不需要原生 cp 或审批,一次一个文件。仍走 agc_write_file 的合同与租约门。",
|
||||
"agc_install_skill_resource.parameters.destinationPath": "当前项目根下的目标相对路径,例如 scripts/validate.mjs",
|
||||
"agc_write_file.description": "把文本写入当前 AGC 项目的相对路径,用于代码、配置、资源依赖或说明文件。",
|
||||
"agc_apply_patch.description": "使用官方 apply_patch 语法修改当前项目,支持 Add/Delete/Update/Move。固定当前项目为工作目录;一次最多64KiB UTF-8、256个操作,并受实际平台参数上限约束。完整检查全部源与目标后执行;失败可能已部分修改,先读取当前文件再提出新补丁。该工具可与独立的读取、生成和计划调用并行;同文件修改与依赖其结果的构建、检查须等待补丁回执。超时、取消或 needsReconciliation=true 时停止,不自动重放。",
|
||||
"agc_update_plan.description": "更新当前回合的进度计划,字段与 update_plan 相同:可选 explanation,以及 plan 中的 step/status(pending、in_progress、completed)。它可与其它独立工具并行;同一计划的连续更新按依赖顺序提交。计划完成只表示进度,不代替宿主交付验收。",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -245,6 +245,25 @@ fn direct_tools_mcp_specs_for_plugins(
|
||||
"additionalProperties": false
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"name": "agc_install_skill_resource",
|
||||
"description": prompt_text!("directTools.agc_install_skill_resource.description"),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"skillName": { "type": "string", "minLength": 1, "maxLength": 120 },
|
||||
"relativePath": { "type": "string", "minLength": 1, "maxLength": 240 },
|
||||
"destinationPath": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 512,
|
||||
"description": prompt_text!("directTools.agc_install_skill_resource.parameters.destinationPath")
|
||||
}
|
||||
},
|
||||
"required": ["skillName", "relativePath", "destinationPath"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"name": "agc_write_file",
|
||||
"description": prompt_text!("directTools.agc_write_file.description"),
|
||||
@@ -1039,11 +1058,7 @@ fn call_agc_read_skill_resource(arguments: &Value) -> Value {
|
||||
Ok(value) => value,
|
||||
Err(error) => return mcp_tool_result(error, Vec::new(), true),
|
||||
};
|
||||
if Path::new(&relative).is_absolute()
|
||||
|| relative.contains("..")
|
||||
|| relative.contains(':')
|
||||
|| relative.contains('\\')
|
||||
{
|
||||
if skill_resource_relative_path_is_unsafe(&relative) {
|
||||
return mcp_tool_result("Skill 资源路径不安全".to_string(), Vec::new(), true);
|
||||
}
|
||||
match read_agc_skill_resource(&format!("{skill}/{relative}")) {
|
||||
@@ -1052,6 +1067,40 @@ fn call_agc_read_skill_resource(arguments: &Value) -> Value {
|
||||
}
|
||||
}
|
||||
|
||||
/// Skill 资源相对路径的安全边界:`agc_read_skill_resource` 与
|
||||
/// `agc_install_skill_resource` 共用同一份判定。
|
||||
fn skill_resource_relative_path_is_unsafe(relative: &str) -> bool {
|
||||
Path::new(relative).is_absolute()
|
||||
|| relative.contains("..")
|
||||
|| relative.contains(':')
|
||||
|| relative.contains('\\')
|
||||
}
|
||||
|
||||
/// 组装 `agc_install_skill_resource` 的落盘请求:宿主侧直接读已审核的内置字节,
|
||||
/// 再复用 `agc_write_file` 的入参规则(项目相对路径 + 内容边界),写入仍走同一条合同 lease 门。
|
||||
///
|
||||
/// 正文不经过模型上下文,因此既不会被大文件截断,也不依赖原生 `cp` 及其审批。
|
||||
fn install_skill_resource_write_arguments(arguments: &Value) -> Result<Value, String> {
|
||||
validate_tool_object_fields(arguments, &["skillName", "relativePath", "destinationPath"])?;
|
||||
let skill = bounded_tool_string(arguments, "skillName", 120)?;
|
||||
let relative = bounded_tool_string(arguments, "relativePath", 240)?;
|
||||
if skill_resource_relative_path_is_unsafe(&relative) {
|
||||
return Err("Skill 资源路径不安全".to_string());
|
||||
}
|
||||
let destination = bounded_tool_string(arguments, "destinationPath", 512)?;
|
||||
let content = read_agc_skill_resource(&format!("{skill}/{relative}"))?;
|
||||
let write_arguments = json!({ "path": destination, "content": content });
|
||||
validate_write_file_arguments(&write_arguments)?;
|
||||
Ok(write_arguments)
|
||||
}
|
||||
|
||||
async fn call_agc_install_skill_resource(arguments: &Value) -> Value {
|
||||
match install_skill_resource_write_arguments(arguments) {
|
||||
Ok(write_arguments) => call_client_tool_bridge("agc_write_file", &write_arguments).await,
|
||||
Err(error) => mcp_tool_result(error, Vec::new(), true),
|
||||
}
|
||||
}
|
||||
|
||||
async fn call_agc_web_search_with_enabled(arguments: &Value, enabled: bool) -> Value {
|
||||
if !enabled {
|
||||
return mcp_tool_result("AGC 受控联网搜索未启用".to_string(), Vec::new(), true);
|
||||
@@ -1536,6 +1585,7 @@ async fn handle_direct_tools_mcp_request(root: &Path, request: Value) -> Option<
|
||||
.await
|
||||
}
|
||||
"agc_write_file" => call_agc_write_file(&arguments).await,
|
||||
"agc_install_skill_resource" => call_agc_install_skill_resource(&arguments).await,
|
||||
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
||||
"agc_cocos_execute" => call_agc_cocos_execute(&arguments).await,
|
||||
#[cfg(all(windows, target_arch = "x86_64", feature = "unity-editor-execute"))]
|
||||
@@ -2752,6 +2802,7 @@ mod tests {
|
||||
"conversation.list",
|
||||
"conversation.read",
|
||||
"agc_read_skill_resource",
|
||||
"agc_install_skill_resource",
|
||||
"agc_write_file",
|
||||
"taonier_prepare_game_art",
|
||||
"agc_generate_image",
|
||||
@@ -3259,6 +3310,66 @@ mod tests {
|
||||
assert_eq!(denied_windows_absolute["isError"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_skill_resource_rewrites_reviewed_bytes_into_one_write_arguments() {
|
||||
let write = install_skill_resource_write_arguments(&json!({
|
||||
"skillName": "agc-project-structure",
|
||||
"relativePath": "references/structure-contract.md",
|
||||
"destinationPath": "scripts/structure-contract.md"
|
||||
}))
|
||||
.expect("reviewed skill resource composes a write");
|
||||
assert_eq!(write["path"], "scripts/structure-contract.md");
|
||||
assert_eq!(
|
||||
write["content"],
|
||||
read_agc_skill_resource("agc-project-structure/references/structure-contract.md")
|
||||
.expect("reviewed bytes")
|
||||
);
|
||||
// 只暴露 agc_write_file 的两个入参,模型自带的正文没有旁路通道。
|
||||
assert_eq!(write.as_object().expect("object").len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_skill_resource_rejects_unreviewed_or_unsafe_targets() {
|
||||
for arguments in [
|
||||
// 未登记的资源
|
||||
json!({
|
||||
"skillName": "agc-project-structure",
|
||||
"relativePath": "references/not-in-manifest.md",
|
||||
"destinationPath": "scripts/x.md"
|
||||
}),
|
||||
// 资源路径穿越
|
||||
json!({
|
||||
"skillName": "agc-project-structure",
|
||||
"relativePath": "../../auth.json",
|
||||
"destinationPath": "scripts/x.md"
|
||||
}),
|
||||
// Windows 绝对路径
|
||||
json!({
|
||||
"skillName": "agc-project-structure",
|
||||
"relativePath": r"C:\temp\SKILL.md",
|
||||
"destinationPath": "scripts/x.md"
|
||||
}),
|
||||
// 目标路径越出项目根
|
||||
json!({
|
||||
"skillName": "agc-project-structure",
|
||||
"relativePath": "references/structure-contract.md",
|
||||
"destinationPath": "../outside.md"
|
||||
}),
|
||||
// 模型自带正文的旁路字段
|
||||
json!({
|
||||
"skillName": "agc-project-structure",
|
||||
"relativePath": "references/structure-contract.md",
|
||||
"destinationPath": "scripts/x.md",
|
||||
"content": "model-supplied"
|
||||
}),
|
||||
] {
|
||||
assert!(
|
||||
install_skill_resource_write_arguments(&arguments).is_err(),
|
||||
"{arguments} must be rejected before any bridge call"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_guides_are_available_through_the_existing_skill_resource_tool() {
|
||||
for (skill, engine) in [("agc-unity-editor", "Unity"), ("agc-godot-editor", "Godot")] {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# 决策记录
|
||||
|
||||
## 2026-10-06 新增 agc_install_skill_resource:Skill 附件由宿主直接落盘,不走模型正文
|
||||
|
||||
- 背景:`vite-export-xhs-minitool` 等审核 Skill 要把自带脚本(`scripts/validate.mjs` 约 40 KB、`pack.mjs`、`vite.config.*.mjs`)原样复制进用户项目。`agc_read_skill_resource` 的大文件正文会被模型上下文截断,重抄必然失真;原生 `cp` 在 DirectProject 只读沙箱下被审批闸门拒绝。两条路都走不通。
|
||||
- 决策:新增内置 MCP 工具 `agc_install_skill_resource`,入参 `skillName` / `relativePath` / `destinationPath`。宿主在 MCP 进程内直接读 `AGC_SKILL_PACK_FILES` 里已审核的内置字节(复用 `read_agc_skill_resource` 的清单校验与路径拒绝口径),组装成 `agc_write_file` 的 `{path, content}` 转发给客户端受控工具桥。写入因此走与 `agc_write_file` 相同的合同、lease、项目写锁与保护面校验;正文不经过模型上下文,没有截断,也不需要原生 `cp` 及其审批。
|
||||
- 边界:工具只暴露 `agc_write_file` 的两个入参,模型无法自带正文旁路审核;一次调用落一个文件,不打包、不改写、不建目录以外的副作用。DirectProject 的只读原生沙箱与 #439 审批口径不变,本工具是替代 `cp` 的受控通道,不是放开沙箱。
|
||||
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs`(工具目录、分发、`install_skill_resource_write_arguments`)、`prompts/runtime/texts/direct-tools.json`、`prompts/runtime/texts/direct.json`,权威说明同步到技术方案。
|
||||
- 验证:`cargo test --offline -p genarrative-ai-game-creator-shell --bin genarrative-ai-game-creator-shell agent::`(736 passed,含新增 `install_skill_resource_*` 两条与 `tool_catalog_preserves_reviewed_resource_contracts`)、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
## 2026-10-06 小红书导出 skill:dist 收尾归 Vite 插件,pack 只读打 zip
|
||||
|
||||
- 背景:`vite-export-xhs-minitool` 的 `pack.mjs` 之前会就地改写 dist(相对路径 / 去 `type="module"` / `crossorigin` / 脚本移到 body 末尾 / 清空目录 / 兜底复制 icon)再打 zip;dist 只有在跑完 pack 后才合规,`validate.mjs` 看到的是中间态,打包脚本也因此同时承担「整理产物」与「打 zip」两件事。
|
||||
|
||||
@@ -1522,7 +1522,7 @@ game-project/
|
||||
- 首页提供“做游戏 / 做方案”两个创作类型(`game` / `doc`),默认“做游戏”;“做素材”入口已退役,素材生成在项目内按实际工作流触发。每次首页提交自动创建一个新项目并进入项目工作台。用户正文原样进入项目对话,`game|doc` 作为受限结构化首轮上下文传给同一 Codex thread;持久草稿里遗留的 `art` 按 `game` 处理(`effectiveCreationType` 映射),不再产生第三种首轮上下文。
|
||||
- 「策划补全」是“做游戏”专属的提交前勾选(2026-09-15 起):勾选时该档提交 `planning`,未勾选时提交 `direct-build`;“做方案”始终走立项策划链路,与本勾选无关。复选框只在“做游戏”档渲染;切换创作类型时首页表面整体重挂,勾选状态随之清除,切回来必须是未勾选——这条可观察契约由 `appSurface` 的「scopes the 策划补全 option to the game entry」与「submits the game entry with planning when 策划补全 is checked」两条用例钉住,实现侧不额外维护重置逻辑。
|
||||
- 2026-09-23 清理了未注册的 DirectHome 用户对话命令及旧附件 sidecar 渲染链;首页仍先创建项目再进入 DirectProject,不恢复无项目对话。自动项目命名和提示润色仍调用内部 `direct_game_creator_home_codex_chat`,其 DirectHome 只读隔离通道与测试继续保留。附件作为 canonical `userItem.content` 中的 `agc_attachment_reference` 携带名称、媒体类型、大小、项目相对路径及状态,经现役 validation/wire 校验与投影;路径映射不等于灌入全文,也不按 GDD 特判。附件清洗与数量上限继续复用 `direct_codex_attachments.rs`,旧 sidecar DTO、header、专属 prompt key 和测试不再是保留合同。
|
||||
- `agc-skill-pack.v1` 包含完整游戏交付流程、项目结构、陶泥儿美术、Web 游戏实现、真实浏览器试玩、客户端资源投影、Unity/Godot 编辑器常用操作、平台抽象收口和小红书小工具导出十项审核 Skill。清单记录用途、触发条件、所需工具、版本和内容 SHA-256;审核文本按 UTF-8 读取并将 CRLF 规范为 LF 后计算指纹和安装,避免混合换行造成 Windows / Linux 构建结果漂移,语义内容变化时必须同步重算对应清单指纹并提升版本。同步统一运行 `npm run agc:skill-pack:sync`,只读校验由 AGC `typecheck` 和 release build 自动执行,发现漂移时直接列出 Skill 与实际摘要,不让失配内容进入构建产物。客户端把审核文件安装到隔离目录后通过 app-server `skills/extraRoots/set + skills/list` 注册并复核,完整正文由 Codex 原生 Skill 机制按意图加载,一层引用只能经 `agc_read_skill_resource` 读取清单内 Markdown。引用路径按平台无关规则拒绝反斜杠、盘符、UNC、绝对路径和 `..`,不能依赖当前宿主的 `std::path` 语义判断其它平台路径。Skill 目录里以 `.` 开头的文件和目录是本地开发辅助(例如 `vite-export-xhs-minitool/.selective_rule.txt`、`scripts/.pack.test.mjs`):不写进清单、不参与指纹、不安装,也不能经 `agc_read_skill_resource` 读取;JS 清单工具 `isHiddenSkillEntryName` 与 Rust `is_hidden_skill_entry_name` 共用同一拒绝口径,新增 Skill 时无需为它们补 `include_bytes!`。
|
||||
- `agc-skill-pack.v1` 包含完整游戏交付流程、项目结构、陶泥儿美术、Web 游戏实现、真实浏览器试玩、客户端资源投影、Unity/Godot 编辑器常用操作、平台抽象收口和小红书小工具导出十项审核 Skill。清单记录用途、触发条件、所需工具、版本和内容 SHA-256;审核文本按 UTF-8 读取并将 CRLF 规范为 LF 后计算指纹和安装,避免混合换行造成 Windows / Linux 构建结果漂移,语义内容变化时必须同步重算对应清单指纹并提升版本。同步统一运行 `npm run agc:skill-pack:sync`,只读校验由 AGC `typecheck` 和 release build 自动执行,发现漂移时直接列出 Skill 与实际摘要,不让失配内容进入构建产物。客户端把审核文件安装到隔离目录后通过 app-server `skills/extraRoots/set + skills/list` 注册并复核,完整正文由 Codex 原生 Skill 机制按意图加载,一层引用只能经 `agc_read_skill_resource` 读取清单内 Markdown;把 Skill 自带脚本或配置原样复制进项目时用 `agc_install_skill_resource`(skillName/relativePath/destinationPath),宿主直接读内置审核字节并按 `agc_write_file` 的入参规则与合同 lease 写盘,正文不经过模型上下文,避免大文件截断和原生 `cp` 审批。引用路径按平台无关规则拒绝反斜杠、盘符、UNC、绝对路径和 `..`,不能依赖当前宿主的 `std::path` 语义判断其它平台路径。Skill 目录里以 `.` 开头的文件和目录是本地开发辅助(例如 `vite-export-xhs-minitool/.selective_rule.txt`、`scripts/.pack.test.mjs`):不写进清单、不参与指纹、不安装,也不能经 `agc_read_skill_resource` 读取;JS 清单工具 `isHiddenSkillEntryName` 与 Rust `is_hidden_skill_entry_name` 共用同一拒绝口径,新增 Skill 时无需为它们补 `include_bytes!`。
|
||||
- DirectProject 连接客户端内置的 `agc_tools` STDIO MCP,并在启动时接入客户端扩展仓库中用户已启用的独立第三方 STDIO/HTTP MCP 配置。内置工具包括审核引用读取、图片生成、标准陶泥儿美术准备、已登记资源有界查询、视频 / 角色动画 / 音效 / BGM 的 create-or-derive 语义生成、已登记图片去背景、desktop/mobile 浏览器试玩和受控 `agc_web_search`。内置 MCP 进程负责协议;真实浏览器、付费平台调用与受控搜索通过随机 loopback 地址回到客户端主进程,GUI 登录态、开发者 Key、项目路径、revision、operation 与幂等键由客户端持有并隔离于模型上下文。内置与用户启用的第三方 MCP 工具沿用 DirectProject 自动批准方式;付费资源工具由客户端绑定稳定回合身份、串行执行并优先恢复匹配账本。`llm.webSearchEnabled` 控制 DirectProject 的 AGC 受控搜索工具暴露与执行。原生工具与审批权限以下方“DirectProject Codex 完整访问覆盖”为准。
|
||||
- 陶泥儿生成复用持久幂等账本、operation 恢复、来源/下载/PNG 解码和 manifest 登记;普通客户端使用当前 AGC 登录会话及账号路由,受控的 ExternalDeveloper 发布模式在客户端内部使用按服务器 origin 隔离的私有 Key。凭据失效、来源不明或结果未知时失败关闭,不能自动换 Key 或重新扣费。
|
||||
- 自定义 LLM API Key 路由在 DirectProject 及内部 DirectHome 辅助调用中经 loopback `/responses` 流式代理转发。代理使用请求自带的 Bearer,并剥离开发网关错误携带的 `X-Codex-*` ChatGPT 账户额度头,按实际 API Provider 响应判断请求结果。
|
||||
|
||||
Reference in New Issue
Block a user