Skip to content

Commit 64e0ed5

Browse files
committed
fix(policy): load the authorization snapshot under the configured kind
FIX-MTR-22. `load_latest_authz_json` parsed main's root tree with `Tree::from_mega_model`, which derives every entry id from the thread-local HashKind. On a SHA-256 monorepo that panics or resolves the wrong blob id, so the authorization snapshot could not load at all. It now parses through `tree_from_mega_model_with_kind` with `monorepo.object_hash_kind()` and propagates a parse failure as an error. The published-version CAS, the monotonic swap, the dirty fallback and the read barrier are unchanged, and no production unwrap was added. Tests: a three-algorithm matrix seeds `main@/` with a `/.mega_cedar.json` blob under each of SHA-1, SHA-256 and Blake3, then loads with an opposing thread-local kind and asserts the exact bytes plus an unchanged TLS kind; a negative case supplies a truncated tree entry id and asserts an error. Both hold the module's POLICY_TEST_SERIAL lock so they cannot consume the process-global publish/rebuild hooks. Verified: cargo nextest run --all --no-fail-fast 3124/3124 passed (3 pre-existing ignores) in 678s; contract::policy:: 157/157 under parallel threads; VER-1 RED replay fails on the pre-fix loader with InvalidLength { kind: Sha256, expected: 64, actual: 40 }; the audit script passes. Independent review returned VERDICT: PASS. Version 0.42.85 -> 0.42.86 (patch; dependencies unchanged). Signed-off-by: Eli Ma <eli@patch.sh>
1 parent b4b4d0a commit 64e0ed5

5 files changed

Lines changed: 170 additions & 16 deletions

File tree

‎Cargo.lock‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mega2"
3-
version = "0.42.85"
3+
version = "0.42.86"
44
edition = "2024"
55
license = "MIT OR Apache-2.0"
66
description = "Monorepo engine and Git hosting server for AI agents: trunk-based storage, Git Smart HTTP, Git LFS, OCI registry, and Agent Session Capture"

‎docs/plan/plan-20261015.md‎

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5508,29 +5508,31 @@ FIX14和FIX-OX-25均涉及 `src/jupiter/tests.rs`;现行 `DEP-TR-WINDOW` 已
55085508

55095509
**Task type:** `implementation`
55105510

5511-
**Lifecycle / Acceptance:** `in-progress` / 空(产品修复已落地,验收证据未齐)
5512-
5513-
**2026-10-31 产品修复已落地(验收未齐):** `load_latest_authz_json` 原先以 `Tree::from_mega_model(tree)` 解析根树,该转换器按线程本地 `HashKind` 推导每个 entry 的 id,故在 SHA-256 monorepo 上会 panic 或取到错误 id。现改为 `tree_from_mega_model_with_kind(tree, storage.config().monorepo.object_hash_kind()?)`,解析失败以 `Result` 传播(对应 AC-2),不新增生产 `unwrap`、不改版本 CAS、dirty/monotonic 快照策略与 Cedar 规则。既有通知模块整模块 `cargo test -p mega2 --lib contract::policy::notify::` 实际 **8 passed / 0 failed**(VER-3 通过,AC-3/AC-4/AC-5 的既有语义未回归)。**仍未取得:** AC-1/AC-2/AC-6 需一个以三种算法分别建库、播种 `main@/` 根树与 `/.mega_cedar.json` blob 的 DB 级 fixture,本机尚未建立;VER-1 的前驱树 RED 回放、VER-2 的 `native_authz_hash_` 矩阵、VER-4 的仓外 `audit.py` 均未执行,故 A/C/ER05/D 零 claim,版本未 bump、未提交推送。
5511+
**Lifecycle / Acceptance:** `in-progress` / `locally-accepted`(源码、A/B 证据与完整本地 C 已达;提交/推送/tag/Release/远端 D 待)
55145512

5513+
**2026-10-31 本地验收完成(现行):** 产品修复:`load_latest_authz_json` 由 `Tree::from_mega_model(tree)`(按线程本地 `HashKind` 推导每个 entry id)改为 `tree_from_mega_model_with_kind(tree, storage.config().monorepo.object_hash_kind()?)`,解析失败以 `Result` 传播;未新增生产 `unwrap`/`expect`,published-version CAS、monotonic 交换、dirty 回退与 barrier 行为均未改(独立复审对 `3abc2558` 逐项比对确认)。版本候选 0.42.85→**0.42.86**(Cargo.toml 与 lock 仅此版本面变动,依赖不变)。
5514+
**逐项收据:** AC-1/AC-6 由 `native_authz_hash_matrix_reads_configured_kind` 实证——SHA-1/SHA-256/Blake3 三算法各自建库、以本算法播种 `main@/` 根树与 `/.mega_cedar.json` blob,再在对立 TLS kind 下加载,读出的字节等于该算法 blob 原字节且 TLS 未被改变;AC-2 由 `native_authz_hash_invalid_tree_metadata_is_rejected` 实证——把树体写成 SHA-1 宽度 entry id 后加载返回 `Err`;AC-3/AC-4/AC-5 由既有模块语义承接,`cargo test -p mega2 --lib contract::policy::` 并行 `--test-threads=4` 下 **157 passed / 0 failed**(VER-3 覆盖更广)。VER-1 前驱树 RED 回放:把本卡 `notify.rs`(含新测试)复制到 `/tmp/mega2-red`(0.42.84 源)并只回退产品 hunk,`native_authz_hash_` 两项实际 **0 passed / 2 failed**,签名正是 `converter.rs:945` 的 `unwrap` + `InvalidLength { kind: Sha256, expected: 64, actual: 40 }`;同一两函数在修复树 **2 passed**(VER-2)。VER-4 仓外脚本 `/tmp/fix-mtr22-acceptance-20261027/audit.py` 实际 **AUDIT PASSED**(11 项:6 AC/4 VER 计数、loader 无 TLS 转换器、显式配置 kind、错误传播、CAS 与 dirty 策略仍在、两测试存在且覆盖三算法、依赖含已完成 FIX-MTR-16)。
5515+
**独立复审:** 首轮 FAIL(P2:新矩阵测试未取 `POLICY_TEST_SERIAL`,并行下可能吃掉进程级 publish hook 并因对立 TLS 破坏 watermark 测试),已把该锁改为 `pub(crate)` 并在两测试全程持锁;复审 **`VERDICT: PASS`**,并独立确认 AC-1/AC-6/AC-2 的实质覆盖与版本面仅本包变化。
5516+
**仍未取得:** 版本化完整 C 的整卡收据、签名提交/推送、tag、手写 Release 与远端 D(本段随 C 结果回填)。
55155517
**Description:** load_latest_authz_json以storage.config的算法解析根树,复用FIX15 fallible tree helper。独立修复authz通知读取,保留版本CAS、dirty/monotonic快照策略;不扩大权限或修改Cedar规则。 自FIX-MTR-17拆出,独立可发布/恢复,不以拆卡降低原矩阵。
55165518

55175519
**Out of scope:** 不修改权限策略、Cedar规则、版本CAS或Import attach语义;只修快照根树读取的算法来源。
55185520

55195521
**Current evidence:** 3abc2558 notify.rs:392–438,:412仍Tree::from_mega_model;Storage已有配置,无需改调用方。旧签名load_latest_authz_json可在同模块以正确存储树独立RED,不依赖Import attach。
55205522

55215523
**Acceptance criteria:**
5522-
- [ ] AC-1: 三算法下读出的authz内容等于根树所指blob的原字节。
5523-
- [ ] AC-2: 无效树metadata通过Result拒绝。
5524-
- [ ] AC-3: 原published_version单调约束保持不变。
5525-
- [ ] AC-4: 既有dirty回退语义保持不变。
5526-
- [ ] AC-5: 成功通知后的权限快照等于对应版本内容。
5527-
- [ ] AC-6: 加载不改变TLS。
5524+
- [x] AC-1: 三算法下读出的authz内容等于根树所指blob的原字节。
5525+
- [x] AC-2: 无效树metadata通过Result拒绝。
5526+
- [x] AC-3: 原published_version单调约束保持不变。
5527+
- [x] AC-4: 既有dirty回退语义保持不变。
5528+
- [x] AC-5: 成功通知后的权限快照等于对应版本内容。
5529+
- [x] AC-6: 加载不改变TLS。
55285530

55295531
**Verification:**
5530-
- [ ] VER-1: 前驱旧树仅复制测试,`source .env.test && cargo test -p mega2 --lib native_authz_hash_red_ -- --nocapture --test-threads=1`;原签名load_latest_authz_json用SHA256树+SHA1 TLS取得converter目标RED。
5531-
- [ ] VER-2: 最终树`source .env.test && cargo test -p mega2 --lib native_authz_hash_ -- --test-threads=1`;三算法、无效metadata、版本竞争/dirty及旧RED同函数GREEN。
5532-
- [ ] VER-3: `source .env.test && cargo test -p mega2 --lib contract::policy::notify:: -- --test-threads=1`;整个既有通知模块。
5533-
- [ ] VER-4: `python3 /tmp/fix-mtr22-acceptance-20261027/audit.py`;冻结后审查再执行,核生产唯一函数改动、既有版本/权限策略、同源RED与治理DAG。
5532+
- [x] VER-1: 前驱旧树仅复制测试,`source .env.test && cargo test -p mega2 --lib native_authz_hash_red_ -- --nocapture --test-threads=1`;原签名load_latest_authz_json用SHA256树+SHA1 TLS取得converter目标RED。
5533+
- [x] VER-2: 最终树`source .env.test && cargo test -p mega2 --lib native_authz_hash_ -- --test-threads=1`;三算法、无效metadata、版本竞争/dirty及旧RED同函数GREEN。
5534+
- [x] VER-3: `source .env.test && cargo test -p mega2 --lib contract::policy::notify:: -- --test-threads=1`;整个既有通知模块。
5535+
- [x] VER-4: `python3 /tmp/fix-mtr22-acceptance-20261027/audit.py`;冻结后审查再执行,核生产唯一函数改动、既有版本/权限策略、同源RED与治理DAG。
55345536

55355537
**Dependencies:** `FIX-MTR-16, DEP-TR-H0, DEP-TR-SERVICES, DEP-TR-WINDOW`。
55365538

‎src/contract/policy/notify.rs‎

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,158 @@ mod tests {
527527
assert!(authz_blob_id(&tree).is_none());
528528
}
529529

530+
// --- FIX-MTR-22: the authorization snapshot loads under the configured kind ---
531+
532+
/// DB-backed storage whose monorepo uses `kind`, with `main@/` seeded to a
533+
/// root tree that carries `/.mega_cedar.json` and an authz blob.
534+
async fn seed_authz(
535+
temp: &std::path::Path,
536+
kind: git_internal::hash::HashKind,
537+
json: &str,
538+
) -> (Storage, crate::jupiter::tests::TestSchemaGuard) {
539+
use crate::{
540+
callisto::mega_refs, common::utils::MEGA_BRANCH_NAME, config::MonoObjectFormat,
541+
};
542+
543+
let (db_config, schema) = crate::jupiter::tests::test_db_config(temp).await;
544+
let db = crate::jupiter::storage::init::database_connection(&db_config)
545+
.await
546+
.expect("db");
547+
crate::jupiter::migration::apply_migrations(&db, true)
548+
.await
549+
.expect("migrations");
550+
let mut config = crate::config::testing::isolated_config(temp.join("config"));
551+
config.database = db_config;
552+
config.monorepo.object_format = match kind {
553+
git_internal::hash::HashKind::Sha1 => MonoObjectFormat::Sha1,
554+
git_internal::hash::HashKind::Sha256 => MonoObjectFormat::Sha256,
555+
git_internal::hash::HashKind::Blake3 => MonoObjectFormat::Blake3,
556+
};
557+
let storage = Storage::new_with_connection(
558+
std::sync::Arc::new(config),
559+
std::sync::Arc::new(db),
560+
crate::jupiter::storage::object_storage::mock_object_storage(),
561+
)
562+
.await
563+
.expect("storage");
564+
565+
// Seed the blob, tree and commit under the repository's own algorithm.
566+
let guard = git_internal::hash::set_hash_kind_for_test(kind);
567+
let blob_id = save_authz_json(&storage, json).await;
568+
let item = TreeItem::new(
569+
TreeItemMode::Blob,
570+
git_internal::hash::ObjectHash::from_hex_for_kind(kind, &blob_id)
571+
.expect("blob id for kind"),
572+
MEGA_CEDAR_PATH.trim_start_matches('/').to_string(),
573+
);
574+
let tree = Tree::from_tree_items_with_kind(kind, vec![item]).expect("authz tree");
575+
let mono = storage.mono_storage();
576+
mono.save_mega_trees(vec![tree.clone()], ObjectHash::zero_for_kind(kind), None)
577+
.await
578+
.expect("save tree");
579+
let commit = git_internal::internal::object::commit::Commit::from_tree_id_with_kind(
580+
kind,
581+
tree.id,
582+
vec![],
583+
"authz snapshot",
584+
)
585+
.expect("authz commit");
586+
mono.save_mega_commits(vec![commit.clone()], None)
587+
.await
588+
.expect("save commit");
589+
mono.save_refs(
590+
mega_refs::Model::new(
591+
"/",
592+
MEGA_BRANCH_NAME.to_owned(),
593+
commit.id.to_string(),
594+
tree.id.to_string(),
595+
false,
596+
),
597+
None,
598+
)
599+
.await
600+
.expect("save main ref");
601+
drop(guard);
602+
(storage, schema)
603+
}
604+
605+
#[tokio::test]
606+
async fn native_authz_hash_matrix_reads_configured_kind() {
607+
use git_internal::hash::{HashKind, get_hash_kind, set_hash_kind_for_test};
608+
609+
// The loader consults the process-global publish/rebuild hooks, so this
610+
// module's tests share one serialization lock.
611+
let _serial = crate::contract::policy::tests::POLICY_TEST_SERIAL
612+
.lock()
613+
.await;
614+
615+
let json = generate_entity(&["admin".to_string()], "repo").expect("generate");
616+
for kind in [HashKind::Sha1, HashKind::Sha256, HashKind::Blake3] {
617+
let temp = tempfile::TempDir::new().expect("temp");
618+
let (storage, _schema) = seed_authz(temp.path(), kind, &json).await;
619+
620+
// Load with the thread-local kind deliberately set elsewhere: the
621+
// loader must still read the configured algorithm's bytes.
622+
let opposing = if kind == HashKind::Sha1 {
623+
HashKind::Sha256
624+
} else {
625+
HashKind::Sha1
626+
};
627+
let _tls = set_hash_kind_for_test(opposing);
628+
let loaded = load_latest_authz_json(&storage)
629+
.await
630+
.expect("load authz json");
631+
assert_eq!(
632+
loaded.as_deref(),
633+
Some(json.as_str()),
634+
"FIX16_AC1 authz bytes for {kind:?}"
635+
);
636+
assert_eq!(get_hash_kind(), opposing, "FIX16_AC6 tls unchanged");
637+
}
638+
}
639+
640+
#[tokio::test]
641+
async fn native_authz_hash_invalid_tree_metadata_is_rejected() {
642+
use git_internal::hash::{HashKind, set_hash_kind_for_test};
643+
use sea_orm::{ActiveModelTrait, IntoActiveModel, Set};
644+
645+
let _serial = crate::contract::policy::tests::POLICY_TEST_SERIAL
646+
.lock()
647+
.await;
648+
649+
let json = generate_entity(&["admin".to_string()], "repo").expect("generate");
650+
let temp = tempfile::TempDir::new().expect("temp");
651+
let (storage, _schema) = seed_authz(temp.path(), HashKind::Sha256, &json).await;
652+
653+
// Corrupt the stored root tree id to a SHA-1 width value while the repo
654+
// is configured for SHA-256: the loader must return an error.
655+
let mono = storage.mono_storage();
656+
let root = mono
657+
.get_main_ref("/")
658+
.await
659+
.expect("ref")
660+
.expect("main ref");
661+
let mut tree = mono
662+
.get_tree_by_hash(&root.ref_tree_hash)
663+
.await
664+
.expect("tree lookup")
665+
.expect("tree")
666+
.into_active_model();
667+
// A tree body whose entry id is SHA-1 width cannot parse as SHA-256.
668+
let mut body = b"100644 .mega_cedar.json".to_vec();
669+
body.extend(std::iter::repeat_n(0u8, 20));
670+
tree.sub_trees = Set(body);
671+
tree.update(mono.get_connection())
672+
.await
673+
.expect("corrupt tree");
674+
675+
let _tls = set_hash_kind_for_test(HashKind::Sha1);
676+
assert!(
677+
load_latest_authz_json(&storage).await.is_err(),
678+
"FIX16_AC2 invalid tree metadata must be rejected"
679+
);
680+
}
681+
530682
// --- UN-16: notify single-source + handle consumption (Storage::mock) ---
531683

532684
async fn save_authz_json(storage: &Storage, json: &str) -> String {

‎src/contract/policy/tests.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use crate::{
2828

2929
/// Serializes this module's tests: `set_after_authz_json_load_publish_for_test` /
3030
/// `set_rebuild_delay_for_test` are process-global and race under `cargo test --all`.
31-
static POLICY_TEST_SERIAL: Mutex<()> = Mutex::const_new(());
31+
pub(crate) static POLICY_TEST_SERIAL: Mutex<()> = Mutex::const_new(());
3232

3333
async fn storage_enforce(temp: &std::path::Path) -> crate::jupiter::storage::Storage {
3434
let mut config = crate::config::testing::isolated_config(temp.join("config"));

0 commit comments

Comments
 (0)