# Track 1: Entry Detail And Artifact Access — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Let a user select an archive entry and inspect/open the files Archivr saved for it, served through a safe stable URL. **Architecture:** Add `resolve_artifact_path` to `archivr-core` for safe path resolution, add a `serve_artifact` handler in `archivr-server` that streams the resolved file via `tower_http::services::ServeFile` (handles HTTP Range requests, ETags, and streaming — no full-file buffering), and expand the static UI's context rail to render all entry metadata and clickable artifact links. **Tech Stack:** Rust, Axum 0.7, `tower_http::services::ServeFile`, static HTML/CSS/JavaScript. No new crate dependencies — `tower` moves from dev-dep to dep within the workspace. --- ## Files | File | Change | |---|---| | `crates/archivr-server/Cargo.toml` | Move `tower` from `[dev-dependencies]` to `[dependencies]` | | `crates/archivr-core/src/archive.rs` | Add `resolve_artifact_path(store_path, artifact)` returning a safe `PathBuf` | | `crates/archivr-server/src/routes.rs` | Add `serve_artifact` handler, register route, add 3 tests | | `crates/archivr-server/static/app.js` | Expand `renderContextDetail` to show all fields + artifact links | | `crates/archivr-server/static/styles.css` | Add `.artifact-list`, `.artifact-link`, `.rail-section` styles | --- ### Task 1: `resolve_artifact_path` in archivr-core **Files:** - Modify: `crates/archivr-core/src/archive.rs` - [ ] **Step 1: Write the failing tests in the existing `mod tests` block** Add to the `mod tests` block at the bottom of `crates/archivr-core/src/archive.rs`: ```rust #[test] fn resolve_artifact_path_returns_absolute_path_within_store() { let dir = tempfile::tempdir().unwrap(); let store_path = dir.path(); std::fs::create_dir_all(store_path.join("raw/a/b")).unwrap(); let artifact_file = store_path.join("raw/a/b/abc.pdf"); std::fs::write(&artifact_file, b"data").unwrap(); let artifact = EntryArtifactSummary { artifact_role: "primary".to_string(), storage_area: "raw".to_string(), relpath: "raw/a/b/abc.pdf".to_string(), byte_size: Some(4), }; let resolved = resolve_artifact_path(store_path, &artifact).unwrap(); assert_eq!(resolved, artifact_file.canonicalize().unwrap()); } #[test] fn resolve_artifact_path_rejects_traversal() { let dir = tempfile::tempdir().unwrap(); let store_path = dir.path(); let artifact = EntryArtifactSummary { artifact_role: "primary".to_string(), storage_area: "raw".to_string(), relpath: "../escaped.txt".to_string(), byte_size: None, }; assert!(resolve_artifact_path(store_path, &artifact).is_err()); } ``` Note: `tempfile` is already a dev-dependency in `archivr-core`. Check that `use super::*;` is already at the top of the `mod tests` block (it is). - [ ] **Step 2: Run the tests to see them fail** ```bash cargo test -p archivr-core resolve_artifact 2>&1 ``` Expected: compile error — `resolve_artifact_path` not defined yet. - [ ] **Step 3: Implement `resolve_artifact_path`** Add this function to `crates/archivr-core/src/archive.rs`, after `list_runs` and before `#[cfg(test)]`: ```rust /// Resolves an artifact to its absolute on-disk path under `store_path`. /// /// `artifact.relpath` is a store-relative path (e.g. `raw/a/b/abc.pdf`). /// The returned path is canonicalized. Returns an error if the resolved path /// escapes `store_path` (path traversal protection) or if the file does not exist. pub fn resolve_artifact_path( store_path: &Path, artifact: &EntryArtifactSummary, ) -> Result { let joined = store_path.join(&artifact.relpath); let canonical_store = store_path .canonicalize() .with_context(|| format!("failed to canonicalize store path: {}", store_path.display()))?; let canonical_artifact = joined .canonicalize() .with_context(|| format!("artifact path does not exist: {}", joined.display()))?; if !canonical_artifact.starts_with(&canonical_store) { bail!( "artifact path escapes store: {}", canonical_artifact.display() ); } Ok(canonical_artifact) } ``` - [ ] **Step 4: Run the tests to see them pass** ```bash cargo test -p archivr-core resolve_artifact 2>&1 ``` Expected: both tests PASS. - [ ] **Step 5: Run full core test suite to confirm no regressions** ```bash cargo test -p archivr-core 2>&1 ``` Expected: all tests PASS. - [ ] **Step 6: Commit** ```bash git add crates/archivr-core/src/archive.rs git commit -m "feat(core): add resolve_artifact_path with path traversal protection" ``` --- ### Task 2: `serve_artifact` route in archivr-server **Files:** - Modify: `crates/archivr-server/Cargo.toml` - Modify: `crates/archivr-server/src/routes.rs` Route: `GET /api/archives/:archive_id/entries/:entry_uid/artifacts/:artifact_index` `artifact_index` is the zero-based index into `EntryDetail.artifacts`. This avoids any raw filesystem path in the URL. `ServeFile` streams the file and handles HTTP Range requests — browsers can seek in `