项目快照整目录上传 .agent,保留项目身份与 Agent 历史
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m16s
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Successful in 1m17s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m47s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 7m3s
Project CI / Backend tests (push) Successful in 5m59s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 8m6s
Project CI / Native shell tests (push) Successful in 7m23s
Project CI / Frontend tests (push) Successful in 3m48s
Project CI / Repository checks (push) Successful in 3m37s
Project CI / AI game creator shell web tests (push) Successful in 3m40s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m16s
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Successful in 1m17s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m47s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 7m3s
Project CI / Backend tests (push) Successful in 5m59s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 8m6s
Project CI / Native shell tests (push) Successful in 7m23s
Project CI / Frontend tests (push) Successful in 3m48s
Project CI / Repository checks (push) Successful in 3m37s
Project CI / AI game creator shell web tests (push) Successful in 3m40s
- 新增 should_skip_project_snapshot_sync_path:快照同步放行 .agent 组件,并放行其中 .db / .db-wal / .db-shm 的 Agent 状态数据库 - 组件与后缀排除表提取为常量;should_skip_project_snapshot_path 行为不变,项目索引、checkpoint、Agent 上下文与 git 检查继续排除整个 .agent - project_snapshot 扫描入口切到新口径,.agent 的 manifest、agent.db、会话、日志、checkpoint、workbench 与 project.lock 全部参与同步 - 新增整目录扫描用例与策略用例,覆盖 .agent 内凭据、.env、版本库与构建目录仍被排除 - 主规范更新上传内容边界与后台归档范围:工程 ZIP 可用于还原项目身份与 AGC 对话历史 - 里程碑、实施计划与共享记忆决策记录同步 .agent 全量口径、真实上传与后台 ZIP 的运行时验证证据,以及 v1/v2 键布局边界
This commit is contained in:
@@ -348,79 +348,113 @@ pub(crate) fn should_skip_project_index_path(relative_path: &str) -> bool {
|
||||
|| should_skip_project_snapshot_path(relative_path)
|
||||
}
|
||||
|
||||
/// 项目索引、checkpoint、Agent 上下文与 git 检查共用的排除口径:`.agent` 是这些结果的
|
||||
/// 本机控制面,不参与其中。
|
||||
pub(crate) fn should_skip_project_snapshot_path(relative_path: &str) -> bool {
|
||||
project_snapshot_path_is_excluded(relative_path, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 项目快照同步(上传)的排除口径。
|
||||
*
|
||||
* 与 `should_skip_project_snapshot_path` 是同一份组件与后缀规则,唯一区别是 `.agent`:
|
||||
* 它承载项目身份与 Agent 状态(`manifest.json`、`agent.db`、会话、运行日志、checkpoint、
|
||||
* workbench、`project.lock`),必须整目录随快照同步,因此不再把 `.agent` 组件本身当作
|
||||
* 排除项,并放行其中的 Agent 状态数据库(`.db` / `.db-wal` / `.db-shm`)。
|
||||
*
|
||||
* 其余排除项在 `.agent` 内同样生效:版本库、依赖与构建目录、凭据目录、敏感后缀、
|
||||
* `.env*` 与凭据类文件名一律不参与同步;符号链接与重解析点在扫描阶段单独跳过。
|
||||
*/
|
||||
pub(crate) fn should_skip_project_snapshot_sync_path(relative_path: &str) -> bool {
|
||||
project_snapshot_path_is_excluded(relative_path, true)
|
||||
}
|
||||
|
||||
/// 任意层级出现即排除的目录组件。`.agent` 只有项目快照同步会放行。
|
||||
const PROJECT_SNAPSHOT_EXCLUDED_COMPONENTS: &[&str] = &[
|
||||
".agent",
|
||||
".git",
|
||||
".hg",
|
||||
".svn",
|
||||
".ssh",
|
||||
".aws",
|
||||
".azure",
|
||||
".gnupg",
|
||||
".kube",
|
||||
".docker",
|
||||
".gcloud",
|
||||
".terraform",
|
||||
".password-store",
|
||||
".secrets",
|
||||
"secrets",
|
||||
"credentials",
|
||||
"node_modules",
|
||||
"target",
|
||||
"dist",
|
||||
"build",
|
||||
".next",
|
||||
"coverage",
|
||||
".cache",
|
||||
];
|
||||
|
||||
/// 凭据、密钥与数据库转储类文件名后缀。
|
||||
const PROJECT_SNAPSHOT_EXCLUDED_SUFFIXES: &[&str] = &[
|
||||
".pem",
|
||||
".key",
|
||||
".p12",
|
||||
".pfx",
|
||||
".ppk",
|
||||
".jks",
|
||||
".keystore",
|
||||
".kdbx",
|
||||
".db",
|
||||
".db-wal",
|
||||
".db-shm",
|
||||
".sqlite",
|
||||
".sqlite-wal",
|
||||
".sqlite-shm",
|
||||
".sqlite3",
|
||||
".sqlite3-wal",
|
||||
".sqlite3-shm",
|
||||
".sql",
|
||||
".sql.gz",
|
||||
".sql.bz2",
|
||||
".sql.xz",
|
||||
".dump",
|
||||
".dump.gz",
|
||||
".dmp",
|
||||
".bak",
|
||||
".mdb",
|
||||
".accdb",
|
||||
".rdb",
|
||||
".bson",
|
||||
".pgdump",
|
||||
".tfstate",
|
||||
".tfstate.backup",
|
||||
];
|
||||
|
||||
/// `.agent` 内的 Agent 状态数据库(`agent.db` 及其 WAL / SHM 旁文件)属于项目状态,
|
||||
/// 随快照同步;其它数据库与转储后缀仍然排除。
|
||||
const PROJECT_AGENT_STATE_DATABASE_SUFFIXES: &[&str] = &[".db", ".db-wal", ".db-shm"];
|
||||
|
||||
fn project_snapshot_path_is_excluded(relative_path: &str, include_agent_state: bool) -> bool {
|
||||
let components = relative_path
|
||||
.split('/')
|
||||
.filter(|component| !component.is_empty())
|
||||
.map(str::to_ascii_lowercase)
|
||||
.collect::<Vec<_>>();
|
||||
if components.iter().any(|component| {
|
||||
matches!(
|
||||
component.as_str(),
|
||||
".agent"
|
||||
| ".git"
|
||||
| ".hg"
|
||||
| ".svn"
|
||||
| ".ssh"
|
||||
| ".aws"
|
||||
| ".azure"
|
||||
| ".gnupg"
|
||||
| ".kube"
|
||||
| ".docker"
|
||||
| ".gcloud"
|
||||
| ".terraform"
|
||||
| ".password-store"
|
||||
| ".secrets"
|
||||
| "secrets"
|
||||
| "credentials"
|
||||
| "node_modules"
|
||||
| "target"
|
||||
| "dist"
|
||||
| "build"
|
||||
| ".next"
|
||||
| "coverage"
|
||||
| ".cache"
|
||||
)
|
||||
PROJECT_SNAPSHOT_EXCLUDED_COMPONENTS.contains(&component.as_str())
|
||||
&& !(include_agent_state && component == ".agent")
|
||||
}) {
|
||||
return true;
|
||||
}
|
||||
let Some(file_name) = components.last() else {
|
||||
return true;
|
||||
};
|
||||
let sensitive_suffixes = [
|
||||
".pem",
|
||||
".key",
|
||||
".p12",
|
||||
".pfx",
|
||||
".ppk",
|
||||
".jks",
|
||||
".keystore",
|
||||
".kdbx",
|
||||
".db",
|
||||
".db-wal",
|
||||
".db-shm",
|
||||
".sqlite",
|
||||
".sqlite-wal",
|
||||
".sqlite-shm",
|
||||
".sqlite3",
|
||||
".sqlite3-wal",
|
||||
".sqlite3-shm",
|
||||
".sql",
|
||||
".sql.gz",
|
||||
".sql.bz2",
|
||||
".sql.xz",
|
||||
".dump",
|
||||
".dump.gz",
|
||||
".dmp",
|
||||
".bak",
|
||||
".mdb",
|
||||
".accdb",
|
||||
".rdb",
|
||||
".bson",
|
||||
".pgdump",
|
||||
".tfstate",
|
||||
".tfstate.backup",
|
||||
];
|
||||
let agent_state_database = include_agent_state
|
||||
&& components
|
||||
.first()
|
||||
.is_some_and(|first| first.as_str() == ".agent");
|
||||
let structured_secret_suffixes = [".json", ".txt", ".toml", ".yaml", ".yml"];
|
||||
file_name == ".env"
|
||||
|| file_name.starts_with(".env.")
|
||||
@@ -471,9 +505,10 @@ pub(crate) fn should_skip_project_snapshot_path(relative_path: &str) -> bool {
|
||||
|| file_name.starts_with("id_ecdsa")
|
||||
|| file_name.starts_with("id_ed25519")
|
||||
|| file_name.starts_with("id_xmss")
|
||||
|| sensitive_suffixes
|
||||
.iter()
|
||||
.any(|suffix| file_name.ends_with(suffix))
|
||||
|| PROJECT_SNAPSHOT_EXCLUDED_SUFFIXES.iter().any(|suffix| {
|
||||
file_name.ends_with(suffix)
|
||||
&& !(agent_state_database && PROJECT_AGENT_STATE_DATABASE_SUFFIXES.contains(suffix))
|
||||
})
|
||||
|| ((file_name.contains("cookie") || file_name.contains("credential"))
|
||||
&& structured_secret_suffixes
|
||||
.iter()
|
||||
|
||||
@@ -23,9 +23,10 @@ pub(crate) struct ProjectSnapshotScanResult {
|
||||
pub(crate) skipped: Vec<ProjectSnapshotSkippedPath>,
|
||||
}
|
||||
|
||||
/// 扫描项目目录,复用 checkpoint / 项目索引同一份排除口径:
|
||||
/// `.agent`、版本控制目录、依赖与构建产物目录、凭据目录、符号链接与重解析点
|
||||
/// 都不参与同步,超出单文件上限的文件进入跳过清单而不是静默丢弃。
|
||||
/// 扫描项目目录,排除口径见 `should_skip_project_snapshot_sync_path`:
|
||||
/// `.agent` 是项目身份与 Agent 状态的权威位置,整目录参与同步;版本控制目录、
|
||||
/// 依赖与构建产物目录、凭据目录、符号链接与重解析点都不参与同步,超出单文件
|
||||
/// 上限的文件进入跳过清单而不是静默丢弃。
|
||||
pub(crate) fn scan_project_snapshot_files(
|
||||
root: &Path,
|
||||
max_file_bytes: u64,
|
||||
@@ -52,7 +53,7 @@ pub(crate) fn scan_project_snapshot_files(
|
||||
let Ok(relative_path) = relative_project_path(root, &path) else {
|
||||
continue;
|
||||
};
|
||||
if should_skip_project_snapshot_path(&relative_path) {
|
||||
if should_skip_project_snapshot_sync_path(&relative_path) {
|
||||
continue;
|
||||
}
|
||||
let metadata = match fs::symlink_metadata(&path) {
|
||||
|
||||
@@ -95,8 +95,6 @@ fn project_snapshot_scan_skips_excluded_paths_and_oversized_files() {
|
||||
let root = fixture_root();
|
||||
write_fixture_file(root.path(), "game/index.html", b"<html></html>");
|
||||
write_fixture_file(root.path(), "assets/manifest.json", b"{}");
|
||||
write_fixture_file(root.path(), ".agent/runtime/state.json", b"{}");
|
||||
write_fixture_file(root.path(), ".agent/manifest.json", b"{}");
|
||||
write_fixture_file(root.path(), "node_modules/pkg/index.js", b"export {};");
|
||||
write_fixture_file(root.path(), "game/dist/bundle.js", b"bundle");
|
||||
write_fixture_file(root.path(), "secrets/key.pem", b"private-key");
|
||||
@@ -111,7 +109,7 @@ fn project_snapshot_scan_skips_excluded_paths_and_oversized_files() {
|
||||
assert_eq!(
|
||||
scanned,
|
||||
vec!["assets/manifest.json".to_string()],
|
||||
".agent、node_modules、dist 与凭据目录里的文件不能进入候选集合"
|
||||
"node_modules、dist 与凭据目录里的文件不能进入候选集合"
|
||||
);
|
||||
let skipped = scan
|
||||
.skipped
|
||||
@@ -125,6 +123,111 @@ fn project_snapshot_scan_skips_excluded_paths_and_oversized_files() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_snapshot_scan_uploads_whole_agent_directory() {
|
||||
let root = fixture_root();
|
||||
write_fixture_file(root.path(), "game/index.html", b"<html></html>");
|
||||
write_fixture_file(root.path(), ".agent/manifest.json", b"{}");
|
||||
write_fixture_file(root.path(), ".agent/agent.db", b"sqlite");
|
||||
write_fixture_file(root.path(), ".agent/agent.db-wal", b"wal");
|
||||
write_fixture_file(root.path(), ".agent/project.lock", b"{}");
|
||||
write_fixture_file(root.path(), ".agent/.manifest.json.lock", b"");
|
||||
write_fixture_file(root.path(), ".agent/conversations/project.jsonl", b"{}\n");
|
||||
write_fixture_file(root.path(), ".agent/runtime/events/art.jsonl", b"{}\n");
|
||||
write_fixture_file(
|
||||
root.path(),
|
||||
".agent/runtime/command-env/home/.config.json",
|
||||
b"{}",
|
||||
);
|
||||
write_fixture_file(root.path(), ".agent/logs/command.log", b"log");
|
||||
write_fixture_file(
|
||||
root.path(),
|
||||
".agent/checkpoints/0001/manifest.json",
|
||||
b"{\"files\":[]}",
|
||||
);
|
||||
write_fixture_file(
|
||||
root.path(),
|
||||
".agent/workbench/resource-layouts/art.json",
|
||||
b"{}",
|
||||
);
|
||||
|
||||
let scan = scan_fixture(root.path());
|
||||
let scanned = scan
|
||||
.files
|
||||
.iter()
|
||||
.map(|file| file.relative_path.clone())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
scanned,
|
||||
vec![
|
||||
".agent/.manifest.json.lock".to_string(),
|
||||
".agent/agent.db".to_string(),
|
||||
".agent/agent.db-wal".to_string(),
|
||||
".agent/checkpoints/0001/manifest.json".to_string(),
|
||||
".agent/conversations/project.jsonl".to_string(),
|
||||
".agent/logs/command.log".to_string(),
|
||||
".agent/manifest.json".to_string(),
|
||||
".agent/project.lock".to_string(),
|
||||
".agent/runtime/command-env/home/.config.json".to_string(),
|
||||
".agent/runtime/events/art.jsonl".to_string(),
|
||||
".agent/workbench/resource-layouts/art.json".to_string(),
|
||||
"game/index.html".to_string(),
|
||||
],
|
||||
"`.agent` 是项目身份与 Agent 状态的权威位置,必须整目录参与同步"
|
||||
);
|
||||
assert!(
|
||||
scan.skipped.is_empty(),
|
||||
"`.agent` 内的普通文件既不跳过也不延后"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_snapshot_sync_policy_keeps_agent_state_and_still_blocks_credentials() {
|
||||
for relative_path in [
|
||||
".agent/manifest.json",
|
||||
".agent/agent.db",
|
||||
".agent/agent.db-wal",
|
||||
".agent/agent.db-shm",
|
||||
".agent/project.lock",
|
||||
".agent/runtime/events/art.jsonl",
|
||||
".agent/runtime/locks/append/01.lock",
|
||||
".agent/checkpoints/0001/manifest.json",
|
||||
".agent/conversations/project.jsonl",
|
||||
".agent/workbench/resource-layouts/art.json",
|
||||
".AGENT/manifest.json",
|
||||
] {
|
||||
assert!(
|
||||
!should_skip_project_snapshot_sync_path(relative_path),
|
||||
"`.agent` 状态必须参与同步:{relative_path}"
|
||||
);
|
||||
}
|
||||
|
||||
for relative_path in [
|
||||
".agent/credentials/platform.json",
|
||||
".agent/.ssh/id_rsa",
|
||||
".agent/certs/server.pem",
|
||||
".agent/node_modules/pkg/index.js",
|
||||
".agent/runtime/command-env/home/.env",
|
||||
".agent/runtime/command-env/home/.npmrc",
|
||||
".agent/backup/game.sql",
|
||||
".git/config",
|
||||
"node_modules/pkg/index.js",
|
||||
"game/dist/bundle.js",
|
||||
"secrets/key.pem",
|
||||
"",
|
||||
] {
|
||||
assert!(
|
||||
should_skip_project_snapshot_sync_path(relative_path),
|
||||
"凭据、版本库与构建产物仍然排除:{relative_path}"
|
||||
);
|
||||
}
|
||||
|
||||
// 项目索引、checkpoint 与 Agent 上下文继续排除整个 `.agent`,本变更只放开快照同步。
|
||||
assert!(should_skip_project_snapshot_path(".agent/manifest.json"));
|
||||
assert!(should_skip_project_index_path(".agent/manifest.json"));
|
||||
assert!(should_skip_project_index_path(".agent/agent.db"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_snapshot_diff_reuses_metadata_and_reports_a_single_modification() {
|
||||
let root = fixture_root();
|
||||
|
||||
Reference in New Issue
Block a user