Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions crates/ragfs-python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<HashMap<String, String>>,
kwargs: Option<&Bound<'_, PyDict>>,
) -> PyResult<Py<PyAny>> {
let svc = self.git_service.clone().ok_or_else(|| {
Expand All @@ -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)
}
Expand Down
14 changes: 12 additions & 2 deletions crates/ragfs/src/git/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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
}

Expand Down
4 changes: 3 additions & 1 deletion openviking/storage/viking_fs/_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 13 additions & 23 deletions tests/agfs/test_viking_fs_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
from __future__ import annotations

import asyncio
import os
import secrets
import shutil
import tempfile
from pathlib import Path
Expand Down Expand Up @@ -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"
Expand Down
Loading