Resolve a `[[repo-file]]` destination against the repository root. `dst` must be a relative path (no `~`, no absolute) — it lands inside the repo. Rejects parent traversal so a manifest can't write outside the repo.
(dst: &str, repo_root: &Path)
| 570 | /// `dst` must be a relative path (no `~`, no absolute) — it lands inside the |
| 571 | /// repo. Rejects parent traversal so a manifest can't write outside the repo. |
| 572 | fn expand_repo_dst(dst: &str, repo_root: &Path) -> AgentResult<PathBuf> { |
| 573 | if dst.starts_with('/') || dst.starts_with('~') { |
| 574 | return Err(AgentError::Integration { |
| 575 | agent: "<any>".to_string(), |
| 576 | reason: format!( |
| 577 | "repo-file dst '{dst}' must be relative to the repo root, not absolute or home" |
| 578 | ), |
| 579 | }); |
| 580 | } |
| 581 | if dst.split('/').any(|c| c == "..") { |
| 582 | return Err(AgentError::Integration { |
| 583 | agent: "<any>".to_string(), |
| 584 | reason: format!("repo-file dst '{dst}' must not escape the repo root"), |
| 585 | }); |
| 586 | } |
| 587 | Ok(repo_root.join(dst)) |
| 588 | } |
| 589 | |
| 590 | /// Copy one file into place with the standard install-decision logic. |
| 591 | /// Shared by [[file]], [[skill]], and [[repo-file]] entries. |