修复跨平台原生壳产物校验
统一使用规范化临时目录规避 macOS 符号链接祖先误判 放宽生成恢复 HTTP 夹具的有界读取时间 补全 Mach-O 正反字节序及 fat 文件头白名单 同步桌面壳架构说明与项目排障记忆
This commit is contained in:
@@ -5708,7 +5708,7 @@ mod canvas_generation_tests {
|
||||
|
||||
fn read_test_http_request(stream: &mut std::net::TcpStream) -> String {
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_secs(2)))
|
||||
.set_read_timeout(Some(Duration::from_secs(10)))
|
||||
.expect("set request read timeout");
|
||||
let mut bytes = Vec::new();
|
||||
let mut buffer = [0_u8; 4096];
|
||||
|
||||
+3
-2
@@ -463,7 +463,8 @@ mod tests {
|
||||
root_source: &str,
|
||||
suffix: &str,
|
||||
) -> String {
|
||||
let temporary = tempfile::tempdir().expect("temporary project root");
|
||||
let temporary =
|
||||
crate::tests::canonical_test_tempdir(&format!("provider-role-overlay-{suffix}-"));
|
||||
let root = temporary.path().join("project");
|
||||
init_local_game_project_at(&root, &format!("overlay-{suffix}"), "role overlay test")
|
||||
.expect("project init");
|
||||
@@ -806,7 +807,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn planning_request_advertises_only_native_mcp_functions() {
|
||||
let directory = tempfile::tempdir().expect("temp project directory");
|
||||
let directory = crate::tests::canonical_test_tempdir("native-mcp-prompt-");
|
||||
let root = directory.path().join("project");
|
||||
init_local_game_project_at(&root, "project-mcp", "MCP 原生函数说明测试")
|
||||
.expect("project init");
|
||||
|
||||
@@ -1332,7 +1332,7 @@ mod pending_recovery_tests {
|
||||
|
||||
#[test]
|
||||
fn observed_unknown_canvas_generation_returns_to_same_approved_action() {
|
||||
let temporary = tempfile::tempdir().expect("create prepared pending project");
|
||||
let temporary = crate::tests::canonical_test_tempdir("prepared-pending-");
|
||||
let root = temporary.path();
|
||||
init_local_game_project_at(root, "prepared-pending", "原俄罗斯方块项目")
|
||||
.expect("init prepared pending project");
|
||||
@@ -1413,7 +1413,7 @@ mod pending_recovery_tests {
|
||||
|
||||
#[test]
|
||||
fn legacy_executing_canvas_generation_returns_to_same_approved_action() {
|
||||
let temporary = tempfile::tempdir().expect("create executing prepared project");
|
||||
let temporary = crate::tests::canonical_test_tempdir("executing-prepared-");
|
||||
let root = temporary.path();
|
||||
init_local_game_project_at(root, "executing-prepared", "旧版俄罗斯方块项目")
|
||||
.expect("init executing prepared project");
|
||||
@@ -1490,7 +1490,7 @@ mod pending_recovery_tests {
|
||||
|
||||
#[test]
|
||||
fn observed_postprocessing_failure_resumes_from_accepted_generation() {
|
||||
let temporary = tempfile::tempdir().expect("create accepted recovery project");
|
||||
let temporary = crate::tests::canonical_test_tempdir("accepted-recovery-");
|
||||
let root = temporary.path();
|
||||
init_local_game_project_at(root, "accepted-recovery", "俄罗斯方块素材后处理恢复")
|
||||
.expect("init accepted recovery project");
|
||||
@@ -1560,7 +1560,7 @@ mod pending_recovery_tests {
|
||||
|
||||
#[test]
|
||||
fn canvas_reconciliation_keeps_the_context_plan_step_active() {
|
||||
let temporary = tempfile::tempdir().expect("create reconciliation context project");
|
||||
let temporary = crate::tests::canonical_test_tempdir("reconciliation-context-");
|
||||
let root = temporary.path();
|
||||
init_local_game_project_at(root, "reconciliation-context", "俄罗斯方块恢复上下文")
|
||||
.expect("init reconciliation context project");
|
||||
|
||||
+2
-2
@@ -22,7 +22,7 @@ fn autonomous_fixture_with_source(
|
||||
AgentRuntimeState,
|
||||
AgentRuntimeAutonomousCompletionContract,
|
||||
) {
|
||||
let temporary = tempfile::tempdir().expect("create autonomous fixture root");
|
||||
let temporary = crate::tests::canonical_test_tempdir("autonomous-fixture-");
|
||||
let root = temporary.path().join("project");
|
||||
init_local_game_project_at(&root, "autonomous-project", task).expect("init project");
|
||||
let session_id = resolve_agent_conversation_session_id_at(
|
||||
@@ -223,7 +223,7 @@ fn autonomous_fixture_with_setup(
|
||||
AgentRuntimeState,
|
||||
AgentRuntimeAutonomousCompletionContract,
|
||||
) {
|
||||
let temporary = tempfile::tempdir().expect("create autonomous fixture root");
|
||||
let temporary = crate::tests::canonical_test_tempdir("autonomous-setup-fixture-");
|
||||
let root = temporary.path().join("project");
|
||||
init_local_game_project_at(&root, "autonomous-project", task).expect("init project");
|
||||
setup(&root);
|
||||
|
||||
@@ -733,7 +733,14 @@ for (const snippet of [
|
||||
'header[3] === 0x46',
|
||||
'(stat.mode & 0o111) === 0',
|
||||
'header.readUInt32BE(0)',
|
||||
'machMagic === 0xcafebabe',
|
||||
'machMagic === 0xbebafeca',
|
||||
'machMagic === 0xcafebabf',
|
||||
'machMagic === 0xbfbafeca',
|
||||
'machMagic === 0xfeedface',
|
||||
'machMagic === 0xcefaedfe',
|
||||
'machMagic === 0xfeedfacf',
|
||||
'machMagic === 0xcffaedfe',
|
||||
'header[0] !== 0x4d',
|
||||
'header[1] !== 0x5a',
|
||||
"console.log('[check:native-shells] desktop-release-binary-artifact')",
|
||||
@@ -755,6 +762,15 @@ for (const snippet of [
|
||||
"'desktop'",
|
||||
'fs.copyFileSync(sourcePath, stagedPath)',
|
||||
'fs.chmodSync(stagedPath, sourceMode & 0o777)',
|
||||
'header.readUInt32BE(0)',
|
||||
'machMagic === 0xcafebabe',
|
||||
'machMagic === 0xbebafeca',
|
||||
'machMagic === 0xcafebabf',
|
||||
'machMagic === 0xbfbafeca',
|
||||
'machMagic === 0xfeedface',
|
||||
'machMagic === 0xcefaedfe',
|
||||
'machMagic === 0xfeedfacf',
|
||||
'machMagic === 0xcffaedfe',
|
||||
"console.log(`[desktop-shell:stage-release-binary] ${stagedPath}`)",
|
||||
]) {
|
||||
if (!stageReleaseBinarySource.includes(snippet)) {
|
||||
|
||||
@@ -49,9 +49,13 @@ function assertExecutable(filePath, label) {
|
||||
const machMagic = header.readUInt32BE(0);
|
||||
const isMachO =
|
||||
machMagic === 0xcafebabe ||
|
||||
machMagic === 0xcafed00d ||
|
||||
machMagic === 0xbebafeca ||
|
||||
machMagic === 0xcafebabf ||
|
||||
machMagic === 0xbfbafeca ||
|
||||
machMagic === 0xfeedface ||
|
||||
machMagic === 0xfeedfacf;
|
||||
machMagic === 0xcefaedfe ||
|
||||
machMagic === 0xfeedfacf ||
|
||||
machMagic === 0xcffaedfe;
|
||||
if (!isMachO || (stat.mode & 0o111) === 0) {
|
||||
throw new Error(`${label} must be an executable Mach-O file`);
|
||||
}
|
||||
|
||||
@@ -4144,3 +4144,17 @@
|
||||
- 风险:Provider 工具约束不是本地安全边界;特别是 `writes + readOnlyHint=true` 自动放行的工具,schema 外字段可能改变外部副作用而不进入预期确认路径。
|
||||
- 处理:使用完整 JSON Schema validator 校验原始 catalog schema,不手写 required/type 子集;native parser、fingerprint enrichment 与实际 MCP 调用边界复用同一校验器。enrichment 错误必须映射回 classified `arguments-schema` repair,不能以普通字符串直接终止 run;执行点重验用于阻断升级前已经落盘的 schema 外 pending。关闭网络和文件 `$ref` 解析,schema 无法安全编译时不广告或不执行。`serde` 类型错误会包含实际字符串值,catalog miss 也会包含模型提交的 server/tool,因此这两类错误同样只能返回稳定类别,不能拼接原始错误、参数值或 schema 内容。
|
||||
- 验证:覆盖 required、additionalProperties、type、enum、本地 `$defs/$ref`、HTTP/file 外部引用、无效 schema、错误脱敏,证明 legacy wrapper 在注入 fingerprint 前进入 repair,并证明带旧有效 fingerprint 的历史 pending 在实际调用前仍被 schema 拒绝。
|
||||
|
||||
## macOS 安全路径测试必须使用规范化临时目录(2026-08-05)
|
||||
|
||||
- 现象:调用仓库上下文、Runtime context bundle 或 pending recovery 的 Rust 测试在 macOS 报“Repository root and its ancestors must not be symbolic links”,Linux CI 却可能通过;本地 HTTP 恢复夹具在完整串行测试中还可能偶发 `WouldBlock`。
|
||||
- 原因:`tempfile::tempdir()` 默认返回 `/var/folders/...`,而 macOS 的 `/var` 是指向 `/private/var` 的符号链接,生产安全校验会按设计拒绝该祖先;恢复测试的服务端读超时若仅为 2 秒,也会与完整测试负载下约 2 秒的首次请求形成窄竞态。
|
||||
- 处理:凡测试会进入仓库可信路径校验,统一使用 `crate::tests::canonical_test_tempdir(...)`,不得削弱生产符号链接拒绝规则;loopback 夹具保留有界超时,但为完整 CI 负载留足稳定裕量。
|
||||
- 验证:在 macOS 上定向运行 provider request、pending recovery、autonomous continuation 与 generation recovery 用例,再运行完整 `npm run check:native-shells`。
|
||||
|
||||
## Mach-O 文件头校验必须覆盖反字节序魔数(2026-08-05)
|
||||
|
||||
- 现象:macOS arm64 的 Tauri release 已成功构建且 `file` 明确认定为 Mach-O,产物 staging 仍报“must be an executable Mach-O file”。
|
||||
- 原因:脚本用 `Buffer.readUInt32BE(0)` 读取文件头,却只比较 `0xfeedfacf` 等正序数值;arm64 常见头字节是 `cf fa ed fe`,读取结果为 `0xcffaedfe`。
|
||||
- 处理:文件头白名单同时覆盖 32/64 位与 fat Mach-O 的正序和反字节序合法魔数,并由桌面配置门禁同时反查 staging 脚本和根级产物检查,不能改成只按扩展名或构建退出码判断。
|
||||
- 验证:在 macOS 上构建真实 desktop-shell release,运行 `npm run desktop-shell:stage-release-binary`,再由 `npm run check:native-shells` 校验 staged 产物。
|
||||
|
||||
@@ -524,7 +524,7 @@ GameBridge 禁止:
|
||||
|
||||
2026-06-19 追加:桌面壳 macOS 媒体权限说明进入门禁。Tauri 桌面壳仍不新增摄像头或麦克风 HostBridge method,不把系统媒体能力暴露成桌面命令;同源 H5 页面可继续使用浏览器标准 `getUserMedia` 承接儿童动作热身 Demo 的实时摄像头输入和汪汪声浪正式 runtime 的实时麦克风输入。macOS 分发包必须通过 `bundle.macOS.infoPlist="Info.plist"` 合并受控用途说明:`NSCameraUsageDescription` 只描述同源 H5 实时动作输入,`NSMicrophoneUsageDescription` 只描述同源 H5 实时声音玩法。`apps/desktop-shell/scripts/check-config.mjs` 会校验 plist 路径和两条文案,并把 `Info.plist` 纳入生产壳替身词扫描,防止桌面包缺少系统授权说明、把媒体权限扩写成通用采集能力,或在 macOS 分发配置里留下临时替身文本。
|
||||
|
||||
2026-06-18 追加:桌面壳 release 构建烟测进入统一验收。`npm run check:native-shells` 会在 H5 HostBridge、Expo 壳和 Tauri 单测通过后执行 `npm run desktop-shell:build -- --no-bundle`,确认 Tauri release 入口指向共享公开主站、受控命令白名单、图标和 Rust release 编译可以共同产出桌面二进制;构建后 `desktop-shell:stage-release-binary` 会把当前平台二进制复制到根目录 `build/native/desktop/genarrative-desktop-shell` 或 `build/native/desktop/genarrative-desktop-shell.exe`,该目录沿用根 `build/` 的 gitignore,只作为本机或 CI 可收集产物目录。统一验收必须检查 staged 二进制存在、非空且符合当前平台可执行文件头。`apps/desktop-shell/scripts/check-config.mjs` 会反查根级门禁仍保留 release build smoke、staging 步骤、二进制路径、Linux ELF / macOS Mach-O / Windows PE 文件头和可执行位检查,避免桌面产物验收被改成只看命令退出码。该烟测不生成平台安装包,避免把 Linux 本机缺少的系统打包器误判为 HostBridge 回归。
|
||||
2026-06-18 追加:桌面壳 release 构建烟测进入统一验收。`npm run check:native-shells` 会在 H5 HostBridge、Expo 壳和 Tauri 单测通过后执行 `npm run desktop-shell:build -- --no-bundle`,确认 Tauri release 入口指向共享公开主站、受控命令白名单、图标和 Rust release 编译可以共同产出桌面二进制;构建后 `desktop-shell:stage-release-binary` 会把当前平台二进制复制到根目录 `build/native/desktop/genarrative-desktop-shell` 或 `build/native/desktop/genarrative-desktop-shell.exe`,该目录沿用根 `build/` 的 gitignore,只作为本机或 CI 可收集产物目录。统一验收必须检查 staged 二进制存在、非空且符合当前平台可执行文件头;macOS 校验同时接受 32/64 位与 fat Mach-O 的大端、反字节序合法魔数,不得把 arm64 常见的 `cf fa ed fe` 文件头误拒绝。`apps/desktop-shell/scripts/check-config.mjs` 会反查根级门禁仍保留 release build smoke、staging 步骤、二进制路径、Linux ELF / macOS Mach-O / Windows PE 文件头和可执行位检查,避免桌面产物验收被改成只看命令退出码。该烟测不生成平台安装包,避免把 Linux 本机缺少的系统打包器误判为 HostBridge 回归。
|
||||
|
||||
2026-06-18 追加:移动壳 Expo managed config 烟测进入统一验收。`npm run check:native-shells` 会执行 `npm run mobile-shell:config`,在 `apps/mobile-shell` 目录内调用 `expo config --type public --json`,校验 Expo CLI 实际解析结果中的包名、scheme、深链、ATS / cleartext / backup / 相机与麦克风权限、启动页、adaptive icon、插件配置和 HostBridge 版本没有漂移。`apps/mobile-shell/scripts/check-config.mjs` 会反查根级门禁仍保留 EAS build profile、Expo config 和 Metro export 三个移动分发烟测,避免移动壳验收退回到只看源码类型检查。
|
||||
|
||||
|
||||
@@ -4915,9 +4915,13 @@ function assertDesktopReleaseBinaryArtifact() {
|
||||
const machMagic = header.readUInt32BE(0);
|
||||
const isMachO =
|
||||
machMagic === 0xcafebabe ||
|
||||
machMagic === 0xcafed00d ||
|
||||
machMagic === 0xbebafeca ||
|
||||
machMagic === 0xcafebabf ||
|
||||
machMagic === 0xbfbafeca ||
|
||||
machMagic === 0xfeedface ||
|
||||
machMagic === 0xfeedfacf;
|
||||
machMagic === 0xcefaedfe ||
|
||||
machMagic === 0xfeedfacf ||
|
||||
machMagic === 0xcffaedfe;
|
||||
if (!isMachO || (stat.mode & 0o111) === 0) {
|
||||
throw new Error(
|
||||
'desktop macOS release binary must be an executable Mach-O file',
|
||||
|
||||
Reference in New Issue
Block a user