tensor-grep 1.76.7__tar.gz → 1.76.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/PKG-INFO +1 -1
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/pyproject.toml +2 -1
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/Cargo.lock +1 -1
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/Cargo.toml +2 -1
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/main.rs +191 -11
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/checkpoint_store.py +29 -22
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/LICENSE +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/NOTICE +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/README.md +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/deny.toml +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/rust-toolchain.toml +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/backend_ast.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/backend_ast_workflow.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/backend_cpu.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/bin/tg_search_fast.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/cli.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/crossover.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/editor_plane.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/gpu_native.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/index.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/index_lock.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/lib.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/mmap_arrow.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/native_search.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/python_sidecar.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/rg_passthrough.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/routing.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/runtime_paths.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_ast_backend.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_ast_cache.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_ast_grep_embed.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_ast_rewrite.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_audit_manifest_symlink_unix.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_gpu_native.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_gpu_native_error_handling.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_gpu_native_graphs.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_gpu_native_integration.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_gpu_native_long_lines.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_gpu_native_streams.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_index.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_index_lock_concurrency.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_mmap_arrow.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_native_search.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_output.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_public_native_cli_parity.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_python_bootstrap.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_release_profile.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_replace.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_resident_worker.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_routing.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_runtime_path_resolution.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_schema_compat.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_search.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_search_golden.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_sidecar_ipc.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_smart_routing.rs +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/__init__.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/__main__.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/backends/__init__.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/backends/ast_backend.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/backends/ast_wrapper_backend.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/backends/base.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/backends/cpu_backend.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/backends/cudf_backend.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/backends/cybert_backend.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/backends/ripgrep_backend.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/backends/rust_backend.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/backends/stringzilla_backend.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/backends/torch_backend.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/__init__.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/_index_lock.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/agent_capsule.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/apply_policy.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/ast_workflows.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/audit_manifest.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/bootstrap.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/codemap.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/commands.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/docs_coverage.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/dogfood.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/evidence_receipt.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/evidence_signing.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/formatters/__init__.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/formatters/base.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/formatters/csv_fmt.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/formatters/json_fmt.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/formatters/ripgrep_fmt.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/formatters/table_fmt.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/inventory.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/lang_go.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/lang_registry.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/lsp_external_provider.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/lsp_provider_setup.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/lsp_server.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/main.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/mcp_server.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/orient_capsule.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/progress.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/repo_map.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/rg_contract.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/rule_packs.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/runtime_paths.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/scan_guardrails.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/session_daemon.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/session_store.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/subprocess_policy.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/__init__.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/config.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/hardware/__init__.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/hardware/device_detect.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/hardware/device_inventory.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/hardware/memory_manager.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/observability.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/pipeline.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/query_analyzer.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/registration_check.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/reranker.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/result.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/retrieval_bm25.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/retrieval_chunker.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/retrieval_dense.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/retrieval_fusion.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/retrieval_late.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/retrieval_lexical.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/retrieval_scoring.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/semantic_index.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/io/__init__.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/io/base.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/io/directory_scanner.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/io/reader_cudf.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/io/reader_dstorage.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/io/reader_fallback.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/io/reader_kvikio.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/perf_guard.py +0 -0
- {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/sidecar.py +0 -0
|
@@ -536,11 +536,12 @@ version_variables = [
|
|
|
536
536
|
|
|
537
537
|
|
|
538
538
|
|
|
539
|
+
|
|
539
540
|
|
|
540
541
|
|
|
541
542
|
[project]
|
|
542
543
|
name = "tensor-grep"
|
|
543
|
-
version = "1.76.
|
|
544
|
+
version = "1.76.8"
|
|
544
545
|
description = "High-performance grep-compatible CLI with Rust, GPU, and AST/NLP routing"
|
|
545
546
|
readme = { file = "rust_core/README.md", content-type = "text/markdown" }
|
|
546
547
|
requires-python = ">=3.11"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "tensor_grep_rs"
|
|
3
|
-
version = "1.76.
|
|
3
|
+
version = "1.76.8"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
description = "Rust core engine for tensor-grep"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -444,6 +444,7 @@ path = "src/bin/tg_search_fast.rs"
|
|
|
444
444
|
|
|
445
445
|
|
|
446
446
|
|
|
447
|
+
|
|
447
448
|
|
|
448
449
|
|
|
449
450
|
[dev-dependencies]
|
|
@@ -5124,9 +5124,12 @@ mod tests {
|
|
|
5124
5124
|
// NIT (v1.76 #602 gate, "NIT-3"): checkpoint_store.py::create_checkpoint (the Python
|
|
5125
5125
|
// side) already wraps its copy-loop + metadata-write in `except BaseException: rmtree
|
|
5126
5126
|
// (snapshot_dir.parent); raise` (audit #125a, shipped in #602) so a failure never
|
|
5127
|
-
// orphans the random-id snapshot dir
|
|
5128
|
-
//
|
|
5129
|
-
// the
|
|
5127
|
+
// orphans the random-id snapshot dir -- audit #178 later widened that same Python guard
|
|
5128
|
+
// to also cover the index write (see checkpoint_store.py::create_checkpoint), closing
|
|
5129
|
+
// the one gap that earlier widening had not yet reached. The Rust `create_checkpoint`
|
|
5130
|
+
// above had NO equivalent at all: a failure in the copy loop, the metadata write, OR
|
|
5131
|
+
// the index write left the just-created checkpoint_id directory (snapshot/ +
|
|
5132
|
+
// metadata.json) behind forever.
|
|
5130
5133
|
//
|
|
5131
5134
|
// Force a deterministic, cross-platform failure at the LAST fallible step (the
|
|
5132
5135
|
// pre-existing index.json fails to parse) so the copy loop and the metadata.json write
|
|
@@ -5166,6 +5169,90 @@ mod tests {
|
|
|
5166
5169
|
);
|
|
5167
5170
|
}
|
|
5168
5171
|
|
|
5172
|
+
#[test]
|
|
5173
|
+
fn test_create_checkpoint_does_not_disclose_symlink_target() {
|
|
5174
|
+
// Item 1 (audit #178, surfaced by the #610 gate): `std::fs::copy` FOLLOWS symlinks, so
|
|
5175
|
+
// a source-tree symlink pointing OUTSIDE the checkpoint root previously had its
|
|
5176
|
+
// TARGET's content copied into the snapshot -- an out-of-root disclosure. Mirrors the
|
|
5177
|
+
// Python-side regression test `test_create_checkpoint_does_not_disclose_symlink_target`
|
|
5178
|
+
// in tests/unit/test_checkpoint_containment.py (checkpoint_store.py:853-855).
|
|
5179
|
+
let repo_parent = tempfile::tempdir().unwrap();
|
|
5180
|
+
let repo_path = repo_parent.path().join("repo");
|
|
5181
|
+
std::fs::create_dir_all(&repo_path).unwrap();
|
|
5182
|
+
std::fs::write(repo_path.join("real.py"), b"in-repo content\n").unwrap();
|
|
5183
|
+
|
|
5184
|
+
let outside_dir = tempfile::tempdir().unwrap();
|
|
5185
|
+
let secret = outside_dir.path().join("secret.txt");
|
|
5186
|
+
std::fs::write(&secret, b"SECRET-OUT-OF-ROOT\n").unwrap();
|
|
5187
|
+
|
|
5188
|
+
let link_path = repo_path.join("link.txt");
|
|
5189
|
+
if let Err(err) = try_symlink_file(&secret, &link_path) {
|
|
5190
|
+
eprintln!(
|
|
5191
|
+
"skipping test_create_checkpoint_does_not_disclose_symlink_target: cannot create a symlink in this environment: {err}"
|
|
5192
|
+
);
|
|
5193
|
+
return;
|
|
5194
|
+
}
|
|
5195
|
+
|
|
5196
|
+
let path_str = repo_path.to_str().unwrap();
|
|
5197
|
+
let result = create_checkpoint(path_str).expect("checkpoint creation must still succeed");
|
|
5198
|
+
|
|
5199
|
+
let scope = detect_checkpoint_scope(path_str);
|
|
5200
|
+
let snapshot_dir = checkpoint_snapshot_dir(&scope.root, &result.checkpoint_id);
|
|
5201
|
+
let snapshotted_link = snapshot_dir.join("link.txt");
|
|
5202
|
+
|
|
5203
|
+
// The snapshot entry must itself be a symlink -- never a regular file holding the
|
|
5204
|
+
// target's bytes -- proving the target's content was never read/copied.
|
|
5205
|
+
let snapshotted_type = std::fs::symlink_metadata(&snapshotted_link)
|
|
5206
|
+
.expect("snapshotted symlink entry must exist")
|
|
5207
|
+
.file_type();
|
|
5208
|
+
assert!(
|
|
5209
|
+
snapshotted_type.is_symlink(),
|
|
5210
|
+
"checkpoint snapshot must store the symlink itself, not its resolved target content"
|
|
5211
|
+
);
|
|
5212
|
+
|
|
5213
|
+
// Belt-and-suspenders: no regular file anywhere under the snapshot may contain the
|
|
5214
|
+
// out-of-root secret's content.
|
|
5215
|
+
for entry in walkdir::WalkDir::new(&snapshot_dir) {
|
|
5216
|
+
let entry = entry.unwrap();
|
|
5217
|
+
if entry.file_type().is_file() {
|
|
5218
|
+
let text = std::fs::read_to_string(entry.path()).unwrap_or_default();
|
|
5219
|
+
assert!(
|
|
5220
|
+
!text.contains("SECRET-OUT-OF-ROOT"),
|
|
5221
|
+
"checkpoint snapshot must not disclose the out-of-root secret's content: {}",
|
|
5222
|
+
entry.path().display()
|
|
5223
|
+
);
|
|
5224
|
+
}
|
|
5225
|
+
}
|
|
5226
|
+
}
|
|
5227
|
+
|
|
5228
|
+
#[test]
|
|
5229
|
+
fn test_create_checkpoint_still_copies_regular_files_byte_identical() {
|
|
5230
|
+
// Regression guard for the copy_checkpoint_entry symlink fix above: an ordinary
|
|
5231
|
+
// (non-symlink) source tree must still be captured with byte-identical content, and a
|
|
5232
|
+
// normal (no-symlink) checkpoint create must be entirely unaffected by the new
|
|
5233
|
+
// is_symlink() branch.
|
|
5234
|
+
let dir = tempfile::tempdir().unwrap();
|
|
5235
|
+
let repo_path = dir.path().join("repo");
|
|
5236
|
+
std::fs::create_dir_all(repo_path.join("pkg")).unwrap();
|
|
5237
|
+
std::fs::write(repo_path.join("a.py"), b"alpha\n").unwrap();
|
|
5238
|
+
std::fs::write(repo_path.join("pkg").join("b.py"), b"beta\n").unwrap();
|
|
5239
|
+
|
|
5240
|
+
let path_str = repo_path.to_str().unwrap();
|
|
5241
|
+
let result = create_checkpoint(path_str).expect("checkpoint creation must succeed");
|
|
5242
|
+
assert_eq!(result.file_count, 2);
|
|
5243
|
+
|
|
5244
|
+
let scope = detect_checkpoint_scope(path_str);
|
|
5245
|
+
let snapshot_dir = checkpoint_snapshot_dir(&scope.root, &result.checkpoint_id);
|
|
5246
|
+
assert_eq!(
|
|
5247
|
+
std::fs::read(snapshot_dir.join("a.py")).unwrap(),
|
|
5248
|
+
b"alpha\n"
|
|
5249
|
+
);
|
|
5250
|
+
assert_eq!(
|
|
5251
|
+
std::fs::read(snapshot_dir.join("pkg").join("b.py")).unwrap(),
|
|
5252
|
+
b"beta\n"
|
|
5253
|
+
);
|
|
5254
|
+
}
|
|
5255
|
+
|
|
5169
5256
|
#[test]
|
|
5170
5257
|
fn test_restore_validation_rollback_snapshots_refuses_symlink_at_file_path() {
|
|
5171
5258
|
// #115 Gap 3: between "apply the edit" and a failed-validation rollback, an edited
|
|
@@ -8419,6 +8506,97 @@ fn collect_checkpoint_entries(scope: &CheckpointScope) -> anyhow::Result<BTreeMa
|
|
|
8419
8506
|
}
|
|
8420
8507
|
}
|
|
8421
8508
|
|
|
8509
|
+
/// Copies one checkpoint source entry into the snapshot, mirroring
|
|
8510
|
+
/// `checkpoint_store.py::create_checkpoint`'s `shutil.copy2(source, destination,
|
|
8511
|
+
/// follow_symlinks=False)` (checkpoint_store.py:853-855, audit HIGH -- symlink disclosure).
|
|
8512
|
+
/// `std::fs::copy` FOLLOWS symlinks, so a plain copy here would read a source symlink's
|
|
8513
|
+
/// (possibly out-of-root) TARGET content and bake it into the snapshot -- an out-of-root
|
|
8514
|
+
/// disclosure (audit #178). Recreate the symlink AS a symlink instead of following it; fall
|
|
8515
|
+
/// back to a normal file copy for every non-symlink entry.
|
|
8516
|
+
///
|
|
8517
|
+
/// Round-trip contract (verified against `checkpoint_store.py::undo_checkpoint`, the only
|
|
8518
|
+
/// restore path -- `tg checkpoint` list/undo is a full Python passthrough, see
|
|
8519
|
+
/// `Commands::Checkpoint` below). Storing the link AS a link (not its target's bytes) is what
|
|
8520
|
+
/// keeps an out-of-root symlink FAIL-CLOSED rather than a disclosure on undo -- but it does NOT
|
|
8521
|
+
/// make every symlink restorable, and this comment must not claim a full round-trip. undo's
|
|
8522
|
+
/// read-only pre-flight `_resolve_within_root` (checkpoint_store.py:124-139, called at
|
|
8523
|
+
/// :1232-1235) RESOLVES each stored snapshot symlink and, before touching any working-tree
|
|
8524
|
+
/// file, REFUSES it (ValueError) when the resolved target escapes the snapshot root; the later
|
|
8525
|
+
/// missing-source probe (:1246-1257) raises CheckpointCorruptError for a dangling target. So an
|
|
8526
|
+
/// out-of-root OR dangling symlink checkpoint is refused fail-closed on undo -- no disclosure,
|
|
8527
|
+
/// no data loss, working tree left intact, but NOT restorable. Only an in-root symlink whose
|
|
8528
|
+
/// resolved target exists inside the snapshot is actually restored. That fail-closed refusal is
|
|
8529
|
+
/// the correct, safe behavior; the alternative (following the link here) would materialize
|
|
8530
|
+
/// out-of-root content into the snapshot and then into the tree on undo.
|
|
8531
|
+
fn copy_checkpoint_entry(source: &Path, destination: &Path) -> std::io::Result<()> {
|
|
8532
|
+
if std::fs::symlink_metadata(source)?.file_type().is_symlink() {
|
|
8533
|
+
let link_target = std::fs::read_link(source)?;
|
|
8534
|
+
create_checkpoint_symlink(&link_target, destination)
|
|
8535
|
+
} else {
|
|
8536
|
+
std::fs::copy(source, destination).map(|_| ())
|
|
8537
|
+
}
|
|
8538
|
+
}
|
|
8539
|
+
|
|
8540
|
+
/// Recreates a symlink (never its target's content) at `link`, pointing at the same raw
|
|
8541
|
+
/// `target` text the original symlink stored (relative or absolute, copied verbatim -- exactly
|
|
8542
|
+
/// what `os.readlink()` + `os.symlink()` do on the Python side, so a relative target resolves
|
|
8543
|
+
/// the same way it always would relative to a symlink's own directory; this is an existing,
|
|
8544
|
+
/// accepted property of symlink-preserving copies -- e.g. Python's own `shutil.copytree(...,
|
|
8545
|
+
/// symlinks=True)` -- not a new gap introduced here).
|
|
8546
|
+
fn create_checkpoint_symlink(target: &Path, link: &Path) -> std::io::Result<()> {
|
|
8547
|
+
#[cfg(unix)]
|
|
8548
|
+
{
|
|
8549
|
+
std::os::unix::fs::symlink(target, link)
|
|
8550
|
+
}
|
|
8551
|
+
#[cfg(windows)]
|
|
8552
|
+
{
|
|
8553
|
+
// Windows symlinks are typed (file vs. directory reparse point). Resolve the target
|
|
8554
|
+
// relative to the link's own parent directory to pick the right type; a target that
|
|
8555
|
+
// cannot be resolved (dangling symlink) defaults to a file symlink, same as Python's
|
|
8556
|
+
// `os.symlink(target, dst)` (which defaults `target_is_directory=False`).
|
|
8557
|
+
let resolved_is_dir = link
|
|
8558
|
+
.parent()
|
|
8559
|
+
.map(|parent| parent.join(target))
|
|
8560
|
+
.and_then(|candidate| std::fs::metadata(candidate).ok())
|
|
8561
|
+
.map(|metadata| metadata.is_dir())
|
|
8562
|
+
.unwrap_or(false);
|
|
8563
|
+
let result = if resolved_is_dir {
|
|
8564
|
+
std::os::windows::fs::symlink_dir(target, link)
|
|
8565
|
+
} else {
|
|
8566
|
+
std::os::windows::fs::symlink_file(target, link)
|
|
8567
|
+
};
|
|
8568
|
+
// audit #178 F2a: creating a symlink on Windows needs SeCreateSymbolicLinkPrivilege
|
|
8569
|
+
// (admin, or Developer Mode). Without it the OS returns ERROR_PRIVILEGE_NOT_HELD (1314),
|
|
8570
|
+
// which the caller's `.with_context("failed to copy ... into checkpoint snapshot ...")`
|
|
8571
|
+
// would otherwise surface as a bare, misleading "copy failed". Re-message that one errno
|
|
8572
|
+
// so the real cause (a privilege gap, not a copy fault) is visible; leave every other
|
|
8573
|
+
// error untouched.
|
|
8574
|
+
result.map_err(|err| {
|
|
8575
|
+
const ERROR_PRIVILEGE_NOT_HELD: i32 = 1314;
|
|
8576
|
+
if err.raw_os_error() == Some(ERROR_PRIVILEGE_NOT_HELD) {
|
|
8577
|
+
std::io::Error::new(
|
|
8578
|
+
err.kind(),
|
|
8579
|
+
format!(
|
|
8580
|
+
"creating checkpoint symlink {} requires administrator privileges or \
|
|
8581
|
+
Windows Developer Mode (ERROR_PRIVILEGE_NOT_HELD)",
|
|
8582
|
+
link.display()
|
|
8583
|
+
),
|
|
8584
|
+
)
|
|
8585
|
+
} else {
|
|
8586
|
+
err
|
|
8587
|
+
}
|
|
8588
|
+
})
|
|
8589
|
+
}
|
|
8590
|
+
#[cfg(not(any(unix, windows)))]
|
|
8591
|
+
{
|
|
8592
|
+
let _ = (target, link);
|
|
8593
|
+
Err(std::io::Error::new(
|
|
8594
|
+
std::io::ErrorKind::Unsupported,
|
|
8595
|
+
"symlinks not supported on this platform",
|
|
8596
|
+
))
|
|
8597
|
+
}
|
|
8598
|
+
}
|
|
8599
|
+
|
|
8422
8600
|
fn create_checkpoint(path: &str) -> anyhow::Result<CheckpointCreateSummary> {
|
|
8423
8601
|
let scope = detect_checkpoint_scope(path);
|
|
8424
8602
|
let root = scope.root.clone();
|
|
@@ -8441,13 +8619,15 @@ fn create_checkpoint(path: &str) -> anyhow::Result<CheckpointCreateSummary> {
|
|
|
8441
8619
|
|
|
8442
8620
|
// NIT (v1.76 #602 gate, "NIT-3"): mirror checkpoint_store.py::create_checkpoint's
|
|
8443
8621
|
// `except BaseException: shutil.rmtree(snapshot_dir.parent, ignore_errors=True); raise`
|
|
8444
|
-
// (audit #125a, shipped in #602
|
|
8445
|
-
//
|
|
8446
|
-
//
|
|
8447
|
-
//
|
|
8448
|
-
//
|
|
8449
|
-
//
|
|
8450
|
-
//
|
|
8622
|
+
// (audit #125a, shipped in #602; widened by audit #178 to also cover the index write, so
|
|
8623
|
+
// it is now a true mirror end-to-end -- see checkpoint_store.py::create_checkpoint) on the
|
|
8624
|
+
// Rust side. Everything from here on is fallible (a file copy, the metadata.json write,
|
|
8625
|
+
// the index.json read/parse/write) and, before this fix, a failure at ANY of those steps
|
|
8626
|
+
// propagated the error via `?` while leaving the just-created random-id checkpoint
|
|
8627
|
+
// directory -- `checkpoint_dir`, holding both snapshot/ and metadata.json -- behind on disk
|
|
8628
|
+
// forever. Run the rest of the write sequence in a closure (stable Rust has no `try`
|
|
8629
|
+
// blocks) and remove that whole directory before propagating any error; the success (`Ok`)
|
|
8630
|
+
// path below is byte-identical to before.
|
|
8451
8631
|
let checkpoint_dir = checkpoint_storage_dir(&root).join(&checkpoint_id);
|
|
8452
8632
|
|
|
8453
8633
|
let write_checkpoint_body = || -> anyhow::Result<CheckpointCreateSummary> {
|
|
@@ -8465,7 +8645,7 @@ fn create_checkpoint(path: &str) -> anyhow::Result<CheckpointCreateSummary> {
|
|
|
8465
8645
|
)
|
|
8466
8646
|
})?;
|
|
8467
8647
|
}
|
|
8468
|
-
|
|
8648
|
+
copy_checkpoint_entry(&source, &destination).with_context(|| {
|
|
8469
8649
|
format!(
|
|
8470
8650
|
"failed to copy {} into checkpoint snapshot {}",
|
|
8471
8651
|
source.display(),
|
|
@@ -871,12 +871,39 @@ def create_checkpoint(path: str = ".") -> CheckpointCreateResult:
|
|
|
871
871
|
scope_kind=scope.scope_kind,
|
|
872
872
|
original_path=scope.original_path,
|
|
873
873
|
)
|
|
874
|
+
|
|
875
|
+
# audit #178 (surfaced by the #610 gate): the guarded region must also extend through
|
|
876
|
+
# the index write below -- q10's load->mutate->write RMW of index.json, cross-process
|
|
877
|
+
# and cross-thread (see _index_lock.index_lock docstring / AGENTS.md Backend Fail-Closed
|
|
878
|
+
# Contract). Before this fix that RMW ran entirely OUTSIDE this try/except: a failure
|
|
879
|
+
# loading, parsing, or writing index.json after the copy loop AND metadata.json had both
|
|
880
|
+
# already fully succeeded still orphaned the fully-populated per-checkpoint directory
|
|
881
|
+
# forever -- audit #125a's widening (below) only reached the metadata write, not this
|
|
882
|
+
# index write. Retention selection stays inside the lock (pure, no I/O); the actual
|
|
883
|
+
# rmtree of dropped (OTHER, already-retired) snapshot dirs happens AFTER this try/except
|
|
884
|
+
# -- once our own new checkpoint is durably indexed, a failure pruning old dirs is a
|
|
885
|
+
# separate, best-effort concern and must not roll back the checkpoint just committed.
|
|
886
|
+
with index_lock(_index_path(root)):
|
|
887
|
+
records = _load_index(root)
|
|
888
|
+
records.insert(
|
|
889
|
+
0,
|
|
890
|
+
CheckpointRecord(
|
|
891
|
+
version=_CHECKPOINT_VERSION,
|
|
892
|
+
checkpoint_id=checkpoint_id,
|
|
893
|
+
mode=mode,
|
|
894
|
+
root=str(root),
|
|
895
|
+
created_at=created_at,
|
|
896
|
+
file_count=len(entries),
|
|
897
|
+
),
|
|
898
|
+
)
|
|
899
|
+
records, dropped_dirs = _select_retained_checkpoints(root, records)
|
|
900
|
+
_write_index(root, records)
|
|
874
901
|
except BaseException:
|
|
875
902
|
# audit #125a: catch BaseException, not Exception -- KeyboardInterrupt/SystemExit
|
|
876
903
|
# subclass BaseException directly (not Exception), so a Ctrl+C here previously escaped
|
|
877
904
|
# this handler entirely and left an uncleaned checkpoint dir behind. The guarded region
|
|
878
|
-
#
|
|
879
|
-
#
|
|
905
|
+
# extends through the metadata write AND (audit #178) the index write above: a failure
|
|
906
|
+
# writing metadata.json OR index.json after a fully successful copy must not orphan that
|
|
880
907
|
# directory either -- audit H4's original cleanup only covered the copy loop. Remove the
|
|
881
908
|
# WHOLE per-checkpoint dir (metadata.json may or may not have been written yet, but
|
|
882
909
|
# snapshot_dir's own parent must go too, not just the snapshot/ subdir) -- matches how
|
|
@@ -885,26 +912,6 @@ def create_checkpoint(path: str = ".") -> CheckpointCreateResult:
|
|
|
885
912
|
shutil.rmtree(snapshot_dir.parent, ignore_errors=True)
|
|
886
913
|
raise
|
|
887
914
|
|
|
888
|
-
# q10 RMW race: load->mutate->write must be atomic w.r.t. every other writer of this
|
|
889
|
-
# index.json, cross-process and cross-thread, or a concurrent insert can be lost (see
|
|
890
|
-
# _index_lock.index_lock docstring / AGENTS.md Backend Fail-Closed Contract). Retention
|
|
891
|
-
# selection stays inside the lock (pure, no I/O); the actual rmtree of dropped snapshot
|
|
892
|
-
# dirs happens AFTER release so the lock is never held across slow directory removal.
|
|
893
|
-
with index_lock(_index_path(root)):
|
|
894
|
-
records = _load_index(root)
|
|
895
|
-
records.insert(
|
|
896
|
-
0,
|
|
897
|
-
CheckpointRecord(
|
|
898
|
-
version=_CHECKPOINT_VERSION,
|
|
899
|
-
checkpoint_id=checkpoint_id,
|
|
900
|
-
mode=mode,
|
|
901
|
-
root=str(root),
|
|
902
|
-
created_at=created_at,
|
|
903
|
-
file_count=len(entries),
|
|
904
|
-
),
|
|
905
|
-
)
|
|
906
|
-
records, dropped_dirs = _select_retained_checkpoints(root, records)
|
|
907
|
-
_write_index(root, records)
|
|
908
915
|
for directory in dropped_dirs:
|
|
909
916
|
shutil.rmtree(directory, ignore_errors=True)
|
|
910
917
|
_prime_bounded_discovery_caches_for_root(root)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_audit_manifest_symlink_unix.rs
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|