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.
Files changed (135) hide show
  1. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/PKG-INFO +1 -1
  2. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/pyproject.toml +2 -1
  3. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/Cargo.lock +1 -1
  4. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/Cargo.toml +2 -1
  5. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/main.rs +191 -11
  6. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/checkpoint_store.py +29 -22
  7. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/LICENSE +0 -0
  8. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/NOTICE +0 -0
  9. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/README.md +0 -0
  10. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/deny.toml +0 -0
  11. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/rust-toolchain.toml +0 -0
  12. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/backend_ast.rs +0 -0
  13. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/backend_ast_workflow.rs +0 -0
  14. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/backend_cpu.rs +0 -0
  15. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/bin/tg_search_fast.rs +0 -0
  16. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/cli.rs +0 -0
  17. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/crossover.rs +0 -0
  18. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/editor_plane.rs +0 -0
  19. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/gpu_native.rs +0 -0
  20. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/index.rs +0 -0
  21. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/index_lock.rs +0 -0
  22. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/lib.rs +0 -0
  23. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/mmap_arrow.rs +0 -0
  24. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/native_search.rs +0 -0
  25. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/python_sidecar.rs +0 -0
  26. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/rg_passthrough.rs +0 -0
  27. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/routing.rs +0 -0
  28. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/src/runtime_paths.rs +0 -0
  29. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_ast_backend.rs +0 -0
  30. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_ast_cache.rs +0 -0
  31. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_ast_grep_embed.rs +0 -0
  32. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_ast_rewrite.rs +0 -0
  33. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_audit_manifest_symlink_unix.rs +0 -0
  34. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_gpu_native.rs +0 -0
  35. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_gpu_native_error_handling.rs +0 -0
  36. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_gpu_native_graphs.rs +0 -0
  37. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_gpu_native_integration.rs +0 -0
  38. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_gpu_native_long_lines.rs +0 -0
  39. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_gpu_native_streams.rs +0 -0
  40. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_index.rs +0 -0
  41. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_index_lock_concurrency.rs +0 -0
  42. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_mmap_arrow.rs +0 -0
  43. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_native_search.rs +0 -0
  44. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_output.rs +0 -0
  45. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_public_native_cli_parity.rs +0 -0
  46. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_python_bootstrap.rs +0 -0
  47. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_release_profile.rs +0 -0
  48. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_replace.rs +0 -0
  49. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_resident_worker.rs +0 -0
  50. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_routing.rs +0 -0
  51. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_runtime_path_resolution.rs +0 -0
  52. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_schema_compat.rs +0 -0
  53. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_search.rs +0 -0
  54. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_search_golden.rs +0 -0
  55. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_sidecar_ipc.rs +0 -0
  56. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/rust_core/tests/test_smart_routing.rs +0 -0
  57. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/__init__.py +0 -0
  58. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/__main__.py +0 -0
  59. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/backends/__init__.py +0 -0
  60. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/backends/ast_backend.py +0 -0
  61. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/backends/ast_wrapper_backend.py +0 -0
  62. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/backends/base.py +0 -0
  63. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/backends/cpu_backend.py +0 -0
  64. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/backends/cudf_backend.py +0 -0
  65. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/backends/cybert_backend.py +0 -0
  66. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/backends/ripgrep_backend.py +0 -0
  67. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/backends/rust_backend.py +0 -0
  68. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/backends/stringzilla_backend.py +0 -0
  69. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/backends/torch_backend.py +0 -0
  70. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/__init__.py +0 -0
  71. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/_index_lock.py +0 -0
  72. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/agent_capsule.py +0 -0
  73. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/apply_policy.py +0 -0
  74. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/ast_workflows.py +0 -0
  75. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/audit_manifest.py +0 -0
  76. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/bootstrap.py +0 -0
  77. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/codemap.py +0 -0
  78. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/commands.py +0 -0
  79. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/docs_coverage.py +0 -0
  80. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/dogfood.py +0 -0
  81. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/evidence_receipt.py +0 -0
  82. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/evidence_signing.py +0 -0
  83. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/formatters/__init__.py +0 -0
  84. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/formatters/base.py +0 -0
  85. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/formatters/csv_fmt.py +0 -0
  86. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/formatters/json_fmt.py +0 -0
  87. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/formatters/ripgrep_fmt.py +0 -0
  88. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/formatters/table_fmt.py +0 -0
  89. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/inventory.py +0 -0
  90. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/lang_go.py +0 -0
  91. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/lang_registry.py +0 -0
  92. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/lsp_external_provider.py +0 -0
  93. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/lsp_provider_setup.py +0 -0
  94. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/lsp_server.py +0 -0
  95. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/main.py +0 -0
  96. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/mcp_server.py +0 -0
  97. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/orient_capsule.py +0 -0
  98. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/progress.py +0 -0
  99. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/repo_map.py +0 -0
  100. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/rg_contract.py +0 -0
  101. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/rule_packs.py +0 -0
  102. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/runtime_paths.py +0 -0
  103. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/scan_guardrails.py +0 -0
  104. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/session_daemon.py +0 -0
  105. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/session_store.py +0 -0
  106. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/cli/subprocess_policy.py +0 -0
  107. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/__init__.py +0 -0
  108. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/config.py +0 -0
  109. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/hardware/__init__.py +0 -0
  110. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/hardware/device_detect.py +0 -0
  111. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/hardware/device_inventory.py +0 -0
  112. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/hardware/memory_manager.py +0 -0
  113. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/observability.py +0 -0
  114. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/pipeline.py +0 -0
  115. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/query_analyzer.py +0 -0
  116. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/registration_check.py +0 -0
  117. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/reranker.py +0 -0
  118. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/result.py +0 -0
  119. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/retrieval_bm25.py +0 -0
  120. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/retrieval_chunker.py +0 -0
  121. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/retrieval_dense.py +0 -0
  122. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/retrieval_fusion.py +0 -0
  123. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/retrieval_late.py +0 -0
  124. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/retrieval_lexical.py +0 -0
  125. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/retrieval_scoring.py +0 -0
  126. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/core/semantic_index.py +0 -0
  127. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/io/__init__.py +0 -0
  128. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/io/base.py +0 -0
  129. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/io/directory_scanner.py +0 -0
  130. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/io/reader_cudf.py +0 -0
  131. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/io/reader_dstorage.py +0 -0
  132. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/io/reader_fallback.py +0 -0
  133. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/io/reader_kvikio.py +0 -0
  134. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/perf_guard.py +0 -0
  135. {tensor_grep-1.76.7 → tensor_grep-1.76.8}/src/tensor_grep/sidecar.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensor-grep
3
- Version: 1.76.7
3
+ Version: 1.76.8
4
4
  Requires-Dist: typer>=0.12,<0.26
5
5
  Requires-Dist: rich>=13.0
6
6
  Requires-Dist: mcp>=1.2.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.7"
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"
@@ -1579,7 +1579,7 @@ dependencies = [
1579
1579
 
1580
1580
  [[package]]
1581
1581
  name = "tensor_grep_rs"
1582
- version = "1.76.7"
1582
+ version = "1.76.8"
1583
1583
  dependencies = [
1584
1584
  "aho-corasick",
1585
1585
  "anyhow",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "tensor_grep_rs"
3
- version = "1.76.7"
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. The Rust `create_checkpoint` above had NO
5128
- // equivalent: a failure in the copy loop, the metadata write, OR the index write left
5129
- // the just-created checkpoint_id directory (snapshot/ + metadata.json) behind forever.
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) on the Rust side. Everything from here on is fallible (a
8445
- // file copy, the metadata.json write, the index.json read/parse/write) and, before this
8446
- // fix, a failure at ANY of those steps propagated the error via `?` while leaving the
8447
- // just-created random-id checkpoint directory -- `checkpoint_dir`, holding both snapshot/
8448
- // and metadata.json -- behind on disk forever. Run the rest of the write sequence in a
8449
- // closure (stable Rust has no `try` blocks) and remove that whole directory before
8450
- // propagating any error; the success (`Ok`) path below is byte-identical to before.
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
- std::fs::copy(&source, &destination).with_context(|| {
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
- # also now extends through the metadata write below (not just the copy loop above): a
879
- # failure writing metadata.json after a fully successful copy must not orphan that
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