From 6f5ba8361654cbc1649a294109d32fee94cdeb11 Mon Sep 17 00:00:00 2001 From: qin-ptr <222410531+qin-ptr@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:32:33 +0800 Subject: [PATCH] =?UTF-8?q?fix(snapshot):=20=E5=A4=8D=E7=94=A8=E5=A4=96?= =?UTF-8?q?=E5=B1=82=E7=9B=AE=E5=BD=95=E9=94=81=E5=AE=8C=E6=88=90=E5=8A=A0?= =?UTF-8?q?=E5=AF=86=E5=BF=AB=E7=85=A7=E6=81=A2=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: qin-ctx --- crates/ragfs-python/src/lib.rs | 6 ++-- crates/ragfs/src/git/service.rs | 14 +++++++-- openviking/storage/viking_fs/_snapshot.py | 4 ++- tests/agfs/test_viking_fs_git.py | 36 ++++++++--------------- 4 files changed, 32 insertions(+), 28 deletions(-) diff --git a/crates/ragfs-python/src/lib.rs b/crates/ragfs-python/src/lib.rs index c415e33bf0..de9219683c 100644 --- a/crates/ragfs-python/src/lib.rs +++ b/crates/ragfs-python/src/lib.rs @@ -1733,10 +1733,11 @@ impl RAGFSBindingClient { } /// Restore a project_dir subtree to the state at source_commit. - #[pyo3(signature = (**kwargs))] + #[pyo3(signature = (*, ctx=None, **kwargs))] fn git_restore( &self, py: Python<'_>, + ctx: Option>, kwargs: Option<&Bound<'_, PyDict>>, ) -> PyResult> { let svc = self.git_service.clone().ok_or_else(|| { @@ -1745,7 +1746,8 @@ impl RAGFSBindingClient { let empty = PyDict::new(py); let kw = kwargs.unwrap_or(&empty); let req = git::parse_restore_request(kw)?; - let resp = py_detach_blocking(py, move || self.rt.block_on(svc.restore(req))) + let resp = self + .run_scoped(py, build_fs_context(ctx), || svc.restore(req)) .map_err(|e| git::map_git_error(py, e))?; git::restore_response_to_pydict(py, resp) } diff --git a/crates/ragfs/src/git/service.rs b/crates/ragfs/src/git/service.rs index e2b58153d0..92db4ecd01 100644 --- a/crates/ragfs/src/git/service.rs +++ b/crates/ragfs/src/git/service.rs @@ -21,7 +21,7 @@ use tracing::warn; use crate::core::filesystem::FileSystem; use crate::core::types::FileInfo; -use crate::core::{FsContextInner, FS_CTX}; +use crate::core::{FsContextInner, FsContextView, PathLockContext, FS_CTX}; use crate::git::{ error::{GitError, ObjectStoreError, RefStoreError}, ignore::{should_track_path, IgnoreMatcher, OVGITIGNORE_PATH}, @@ -984,7 +984,17 @@ impl GitService { validate_project_dir(project_dir)?; } - let ctx = Arc::new(FsContextInner::new(req.account.clone())); + // Encrypted writeback must reuse the caller's tree lock rather than + // acquire a conflicting lock. The request remains the tenant authority. + let ctx = Arc::new(FsContextInner::with_pathlock( + req.account.clone(), + PathLockContext { + lease_ref: FsContextView::current() + .pathlock_lease_ref() + .map(str::to_owned), + ..Default::default() + }, + )); FS_CTX.scope(ctx, self.restore_impl(req)).await } diff --git a/openviking/storage/viking_fs/_snapshot.py b/openviking/storage/viking_fs/_snapshot.py index c5f561947c..0b1fc74216 100644 --- a/openviking/storage/viking_fs/_snapshot.py +++ b/openviking/storage/viking_fs/_snapshot.py @@ -436,7 +436,9 @@ async def restore( ctx=real_ctx, ) try: - result = await self._async_agfs.run("git_restore", **kwargs) + result = await self._async_agfs.run( + "git_restore", **kwargs, ctx=self._pathlock_fs_ctx(real_ctx, lease) + ) except GitRestoreWritebackPartialError as exc: # The ref already advanced — capture the exception and # finish the lock scope cleanly. Reindex scheduling and diff --git a/tests/agfs/test_viking_fs_git.py b/tests/agfs/test_viking_fs_git.py index 5eeacac5c3..412f4d777f 100644 --- a/tests/agfs/test_viking_fs_git.py +++ b/tests/agfs/test_viking_fs_git.py @@ -8,8 +8,6 @@ from __future__ import annotations import asyncio -import os -import secrets import shutil import tempfile from pathlib import Path @@ -439,34 +437,26 @@ async def test_account_isolation_show_misses_other_account(vfs): @pytest.fixture -def encryptor(workspace): - from openviking.crypto.encryptor import FileEncryptor - from openviking.crypto.providers import LocalFileProvider - - key_file = workspace / "master.key" - key_file.write_text(secrets.token_bytes(32).hex()) - os.chmod(key_file, 0o600) - provider = LocalFileProvider(key_file=str(key_file)) - return FileEncryptor(provider) - - -@pytest.fixture -def vfs_encrypted(workspace, encryptor): +def vfs_encrypted(workspace): cfg, fs_root = _write_workspace(workspace) - client = _build_client(cfg, fs_root) + client = ragfs_python.RAGFSBindingClient( + git_config_path=str(cfg), + config={ + "service_name": "snapshot-test", + "encryption": {"root_key": b"k" * 32, "provider_type": 1}, + }, + ) + client.mount("localfs", "/local", {"local_dir": str(fs_root)}) try: - yield VikingFS(agfs=client, encryptor=encryptor) + yield VikingFS(agfs=client) finally: - pass + client.close() @pytest.mark.asyncio async def test_double_encryption_restore_preserves_plaintext(vfs_encrypted): - """Write plaintext via encrypted VikingFS, commit (ciphertext stored in - git), modify, restore. After restore, VikingFS.read MUST return the - original plaintext — proving the Rust restore path bypasses the - VikingFS encryption layer (writes ciphertext back through MountableFS, - which then decrypts correctly on read). + """Restore through the encrypted backend under the outer tree lock; + reads must return the original plaintext without double encryption. """ ctx = _make_ctx(account="acct_enc") plaintext_v1 = b"top-secret-v1"