Compare commits
259 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 15c633986d | |||
| 5cb3801ba4 | |||
| 6dbc4a39cb | |||
| e5df8f11b8 | |||
| 2eff7d74c6 | |||
| bd4dcfeacf | |||
| 5e1e5a2e5f | |||
| 35f3a9cf8a | |||
| a0325d653e | |||
| b617873f46 | |||
| 513c25898c | |||
| 9856bb7057 | |||
| 16db7ae8aa | |||
| 0ea7c7745c | |||
| 6baf358bc7 | |||
| 462fc856aa | |||
| 274bb1a3d2 | |||
| 8a96190db8 | |||
| 584da5b76b | |||
| d1a03aec0e | |||
| b90d12c5fb | |||
| 8135b4c374 | |||
| 124d94de93 | |||
| 103fe6fcd6 | |||
| 1370094c52 | |||
| be8155c4a8 | |||
| 9052ff14f8 | |||
| 9d9f1b8164 | |||
| 8629a228a1 | |||
| a92121e09d | |||
| e49d6741ca | |||
| f7666339fe | |||
| 891cf50c0e | |||
| b15a95e774 | |||
| e0f743b608 | |||
| a19684a5f5 | |||
| 46b0871e6d | |||
| 5c40abce47 | |||
| 47d09d51ef | |||
| f05f6e49ab | |||
| c7fe531b16 | |||
| d44c6ab62c | |||
| 6764950aec | |||
| 0944667c37 | |||
| bb2f080d3b | |||
| 413707da33 | |||
| 6235fa3298 | |||
| 8a9d37d5ad | |||
| 9bbfc56cd7 | |||
| e9d0180054 | |||
| 664bdd6877 | |||
| 39a64a864a | |||
| f055961714 | |||
| 3bcabe36fa | |||
| 2bfc870854 | |||
| b0e9741e81 | |||
| b52a33cc28 | |||
| 398edd6e86 | |||
| 9788087271 | |||
| 329de5fce9 | |||
| e4d70fbe1b | |||
| aaa9d3fb6f | |||
| bd127846f7 | |||
| b1751784ba | |||
| 379611d552 | |||
| ef04a19d5a | |||
| 206b4830a1 | |||
| 4b8e268816 | |||
| 7e7d6f1563 | |||
| a4b1d81123 | |||
| 983b15c3a7 | |||
| cdb6fcfe97 | |||
| d8a4e35232 | |||
| 0726106818 | |||
| 8beade5b4d | |||
| 46984a4945 | |||
| a8a30ecbc8 | |||
| 48e669d167 | |||
| 23fdc005fe | |||
| b8a4523a4f | |||
| 9c04561419 | |||
| 551fb6609a | |||
| d05440dcca | |||
| 7edf4a8f8b | |||
| afc9ecbd51 | |||
| fc2f4b71ce | |||
| 545f64cf9e | |||
| 2a50614c11 | |||
| 915006c98f | |||
| 69ce232a74 | |||
| 319aa69d6e | |||
| bec468bdc9 | |||
| eada20c199 | |||
| e8cf773aac | |||
| 12fbb61fad | |||
| 478e08fb92 | |||
| 0e67fbf80a | |||
| ed2cfbad36 | |||
| 560422286b | |||
| e04075fd95 | |||
| d7776f2a17 | |||
| 4d9e69841e | |||
| 49e88bbd9f | |||
| 86c268eca3 | |||
| c86221e3da | |||
| e7af7cf4e4 | |||
| e7cd008d7c | |||
| c743ca146e | |||
| 8c8b6cc75a | |||
| 4cf3642b53 | |||
| f933db169e | |||
| a5e936dd27 | |||
| 53008959c5 | |||
| 57dbb3de52 | |||
| e189aff485 | |||
| 248c094da9 | |||
| e66eb2fb55 | |||
| d455dc18a9 | |||
| 04a0d21351 | |||
| a9358bf3ce | |||
| c3de2ce958 | |||
| 287d2f94da | |||
| 1343f487d0 | |||
| 73fb89a8f4 | |||
| bf2ffd2bf9 | |||
| 3f55f043a1 | |||
| 26677b193c | |||
| b8691add3a | |||
| bfe010a192 | |||
| 6db6de5b99 | |||
| 87a7294a04 | |||
| 3774d7c1e1 | |||
| 8d15125f12 | |||
| cf68b8212d | |||
| 8d7ccc0d75 | |||
| 2af3d9a965 | |||
| 4749339bad | |||
| 3e85c71060 | |||
| da19bde99a | |||
| 4a32ee37c4 | |||
| 31fd1da495 | |||
| 4c88e5e924 | |||
| 99f4961c5f | |||
| 0cebc39b6a | |||
| 842d080a75 | |||
| 8cd6e34af0 | |||
| b357d36262 | |||
| 48c5638893 | |||
| 46df12c086 | |||
| 4bdbab29c0 | |||
| 9acc23b848 | |||
| b9b4ebd38e | |||
| 8f680ef376 | |||
| 7c7d55dcab | |||
| 1303592a86 | |||
| 1db7597141 | |||
| 13701d0144 | |||
| 5c83142fec | |||
| 95ed49f264 | |||
| b8d8072267 | |||
| 462caa33c5 | |||
| 4e1064266f | |||
| a50cbfb6c8 | |||
| 1eb4bd5ec0 | |||
| 5b441d389c | |||
| f200cb4d3c | |||
| ae2f5fc80b | |||
| b69bd88e5e | |||
| c5c7339c4a | |||
| 4bbc77fc32 | |||
| 8c521ca891 | |||
| 69a0804c03 | |||
| 74f726e65d | |||
| 1509235c1b | |||
| 1c992c69fe | |||
| d9b153ed78 | |||
| 30c469e9a1 | |||
| 51f629b2f9 | |||
| d6c720ecdd | |||
| 086aa91d70 | |||
| 3733e86272 | |||
| 5d0f3e6be6 | |||
| 989440173f | |||
| 22d9958d18 | |||
| 56f8c9a6a1 | |||
| c84df3d88f | |||
| 761d32cebf | |||
| b3d6543d73 | |||
| 8cd7c43b9e | |||
| 7fb7d246c0 | |||
| 5844afa14d | |||
| 615d1a28cd | |||
| 3e1e172f9c | |||
| 53a919e39b | |||
| d4b5f9fc14 | |||
| 6e4f9a6f1f | |||
| 8a0dcf8be7 | |||
| aa2a78f7e0 | |||
| 7aa96e8438 | |||
| feb6bc0ad4 | |||
| f349350d60 | |||
| df3a172bc9 | |||
| 9b4924c73e | |||
| d30bc462c5 | |||
| 26b9ad7fad | |||
| bc5398894e | |||
| 11d4d3d930 | |||
| 418d5732de | |||
| 592b6b4ee7 | |||
| 99c19e3ed9 | |||
| 15e30acb0f | |||
| 56fff8325b | |||
| 884c5ec5ee | |||
| 207b0820dd | |||
| 19f3d62d78 | |||
| 3c1e7bd599 | |||
| dda6b3946a | |||
| aff40f794d | |||
| 187faa607d | |||
| 109cc81740 | |||
| f796aef7e2 | |||
| 4cc7f4dd64 | |||
| b2d4690f94 | |||
| 0400103a19 | |||
| c049d68b8e | |||
| acfcf0c158 | |||
| 5bbedd9a5c | |||
| 40316d82c4 | |||
| 6c77de4a36 | |||
| b3693765ef | |||
| f7edc9d0ee | |||
| f60771dd00 | |||
| f7cccd8592 | |||
| 5ee2d940c1 | |||
| c75a7725ce | |||
| 3a95715990 | |||
| ed86963de2 | |||
| 65e73eee02 | |||
| 6068eeda83 | |||
| 0e244c64ea | |||
| ad7c059603 | |||
| 55d2a393d1 | |||
| e7f66ce950 | |||
| 35dcfea3ef | |||
| cea8bf77fa | |||
| 0136bfe564 | |||
| 22785b4d7b | |||
| a86a2ee4d2 | |||
| b22eae2726 | |||
| 35d0db6377 | |||
| f087ba3e2b | |||
| 7fc1b841be | |||
| a576fdcc37 | |||
| 4dcf38715b | |||
| d1f9f4e1ef | |||
| 5836aaec26 | |||
| 18d60feb04 | |||
| 4e1316e272 | |||
| 302b4addae |
+87
-202
@@ -27,26 +27,9 @@ env:
|
||||
RUSTC_WRAPPER: ''
|
||||
CARGO_BUILD_RUSTC_WRAPPER: ''
|
||||
|
||||
# job 声明顺序就是 runner 领取顺序,因此把最长尾的客户端 Rust 门禁排在前面,
|
||||
# 让它在最少的等待下占用并发槽位;其余 job 按时长递减排列。
|
||||
#
|
||||
# 客户端(微信壳 / Expo 移动壳 / Tauri 桌面壳 / AI 游戏创作壳)门禁原先全部串在
|
||||
# `Native shell tests` 一个 job 里,实测 18 分 37 秒。现在按门禁组拆成三个 job:
|
||||
# `Native shell tests`(契约 + H5 / 微信 / 移动 / 桌面壳门禁 + 发布构建 smoke)、
|
||||
# `AI game creator shell web tests`(typecheck + 壳内测试)与
|
||||
# `AI game creator shell Rust tests`(AGC 壳 bin 单测分片并行 + agent-run smoke),
|
||||
# 再把 AGC 壳依赖的共享 / 平台 crate 测试拆成 `AI game creator shell Rust crates`。
|
||||
# 各自的命令与拆分前逐一对应,本地 `npm run check:native-shells` 仍是同一条串行序列。
|
||||
#
|
||||
# AGC 壳的 bin 单测(2466 条)按名单分 4 片、每片一个进程并行执行,片内保持
|
||||
# `--test-threads=1`:当年线程并行会互相干扰的是进程内后台锁与异步终态,进程分片
|
||||
# 天然隔离,每片另有独立 TMPDIR,因此不必再让整套用例串成一小时级的尾巴。
|
||||
jobs:
|
||||
# 客户端 AGC 壳自身的 Rust 门禁:bin target 单测按名单分 4 片并行(片内仍保持
|
||||
# `--test-threads=1`)加 agent-run smoke。这里不装 npm 依赖:壳 Rust 门禁与 smoke
|
||||
# 只用 cargo 与 node 内建模块,也只需要 AGC 壳自己那份锁定依赖。
|
||||
ai-game-creator-shell-rust-tests:
|
||||
name: AI game creator shell Rust tests
|
||||
repository-checks:
|
||||
name: Repository checks
|
||||
runs-on: genarrative-ci
|
||||
steps:
|
||||
- name: Checkout full history from Gitea
|
||||
@@ -58,94 +41,76 @@ jobs:
|
||||
- name: Validate preinstalled CI job image and sandbox
|
||||
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
||||
|
||||
- name: Prepare AI game creator shell Rust dependencies
|
||||
- name: Resolve comparison base
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# AGC 壳有独立 Cargo.lock,其 path 依赖已含 platform-llm、platform-agent、
|
||||
# agent-runtime-core 与 shared-contracts,因此只锁这一份 manifest 就能覆盖壳测试
|
||||
# 与 smoke 的全部第三方依赖;server-rs 那次预热归 crate 级 job,不在这里重复。
|
||||
for attempt in $(seq 1 5); do
|
||||
if cargo fetch --locked \
|
||||
--target x86_64-unknown-linux-gnu \
|
||||
--manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml; then
|
||||
break
|
||||
fi
|
||||
if [[ "${attempt}" -eq 5 ]]; then
|
||||
echo 'AI game creator shell Cargo dependency fetch failed after 5 attempts.' >&2
|
||||
base_ref="$(node -e '
|
||||
const fs = require("node:fs");
|
||||
const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8"));
|
||||
process.stdout.write(event.pull_request?.base?.sha ?? event.before ?? "");
|
||||
')"
|
||||
if [[ -n "${base_ref}" && ! "${base_ref}" =~ ^0+$ ]]; then
|
||||
git cat-file -e "${base_ref}^{commit}" 2>/dev/null || {
|
||||
echo "comparison base commit is unavailable: ${base_ref}" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep $((attempt * 2))
|
||||
done
|
||||
}
|
||||
else
|
||||
base_ref="$(git merge-base HEAD origin/master 2>/dev/null || git rev-parse HEAD)"
|
||||
fi
|
||||
resolved_base_ref="$(git rev-parse --verify "${base_ref}^{commit}" 2>/dev/null || true)"
|
||||
head_ref="$(git rev-parse HEAD)"
|
||||
if [[ "${resolved_base_ref}" == "${head_ref}" ]]; then
|
||||
resolved_base_ref="$(git rev-parse --verify HEAD^ 2>/dev/null || true)"
|
||||
fi
|
||||
if [[ -z "${resolved_base_ref}" ]]; then
|
||||
echo 'comparison base must resolve to a commit distinct from HEAD.' >&2
|
||||
exit 1
|
||||
fi
|
||||
base_ref="${resolved_base_ref}"
|
||||
if [[ "${GITHUB_EVENT_NAME:-}" == 'pull_request' ]] \
|
||||
&& ! git merge-base --is-ancestor "${base_ref}" HEAD; then
|
||||
echo 'pull request head does not contain the latest base commit; update the branch and rerun CI.' >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "SPACETIME_SCHEMA_BASE_REF=${base_ref}" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Run AI game creator shell Rust gates
|
||||
run: npm run check:native-shells:agc-rust-shell
|
||||
- name: Install npm dependencies
|
||||
run: bash scripts/ci-npm-ci-with-retry.sh
|
||||
|
||||
# AGC 壳依赖的共享 / 平台 crate 测试用的是 server-rs workspace 与两个无锁独立 crate
|
||||
# 的 manifest,属另一套依赖图,因此单独一个 job 预热、单独跑。
|
||||
ai-game-creator-shell-rust-crates:
|
||||
name: AI game creator shell Rust crates
|
||||
- name: Run repository checks
|
||||
run: npm run check:repository-ci
|
||||
|
||||
frontend-tests:
|
||||
name: Frontend tests
|
||||
runs-on: genarrative-ci
|
||||
steps:
|
||||
- name: Checkout full history from Gitea
|
||||
- name: Checkout source from Gitea
|
||||
env:
|
||||
GENARRATIVE_GITEA_FETCH_DEPTH: '0'
|
||||
GENARRATIVE_GITEA_FETCH_DEPTH: '1'
|
||||
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
|
||||
run: genarrative-gitea-checkout
|
||||
|
||||
- name: Validate preinstalled CI job image and sandbox
|
||||
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
||||
|
||||
- name: Prepare server-rs Rust dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for attempt in $(seq 1 5); do
|
||||
if cargo fetch --locked \
|
||||
--target x86_64-unknown-linux-gnu \
|
||||
--manifest-path server-rs/Cargo.toml; then
|
||||
break
|
||||
fi
|
||||
if [[ "${attempt}" -eq 5 ]]; then
|
||||
echo 'server-rs Cargo dependency fetch failed after 5 attempts.' >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep $((attempt * 2))
|
||||
done
|
||||
- name: Install npm dependencies
|
||||
run: bash scripts/ci-npm-ci-with-retry.sh
|
||||
|
||||
- name: Prepare standalone Rust crate dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# agent-runtime-core / agent-runtime-orchestration 被 server-rs/Cargo.toml 的
|
||||
# exclude 排除,不参与上面的 workspace 锁文件,因此上面那次锁定 fetch 覆盖不到它们;
|
||||
# 而 `npm run ai-game-creator-shell:check:rust:crates` 会用
|
||||
# `cargo test --manifest-path` 单独跑这两个 crate。不在这里预热的话,这两条测试
|
||||
# 会在测试阶段自己 `Updating crates.io index`,crates.io 一抖动整条 job 就红
|
||||
# (见 #327 / PR #316 run 1950)。
|
||||
# 两个 crate 都没有提交 Cargo.lock,所以这里只能做不带锁标志的 fetch:
|
||||
# 加锁标志会因为缺少锁文件直接失败。生成的 Cargo.lock 落在两个 crate 目录内,
|
||||
# 已被各自的 .gitignore 忽略,只留在容器里;随后的测试阶段因此能用锁定版本
|
||||
# 解析,不再触碰 registry index。
|
||||
for manifest_path in \
|
||||
server-rs/crates/agent-runtime-core/Cargo.toml \
|
||||
server-rs/crates/agent-runtime-orchestration/Cargo.toml; do
|
||||
for attempt in $(seq 1 5); do
|
||||
if cargo fetch \
|
||||
--target x86_64-unknown-linux-gnu \
|
||||
--manifest-path "${manifest_path}"; then
|
||||
break
|
||||
fi
|
||||
if [[ "${attempt}" -eq 5 ]]; then
|
||||
echo "standalone crate dependency fetch failed after 5 attempts: ${manifest_path}" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep $((attempt * 2))
|
||||
done
|
||||
done
|
||||
- name: Run frontend and script tests
|
||||
run: npm run test
|
||||
|
||||
- name: Run AI game creator shell shared crate gates
|
||||
run: npm run check:native-shells:agc-rust-crates
|
||||
- name: Run BgFilter worker smoke harness tests
|
||||
run: npm run bgfilter-worker:smoke-test
|
||||
|
||||
- name: Validate production health patrol behavior
|
||||
run: npm run check:production-health-patrol
|
||||
|
||||
- name: Validate production API release behavior
|
||||
run: npm run check:production-api-release
|
||||
|
||||
- name: Validate production API deploy behavior
|
||||
run: npm run check:production-api-deploy
|
||||
|
||||
backend-tests:
|
||||
name: Backend tests
|
||||
@@ -229,8 +194,6 @@ jobs:
|
||||
- name: Check SpacetimeDB module
|
||||
run: cargo check --locked -p spacetime-module --manifest-path server-rs/Cargo.toml
|
||||
|
||||
# 客户端的壳级与契约门禁:静态契约断言、H5 / 微信 / 移动 / 桌面壳运行时门禁,
|
||||
# 以及依赖发布产物的构建 smoke。
|
||||
native-shell-tests:
|
||||
name: Native shell tests
|
||||
runs-on: genarrative-ci
|
||||
@@ -252,6 +215,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for manifest_path in \
|
||||
server-rs/Cargo.toml \
|
||||
apps/desktop-shell/src-tauri/Cargo.toml \
|
||||
apps/ai-game-creator-shell/src-tauri/Cargo.toml; do
|
||||
for attempt in $(seq 1 5); do
|
||||
@@ -268,118 +232,39 @@ jobs:
|
||||
done
|
||||
done
|
||||
|
||||
- name: Run native shell contract gates
|
||||
run: npm run check:native-shells:contract
|
||||
|
||||
- name: Run native shell gates
|
||||
run: npm run check:native-shells:shells
|
||||
|
||||
- name: Run native shell release build smoke
|
||||
run: npm run check:native-shells:release
|
||||
|
||||
- name: Ensure native lockfiles are unchanged
|
||||
run: git diff --exit-code -- apps/desktop-shell/src-tauri/Cargo.lock apps/ai-game-creator-shell/src-tauri/Cargo.lock
|
||||
|
||||
frontend-tests:
|
||||
name: Frontend tests
|
||||
runs-on: genarrative-ci
|
||||
steps:
|
||||
- name: Checkout source from Gitea
|
||||
env:
|
||||
GENARRATIVE_GITEA_FETCH_DEPTH: '1'
|
||||
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
|
||||
run: genarrative-gitea-checkout
|
||||
|
||||
- name: Validate preinstalled CI job image and sandbox
|
||||
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: bash scripts/ci-npm-ci-with-retry.sh
|
||||
|
||||
- name: Run frontend and script tests
|
||||
run: npm run test
|
||||
|
||||
- name: Run BgFilter worker smoke harness tests
|
||||
run: npm run bgfilter-worker:smoke-test
|
||||
|
||||
- name: Validate production health patrol behavior
|
||||
run: npm run check:production-health-patrol
|
||||
|
||||
- name: Validate production API release behavior
|
||||
run: npm run check:production-api-release
|
||||
|
||||
- name: Validate production API deploy behavior
|
||||
run: npm run check:production-api-deploy
|
||||
|
||||
repository-checks:
|
||||
name: Repository checks
|
||||
runs-on: genarrative-ci
|
||||
steps:
|
||||
- name: Checkout full history from Gitea
|
||||
env:
|
||||
GENARRATIVE_GITEA_FETCH_DEPTH: '0'
|
||||
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
|
||||
run: genarrative-gitea-checkout
|
||||
|
||||
- name: Validate preinstalled CI job image and sandbox
|
||||
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
||||
|
||||
- name: Resolve comparison base
|
||||
- name: Prepare standalone Rust crate dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
base_ref="$(node -e '
|
||||
const fs = require("node:fs");
|
||||
const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8"));
|
||||
process.stdout.write(event.pull_request?.base?.sha ?? event.before ?? "");
|
||||
')"
|
||||
if [[ -n "${base_ref}" && ! "${base_ref}" =~ ^0+$ ]]; then
|
||||
git cat-file -e "${base_ref}^{commit}" 2>/dev/null || {
|
||||
echo "comparison base commit is unavailable: ${base_ref}" >&2
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
base_ref="$(git merge-base HEAD origin/master 2>/dev/null || git rev-parse HEAD)"
|
||||
fi
|
||||
resolved_base_ref="$(git rev-parse --verify "${base_ref}^{commit}" 2>/dev/null || true)"
|
||||
head_ref="$(git rev-parse HEAD)"
|
||||
if [[ "${resolved_base_ref}" == "${head_ref}" ]]; then
|
||||
resolved_base_ref="$(git rev-parse --verify HEAD^ 2>/dev/null || true)"
|
||||
fi
|
||||
if [[ -z "${resolved_base_ref}" ]]; then
|
||||
echo 'comparison base must resolve to a commit distinct from HEAD.' >&2
|
||||
exit 1
|
||||
fi
|
||||
base_ref="${resolved_base_ref}"
|
||||
if [[ "${GITHUB_EVENT_NAME:-}" == 'pull_request' ]] \
|
||||
&& ! git merge-base --is-ancestor "${base_ref}" HEAD; then
|
||||
echo 'pull request head does not contain the latest base commit; update the branch and rerun CI.' >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "SPACETIME_SCHEMA_BASE_REF=${base_ref}" >> "${GITHUB_ENV}"
|
||||
# agent-runtime-core / agent-runtime-orchestration 被 server-rs/Cargo.toml 的
|
||||
# exclude 排除,不参与上面的 workspace 锁文件,因此上面那次锁定 fetch 覆盖不到它们;
|
||||
# 而 check:native-shells 会经 agent-runtime-*:check 用 `cargo test --manifest-path`
|
||||
# 单独跑这两个 crate。不在这里预热的话,这两条测试会在测试阶段自己
|
||||
# `Updating crates.io index`,crates.io 一抖动整条 native shell 作业就红
|
||||
# (见 #327 / PR #316 run 1950)。
|
||||
# 两个 crate 都没有提交 Cargo.lock,所以这里只能做不带锁标志的 fetch:
|
||||
# 加锁标志会因为缺少锁文件直接失败。生成的 Cargo.lock 落在两个 crate 目录内,
|
||||
# 已被各自的 .gitignore 忽略,只留在容器里;随后的测试阶段因此能用锁定版本
|
||||
# 解析,不再触碰 registry index。
|
||||
for manifest_path in \
|
||||
server-rs/crates/agent-runtime-core/Cargo.toml \
|
||||
server-rs/crates/agent-runtime-orchestration/Cargo.toml; do
|
||||
for attempt in $(seq 1 5); do
|
||||
if cargo fetch \
|
||||
--target x86_64-unknown-linux-gnu \
|
||||
--manifest-path "${manifest_path}"; then
|
||||
break
|
||||
fi
|
||||
if [[ "${attempt}" -eq 5 ]]; then
|
||||
echo "standalone crate dependency fetch failed after 5 attempts: ${manifest_path}" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep $((attempt * 2))
|
||||
done
|
||||
done
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: bash scripts/ci-npm-ci-with-retry.sh
|
||||
- name: Run native shell gates
|
||||
run: npm run check:native-shells
|
||||
|
||||
- name: Run repository checks
|
||||
run: npm run check:repository-ci
|
||||
|
||||
# 客户端的 AI 游戏创作壳前端门禁:typecheck 与壳内测试,不触碰 Cargo。
|
||||
ai-game-creator-shell-web-tests:
|
||||
name: AI game creator shell web tests
|
||||
runs-on: genarrative-ci
|
||||
steps:
|
||||
- name: Checkout full history from Gitea
|
||||
env:
|
||||
GENARRATIVE_GITEA_FETCH_DEPTH: '0'
|
||||
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
|
||||
run: genarrative-gitea-checkout
|
||||
|
||||
- name: Validate preinstalled CI job image and sandbox
|
||||
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: bash scripts/ci-npm-ci-with-retry.sh
|
||||
|
||||
- name: Run AI game creator shell web gates
|
||||
run: npm run check:native-shells:agc-web
|
||||
- name: Ensure native lockfiles are unchanged
|
||||
run: git diff --exit-code -- apps/desktop-shell/src-tauri/Cargo.lock apps/ai-game-creator-shell/src-tauri/Cargo.lock
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@genarrative/ai-game-creator-shell",
|
||||
"private": true,
|
||||
"version": "0.1.29",
|
||||
"version": "0.1.27",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node scripts/start-tauri-dev.mjs",
|
||||
|
||||
@@ -108,6 +108,8 @@ const rustSharedContractSource = fs.readFileSync(
|
||||
);
|
||||
const allowedUncalledTauriCommands = [
|
||||
'append_direct_project_conversation_message',
|
||||
// TODO: Remove the retired binding command after the legacy runtime path is removed.
|
||||
'bind_components',
|
||||
'chat_with_game_creator_agent',
|
||||
'check_ui_editor_font_glyph_coverage',
|
||||
'create_ui_design_resource',
|
||||
|
||||
@@ -1,426 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// AGC 壳 Rust 套件的分片运行器。
|
||||
//
|
||||
// 背景:AGC 壳的 Rust 单测集中在 src/main.rs 的 bin target(实测 2466 条),仓库口径用
|
||||
// `--test-threads=1` 跑,理由是并行调度会让共享 Agent Runtime 后台锁与异步终态的用例在
|
||||
// **同一进程内**互相干扰(见 development-workflow 的 Tauri suite 单线程口径)。代价是
|
||||
// 整套用例串行跑满 507 秒,占掉 CI 上 `AI game creator shell Rust tests` job 的大头。
|
||||
//
|
||||
// 这里保留「片内串行」的既有口径,只把用例集合切成 N 片、让每片在**独立进程**里并行:
|
||||
// 当年线程并行的两个根因(进程内全局锁、异步终态)在多进程下不存在,每片还会拿到自己的
|
||||
// TMPDIR,避免 tempfile 目录互相踩。片并集必须等于全集、且不得重复,数量不符即失败,
|
||||
// 防止分片规则改动后静默漏跑。
|
||||
//
|
||||
// 用法:
|
||||
// node scripts/run-rust-shell-test-shards.mjs [--shards=4] [--concurrency=4]
|
||||
// node scripts/run-rust-shell-test-shards.mjs --manifest=<Cargo.toml> --target-kind=lib --no-locked
|
||||
//
|
||||
// 参数:
|
||||
// --shards=<n> 分片数,默认 4
|
||||
// --concurrency=<n> 同时运行的片数,默认等于分片数
|
||||
// --manifest=<path> Cargo.toml,默认 ../src-tauri/Cargo.toml(相对本脚本)
|
||||
// --target-kind=<k> bin | lib,默认 bin(本地自测小 crate 时用 lib)
|
||||
// --bin=<name> bin target 名,默认 genarrative-ai-game-creator-shell
|
||||
// --package=<name> target-kind=lib 时要跑的包名(配合 lib 目标使用)
|
||||
// --no-locked 传给 cargo 时不带 --locked(只对没有提交 Cargo.lock 的 crate 需要)
|
||||
// --shard-tmp-root=<path> 片专属 TMPDIR 的父目录,默认 <系统临时目录>/agc-rust-shards
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
|
||||
const shellRoot = path.resolve(scriptDirectory, '..');
|
||||
|
||||
function fail(message) {
|
||||
console.error(`[rust-shards] ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function parsePositiveInteger(name, rawValue) {
|
||||
if (!/^[1-9][0-9]*$/.test(rawValue)) {
|
||||
fail(`${name} must be a positive integer, received: ${rawValue}`);
|
||||
}
|
||||
|
||||
return Number(rawValue);
|
||||
}
|
||||
|
||||
const options = {
|
||||
shards: 4,
|
||||
concurrency: undefined,
|
||||
manifestPath: path.join(shellRoot, 'src-tauri', 'Cargo.toml'),
|
||||
targetKind: 'bin',
|
||||
binName: 'genarrative-ai-game-creator-shell',
|
||||
packageName: undefined,
|
||||
locked: true,
|
||||
shardTmpRoot: path.join(os.tmpdir(), 'agc-rust-shards'),
|
||||
};
|
||||
|
||||
for (const rawArgument of process.argv.slice(2)) {
|
||||
if (rawArgument === '--no-locked') {
|
||||
options.locked = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
const separatorIndex = rawArgument.indexOf('=');
|
||||
if (!rawArgument.startsWith('--') || separatorIndex === -1) {
|
||||
fail(`unexpected argument: ${rawArgument}`);
|
||||
}
|
||||
|
||||
const name = rawArgument.slice(2, separatorIndex);
|
||||
const value = rawArgument.slice(separatorIndex + 1);
|
||||
switch (name) {
|
||||
case 'shards':
|
||||
options.shards = parsePositiveInteger('--shards', value);
|
||||
break;
|
||||
case 'concurrency':
|
||||
options.concurrency = parsePositiveInteger('--concurrency', value);
|
||||
break;
|
||||
case 'manifest':
|
||||
options.manifestPath = path.resolve(process.cwd(), value);
|
||||
break;
|
||||
case 'target-kind':
|
||||
if (value !== 'bin' && value !== 'lib') {
|
||||
fail(`--target-kind must be bin or lib, received: ${value}`);
|
||||
}
|
||||
options.targetKind = value;
|
||||
break;
|
||||
case 'bin':
|
||||
options.binName = value;
|
||||
break;
|
||||
case 'package':
|
||||
options.packageName = value;
|
||||
break;
|
||||
case 'shard-tmp-root':
|
||||
options.shardTmpRoot = path.resolve(process.cwd(), value);
|
||||
break;
|
||||
default:
|
||||
fail(`unexpected argument: ${rawArgument}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(options.manifestPath)) {
|
||||
fail(`manifest does not exist: ${options.manifestPath}`);
|
||||
}
|
||||
|
||||
const concurrency = options.concurrency ?? options.shards;
|
||||
const crateRoot = path.dirname(options.manifestPath);
|
||||
|
||||
function buildCargoArguments(target) {
|
||||
const cargoArguments = ['test'];
|
||||
if (options.locked) {
|
||||
cargoArguments.push('--locked');
|
||||
}
|
||||
cargoArguments.push('--manifest-path', options.manifestPath);
|
||||
if (target.kind === 'lib') {
|
||||
if (options.packageName !== undefined) {
|
||||
cargoArguments.push('-p', options.packageName);
|
||||
}
|
||||
cargoArguments.push('--lib');
|
||||
return cargoArguments;
|
||||
}
|
||||
cargoArguments.push('--bin', target.name);
|
||||
return cargoArguments;
|
||||
}
|
||||
|
||||
function formatDuration(milliseconds) {
|
||||
return `${(milliseconds / 1000).toFixed(1)}s`;
|
||||
}
|
||||
|
||||
// 编译一次,直接拿到测试可执行文件:后续每片都运行同一个二进制,不再各自调用 cargo,
|
||||
// 免得 N 个 cargo 去争 package cache 与 target 目录锁。
|
||||
function resolveTestExecutable() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cargoArguments = buildCargoArguments({
|
||||
kind: options.targetKind,
|
||||
name: options.binName,
|
||||
});
|
||||
cargoArguments.push('--no-run', '--message-format=json');
|
||||
console.log(`[rust-shards] cargo ${cargoArguments.join(' ')}`);
|
||||
|
||||
const child = spawn('cargo', cargoArguments, {
|
||||
cwd: crateRoot,
|
||||
env: process.env,
|
||||
stdio: ['ignore', 'pipe', 'inherit'],
|
||||
});
|
||||
|
||||
let buffered = '';
|
||||
const executables = [];
|
||||
child.stdout.setEncoding('utf8');
|
||||
child.stdout.on('data', (chunk) => {
|
||||
buffered += chunk;
|
||||
const lines = buffered.split('\n');
|
||||
buffered = lines.pop() ?? '';
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith('{')) {
|
||||
continue;
|
||||
}
|
||||
let message;
|
||||
try {
|
||||
message = JSON.parse(line);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
message.reason === 'compiler-artifact' &&
|
||||
typeof message.executable === 'string'
|
||||
) {
|
||||
executables.push(message.executable);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
reject(new Error(`unable to start cargo: ${error.message}`));
|
||||
});
|
||||
child.on('close', (code) => {
|
||||
if (code !== 0) {
|
||||
reject(
|
||||
new Error(
|
||||
`cargo ${cargoArguments.join(' ')} failed with exit code ${code}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const uniqueExecutables = [...new Set(executables)];
|
||||
if (uniqueExecutables.length !== 1) {
|
||||
reject(
|
||||
new Error(
|
||||
`expected exactly one test executable for the ${options.targetKind} target, found ${uniqueExecutables.length}: ${uniqueExecutables.join(', ')}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
resolve(uniqueExecutables[0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function listTestNames(executable) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(executable, ['--list'], {
|
||||
cwd: crateRoot,
|
||||
env: process.env,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
child.stdout.setEncoding('utf8');
|
||||
child.stderr.setEncoding('utf8');
|
||||
child.stdout.on('data', (chunk) => {
|
||||
stdout += chunk;
|
||||
});
|
||||
child.stderr.on('data', (chunk) => {
|
||||
stderr += chunk;
|
||||
});
|
||||
child.on('error', (error) => {
|
||||
reject(new Error(`unable to list tests: ${error.message}`));
|
||||
});
|
||||
child.on('close', (code) => {
|
||||
if (code !== 0) {
|
||||
reject(
|
||||
new Error(
|
||||
`listing tests failed with exit code ${code}: ${stderr.trim()}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const names = [];
|
||||
for (const line of stdout.split('\n')) {
|
||||
const match = /^(.*): test$/.exec(line.trim());
|
||||
if (match !== null && match[1].length > 0) {
|
||||
names.push(match[1]);
|
||||
}
|
||||
}
|
||||
resolve(names);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function splitTestNames(testNames, shardCount) {
|
||||
const sortedNames = [...testNames].sort();
|
||||
const shards = Array.from({ length: shardCount }, () => []);
|
||||
sortedNames.forEach((testName, index) => {
|
||||
shards[index % shardCount].push(testName);
|
||||
});
|
||||
return shards;
|
||||
}
|
||||
|
||||
function assertShardsCoverEveryTest(testNames, shards) {
|
||||
const flattened = shards.flat();
|
||||
if (flattened.length !== testNames.length) {
|
||||
fail(
|
||||
`shard split covered ${flattened.length} of ${testNames.length} tests; the split rule must be exhaustive`,
|
||||
);
|
||||
}
|
||||
if (new Set(flattened).size !== flattened.length) {
|
||||
fail(
|
||||
'shard split selected the same test more than once; the split rule must be disjoint',
|
||||
);
|
||||
}
|
||||
const missing = testNames.filter((testName) => !flattened.includes(testName));
|
||||
if (missing.length > 0) {
|
||||
fail(
|
||||
`shard split is missing tests, for example: ${missing.slice(0, 5).join(', ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function runShard(executable, shardIndex, shardCount, shardTestNames) {
|
||||
const label = `shard ${shardIndex + 1}/${shardCount}`;
|
||||
const shardTmpDirectory = path.join(
|
||||
options.shardTmpRoot,
|
||||
`shard-${shardIndex + 1}`,
|
||||
);
|
||||
fs.rmSync(shardTmpDirectory, { recursive: true, force: true });
|
||||
fs.mkdirSync(shardTmpDirectory, { recursive: true });
|
||||
const startedAt = Date.now();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(
|
||||
executable,
|
||||
['--exact', ...shardTestNames, '--test-threads=1'],
|
||||
{
|
||||
cwd: crateRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
TMPDIR: shardTmpDirectory,
|
||||
TMP: shardTmpDirectory,
|
||||
TEMP: shardTmpDirectory,
|
||||
},
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
},
|
||||
);
|
||||
|
||||
const failureLines = [];
|
||||
let inFailureList = false;
|
||||
let stderr = '';
|
||||
const consumeLine = (rawLine) => {
|
||||
const line = rawLine.replace(/\r$/, '');
|
||||
if (line.includes('failures:')) {
|
||||
inFailureList = true;
|
||||
return;
|
||||
}
|
||||
if (inFailureList) {
|
||||
if (line.trim().length === 0) {
|
||||
inFailureList = false;
|
||||
return;
|
||||
}
|
||||
failureLines.push(line.trim());
|
||||
}
|
||||
};
|
||||
|
||||
child.stdout.setEncoding('utf8');
|
||||
child.stderr.setEncoding('utf8');
|
||||
let stdoutBuffer = '';
|
||||
child.stdout.on('data', (chunk) => {
|
||||
stdoutBuffer += chunk;
|
||||
const lines = stdoutBuffer.split('\n');
|
||||
stdoutBuffer = lines.pop() ?? '';
|
||||
for (const line of lines) {
|
||||
consumeLine(line);
|
||||
}
|
||||
});
|
||||
child.stderr.on('data', (chunk) => {
|
||||
stderr += chunk;
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
resolve({
|
||||
label,
|
||||
ok: false,
|
||||
durationMs: Date.now() - startedAt,
|
||||
testCount: shardTestNames.length,
|
||||
failures: [`unable to start test binary: ${error.message}`],
|
||||
stderr,
|
||||
});
|
||||
});
|
||||
child.on('close', (code) => {
|
||||
resolve({
|
||||
label,
|
||||
ok: code === 0,
|
||||
durationMs: Date.now() - startedAt,
|
||||
testCount: shardTestNames.length,
|
||||
failures: failureLines,
|
||||
stderr,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function runWithConcurrency(shards, runner) {
|
||||
const results = new Array(shards.length);
|
||||
let nextIndex = 0;
|
||||
const workers = Array.from(
|
||||
{ length: Math.min(concurrency, shards.length) },
|
||||
async () => {
|
||||
while (nextIndex < shards.length) {
|
||||
const index = nextIndex;
|
||||
nextIndex += 1;
|
||||
results[index] = await runner(shards[index], index);
|
||||
}
|
||||
},
|
||||
);
|
||||
await Promise.all(workers);
|
||||
return results;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const executable = await resolveTestExecutable();
|
||||
const testNames = await listTestNames(executable);
|
||||
if (testNames.length === 0) {
|
||||
fail(
|
||||
`no tests discovered in ${options.manifestPath} (${options.targetKind})`,
|
||||
);
|
||||
}
|
||||
|
||||
const shards = splitTestNames(testNames, options.shards);
|
||||
assertShardsCoverEveryTest(testNames, shards);
|
||||
|
||||
console.log(
|
||||
`[rust-shards] ${testNames.length} tests, ${shards.length} shard(s), concurrency ${Math.min(concurrency, shards.length)}`,
|
||||
);
|
||||
shards.forEach((shardTestNames, index) => {
|
||||
console.log(
|
||||
`[rust-shards] shard ${index + 1}/${shards.length}: ${shardTestNames.length} test(s)`,
|
||||
);
|
||||
});
|
||||
|
||||
const results = await runWithConcurrency(shards, (shardTestNames, index) =>
|
||||
runShard(executable, index, shards.length, shardTestNames),
|
||||
);
|
||||
|
||||
let failed = false;
|
||||
for (const result of results) {
|
||||
if (result.ok) {
|
||||
console.log(
|
||||
`[rust-shards] ${result.label} ok: ${result.testCount} test(s) in ${formatDuration(result.durationMs)}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
failed = true;
|
||||
console.error(
|
||||
`[rust-shards] ${result.label} FAILED: ${result.testCount} test(s) in ${formatDuration(result.durationMs)}`,
|
||||
);
|
||||
for (const failure of result.failures) {
|
||||
console.error(`[rust-shards] ${failure}`);
|
||||
}
|
||||
if (result.stderr.trim().length > 0) {
|
||||
console.error(
|
||||
`[rust-shards] stderr: ${result.stderr.trim().split('\n').slice(-20).join('\n[rust-shards] ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('[rust-shards] OK');
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
fail(error instanceof Error ? error.message : String(error));
|
||||
});
|
||||
@@ -84,7 +84,6 @@ function resolveBackendTargetsFromState(
|
||||
requireAgcBackend = false,
|
||||
expectedDatabase = backendDatabase,
|
||||
expectedSpacetimeDataDir = backendSpacetimeDataDir,
|
||||
expectedRepoRoot = repoRoot,
|
||||
fallbackApiTarget = defaultApiTarget,
|
||||
} = {},
|
||||
) {
|
||||
@@ -102,25 +101,7 @@ function resolveBackendTargetsFromState(
|
||||
const hasMatchingDataDir =
|
||||
Boolean(spacetimeDataDir) &&
|
||||
spacetimeDataDir === resolve(expectedSpacetimeDataDir);
|
||||
const instanceId =
|
||||
typeof state?.instanceId === 'string' ? state.instanceId.trim() : '';
|
||||
const hasMatchingRepoRoot =
|
||||
typeof state?.repoRoot === 'string' &&
|
||||
resolve(state.repoRoot) === resolve(expectedRepoRoot);
|
||||
const hasMatchingInstance =
|
||||
Boolean(instanceId) &&
|
||||
[apiServer, spacetime, bgfilterWorker]
|
||||
.filter(Boolean)
|
||||
.every(
|
||||
(service) =>
|
||||
service.repoRoot &&
|
||||
resolve(service.repoRoot) === resolve(expectedRepoRoot) &&
|
||||
service.instanceId === instanceId,
|
||||
);
|
||||
const hasMatchingBackend =
|
||||
hasMatchingDatabase &&
|
||||
hasMatchingDataDir &&
|
||||
(!requireAgcBackend || (hasMatchingRepoRoot && hasMatchingInstance));
|
||||
const hasMatchingBackend = hasMatchingDatabase && hasMatchingDataDir;
|
||||
const canReuseState = !requireAgcBackend || hasMatchingBackend;
|
||||
const apiUrl =
|
||||
canReuseState && isActive(apiServer) && apiServer.url
|
||||
@@ -146,8 +127,6 @@ function resolveBackendTargetsFromState(
|
||||
spacetimeDataDir,
|
||||
hasMatchingDatabase,
|
||||
hasMatchingDataDir,
|
||||
hasMatchingRepoRoot,
|
||||
hasMatchingInstance,
|
||||
hasMatchingBackend,
|
||||
};
|
||||
}
|
||||
|
||||
+1
-1
@@ -1725,7 +1725,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "genarrative-ai-game-creator-shell"
|
||||
version = "0.1.29"
|
||||
version = "0.1.27"
|
||||
dependencies = [
|
||||
"agent-runtime-core",
|
||||
"axum",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "genarrative-ai-game-creator-shell"
|
||||
version = "0.1.29"
|
||||
version = "0.1.27"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
处理代码任务时先用 project.search 定位,再用带行号的 file.read 获取足够上下文;单文件小改优先使用 file.patch;涉及多个文件时优先使用 project.patchset,它会自动创建 checkpoint,无需额外调用 project.checkpoint,并在成功后用返回的 checkpointId 调用 project.diff(includeContent=true) 审查整体变更;只有确认文件已废弃时才删除。
|
||||
|
||||
每次成功执行 file.write、file.patch、file.delete、project.patchset 或 project.restore,以及每次真正启动 command.exec、command.start 或 project.bootstrap,都会产生新的项目 revision;DirectProject 的 npm/Phaser 工程先用 project.bootstrap {cwd:"game"} 执行受控无参数 npm install,再用 project.verify {cwd:"game",script:"build",expectedCommand:从 game/package.json 原样读取} 构建,并确认 game/dist/index.html 存在。最后一次修改后必须成功执行 project.verify、可验证 command.exec,或成功执行 command.run_limited 的 game.static_smoke,才能调用 respond_to_user 收束。文件回读不能替代可执行验证,验证后再次修改必须重新验证。需要执行 package.json 中的验证脚本时,先读取 package.json,再把真实脚本名和读到的完整命令原样提交给 project.verify;script 可以是 check、typecheck、test、lint、build,或使用 check:<name>、test:<name>(例如 test:unit)、lint:<name>、typecheck:<name>、build:<name>、verify:<name>、validate:<name> 形式的命名脚本,其中冒号后的每个非空段必须以字母或数字开头且只能包含字母、数字、连字符、下划线或点;不得猜测或改写 expectedCommand。
|
||||
每次成功执行 file.write、file.patch、file.delete、project.patchset 或 project.restore,以及每次真正启动 command.exec 或 command.start,都会产生新的项目 revision;最后一次修改后必须成功执行 project.verify、可验证 command.exec,或成功执行 command.run_limited 的 game.static_smoke,才能调用 respond_to_user 收束。文件回读不能替代可执行验证,验证后再次修改必须重新验证。需要执行 package.json 中的验证脚本时,先读取 package.json,再把真实脚本名和读到的完整命令原样提交给 project.verify;script 可以是 check、typecheck、test、lint、build,或使用 check:<name>、test:<name>(例如 test:unit)、lint:<name>、typecheck:<name>、build:<name>、verify:<name>、validate:<name> 形式的命名脚本,其中冒号后的每个非空段必须以字母或数字开头且只能包含字母、数字、连字符、下划线或点;不得猜测或改写 expectedCommand。
|
||||
|
||||
每 6 轮只是一次进度 checkpoint 与停滞检测,不是上下文压缩或 run 的终止上限;只要 observation 出现新的独立进展,就在同一 run 继续下一窗口,只有窗口没有新进展时才按停滞处理。真正的上下文压缩仅由 token 阈值或显式 compact 触发。Agent 私有记忆只能由本人写入,跨 Agent 共享稳定结论用 blackboard.write,给单个 Agent 留上下文用 agent.message。
|
||||
|
||||
|
||||
@@ -1378,19 +1378,15 @@ fn codex_app_server_thread_start_params(
|
||||
base_instructions: String,
|
||||
use_model_provider: bool,
|
||||
) -> serde_json::Value {
|
||||
// DirectProject is an autonomous Codex session with explicit full OS
|
||||
// access; approval prompts are not a second harness that can stall a turn.
|
||||
// DirectHome/ToolHost stay passive.
|
||||
// DirectProject is an autonomous Codex session. The app-server sandbox
|
||||
// remains the hard write boundary; approval prompts are not a second
|
||||
// harness that can stall a turn. DirectHome/ToolHost stay passive.
|
||||
let approval_policy = "never";
|
||||
let mut params = serde_json::json!({
|
||||
"model": model,
|
||||
"cwd": workspace_path,
|
||||
"approvalPolicy": approval_policy,
|
||||
"sandbox": if workspace_mode == CodexAppServerWorkspaceMode::DirectProject {
|
||||
"danger-full-access"
|
||||
} else {
|
||||
"read-only"
|
||||
},
|
||||
"sandbox": if workspace_mode.allows_workspace_writes() { "workspace-write" } else { "read-only" },
|
||||
"ephemeral": true,
|
||||
"baseInstructions": base_instructions
|
||||
});
|
||||
@@ -1408,6 +1404,7 @@ fn codex_app_server_turn_start_params(
|
||||
thread_id: &str,
|
||||
input: serde_json::Value,
|
||||
model: &str,
|
||||
workspace_path: &std::path::Path,
|
||||
workspace_mode: CodexAppServerWorkspaceMode,
|
||||
client_user_message_id: Option<&str>,
|
||||
) -> serde_json::Value {
|
||||
@@ -1418,12 +1415,14 @@ fn codex_app_server_turn_start_params(
|
||||
"model": model,
|
||||
"approvalPolicy": approval_policy,
|
||||
});
|
||||
if workspace_mode == CodexAppServerWorkspaceMode::DirectProject {
|
||||
// DirectProject is an explicitly user-selected local Codex session.
|
||||
// Give the native Codex tools the full OS sandbox profile so they are
|
||||
// not narrowed by a project-root writableRoots allowlist.
|
||||
if workspace_mode.allows_workspace_writes() {
|
||||
// npm install/build must resolve project dependencies. Network access
|
||||
// is enabled only for DirectProject; writableRoots keeps the file-write
|
||||
// boundary at the real game workspace.
|
||||
params["sandboxPolicy"] = serde_json::json!({
|
||||
"type": "dangerFullAccess"
|
||||
"type": "workspaceWrite",
|
||||
"writableRoots": [workspace_path],
|
||||
"networkAccess": true
|
||||
});
|
||||
}
|
||||
if let Some(client_user_message_id) = client_user_message_id
|
||||
@@ -1437,27 +1436,40 @@ fn codex_app_server_turn_start_params(
|
||||
}
|
||||
|
||||
fn game_creator_codex_app_server_interaction_response(
|
||||
workspace_path: &std::path::Path,
|
||||
workspace_mode: CodexAppServerWorkspaceMode,
|
||||
id: u64,
|
||||
_method: &str,
|
||||
_requested_grant_root: Option<&str>,
|
||||
method: &str,
|
||||
requested_grant_root: Option<&str>,
|
||||
) -> serde_json::Value {
|
||||
if workspace_mode == CodexAppServerWorkspaceMode::DirectProject {
|
||||
// Full-access DirectProject sessions do not use a file-root allowlist
|
||||
// or a second approval gate. The declared sandbox policy is the only
|
||||
// capability boundary for native Codex operations.
|
||||
return serde_json::json!({
|
||||
let direct_workspace = workspace_mode.allows_workspace_writes();
|
||||
let file_change_within_workspace = game_creator_codex_file_change_request_is_allowed(
|
||||
workspace_path,
|
||||
method,
|
||||
requested_grant_root,
|
||||
);
|
||||
if direct_workspace && file_change_within_workspace {
|
||||
serde_json::json!({
|
||||
"id": id,
|
||||
"result": { "decision": "accept" }
|
||||
});
|
||||
})
|
||||
} else if direct_workspace && method == "item/fileChange/requestApproval" {
|
||||
serde_json::json!({
|
||||
"id": id,
|
||||
"error": {
|
||||
"code": -32602,
|
||||
"message": "Genarrative AGC 只允许当前项目工作区的文件变更审批"
|
||||
}
|
||||
})
|
||||
} else {
|
||||
serde_json::json!({
|
||||
"id": id,
|
||||
"error": {
|
||||
"code": -32601,
|
||||
"message": "Genarrative AGC 拒绝 app-server 的交互、审批与工具请求"
|
||||
}
|
||||
})
|
||||
}
|
||||
serde_json::json!({
|
||||
"id": id,
|
||||
"error": {
|
||||
"code": -32601,
|
||||
"message": "Genarrative AGC 拒绝 app-server 的交互、审批与工具请求"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -2722,6 +2734,7 @@ impl CodexAppServerConnection {
|
||||
&thread_id,
|
||||
input,
|
||||
model,
|
||||
&self.inner.workspace_path,
|
||||
self.inner.workspace_mode,
|
||||
direct_client_turn_id,
|
||||
);
|
||||
@@ -3280,6 +3293,7 @@ async fn read_game_creator_codex_app_server_stdout(
|
||||
);
|
||||
}
|
||||
let response = game_creator_codex_app_server_interaction_response(
|
||||
&inner.workspace_path,
|
||||
inner.workspace_mode,
|
||||
id,
|
||||
method,
|
||||
@@ -3621,6 +3635,82 @@ async fn read_game_creator_codex_app_server_stderr(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn game_creator_codex_workspace_path_key(path: &std::path::Path) -> String {
|
||||
let value = path.to_string_lossy();
|
||||
value
|
||||
.strip_prefix("\\\\?\\")
|
||||
.unwrap_or(value.as_ref())
|
||||
.replace('/', "\\")
|
||||
.trim_end_matches('\\')
|
||||
.to_ascii_lowercase()
|
||||
}
|
||||
|
||||
fn game_creator_codex_grant_root_is_within_workspace(
|
||||
workspace: &std::path::Path,
|
||||
grant_root: &str,
|
||||
) -> bool {
|
||||
fn canonicalize_with_missing_tail(path: &std::path::Path) -> Option<std::path::PathBuf> {
|
||||
if !path.is_absolute()
|
||||
|| path
|
||||
.components()
|
||||
.any(|component| matches!(component, std::path::Component::ParentDir))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let mut existing = path.to_path_buf();
|
||||
let mut missing_tail = Vec::new();
|
||||
while !existing.exists() {
|
||||
missing_tail.push(existing.file_name()?.to_os_string());
|
||||
if !existing.pop() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let mut normalized = existing.canonicalize().ok()?;
|
||||
for component in missing_tail.iter().rev() {
|
||||
normalized.push(component);
|
||||
}
|
||||
Some(normalized)
|
||||
}
|
||||
|
||||
let workspace = workspace
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| workspace.to_path_buf());
|
||||
let Some(grant_root) = canonicalize_with_missing_tail(std::path::Path::new(grant_root)) else {
|
||||
return false;
|
||||
};
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let workspace_key = game_creator_codex_workspace_path_key(&workspace);
|
||||
let grant_key = game_creator_codex_workspace_path_key(&grant_root);
|
||||
grant_key == workspace_key
|
||||
|| grant_key
|
||||
.strip_prefix(&workspace_key)
|
||||
.is_some_and(|suffix| suffix.starts_with('\\'))
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
grant_root == workspace || grant_root.starts_with(&workspace)
|
||||
}
|
||||
}
|
||||
|
||||
fn game_creator_codex_file_change_request_is_allowed(
|
||||
workspace: &std::path::Path,
|
||||
method: &str,
|
||||
grant_root: Option<&str>,
|
||||
) -> bool {
|
||||
if method != "item/fileChange/requestApproval" {
|
||||
return false;
|
||||
}
|
||||
// `grantRoot: null` means the already-declared turn sandbox root. It is
|
||||
// valid only for file changes; it must never authorize another capability
|
||||
// or a broader permission request.
|
||||
grant_root.is_none()
|
||||
|| grant_root.is_some_and(|grant_root| {
|
||||
game_creator_codex_grant_root_is_within_workspace(workspace, grant_root)
|
||||
})
|
||||
}
|
||||
|
||||
async fn fail_game_creator_codex_app_server_connection(
|
||||
inner: &Weak<CodexAppServerInner>,
|
||||
error: String,
|
||||
@@ -4391,6 +4481,7 @@ mod tests {
|
||||
"home-thread",
|
||||
serde_json::json!([{ "type": "text", "text": "你好" }]),
|
||||
"fixture-model",
|
||||
workspace,
|
||||
CodexAppServerWorkspaceMode::DirectHome,
|
||||
None,
|
||||
);
|
||||
@@ -4404,6 +4495,7 @@ mod tests {
|
||||
"item/permissions/requestApproval",
|
||||
] {
|
||||
let response = game_creator_codex_app_server_interaction_response(
|
||||
workspace,
|
||||
CodexAppServerWorkspaceMode::DirectHome,
|
||||
7,
|
||||
method,
|
||||
@@ -4421,10 +4513,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_project_protocol_uses_full_access_without_a_root_allowlist() {
|
||||
fn direct_project_protocol_and_interactions_expose_only_the_real_game_workspace() {
|
||||
let temp = tempfile::tempdir().expect("temp dir");
|
||||
let project_root = temp.path().join("project");
|
||||
let assets = project_root.join("assets");
|
||||
let agent = project_root.join(".agent");
|
||||
std::fs::create_dir_all(&project_root).expect("project root");
|
||||
std::fs::create_dir(&assets).expect("assets directory");
|
||||
std::fs::create_dir(&agent).expect("agent directory");
|
||||
let workspace =
|
||||
resolve_direct_codex_game_workspace(&project_root).expect("resolve project workspace");
|
||||
assert_eq!(
|
||||
@@ -4440,40 +4536,83 @@ mod tests {
|
||||
true,
|
||||
);
|
||||
assert_eq!(thread["cwd"], serde_json::json!(workspace));
|
||||
assert_eq!(thread["sandbox"], "danger-full-access");
|
||||
assert_eq!(thread["sandbox"], "workspace-write");
|
||||
|
||||
let turn = codex_app_server_turn_start_params(
|
||||
"project-thread",
|
||||
serde_json::json!([{ "type": "text", "text": "修复游戏" }]),
|
||||
"fixture-model",
|
||||
&workspace,
|
||||
CodexAppServerWorkspaceMode::DirectProject,
|
||||
Some("direct-turn-0001"),
|
||||
);
|
||||
assert_eq!(turn["clientUserMessageId"], "direct-turn-0001");
|
||||
assert_eq!(
|
||||
turn.pointer("/sandboxPolicy/type"),
|
||||
Some(&serde_json::json!("dangerFullAccess"))
|
||||
turn.pointer("/sandboxPolicy/writableRoots/0"),
|
||||
Some(&serde_json::json!(workspace))
|
||||
);
|
||||
assert_eq!(
|
||||
turn.pointer("/sandboxPolicy/networkAccess"),
|
||||
Some(&serde_json::json!(true))
|
||||
);
|
||||
let authority_paths = [
|
||||
turn.get("cwd"),
|
||||
turn.pointer("/sandboxPolicy/writableRoots/0"),
|
||||
];
|
||||
assert!(
|
||||
authority_paths
|
||||
.iter()
|
||||
.flatten()
|
||||
.all(|value| value.as_str() == Some(workspace.to_string_lossy().as_ref())),
|
||||
"writable params must be exactly the project workspace"
|
||||
);
|
||||
assert!(turn.pointer("/sandboxPolicy/writableRoots").is_none());
|
||||
assert!(turn.pointer("/sandboxPolicy/networkAccess").is_none());
|
||||
|
||||
for (id, method) in [
|
||||
(9, "item/fileChange/requestApproval"),
|
||||
(10, "item/commandExecution/requestApproval"),
|
||||
(11, "item/permissions/requestApproval"),
|
||||
(12, "item/tool/call"),
|
||||
] {
|
||||
let workspace_string = workspace.to_string_lossy().into_owned();
|
||||
for allowed_root in [None, Some(workspace_string.as_str())] {
|
||||
let response = game_creator_codex_app_server_interaction_response(
|
||||
&workspace,
|
||||
CodexAppServerWorkspaceMode::DirectProject,
|
||||
id,
|
||||
method,
|
||||
Some("C:\\outside-project"),
|
||||
9,
|
||||
"item/fileChange/requestApproval",
|
||||
allowed_root,
|
||||
);
|
||||
assert_eq!(
|
||||
response.pointer("/result/decision"),
|
||||
Some(&serde_json::json!("accept"))
|
||||
);
|
||||
}
|
||||
for forbidden_root in [assets, agent] {
|
||||
let forbidden_root = forbidden_root.to_string_lossy().into_owned();
|
||||
let response = game_creator_codex_app_server_interaction_response(
|
||||
&workspace,
|
||||
CodexAppServerWorkspaceMode::DirectProject,
|
||||
10,
|
||||
"item/fileChange/requestApproval",
|
||||
Some(&forbidden_root),
|
||||
);
|
||||
assert_eq!(
|
||||
response.pointer("/result/decision"),
|
||||
Some(&serde_json::json!("accept")),
|
||||
"project-root children must stay writable: {forbidden_root}"
|
||||
);
|
||||
}
|
||||
for method in [
|
||||
"item/commandExecution/requestApproval",
|
||||
"item/permissions/requestApproval",
|
||||
"item/tool/call",
|
||||
] {
|
||||
for requested_root in [None, Some(workspace_string.as_str())] {
|
||||
let response = game_creator_codex_app_server_interaction_response(
|
||||
&workspace,
|
||||
CodexAppServerWorkspaceMode::DirectProject,
|
||||
11,
|
||||
method,
|
||||
requested_root,
|
||||
);
|
||||
assert!(response.get("error").is_some());
|
||||
assert!(response.get("result").is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4491,6 +4630,26 @@ mod tests {
|
||||
assert!(resolve_direct_codex_game_workspace(&file_root).is_err());
|
||||
}
|
||||
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
#[test]
|
||||
fn direct_project_grant_root_comparison_remains_case_sensitive() {
|
||||
let temp = tempfile::tempdir().expect("temp dir");
|
||||
let project_root = temp.path().join("Project");
|
||||
let child = project_root.join("assets");
|
||||
let different_case = temp.path().join("project").join("assets");
|
||||
std::fs::create_dir_all(&child).expect("child directory");
|
||||
std::fs::create_dir_all(&different_case).expect("different-case directory");
|
||||
|
||||
assert!(game_creator_codex_grant_root_is_within_workspace(
|
||||
&project_root,
|
||||
project_root.to_string_lossy().as_ref()
|
||||
));
|
||||
assert!(!game_creator_codex_grant_root_is_within_workspace(
|
||||
&project_root,
|
||||
different_case.to_string_lossy().as_ref()
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn direct_project_rejects_a_non_directory_workspace() {
|
||||
@@ -5087,25 +5246,51 @@ while IFS= read -r line; do :; done
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_project_interactions_accept_full_access_without_a_root_allowlist() {
|
||||
for method in [
|
||||
fn direct_file_change_approval_is_limited_to_workspace() {
|
||||
let temp = tempfile::tempdir().expect("temp dir");
|
||||
let workspace = temp.path().join("demo");
|
||||
let child = workspace.join("assets");
|
||||
let sibling = temp.path().join("demolition");
|
||||
std::fs::create_dir_all(&child).expect("workspace child");
|
||||
std::fs::create_dir(&sibling).expect("sibling");
|
||||
assert!(game_creator_codex_file_change_request_is_allowed(
|
||||
&workspace,
|
||||
"item/fileChange/requestApproval",
|
||||
"item/commandExecution/requestApproval",
|
||||
"item/permissions/requestApproval",
|
||||
"item/tool/call",
|
||||
] {
|
||||
let response = game_creator_codex_app_server_interaction_response(
|
||||
CodexAppServerWorkspaceMode::DirectProject,
|
||||
1,
|
||||
method,
|
||||
Some("C:\\outside-project"),
|
||||
);
|
||||
assert_eq!(
|
||||
response.pointer("/result/decision"),
|
||||
Some(&serde_json::json!("accept"))
|
||||
);
|
||||
assert!(response.get("error").is_none());
|
||||
}
|
||||
None
|
||||
));
|
||||
assert!(game_creator_codex_grant_root_is_within_workspace(
|
||||
&workspace,
|
||||
workspace.to_string_lossy().as_ref()
|
||||
));
|
||||
assert!(game_creator_codex_grant_root_is_within_workspace(
|
||||
&workspace,
|
||||
child.to_string_lossy().as_ref()
|
||||
));
|
||||
assert!(!game_creator_codex_grant_root_is_within_workspace(
|
||||
&workspace,
|
||||
sibling.to_string_lossy().as_ref()
|
||||
));
|
||||
assert!(!game_creator_codex_file_change_request_is_allowed(
|
||||
&workspace,
|
||||
"item/fileChange/requestApproval",
|
||||
Some(sibling.to_string_lossy().as_ref())
|
||||
));
|
||||
assert!(!game_creator_codex_grant_root_is_within_workspace(
|
||||
&workspace,
|
||||
sibling.join("missing").to_string_lossy().as_ref()
|
||||
));
|
||||
assert!(game_creator_codex_grant_root_is_within_workspace(
|
||||
&workspace,
|
||||
workspace.join("missing").to_string_lossy().as_ref()
|
||||
));
|
||||
assert!(!game_creator_codex_grant_root_is_within_workspace(
|
||||
&workspace,
|
||||
workspace
|
||||
.join("..")
|
||||
.join("outside")
|
||||
.to_string_lossy()
|
||||
.as_ref()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -16,7 +16,7 @@ JSON schema:
|
||||
{ "group": "balance", "role": "Difficulty", "summary": "数值交接摘要", "outputs": ["game/balance.json"], "next": "交给程序组读取" },
|
||||
{ "group": "art", "role": "Asset", "summary": "美术交接摘要", "outputs": ["assets/manifest.art.json"], "next": "进入画板或本地资产登记" },
|
||||
{ "group": "audio", "role": "SFX", "summary": "音乐音效交接摘要", "outputs": ["assets/manifest.audio.json"], "next": "进入画板音频链路" },
|
||||
{ "group": "code", "role": "Code", "summary": "程序交接摘要", "outputs": ["game/index.html", "game/game.js", "game/package.json", "game/vite.config.js"], "next": "先 project.bootstrap,再 project.verify(build),交给 Playtest" },
|
||||
{ "group": "code", "role": "Code", "summary": "程序交接摘要", "outputs": ["game/index.html"], "next": "交给 Playtest" },
|
||||
{ "group": "publishing", "role": "Publish", "summary": "运营交接摘要", "outputs": ["exports/README.md"], "next": "等待预览验收" }
|
||||
],
|
||||
"handoffSummary": "六组 agent 的交接摘要,每组一行",
|
||||
@@ -24,7 +24,6 @@ JSON schema:
|
||||
}
|
||||
|
||||
gameHtml 规则:
|
||||
- 本次若目标是 Phaser/npm,workspaceMode 必须为 DirectProject;先写入完整 game/ 工程文件,再由受控项目工具安装与构建。
|
||||
- 此 JSON 协议仅用于已有的单文件 HTML 项目;npm / Phaser 项目必须使用 DirectProject,不能通过 gameHtml 交付 package.json 或模块源码。
|
||||
- 必须是单文件 HTML,不能加载远程脚本、远程图片、远程 CSS 或 CDN。
|
||||
- 必须包含 canvas、canvas getContext、实际绘制调用、键盘或鼠标输入、requestAnimationFrame 主循环、目标、失败或胜利状态、R 或按钮重开。
|
||||
|
||||
@@ -1156,6 +1156,10 @@ mod tests {
|
||||
assert!(with_canvas.contains("根据当前玩法需求编写规格和界面建议"));
|
||||
assert!(with_canvas.contains("用途、数量、输出路径、尺寸、参考资源和是否需要 spritesheet"));
|
||||
assert!(with_canvas.contains("再调用 canvas.asset_generate"));
|
||||
assert!(with_canvas.contains("调用 canvas.asset_generate"));
|
||||
assert!(with_canvas.contains("不要使用固定图片合同"));
|
||||
assert!(with_canvas.contains("不修改 game/index.html"));
|
||||
assert!(!with_canvas.contains("不调用 canvas.asset_generate"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -801,7 +801,7 @@ fn agent_runtime_action_receipt_safe_detail_with_owner(
|
||||
let initial_step = route.get("initialStep")?.as_str()?;
|
||||
let render_mode = route.get("renderMode")?.as_str()?;
|
||||
if resource_id.is_empty()
|
||||
|| initial_step != "visual-binding"
|
||||
|| initial_step != "asset-separation"
|
||||
|| render_mode != "final-preview"
|
||||
{
|
||||
return None;
|
||||
@@ -1192,7 +1192,6 @@ pub(in crate::agent) fn agent_runtime_public_action_input_summary(
|
||||
| "project.patchset"
|
||||
| "project.search"
|
||||
| "project.verify"
|
||||
| "project.bootstrap"
|
||||
| "file.list"
|
||||
| "file.read"
|
||||
| "file.write"
|
||||
@@ -1260,7 +1259,6 @@ pub(crate) fn agent_runtime_tool_action_fingerprint(
|
||||
| "command.stdin"
|
||||
| "command.terminate"
|
||||
| "project.verify"
|
||||
| "project.bootstrap"
|
||||
)
|
||||
.then(|| {
|
||||
let metadata = command_sandbox_platform_metadata();
|
||||
@@ -1470,23 +1468,11 @@ pub(crate) fn agent_runtime_tool_action_input_summary(
|
||||
.and_then(|value| value.as_bool())
|
||||
.unwrap_or(false)
|
||||
),
|
||||
"project.bootstrap" => {
|
||||
format!(
|
||||
"cwd={} · timeoutSeconds={}",
|
||||
relative_path(&["cwd"]),
|
||||
input
|
||||
.get("timeoutSeconds")
|
||||
.or_else(|| input.get("timeout_seconds"))
|
||||
.and_then(|value| value.as_u64())
|
||||
.unwrap_or(300)
|
||||
)
|
||||
}
|
||||
"project.verify" => {
|
||||
let expected_command = text(&["expectedCommand", "expected_command"]);
|
||||
let command_chars = expected_command.chars().count();
|
||||
format!(
|
||||
"cwd={} · script={} · expectedCommandSha256={:x} · expectedCommandChars={} · timeoutSeconds={}",
|
||||
relative_path(&["cwd"]),
|
||||
"script={} · expectedCommandSha256={:x} · expectedCommandChars={} · timeoutSeconds={}",
|
||||
text(&["script"]),
|
||||
Sha256::digest(expected_command.as_bytes()),
|
||||
command_chars,
|
||||
|
||||
@@ -184,17 +184,6 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
|
||||
)
|
||||
.await
|
||||
}
|
||||
"project.bootstrap" => {
|
||||
observe_agent_runtime_project_bootstrap(
|
||||
root,
|
||||
agent_id,
|
||||
run_id,
|
||||
action_id,
|
||||
&action_fingerprint,
|
||||
&action.input,
|
||||
)
|
||||
.await
|
||||
}
|
||||
"project.checkpoint" => observe_agent_runtime_project_checkpoint(root),
|
||||
"project.restore" => {
|
||||
observe_agent_runtime_project_restore(root, agent_id, run_id, &action.input)
|
||||
|
||||
@@ -8,15 +8,10 @@ pub(crate) struct AgentRuntimeAutonomousSourcePayloadStats {
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn agent_runtime_autonomous_project_verify_available(root: &Path) -> bool {
|
||||
[root.join("package.json"), root.join("game/package.json")]
|
||||
.into_iter()
|
||||
.any(|package_path| {
|
||||
fs::symlink_metadata(package_path)
|
||||
.map(|metadata| {
|
||||
metadata.file_type().is_file() && !metadata.file_type().is_symlink()
|
||||
})
|
||||
.unwrap_or(false)
|
||||
})
|
||||
let package_path = root.join("package.json");
|
||||
fs::symlink_metadata(package_path)
|
||||
.map(|metadata| metadata.file_type().is_file() && !metadata.file_type().is_symlink())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn add_agent_runtime_autonomous_source_field(
|
||||
|
||||
+1
-1
@@ -345,7 +345,7 @@ fn build_game_creator_agent_background_tool_plan_request_at(
|
||||
let project_tools_contract = if runtime_owner_artifact_validation_available {
|
||||
"project.search 使用 {\"query\":\"要查找的字面文本\",\"path\":\"\",\"maxResults\":20,\"caseSensitive\":false},path 为空字符串时搜索整个项目,返回 path:line 和匹配行;当前固定 owner 的函数目录不广告 project.verify;project.checkpoint 使用空对象,只用于多个 file.* 写动作前或需要独立回退点时创建本地 checkpoint;project.patchset 会自动创建 checkpoint,不要为同一批变更额外调用 project.checkpoint;project.restore 使用 {\"checkpointId\":\"checkpoint id\"};project.diff 使用 {\"checkpointId\":\"checkpoint id\",\"includeContent\":true,\"maxFiles\":20,\"maxChars\":24000};git.inspect 使用 {\"includeDiff\":true,\"maxFiles\":20,\"maxChars\":24000},只读项目根 Git 状态和有界 diff,不得用它提交、暂存、切分支、合并、重置、stash、worktree 或访问 remote。".to_string()
|
||||
} else {
|
||||
"project.search 使用 {\"query\":\"要查找的字面文本\",\"path\":\"\",\"maxResults\":20,\"caseSensitive\":false},path 为空字符串时搜索整个项目,返回 path:line 和匹配行;project.bootstrap 使用 {\"cwd\":\"game\",\"timeoutSeconds\":300},只执行 game 目录无参数 npm install 并记录 package/lock 指纹;project.verify 使用 {\"script\":\"check|typecheck|test|lint|build\",\"expectedCommand\":\"从对应 cwd 的 package.json 读取的完整原始脚本\",\"timeoutSeconds\":120,\"cwd\":\"game\"},只执行对应 cwd package.json 中同名 npm 脚本,build 必须确认 game/dist/index.html;expectedCommand 不一致时拒绝执行,确认策略以当前工具策略中 project.verify 的独立权限为准;project.checkpoint 使用空对象,只用于多个 file.* 写动作前或需要独立回退点时创建本地 checkpoint;project.patchset 会自动创建 checkpoint,不要为同一批变更额外调用 project.checkpoint;project.restore 使用 {\"checkpointId\":\"checkpoint id\"};project.diff 使用 {\"checkpointId\":\"checkpoint id\",\"includeContent\":true,\"maxFiles\":20,\"maxChars\":24000};git.inspect 使用 {\"includeDiff\":true,\"maxFiles\":20,\"maxChars\":24000},只读项目根 Git 状态和有界 diff,不得用它提交、暂存、切分支、合并、重置、stash、worktree 或访问 remote。".to_string()
|
||||
"project.search 使用 {\"query\":\"要查找的字面文本\",\"path\":\"\",\"maxResults\":20,\"caseSensitive\":false},path 为空字符串时搜索整个项目,返回 path:line 和匹配行;project.verify 使用 {\"script\":\"check|typecheck|test|lint|build\",\"expectedCommand\":\"从 package.json 读取的完整原始脚本\",\"timeoutSeconds\":120},只执行项目根 package.json 中同名 npm 脚本,expectedCommand 不一致时拒绝执行,确认策略以当前工具策略中 project.verify 的独立权限为准;project.checkpoint 使用空对象,只用于多个 file.* 写动作前或需要独立回退点时创建本地 checkpoint;project.patchset 会自动创建 checkpoint,不要为同一批变更额外调用 project.checkpoint;project.restore 使用 {\"checkpointId\":\"checkpoint id\"};project.diff 使用 {\"checkpointId\":\"checkpoint id\",\"includeContent\":true,\"maxFiles\":20,\"maxChars\":24000};git.inspect 使用 {\"includeDiff\":true,\"maxFiles\":20,\"maxChars\":24000},只读项目根 Git 状态和有界 diff,不得用它提交、暂存、切分支、合并、重置、stash、worktree 或访问 remote。".to_string()
|
||||
};
|
||||
let prompt = format!(
|
||||
concat!(
|
||||
|
||||
@@ -28,7 +28,6 @@ pub(crate) fn agent_runtime_executable_tools() -> Vec<&'static str> {
|
||||
"project.index",
|
||||
"project.search",
|
||||
"project.verify",
|
||||
"project.bootstrap",
|
||||
"project.checkpoint",
|
||||
"project.restore",
|
||||
"project.diff",
|
||||
@@ -84,7 +83,6 @@ pub(crate) fn agent_runtime_acceptance_evidence_tools() -> BTreeSet<&'static str
|
||||
"project.index",
|
||||
"project.search",
|
||||
"project.verify",
|
||||
"project.bootstrap",
|
||||
"project.diff",
|
||||
"git.inspect",
|
||||
"project.patchset",
|
||||
|
||||
@@ -156,7 +156,6 @@ pub(super) const AGENT_RUNTIME_AUTONOMOUS_GAME_BUILD_AUTO_COMMAND_IDS: &[&str] =
|
||||
"file.delete",
|
||||
"project.patchset",
|
||||
"project.verify",
|
||||
"project.bootstrap",
|
||||
"command.run_limited",
|
||||
"preview.validate",
|
||||
"canvas.asset_generate",
|
||||
|
||||
@@ -109,7 +109,6 @@ pub(crate) fn agent_runtime_tool_requires_repository_context_fingerprint_gate(to
|
||||
| "project.index"
|
||||
| "project.search"
|
||||
| "project.verify"
|
||||
| "project.bootstrap"
|
||||
| "project.checkpoint"
|
||||
| "project.patchset"
|
||||
| "project.restore"
|
||||
|
||||
@@ -857,12 +857,6 @@ pub(crate) async fn observe_agent_runtime_project_verify(
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let cwd = input
|
||||
.get("cwd")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or(".")
|
||||
.to_string();
|
||||
let timeout_seconds =
|
||||
agent_runtime_tool_input_usize(input, &["timeoutSeconds", "timeout_seconds"])
|
||||
.unwrap_or(AGENT_RUNTIME_PROJECT_VERIFY_DEFAULT_TIMEOUT_SECONDS);
|
||||
@@ -902,7 +896,6 @@ pub(crate) async fn observe_agent_runtime_project_verify(
|
||||
script.as_str(),
|
||||
expected_command.as_str(),
|
||||
timeout_seconds,
|
||||
cwd.as_str(),
|
||||
|| {
|
||||
verification_state = Some(begin_agent_runtime_project_verification_locked(
|
||||
root,
|
||||
@@ -938,7 +931,6 @@ pub(crate) async fn observe_agent_runtime_project_verify(
|
||||
"script": verification.script,
|
||||
"expectedCommand": audit_expected_command,
|
||||
"packageManager": verification.package_manager,
|
||||
"cwd": verification.cwd_relative,
|
||||
"status": verification.status,
|
||||
"exitCode": verification.exit_code,
|
||||
"timedOut": verification.timed_out,
|
||||
@@ -1047,81 +1039,3 @@ pub(crate) async fn observe_agent_runtime_project_verify(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn observe_agent_runtime_project_bootstrap(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
action_id: Option<&str>,
|
||||
action_fingerprint: &str,
|
||||
input: &serde_json::Value,
|
||||
) -> AgentRuntimeToolObservation {
|
||||
let cwd = input
|
||||
.get("cwd")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let timeout = agent_runtime_tool_input_usize(input, &["timeoutSeconds", "timeout_seconds"])
|
||||
.unwrap_or(300);
|
||||
if cwd != "game" {
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "project.bootstrap".to_string(),
|
||||
status: "failed".to_string(),
|
||||
summary: "project.bootstrap 只允许 cwd=game".to_string(),
|
||||
detail: None,
|
||||
};
|
||||
}
|
||||
let _lock = match acquire_project_write_lock(root, "project.bootstrap") {
|
||||
Ok(lock) => lock,
|
||||
Err(error) => {
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "project.bootstrap".to_string(),
|
||||
status: "failed".to_string(),
|
||||
summary: "project.bootstrap 无法取得项目执行锁".to_string(),
|
||||
detail: Some(redact_agent_runtime_project_paths(root, &error, 240)),
|
||||
}
|
||||
}
|
||||
};
|
||||
let result = crate::project::run_project_bootstrap_at(root, timeout as u64).await;
|
||||
match result {
|
||||
Ok(value) if value.status == "completed" => {
|
||||
if let Err(error) = append_agent_db_record(
|
||||
root,
|
||||
serde_json::json!({
|
||||
"recordType":"agent.runtime.project.bootstrap", "agentId":agent_id, "runId":run_id,
|
||||
"actionId":action_id, "actionFingerprint":action_fingerprint, "cwd":"game",
|
||||
"packageSha256":value.package_sha256, "lockSha256":value.lock_sha256,
|
||||
"status":value.status, "logPath":value.log_path, "output":value.output
|
||||
}),
|
||||
) {
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "project.bootstrap".to_string(),
|
||||
status: AGENT_RUNTIME_TOOL_OBSERVATION_STATUS_NEEDS_RECONCILIATION.to_string(),
|
||||
summary: "project.bootstrap 已安装,但审计记录无法落盘".to_string(),
|
||||
detail: Some(redact_agent_runtime_project_paths(root, &error, 500)),
|
||||
};
|
||||
}
|
||||
AgentRuntimeToolObservation {
|
||||
tool: "project.bootstrap".to_string(),
|
||||
status: "ok".to_string(),
|
||||
summary: "game 依赖安装已完成".to_string(),
|
||||
detail: Some(format!(
|
||||
"cwd=game · packageSha256={} · lockSha256={}",
|
||||
value.package_sha256,
|
||||
value.lock_sha256.as_deref().unwrap_or("none")
|
||||
)),
|
||||
}
|
||||
}
|
||||
Ok(value) => AgentRuntimeToolObservation {
|
||||
tool: "project.bootstrap".to_string(),
|
||||
status: "failed".to_string(),
|
||||
summary: "game 依赖安装失败".to_string(),
|
||||
detail: Some(value.output),
|
||||
},
|
||||
Err(error) => AgentRuntimeToolObservation {
|
||||
tool: "project.bootstrap".to_string(),
|
||||
status: "failed".to_string(),
|
||||
summary: redact_agent_runtime_project_paths(root, &error, 240),
|
||||
detail: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1009,8 +1009,7 @@ fn runtime_tool_description(tool: &str) -> &'static str {
|
||||
}
|
||||
"project.index" => "刷新并读取有界仓库启动上下文。",
|
||||
"project.search" => "在项目文本文件中做有界字面量搜索。",
|
||||
"project.verify" => "在项目或指定相对 cwd 中运行 package.json 原样声明的验证脚本,并检查构建产物。",
|
||||
"project.bootstrap" => "仅在项目 game 目录受控执行无参数 npm install,并记录依赖文件指纹。",
|
||||
"project.verify" => "运行 package.json 中原样声明的验证脚本。",
|
||||
"project.checkpoint" => "创建项目本地 checkpoint。",
|
||||
"project.restore" => "从 checkpoint 恢复当前项目。",
|
||||
"project.diff" => "读取 checkpoint 与当前项目之间的有界差异。",
|
||||
@@ -1155,18 +1154,10 @@ fn runtime_tool_input_schema(tool: &str) -> Value {
|
||||
}
|
||||
}),
|
||||
"project.verify" => json!({
|
||||
"type": "object", "required": ["script", "expectedCommand", "timeoutSeconds", "cwd"], "additionalProperties": false,
|
||||
"type": "object", "required": ["script", "expectedCommand", "timeoutSeconds"], "additionalProperties": false,
|
||||
"properties": {
|
||||
"script": { "type": "string", "minLength": 1 },
|
||||
"expectedCommand": { "type": "string", "minLength": 1 },
|
||||
"timeoutSeconds": { "type": "integer", "minimum": 1, "maximum": 600 },
|
||||
"cwd": { "type": ["string", "null"], "pattern": "^[A-Za-z0-9._/-]+$" }
|
||||
}
|
||||
}),
|
||||
"project.bootstrap" => json!({
|
||||
"type": "object", "required": ["cwd", "timeoutSeconds"], "additionalProperties": false,
|
||||
"properties": {
|
||||
"cwd": { "type": "string", "const": "game" },
|
||||
"timeoutSeconds": { "type": "integer", "minimum": 1, "maximum": 600 }
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -647,9 +647,9 @@ pub(crate) fn register_design_artifacts_at(root: &Path) -> Result<usize, String>
|
||||
register_local_asset_at(
|
||||
root,
|
||||
&relative,
|
||||
"document",
|
||||
"design-document",
|
||||
media_type,
|
||||
"document",
|
||||
"design-document",
|
||||
GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Uploaded,
|
||||
canvas_project_id: None,
|
||||
|
||||
@@ -321,44 +321,6 @@ pub(crate) fn resolve_project_command_spec_at(
|
||||
.map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Validation, error))
|
||||
}
|
||||
|
||||
/// Resolves the one privileged npm operation used to hydrate a DirectProject.
|
||||
/// It intentionally bypasses the general command.exec npm allow-list: callers
|
||||
/// must use the dedicated `project.bootstrap` action, which only accepts the
|
||||
/// literal `npm install` in the project's `game` directory.
|
||||
pub(crate) fn resolve_project_bootstrap_spec_at(
|
||||
root: &Path,
|
||||
timeout_seconds: u64,
|
||||
) -> Result<ProjectCommandSpec, ProjectCommandError> {
|
||||
validate_project_root(root)
|
||||
.map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Validation, error))?;
|
||||
let cwd_relative = "game".to_string();
|
||||
let cwd = resolve_local_project_path(root, &cwd_relative)
|
||||
.map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Validation, error))?;
|
||||
validate_project_command_cwd_components(root, &cwd_relative)
|
||||
.map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Validation, error))?;
|
||||
if !(PROJECT_COMMAND_MIN_TIMEOUT_SECONDS..=PROJECT_COMMAND_MAX_TIMEOUT_SECONDS)
|
||||
.contains(&timeout_seconds)
|
||||
{
|
||||
return Err(ProjectCommandError::new(
|
||||
ProjectCommandErrorStage::Validation,
|
||||
format!("project.bootstrap timeoutSeconds 必须在 {PROJECT_COMMAND_MIN_TIMEOUT_SECONDS}-{PROJECT_COMMAND_MAX_TIMEOUT_SECONDS} 之间"),
|
||||
));
|
||||
}
|
||||
let program = "npm".to_string();
|
||||
let (executable, safe_path) = resolve_project_command_executable(root, &program)
|
||||
.map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Validation, error))?;
|
||||
Ok(ProjectCommandSpec {
|
||||
program,
|
||||
executable,
|
||||
safe_path,
|
||||
arguments: vec!["install".to_string()],
|
||||
cwd_relative,
|
||||
cwd,
|
||||
timeout_seconds,
|
||||
verification_eligible: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_project_command_spec_inner(
|
||||
root: &Path,
|
||||
program: &str,
|
||||
@@ -1240,19 +1202,6 @@ pub(crate) fn prepare_project_command_launch_spec(
|
||||
(OsString::from("NO_PROXY"), OsString::new()),
|
||||
(OsString::from("PATH"), spec.safe_path.clone()),
|
||||
];
|
||||
if spec
|
||||
.arguments
|
||||
.first()
|
||||
.is_some_and(|argument| argument == "install")
|
||||
{
|
||||
for (name, value) in &mut environment {
|
||||
match name.to_string_lossy().as_ref() {
|
||||
"npm_config_offline" => *value = OsString::from("false"),
|
||||
"HTTP_PROXY" | "HTTPS_PROXY" | "ALL_PROXY" => *value = OsString::new(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
for name in ["SystemRoot", "PATHEXT", "RUSTUP_HOME"] {
|
||||
if let Some(value) = std::env::var_os(name) {
|
||||
environment.push((OsString::from(name), value));
|
||||
@@ -2049,16 +1998,6 @@ pub(crate) async fn run_project_command_with_output_at(
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn run_project_bootstrap_command_at(
|
||||
root: &Path,
|
||||
timeout_seconds: u64,
|
||||
) -> Result<ProjectCommandResult, ProjectCommandError> {
|
||||
let spec = resolve_project_bootstrap_spec_at(root, timeout_seconds)?;
|
||||
let launch = prepare_project_command_launch_spec(root, &spec)?;
|
||||
let staged = stage_project_command_launch_spec(&spec, launch)?;
|
||||
run_prepared_project_command_with_output_at(root, &spec, staged, None, || Ok(())).await
|
||||
}
|
||||
|
||||
pub(crate) async fn run_prepared_project_command_with_output_at<F>(
|
||||
root: &Path,
|
||||
spec: &ProjectCommandSpec,
|
||||
|
||||
@@ -302,19 +302,7 @@ mod linux {
|
||||
cwd: &Path,
|
||||
environment: &[(OsString, OsString)],
|
||||
) -> Result<CommandSandboxLaunch, CommandSandboxError> {
|
||||
let mut metadata = CommandSandboxMetadata::enforced_linux();
|
||||
let network_enabled = executable
|
||||
.file_name()
|
||||
.and_then(OsStr::to_str)
|
||||
.is_some_and(|name| {
|
||||
name.eq_ignore_ascii_case("npm") || name.eq_ignore_ascii_case("npm.cmd")
|
||||
})
|
||||
&& arguments
|
||||
.first()
|
||||
.is_some_and(|argument| argument == "install");
|
||||
if network_enabled {
|
||||
metadata.network = "enabled";
|
||||
}
|
||||
let metadata = CommandSandboxMetadata::enforced_linux();
|
||||
let bwrap = find_trusted_bwrap().map_err(|error| {
|
||||
CommandSandboxError::new(
|
||||
format!("command sandbox unavailable: {error}"),
|
||||
@@ -375,7 +363,7 @@ mod linux {
|
||||
)?;
|
||||
let fixed_system_read_only = collect_fixed_system_mounts();
|
||||
|
||||
let mut launch = build_linux_bwrap_launch(LinuxSandboxPlan {
|
||||
let launch = build_linux_bwrap_launch(LinuxSandboxPlan {
|
||||
bwrap,
|
||||
root,
|
||||
cwd,
|
||||
@@ -387,7 +375,6 @@ mod linux {
|
||||
external_read_only,
|
||||
fixed_system_read_only,
|
||||
});
|
||||
launch.metadata = metadata.clone();
|
||||
run_project_mount_preflight(&launch).map_err(|error| {
|
||||
CommandSandboxError::new(
|
||||
format!("command sandbox project mount preflight 失败:{error}"),
|
||||
@@ -615,19 +602,7 @@ mod linux {
|
||||
|
||||
fn build_linux_bwrap_launch(plan: LinuxSandboxPlan) -> CommandSandboxLaunch {
|
||||
let mut args = Vec::<OsString>::new();
|
||||
push_namespace_arguments(
|
||||
&mut args,
|
||||
plan.executable
|
||||
.file_name()
|
||||
.and_then(OsStr::to_str)
|
||||
.is_some_and(|name| {
|
||||
name.eq_ignore_ascii_case("npm") || name.eq_ignore_ascii_case("npm.cmd")
|
||||
})
|
||||
&& plan
|
||||
.arguments
|
||||
.first()
|
||||
.is_some_and(|argument| argument == "install"),
|
||||
);
|
||||
push_namespace_arguments(&mut args);
|
||||
push_ro_bind(&mut args, Path::new("/usr"), Path::new("/usr"));
|
||||
for (target, destination) in &plan.merged_usr_links {
|
||||
push_option(
|
||||
@@ -698,7 +673,7 @@ mod linux {
|
||||
}
|
||||
}
|
||||
|
||||
fn push_namespace_arguments(args: &mut Vec<OsString>, share_network: bool) {
|
||||
fn push_namespace_arguments(args: &mut Vec<OsString>) {
|
||||
for argument in [
|
||||
"--die-with-parent",
|
||||
"--unshare-all",
|
||||
@@ -711,9 +686,6 @@ mod linux {
|
||||
] {
|
||||
args.push(OsString::from(argument));
|
||||
}
|
||||
if share_network {
|
||||
args.push(OsString::from("--share-net"));
|
||||
}
|
||||
}
|
||||
|
||||
fn push_option<'a, I>(args: &mut Vec<OsString>, option: &str, values: I)
|
||||
@@ -857,7 +829,7 @@ mod linux {
|
||||
merged_usr_links: &[(OsString, PathBuf)],
|
||||
) -> Result<(), String> {
|
||||
let mut args = Vec::<OsString>::new();
|
||||
push_namespace_arguments(&mut args, false);
|
||||
push_namespace_arguments(&mut args);
|
||||
push_ro_bind(&mut args, Path::new("/usr"), Path::new("/usr"));
|
||||
for (target, destination) in merged_usr_links {
|
||||
push_option(
|
||||
|
||||
@@ -1945,36 +1945,28 @@ pub(crate) fn read_platform_account_session_generation() -> u64 {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn install_platform_account_session(
|
||||
pub(crate) fn install_platform_account_session(
|
||||
user_id: String,
|
||||
access_token: String,
|
||||
api_base_url: String,
|
||||
generation: u64,
|
||||
) -> Result<(), String> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
validate_platform_session_input(&user_id, &access_token, &api_base_url, generation)?;
|
||||
install_external_agent_runner_platform_session(
|
||||
&user_id,
|
||||
&access_token,
|
||||
&api_base_url,
|
||||
generation,
|
||||
)?;
|
||||
install_platform_session(&user_id, &access_token, &api_base_url, generation)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("安装本地运行时会话任务意外终止:{error}"))?
|
||||
validate_platform_session_input(&user_id, &access_token, &api_base_url, generation)?;
|
||||
install_external_agent_runner_platform_session(
|
||||
&user_id,
|
||||
&access_token,
|
||||
&api_base_url,
|
||||
generation,
|
||||
)?;
|
||||
install_platform_session(&user_id, &access_token, &api_base_url, generation)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn clear_platform_account_session(generation: u64) -> Result<(), String> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
shutdown_game_creator_codex_app_servers()?;
|
||||
clear_external_agent_runner_platform_session(generation)?;
|
||||
clear_platform_session(generation);
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("清除本地运行时会话任务意外终止:{error}"))?
|
||||
pub(crate) fn clear_platform_account_session(generation: u64) -> Result<(), String> {
|
||||
shutdown_game_creator_codex_app_servers()?;
|
||||
clear_external_agent_runner_platform_session(generation)?;
|
||||
clear_platform_session(generation);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user