Skip to content

capture-file: constrain globs to the project root - #1146

Draft
shunichironomura wants to merge 4 commits into
project-relative-pathfrom
capture-file-project-containment
Draft

shunichironomura wants to merge 4 commits into
project-relative-pathfrom
capture-file-project-containment

Conversation

@shunichironomura

@shunichironomura shunichironomura commented Jul 22, 2026 •

Copy link
Copy Markdown
Member

Summary

  • reject absolute and parent-traversing capture-file glob patterns
  • enumerate matches beneath the canonical project root without following symbolic links
  • validate the full match set before hashing, copying, or moving files
  • preserve existing * and ** semantics and literal Unix backslashes
  • document the new path restrictions and add security regression coverage

Closes #1067.

This PR is stacked on #1098, which provides ResolvedProjectPath.

Testing

  • just lint
  • just test

AI assistance

  • AI used: yes — implementation, tests, documentation, and PR description were prepared with an AI coding agent
  • Human review of AI-assisted code: pending

Breaking changes

  • No breaking changes
  • Breaking changes; the breaking change label is applied

Absolute, parent-relative, and symlink-based capture patterns that previously worked are now rejected or not traversed.


This is part 3 of 4 in a stack made with GitButler:

@codecov-commenter

codecov-commenter commented Jul 22, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.45614% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 55.33%. Comparing base (0806b44) to head (1d0437b).

Files with missing lines Patch % Lines
crates/capsula-capture-file/src/lib.rs 82.45% 20 Missing ⚠️
Additional details and impacted files
@@                    Coverage Diff                    @@
##           project-relative-path    #1146      +/-   ##
=========================================================
+ Coverage                  54.61%   55.33%   +0.72%     
=========================================================
  Files                         42       42              
  Lines                       4283     4384     +101     
=========================================================
+ Hits                        2339     2426      +87     
- Misses                      1944     1958      +14     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@shunichironomura
shunichironomura force-pushed the capture-file-project-containment branch from 284bbe6 to 1d0437b Compare July 22, 2026 22:53
shunsuke-shimomura added a commit that referenced this pull request Aug 5, 2026
Add a pull command that downloads a run from a Capsula server and
restores it under the local vault, as the counterpart of capsula push
(#1177):

- Extend GET /api/v1/runs/{id} with a files array (path, size, hash);
  the server-internal storage_path is not exposed
- Reconstruct the run directory deterministically from the run's ULID
  and name via the new capsula_core::run::run_dir_relative_path (shared
  with local run creation)
- Verify downloaded files against server-recorded SHA-256 hashes;
  restore _capsula metadata best-effort from the server's structured
  data and write a _capsula/pulled.json origin marker
- Download into a temporary directory and rename atomically; existing
  run directories require --force to replace
- Restore only relative paths contained in the run directory (syntactic
  check); full path-safety hardening follows the ResolvedProjectPath
  work (#1098, #1146)
- Encode URL file paths segment-wise with NON_ALPHANUMERIC (allowlist)
  so arbitrary filenames round-trip without a curated character list
shunsuke-shimomura added a commit that referenced this pull request Sep 8, 2026
Add a pull command that downloads a run from a Capsula server and
restores it under the local vault, as the counterpart of capsula push
(#1177):

- Extend GET /api/v1/runs/{id} with a files array (path, size, hash);
  the server-internal storage_path is not exposed
- Reconstruct the run directory deterministically from the run's ULID
  and name via the new capsula_core::run::run_dir_relative_path (shared
  with local run creation)
- Verify downloaded files against server-recorded SHA-256 hashes;
  restore _capsula metadata best-effort from the server's structured
  data and write a _capsula/pulled.json origin marker
- Download into a temporary directory and rename atomically; existing
  run directories require --force to replace
- Restore only relative paths contained in the run directory (syntactic
  check); full path-safety hardening follows the ResolvedProjectPath
  work (#1098, #1146)
- Encode URL file paths segment-wise with NON_ALPHANUMERIC (allowlist)
  so arbitrary filenames round-trip without a curated character list
shunsuke-shimomura added a commit that referenced this pull request Sep 8, 2026
Add a pull command that downloads a run from a Capsula server and
restores it under the local vault, as the counterpart of capsula push
(#1177):

- Extend GET /api/v1/runs/{id} with a files array (path, size, hash);
  the server-internal storage_path is not exposed
- Reconstruct the run directory deterministically from the run's ULID
  and name via the new capsula_core::run::run_dir_relative_path (shared
  with local run creation)
- Verify downloaded files against server-recorded SHA-256 hashes;
  restore _capsula metadata best-effort from the server's structured
  data and write a _capsula/pulled.json origin marker
- Download into a temporary directory and rename atomically; existing
  run directories require --force to replace
- Restore only relative paths contained in the run directory (syntactic
  check); full path-safety hardening follows the ResolvedProjectPath
  work (#1098, #1146)
- Encode URL file paths segment-wise with NON_ALPHANUMERIC (allowlist)
  so arbitrary filenames round-trip without a curated character list
shunsuke-shimomura added a commit that referenced this pull request Sep 9, 2026
Add a pull command that downloads a run from a Capsula server and
restores it under the local vault, as the counterpart of capsula push
(#1177):

- Extend GET /api/v1/runs/{id} with a files array (path, size, hash);
  the server-internal storage_path is not exposed
- Reconstruct the run directory deterministically from the run's ULID
  and name via the new capsula_core::run::run_dir_relative_path (shared
  with local run creation)
- Verify downloaded files against server-recorded SHA-256 hashes;
  restore _capsula metadata best-effort from the server's structured
  data and write a _capsula/pulled.json origin marker
- Download into a temporary directory and rename atomically; existing
  run directories require --force to replace
- Restore only relative paths contained in the run directory (syntactic
  check); full path-safety hardening follows the ResolvedProjectPath
  work (#1098, #1146)
- Encode URL file paths segment-wise with NON_ALPHANUMERIC (allowlist)
  so arbitrary filenames round-trip without a curated character list
shunsuke-shimomura added a commit that referenced this pull request Sep 9, 2026
* feat: add capsula pull command

Add a pull command that downloads a run from a Capsula server and
restores it under the local vault, as the counterpart of capsula push
(#1177):

- Extend GET /api/v1/runs/{id} with a files array (path, size, hash);
  the server-internal storage_path is not exposed
- Reconstruct the run directory deterministically from the run's ULID
  and name via the new capsula_core::run::run_dir_relative_path (shared
  with local run creation)
- Verify downloaded files against server-recorded SHA-256 hashes;
  restore _capsula metadata best-effort from the server's structured
  data and write a _capsula/pulled.json origin marker
- Download into a temporary directory and rename atomically; existing
  run directories require --force to replace
- Restore only relative paths contained in the run directory (syntactic
  check); full path-safety hardening follows the ResolvedProjectPath
  work (#1098, #1146)
- Encode URL file paths segment-wise with NON_ALPHANUMERIC (allowlist)
  so arbitrary filenames round-trip without a curated character list

* fix: address PR #1178 review findings (F1-F4, F6-F10, F13-F17)

Merge blockers:
- F1/F3/F6: --force now only replaces runs carrying the
  _capsula/pulled.json marker; locally produced runs are never replaced
  and the already-exists errors distinguish the two cases. push (and
  push --all) refuses/skips pulled runs so lossy reconstructions cannot
  degrade the server's copy. The existence check is re-done at
  replacement time, so a directory appearing during the download is no
  longer clobbered.
- F2: the download temp directory now lives directly under the vault
  root (same filesystem, atomic rename) instead of vault/<date>/, so an
  interrupted pull cannot leave a phantom run at the depth the vault
  scanners walk.

Non-blockers taken along:
- F4 (minimum): non-ULID pull arguments fail fast with a specific error
- F7/F8: run name and vault name are validated as single path
  components before being joined into paths
- F9: reuse capsula_core::util::hex_encode instead of local copies
- F10: rename the pull-side sanitizer to checked_run_relative_path to
  stop sharing a name with the server's normalizing variant
- F13: percent-encoding no longer escapes RFC 3986 unreserved chars
- F14: shared fetch_run_files used by both run-detail handlers
- F15: verify downloaded sizes; warn on negative duration_ms; align
  duration_ms width with the server model
- F16: document the round-trip fidelity gaps in the CLI reference
- F17: regression tests for all of the above (16 integration tests)

* fix: address PR #1178 review comments

Pull runs of a non-configured vault into the default vault location.

The configured `[vault] path` describes the configured vault only, so
restoring another vault's run next to it placed the run in a directory
that was never chosen for it (an external directory, for instance).
The default `.capsula/<name>` layout only existed as an inline format
string inside `VaultConfig`'s Deserialize impl, which is why `pull`
could not ask for it. Extract it as `DEFAULT_VAULT_ROOT` plus
`default_vault_path()` in capsula-config and use it from both the
config default and `pull`.

A run of another vault now defaults to `.capsula/<vault>/` under the
project root. An explicit `--vault-path` / `CAPSULA_VAULT_PATH`
override still wins, so `resolve_vault_path()` reports its source
through `VaultPathSource` and `LoadedConfig` carries it. The decision
lives in `resolve_pull_vault_dir()` so it is unit-testable.

Also centralize the pulled-run check: `is_pulled_run()` is now public
and replaces the open-coded `pulled.json` checks in `push_single_run()`
and in the `push --all` skip.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

capture-file glob escapes the project root and follows symlinks (arbitrary file capture)

2 participants